From 672c1be4cd81119d74aaa298a662e2c4a272fd20 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 25 Mar 2026 15:10:01 +0100 Subject: [PATCH 001/335] Refactor: Decompose WorkspaceDocument god object into focused services WorkspaceDocument was a 1063-line mega-hub owning every manager and view model in the app. This refactor extracts its responsibilities into independently injectable services, reducing it to a thin NSDocument lifecycle manager. Key changes: - Extract WorkspaceStatePersistence for UserDefaults-backed UI state - Extract SearchState as a top-level class (was nested in WorkspaceDocument) - Decouple NotificationPanelViewModel from WorkspaceDocument (uses windowController directly instead of workspace reference) - Decouple Editor/EditorInstance from WorkspaceDocument (depend on SearchState directly instead of full workspace) - Inject NotificationPanelViewModel as direct EnvironmentObject - Remove unused @EnvironmentObject workspace declarations (5 views) - Remove NSToolbarDelegate conformance (unused on WorkspaceDocument) - Move search extension files to Features/Search/ --- .../Views/CEWorkspaceSettingsView.swift | 1 - .../CodeEditSplitViewController.swift | 19 ++--- .../CodeEditWindowController+Toolbar.swift | 6 +- .../CodeEditWindowController.swift | 2 +- .../WorkspaceDocument+SearchState.swift | 78 ------------------ .../WorkspaceDocument/WorkspaceDocument.swift | 68 +++++----------- .../WorkspaceStatePersistence.swift | 41 ++++++++++ .../Editor/Models/Editor/Editor.swift | 29 ++++--- .../Editor/Models/EditorInstance.swift | 34 ++++---- .../EditorLayout+StateRestoration.swift | 55 ++++++++----- .../TabBar/Tabs/Tab/EditorTabView.swift | 2 +- .../Views/EditorTabBarContextMenu.swift | 2 +- .../EditorTabBarTrailingAccessories.swift | 4 +- .../FindNavigator/FindModePicker.swift | 2 - .../FindNavigator/FindNavigatorForm.swift | 4 +- .../FindNavigator/FindNavigatorIndexBar.swift | 6 +- .../FindNavigatorResultList.swift | 2 +- .../FindNavigator/FindNavigatorView.swift | 8 +- .../NotificationPanelViewModel.swift | 33 ++++---- .../Views/NotificationBannerView.swift | 6 +- .../Views/NotificationPanelView.swift | 46 +++++------ .../Views/NotificationToolbarItem.swift | 6 +- .../Views/OpenQuicklyPreviewView.swift | 2 +- .../SearchState+Find.swift} | 6 +- .../SearchState+FindAndReplace.swift} | 4 +- .../SearchState+Index.swift} | 6 +- CodeEdit/Features/Search/SearchState.swift | 79 +++++++++++++++++++ .../Views/SourceControlSwitchView.swift | 1 - .../ViewModels/UtilityAreaViewModel.swift | 16 ++-- CodeEdit/WorkspaceView.swift | 2 +- ...ment+SearchState+FindAndReplaceTests.swift | 2 +- ...kspaceDocument+SearchState+FindTests.swift | 4 +- ...spaceDocument+SearchState+IndexTests.swift | 2 +- 33 files changed, 305 insertions(+), 273 deletions(-) delete mode 100644 CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+SearchState.swift create mode 100644 CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift rename CodeEdit/Features/{Documents/WorkspaceDocument/WorkspaceDocument+Find.swift => Search/SearchState+Find.swift} (99%) rename CodeEdit/Features/{Documents/WorkspaceDocument/WorkspaceDocument+FindAndReplace.swift => Search/SearchState+FindAndReplace.swift} (98%) rename CodeEdit/Features/{Documents/WorkspaceDocument/WorkspaceDocument+Index.swift => Search/SearchState+Index.swift} (96%) create mode 100644 CodeEdit/Features/Search/SearchState.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift index 451ed2a38f..ae30cbbaea 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift @@ -11,7 +11,6 @@ struct CEWorkspaceSettingsView: View { var dismiss: () -> Void @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings - @EnvironmentObject var workspace: WorkspaceDocument @State var selectedTaskID: UUID? @State var showAddTaskSheet: Bool = false diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 39735c8de1..f3d543434b 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -17,6 +17,7 @@ final class CodeEditSplitViewController: NSSplitViewController { private weak var workspace: WorkspaceDocument? private weak var navigatorViewModel: NavigatorAreaViewModel? private weak var windowRef: NSWindow? + private weak var statePersistence: WorkspaceStatePersistence? private unowned var hapticPerformer: NSHapticFeedbackPerformer // MARK: - Initialization @@ -30,6 +31,7 @@ final class CodeEditSplitViewController: NSSplitViewController { self.workspace = workspace self.navigatorViewModel = navigatorViewModel self.windowRef = windowRef + self.statePersistence = workspace.statePersistence self.hapticPerformer = hapticPerformer super.init(nibName: nil, bundle: nil) } @@ -77,6 +79,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(utilityAreaModel) .environmentObject(taskManager) .environmentObject(workspace.undoRegistration) + .environmentObject(workspace.notificationPanel) } } @@ -119,24 +122,22 @@ final class CodeEditSplitViewController: NSSplitViewController { override func viewWillAppear() { super.viewWillAppear() - guard let workspace else { return } - - let navigatorWidth = workspace.getFromWorkspaceState(.splitViewWidth) as? CGFloat + let navigatorWidth = statePersistence?.get(.splitViewWidth) as? CGFloat splitView.setPosition(navigatorWidth ?? Self.minSidebarWidth, ofDividerAt: 0) if let firstSplitView = splitViewItems.first { - firstSplitView.isCollapsed = workspace.getFromWorkspaceState( + firstSplitView.isCollapsed = statePersistence?.get( .navigatorCollapsed ) as? Bool ?? false } if let lastSplitView = splitViewItems.last { - lastSplitView.isCollapsed = workspace.getFromWorkspaceState( + lastSplitView.isCollapsed = statePersistence?.get( .inspectorCollapsed ) as? Bool ?? true } - workspace.notificationPanel.updateToolbarItem() + workspace?.notificationPanel.updateToolbarItem() } // MARK: - NSSplitViewDelegate @@ -203,16 +204,16 @@ final class CodeEditSplitViewController: NSSplitViewController { let panel = splitView.subviews[0] let width = panel.frame.size.width if width > 0 { - workspace?.addToWorkspaceState(key: .splitViewWidth, value: width) + statePersistence?.set(key: .splitViewWidth, value: width) } } } func saveNavigatorCollapsedState(isCollapsed: Bool) { - workspace?.addToWorkspaceState(key: .navigatorCollapsed, value: isCollapsed) + statePersistence?.set(key: .navigatorCollapsed, value: isCollapsed) } func saveInspectorCollapsedState(isCollapsed: Bool) { - workspace?.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed) + statePersistence?.set(key: .inspectorCollapsed, value: isCollapsed) } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index fbb536a074..d86ce792ea 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -101,7 +101,7 @@ extension CodeEditWindowController { func toggleToolbar() { toolbarCollapsed.toggle() - workspace?.addToWorkspaceState(key: .toolbarCollapsed, value: toolbarCollapsed) + workspace?.statePersistence?.set(key: .toolbarCollapsed, value: toolbarCollapsed) updateToolbarVisibility() } @@ -228,7 +228,9 @@ extension CodeEditWindowController { private func notificationItem() -> NSToolbarItem? { let toolbarItem = NSToolbarItem(itemIdentifier: .notificationItem) guard let workspace = workspace else { return nil } - let view = NSHostingView(rootView: NotificationToolbarItem().environmentObject(workspace)) + let view = NSHostingView( + rootView: NotificationToolbarItem().environmentObject(workspace.notificationPanel) + ) toolbarItem.view = view return toolbarItem } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index be082d6fee..be20a708df 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -44,7 +44,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs window?.delegate = self guard let workspace else { return } self.workspace = workspace - self.toolbarCollapsed = workspace.getFromWorkspaceState(.toolbarCollapsed) as? Bool ?? false + self.toolbarCollapsed = workspace.statePersistence?.get(.toolbarCollapsed) as? Bool ?? false guard let splitViewController = setupSplitView(with: workspace) else { fatalError("Failed to set up content view.") } diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+SearchState.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+SearchState.swift deleted file mode 100644 index 2ee8305a5a..0000000000 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+SearchState.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// WorkspaceDocument+SearchState.swift -// CodeEdit -// -// Created by Tom Ludwig on 16.01.24. -// - -import Foundation - -extension WorkspaceDocument { - final class SearchState: ObservableObject { - enum IndexStatus: Equatable { - case none - case indexing(progress: Double) - case done - } - - enum FindNavigatorStatus: Equatable { - case none - case searching - case replacing - case found - case replaced(updatedFiles: Int) - case failed(errorMessage: String) - } - - @Published var searchResult: [SearchResultModel] = [] - @Published var searchResultsFileCount: Int = 0 - @Published var searchResultsCount: Int = 0 - /// Stores the user's input, shown when no files are found, and persists across navigation items. - @Published var searchQuery: String = "" - @Published var replaceText: String = "" - - @Published var indexStatus: IndexStatus = .none - - @Published var findNavigatorStatus: FindNavigatorStatus = .none - - @Published var shouldFocusSearchField: Bool = false - - unowned var workspace: WorkspaceDocument - var tempSearchResults = [SearchResultModel]() - var caseSensitive: Bool = false - var indexer: SearchIndexer? - var selectedMode: [SearchModeModel] = [ - .Find, - .Text, - .Containing - ] - - init(_ workspace: WorkspaceDocument) { - self.workspace = workspace - self.indexer = SearchIndexer.Memory.create() - addProjectToIndex() - } - - /// Represents the compare options to be used for find and replace. - /// - /// The `replaceOptions` property is a lazy, computed property that dynamically calculates - /// the compare options based on the values of `selectedMode` and `ignoreCase`. It is used - /// for controlling string replacement behavior for the find and replace functions. - /// - /// - Note: This property is implemented as a lazy property in the main class body because - /// extensions cannot contain stored properties directly. - lazy var replaceOptions: NSString.CompareOptions = { - var options: NSString.CompareOptions = [] - - if selectedMode.second == .RegularExpression { - options.insert(.regularExpression) - } - - if !caseSensitive { - options.insert(.caseInsensitive) - } - - return options - }() - } -} diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift index 4671b57f4f..c7775f4592 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift @@ -7,29 +7,16 @@ import AppKit import SwiftUI -import Combine import Foundation -import LanguageServerProtocol @objc(WorkspaceDocument) -final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { +final class WorkspaceDocument: NSDocument, ObservableObject { @Published var sortFoldersOnTop: Bool = true /// A string used to filter the displayed files and folders in the project navigator area based on user input. @Published var navigatorFilter: String = "" /// Whether the workspace only shows files with changes. @Published var sourceControlFilter = false - private var workspaceState: [String: Any] { - get { - let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")" - return UserDefaults.standard.object(forKey: key) as? [String: Any] ?? [:] - } - set { - let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")" - UserDefaults.standard.set(newValue, forKey: key) - } - } - var workspaceFileManager: CEWorkspaceFileManager? var editorManager: EditorManager? = EditorManager() @@ -45,41 +32,16 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { var workspaceSettingsManager: CEWorkspaceSettings? var taskNotificationHandler: TaskNotificationHandler = TaskNotificationHandler() + var statePersistence: WorkspaceStatePersistence? + var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() var notificationPanel = NotificationPanelViewModel() - private var cancellables = Set() - - override init() { - super.init() - notificationPanel.workspace = self - - // Observe changes to notification panel - notificationPanel.objectWillChange - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.objectWillChange.send() - } - .store(in: &cancellables) - } deinit { - cancellables.forEach { $0.cancel() } NotificationCenter.default.removeObserver(self) } - func getFromWorkspaceState(_ key: WorkspaceStateKey) -> Any? { - return workspaceState[key.rawValue] - } - - func addToWorkspaceState(key: WorkspaceStateKey, value: Any?) { - if let value { - workspaceState.updateValue(value, forKey: key.rawValue) - } else { - workspaceState.removeValue(forKey: key.rawValue) - } - } - // MARK: NSDocument private let ignoredFilesAndDirectory = [ @@ -114,7 +76,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { workspace: self ) - if let rectString = getFromWorkspaceState(.workspaceWindowSize) as? String { + if let rectString = statePersistence?.get(.workspaceWindowSize) as? String { window.setFrame(NSRectFromString(rectString), display: true, animate: false) } else { window.setFrame(NSRect(x: 0, y: 0, width: 1400, height: 900), display: true, animate: false) @@ -125,6 +87,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { window.setAccessibilityDocument(self.fileURL?.absoluteString) self.addWindowController(windowController) + notificationPanel.windowController = windowController window.makeKeyAndOrderFront(nil) } @@ -141,6 +104,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { self.fileURL = url self.displayName = url.lastPathComponent + self.statePersistence = WorkspaceStatePersistence(workspaceURL: url) let sourceControlManager = SourceControlManager( workspaceURL: url, @@ -154,7 +118,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { ) self.sourceControlManager = sourceControlManager sourceControlManager.fileManager = workspaceFileManager - self.searchState = .init(self) + self.searchState = SearchState(workspaceURL: url) self.openQuicklyViewModel = .init(fileURL: url) self.commandsPaletteState = .init() self.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) @@ -167,8 +131,14 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { self.taskNotificationHandler.workspaceURL = url workspaceFileManager?.addObserver(undoRegistration) - editorManager?.restoreFromState(self) - utilityAreaModel?.restoreFromState(self) + if let statePersistence { + editorManager?.restoreFromState( + statePersistence: statePersistence, + fileManager: workspaceFileManager, + searchState: searchState + ) + utilityAreaModel?.restoreFromState(statePersistence) + } } override func read(from url: URL, ofType typeName: String) throws { @@ -181,10 +151,11 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { override func close() { super.close() - editorManager?.saveRestorationState(self) - utilityAreaModel?.saveRestorationState(self) + if let statePersistence { + editorManager?.saveRestorationState(statePersistence) + utilityAreaModel?.saveRestorationState(statePersistence) + } - cancellables.forEach({ $0.cancel() }) statusBarViewModel = nil utilityAreaModel = nil searchState = nil @@ -197,6 +168,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { workspaceSettingsManager?.cleanUp() workspaceSettingsManager = nil taskManager = nil + statePersistence = nil } /// Determines the windows should be closed. diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift new file mode 100644 index 0000000000..c6731a95a4 --- /dev/null +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift @@ -0,0 +1,41 @@ +// +// WorkspaceStatePersistence.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 25.03.26. +// + +import Foundation + +/// A standalone service for persisting workspace-specific UI state (window size, collapsed panels, etc.) +/// via UserDefaults. Extracted from WorkspaceDocument to enable independent injection and testing. +final class WorkspaceStatePersistence: ObservableObject { + private let workspaceURL: URL + + private var workspaceState: [String: Any] { + get { + let key = "workspaceState-\(workspaceURL.absoluteString)" + return UserDefaults.standard.object(forKey: key) as? [String: Any] ?? [:] + } + set { + let key = "workspaceState-\(workspaceURL.absoluteString)" + UserDefaults.standard.set(newValue, forKey: key) + } + } + + init(workspaceURL: URL) { + self.workspaceURL = workspaceURL + } + + func get(_ key: WorkspaceStateKey) -> Any? { + workspaceState[key.rawValue] + } + + func set(key: WorkspaceStateKey, value: Any?) { + if let value { + workspaceState.updateValue(value, forKey: key.rawValue) + } else { + workspaceState.removeValue(forKey: key.rawValue) + } + } +} diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index 782b956b71..d9db395eaf 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -60,7 +60,10 @@ final class Editor: ObservableObject, Identifiable { var id = UUID() weak var parent: SplitViewData? - weak var workspace: WorkspaceDocument? + weak var searchState: SearchState? + + /// Whether this editor is attached to a workspace. Used to guard file loading operations. + var isAttachedToWorkspace: Bool = false private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "Editor") @@ -68,7 +71,7 @@ final class Editor: ObservableObject, Identifiable { self.tabs = [] self.temporaryTab = nil self.parent = nil - self.workspace = nil + self.searchState = nil } init( @@ -76,17 +79,17 @@ final class Editor: ObservableObject, Identifiable { selectedTab: Tab? = nil, temporaryTab: Tab? = nil, parent: SplitViewData? = nil, - workspace: WorkspaceDocument? = nil + searchState: SearchState? = nil ) { self.parent = parent - self.workspace = workspace + self.searchState = searchState // If we open the files without a valid workspace, we risk creating a file we lose track of but stays in memory - if workspace != nil { + if isAttachedToWorkspace { files.forEach { openTab(file: $0) } } else { - self.tabs = OrderedSet(files.map { EditorInstance(workspace: workspace, file: $0) }) + self.tabs = OrderedSet(files.map { EditorInstance(searchState: searchState, file: $0) }) } - self.selectedTab = selectedTab ?? (files.isEmpty ? nil : Tab(workspace: workspace, file: files.first!)) + self.selectedTab = selectedTab ?? (files.isEmpty ? nil : Tab(searchState: searchState, file: files.first!)) self.temporaryTab = temporaryTab } @@ -95,11 +98,11 @@ final class Editor: ObservableObject, Identifiable { selectedTab: Tab? = nil, temporaryTab: Tab? = nil, parent: SplitViewData? = nil, - workspace: WorkspaceDocument? = nil + searchState: SearchState? = nil ) { self.tabs = [] self.parent = parent - self.workspace = workspace + self.searchState = searchState files.forEach { openTab(file: $0.file) } self.selectedTab = selectedTab ?? tabs.first self.temporaryTab = temporaryTab @@ -152,7 +155,7 @@ final class Editor: ObservableObject, Identifiable { clearFuture() } if file != selectedTab?.file { - addToHistory(EditorInstance(workspace: workspace, file: file)) + addToHistory(EditorInstance(searchState: searchState, file: file)) } removeTab(file) if let selectedTab { @@ -182,7 +185,7 @@ final class Editor: ObservableObject, Identifiable { /// - file: the file to open. /// - asTemporary: indicates whether the tab should be opened as a temporary tab or a permanent tab. func openTab(file: CEWorkspaceFile, asTemporary: Bool) { - let item = EditorInstance(workspace: workspace, file: file) + let item = EditorInstance(searchState: searchState, file: file) // Item is already opened in a tab. guard !tabs.contains(item) || !asTemporary else { selectedTab = item @@ -240,7 +243,7 @@ final class Editor: ObservableObject, Identifiable { /// - index: Index where the tab needs to be added. If nil, it is added to the back. /// - fromHistory: Indicates whether the tab has been opened from going back in history. func openTab(file: CEWorkspaceFile, at index: Int? = nil, fromHistory: Bool = false) { - let item = Tab(workspace: workspace, file: file) + let item = Tab(searchState: searchState, file: file) if let index { tabs.insert(item, at: index) } else { @@ -269,7 +272,7 @@ final class Editor: ObservableObject, Identifiable { return } - guard workspace != nil else { + guard isAttachedToWorkspace else { throw EditorError.noWorkspaceAttached } diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index fd11333bb7..4cb9cd6440 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -33,14 +33,14 @@ class EditorInstance: ObservableObject, Hashable { // MARK: - Init - init(workspace: WorkspaceDocument?, file: CEWorkspaceFile, cursorPositions: [CursorPosition]? = nil) { + init(searchState: SearchState?, file: CEWorkspaceFile, cursorPositions: [CursorPosition]? = nil) { self.file = file let url = file.url let editorState = EditorStateRestoration.shared?.restorationState(for: url) - findText = workspace?.searchState?.searchQuery + findText = searchState?.searchQuery findTextSubject = PassthroughSubject() - replaceText = workspace?.searchState?.replaceText + replaceText = searchState?.replaceText replaceTextSubject = PassthroughSubject() self.cursorPositions = ( @@ -64,14 +64,14 @@ class EditorInstance: ObservableObject, Hashable { } .store(in: &cancellables) - listenToFindText(workspace: workspace) - listenToReplaceText(workspace: workspace) + listenToFindText(searchState: searchState) + listenToReplaceText(searchState: searchState) } // MARK: - Find/Replace Listeners - func listenToFindText(workspace: WorkspaceDocument?) { - workspace?.searchState?.$searchQuery + func listenToFindText(searchState: SearchState?) { + searchState?.$searchQuery .receive(on: RunLoop.main) .sink { [weak self] newQuery in if self?.findText != newQuery { @@ -81,17 +81,17 @@ class EditorInstance: ObservableObject, Hashable { .store(in: &cancellables) findTextSubject .receive(on: RunLoop.main) - .sink { [weak workspace, weak self] newFindText in - if let newFindText, workspace?.searchState?.searchQuery != newFindText { - workspace?.searchState?.searchQuery = newFindText + .sink { [weak searchState, weak self] newFindText in + if let newFindText, searchState?.searchQuery != newFindText { + searchState?.searchQuery = newFindText } - self?.findText = workspace?.searchState?.searchQuery + self?.findText = searchState?.searchQuery } .store(in: &cancellables) } - func listenToReplaceText(workspace: WorkspaceDocument?) { - workspace?.searchState?.$replaceText + func listenToReplaceText(searchState: SearchState?) { + searchState?.$replaceText .receive(on: RunLoop.main) .sink { [weak self] newText in if self?.replaceText != newText { @@ -101,11 +101,11 @@ class EditorInstance: ObservableObject, Hashable { .store(in: &cancellables) replaceTextSubject .receive(on: RunLoop.main) - .sink { [weak workspace, weak self] newReplaceText in - if let newReplaceText, workspace?.searchState?.replaceText != newReplaceText { - workspace?.searchState?.replaceText = newReplaceText + .sink { [weak searchState, weak self] newReplaceText in + if let newReplaceText, searchState?.replaceText != newReplaceText { + searchState?.replaceText = newReplaceText } - self?.replaceText = workspace?.searchState?.replaceText + self?.replaceText = searchState?.replaceText } .store(in: &cancellables) } diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index bb273a09a1..0cc3cf2f60 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -11,17 +11,25 @@ import OrderedCollections extension EditorManager { /// Restores the tab manager from a captured state obtained using `saveRestorationState` - /// - Parameter workspace: The workspace to retrieve state from. - func restoreFromState(_ workspace: WorkspaceDocument) { + /// - Parameters: + /// - statePersistence: The persistence service to retrieve saved state from. + /// - fileManager: The file manager to resolve file references. + /// - searchState: The search state for editor instances. + func restoreFromState( + statePersistence: WorkspaceStatePersistence, + fileManager: CEWorkspaceFileManager?, + searchState: SearchState? + ) { defer { - // No matter what, set the workspace on each editor. Even if we fail to read data. + // No matter what, set up each editor. Even if we fail to read data. flattenedEditors.forEach { editor in - editor.workspace = workspace + editor.searchState = searchState + editor.isAttachedToWorkspace = true } } do { - guard let data = workspace.getFromWorkspaceState(.openTabs) as? Data else { + guard let data = statePersistence.get(.openTabs) as? Data else { return } @@ -41,7 +49,7 @@ extension EditorManager { return } - try fixRestoredEditorLayout(state.groups, workspace: workspace) + try fixRestoredEditorLayout(state.groups, fileManager: fileManager, searchState: searchState) self.editorLayout = state.groups self.activeEditor = activeEditor @@ -60,17 +68,21 @@ extension EditorManager { /// - Parameters: /// - group: The tab group to fix. /// - fileManager: The file manager to use to map files. - private func fixRestoredEditorLayout(_ group: EditorLayout, workspace: WorkspaceDocument) throws { + private func fixRestoredEditorLayout( + _ group: EditorLayout, + fileManager: CEWorkspaceFileManager?, + searchState: SearchState? + ) throws { switch group { case let .one(data): - try fixEditor(data, workspace: workspace) + try fixEditor(data, fileManager: fileManager, searchState: searchState) case let .vertical(splitData): try splitData.editorLayouts.forEach { group in - try fixRestoredEditorLayout(group, workspace: workspace) + try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) } case let .horizontal(splitData): try splitData.editorLayouts.forEach { group in - try fixRestoredEditorLayout(group, workspace: workspace) + try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) } } } @@ -94,18 +106,23 @@ extension EditorManager { /// - Parameters: /// - data: The tab group to fix. /// - fileManager: The file manager to use to map files.a - private func fixEditor(_ editor: Editor, workspace: WorkspaceDocument) throws { - guard let fileManager = workspace.workspaceFileManager else { return } + private func fixEditor( + _ editor: Editor, + fileManager: CEWorkspaceFileManager?, + searchState: SearchState? + ) throws { + guard let fileManager else { return } let resolvedTabs = editor .tabs .compactMap({ fileManager.getFile($0.file.url.path(percentEncoded: false), createIfNotFound: true) }) - .map({ EditorInstance(workspace: workspace, file: $0) }) + .map({ EditorInstance(searchState: searchState, file: $0) }) for tab in resolvedTabs { try tab.file.loadCodeFile() } - editor.workspace = workspace + editor.searchState = searchState + editor.isAttachedToWorkspace = true editor.tabs = OrderedSet(resolvedTabs) if let selectedTab = editor.selectedTab { @@ -120,13 +137,13 @@ extension EditorManager { } } - func saveRestorationState(_ workspace: WorkspaceDocument) { + func saveRestorationState(_ statePersistence: WorkspaceStatePersistence) { if let data = try? JSONEncoder().encode( EditorRestorationState(activeEditor: activeEditor.id, groups: editorLayout) ) { - workspace.addToWorkspaceState(key: .openTabs, value: data) + statePersistence.set(key: .openTabs, value: data) } else { - workspace.addToWorkspaceState(key: .openTabs, value: nil) + statePersistence.set(key: .openTabs, value: nil) } } } @@ -233,11 +250,11 @@ extension Editor: Codable { self.init( files: OrderedSet(fileURLs.map { CEWorkspaceFile(url: $0) }), selectedTab: selectedTab == nil ? nil : EditorInstance( - workspace: nil, + searchState: nil, file: CEWorkspaceFile(url: selectedTab!) ), parent: nil, - workspace: nil + searchState: nil ) self.id = id } diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 006716749e..0c5112789c 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -92,7 +92,7 @@ struct EditorTabView: View { // Only set the `selectedId` when they are not equal to avoid performance issue for now. editorManager.activeEditor = editor if editor.selectedTab?.file != tabFile { - let tabItem = EditorInstance(workspace: workspace, file: tabFile) + let tabItem = EditorInstance(searchState: editor.searchState, file: tabFile) editor.setSelectedTab(tabFile) editor.clearFuture() editor.addToHistory(tabItem) diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index 29e539cf14..d5db182aa5 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -137,7 +137,7 @@ struct EditorTabBarContextMenu: ViewModifier { } func moveToNewSplit(_ edge: Edge) { - let newEditor = Editor(files: [item], workspace: workspace) + let newEditor = Editor(files: [item], searchState: tabs.searchState) splitEditor(edge, newEditor) tabs.closeTab(file: item) workspace.editorManager?.activeEditor = newEditor diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 8333856606..295fde8d77 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -22,8 +22,6 @@ struct EditorTabBarTrailingAccessories: View { @Environment(\.controlActiveState) private var activeState - @EnvironmentObject var workspace: WorkspaceDocument - @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var editor: Editor @@ -99,7 +97,7 @@ struct EditorTabBarTrailingAccessories: View { func split(edge: Edge) { let newEditor: Editor if let tab = editor.selectedTab { - newEditor = .init(files: [tab], temporaryTab: tab, workspace: workspace) + newEditor = .init(files: [tab], temporaryTab: tab, searchState: editor.searchState) } else { newEditor = .init() } diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift index 308a15af5e..bb3b09bf70 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift @@ -21,8 +21,6 @@ struct FindModePicker: View { @Environment(\.controlActiveState) private var activeState - @EnvironmentObject var workspace: WorkspaceDocument - @State var position: NSPoint? @State var isHovering: Bool = false @State private var button: NSPopUpButton? diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift index bb10b029b2..cbf2807401 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift @@ -8,7 +8,7 @@ import SwiftUI struct FindNavigatorForm: View { - @ObservedObject private var state: WorkspaceDocument.SearchState + @ObservedObject private var state: SearchState @State private var selectedMode: [SearchModeModel] { didSet { @@ -27,7 +27,7 @@ struct FindNavigatorForm: View { @State private var excludeSettings: Bool = true @FocusState private var isSearchFieldFocused: Bool - init(state: WorkspaceDocument.SearchState) { + init(state: SearchState) { self.state = state selectedMode = state.selectedMode } diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorIndexBar.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorIndexBar.swift index c7c903d359..c0c1c6d0bc 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorIndexBar.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorIndexBar.swift @@ -8,11 +8,11 @@ import SwiftUI struct FindNavigatorIndexBar: View { - @ObservedObject private var state: WorkspaceDocument.SearchState + @ObservedObject private var state: SearchState @State private var progress: Double = 0.0 @State private var shouldShow: Bool = false - init(state: WorkspaceDocument.SearchState) { + init(state: SearchState) { self.state = state } @@ -45,7 +45,7 @@ struct FindNavigatorIndexBar: View { /// Updates the bar with a new status update. /// - Parameter status: The new status. - private func updateWithNewStatus(_ status: WorkspaceDocument.SearchState.IndexStatus) { + private func updateWithNewStatus(_ status: SearchState.IndexStatus) { switch status { case .none: self.progress = 0.0 diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift index 8e5449ef29..9dd5ec2567 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift @@ -43,7 +43,7 @@ struct FindNavigatorResultList: NSViewControllerRepresentable { } class Coordinator: NSObject { - init(state: WorkspaceDocument.SearchState?, controller: FindNavigatorListViewController?) { + init(state: SearchState?, controller: FindNavigatorListViewController?) { self.controller = controller super.init() self.listener = state? diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift index 5e8fb370be..1db46c42f7 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift @@ -10,13 +10,15 @@ import SwiftUI struct FindNavigatorView: View { @EnvironmentObject private var workspace: WorkspaceDocument - private var state: WorkspaceDocument.SearchState { - workspace.searchState ?? .init(workspace) + private var state: SearchState { + // SearchState is always initialized in WorkspaceDocument.initWorkspaceState + // before any views are created, so this is safe to force unwrap. + workspace.searchState! } @State private var foundFilesCount: Int = 0 @State private var searchResultCount: Int = 0 - @State private var findNavigatorStatus: WorkspaceDocument.SearchState.FindNavigatorStatus = .none + @State private var findNavigatorStatus: SearchState.FindNavigatorStatus = .none @State private var findResultMessage: String? var body: some View { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift index 6fdcc89143..c07f0f2a3b 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -35,7 +35,7 @@ final class NotificationPanelViewModel: ObservableObject { activeNotifications.filter { !hiddenNotificationIds.contains($0.id) } } - weak var workspace: WorkspaceDocument? + weak var windowController: NSWindowController? /// Whether a notification should be visible in the panel func isNotificationVisible(_ notification: CENotification) -> Bool { @@ -232,25 +232,24 @@ final class NotificationPanelViewModel: ObservableObject { func updateToolbarItem() { if #available(macOS 15.0, *) { - self.workspace?.windowControllers.forEach { controller in - guard let toolbar = controller.window?.toolbar else { + guard let windowController, let toolbar = windowController.window?.toolbar else { + return + } + + let shouldShow = !self.visibleNotifications.isEmpty || NotificationManager.shared.unreadCount > 0 + if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { + guard let activityItemIdx = toolbar.items + .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { return } - let shouldShow = !self.visibleNotifications.isEmpty || NotificationManager.shared.unreadCount > 0 - if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { - guard let activityItemIdx = toolbar.items - .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { - return - } - toolbar.insertItem(withItemIdentifier: .space, at: activityItemIdx + 1) - toolbar.insertItem(withItemIdentifier: .notificationItem, at: activityItemIdx + 2) - } + toolbar.insertItem(withItemIdentifier: .space, at: activityItemIdx + 1) + toolbar.insertItem(withItemIdentifier: .notificationItem, at: activityItemIdx + 2) + } - if !shouldShow, let index = toolbar.items - .firstIndex(where: { $0.itemIdentifier == .notificationItem }) { - toolbar.removeItem(at: index) - toolbar.removeItem(at: index) - } + if !shouldShow, let index = toolbar.items + .firstIndex(where: { $0.itemIdentifier == .notificationItem }) { + toolbar.removeItem(at: index) + toolbar.removeItem(at: index) } } } diff --git a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift index 11a90696ac..f11bedb933 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift @@ -11,7 +11,7 @@ struct NotificationBannerView: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @ObservedObject private var notificationManager = NotificationManager.shared let notification: CENotification @@ -150,9 +150,9 @@ struct NotificationBannerView: View { } if hovering { - workspace.notificationPanel.pauseTimer() + notificationPanel.pauseTimer() } else { - workspace.notificationPanel.resumeTimer() + notificationPanel.resumeTimer() } } } diff --git a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift b/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift index 0474639a5f..a5d234a743 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift @@ -8,7 +8,7 @@ import SwiftUI struct NotificationPanelView: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @Environment(\.controlActiveState) private var controlActiveState @@ -33,8 +33,8 @@ struct NotificationPanelView: View { } @ViewBuilder var notifications: some View { - let visibleNotifications = workspace.notificationPanel.activeNotifications.filter { - workspace.notificationPanel.isNotificationVisible($0) + let visibleNotifications = notificationPanel.activeNotifications.filter { + notificationPanel.isNotificationVisible($0) } VStack(spacing: 8) { @@ -42,15 +42,15 @@ struct NotificationPanelView: View { NotificationBannerView( notification: notification, onDismiss: { - workspace.notificationPanel.dismissNotification(notification) + notificationPanel.dismissNotification(notification) }, onAction: { notification.action() - if workspace.notificationPanel.isPresented { - workspace.notificationPanel.toggleNotificationsVisibility() - workspace.notificationPanel.dismissNotification(notification, disableAnimation: true) + if notificationPanel.isPresented { + notificationPanel.toggleNotificationsVisibility() + notificationPanel.dismissNotification(notification, disableAnimation: true) } else { - workspace.notificationPanel.dismissNotification(notification) + notificationPanel.dismissNotification(notification) } } ) @@ -79,10 +79,10 @@ struct NotificationPanelView: View { } ) .onPreferenceChange(ViewOffsetKey.self) { - if $0 <= 0.0 && !workspace.notificationPanel.scrolledToTop { - workspace.notificationPanel.scrolledToTop = true - } else if $0 > 0.0 && workspace.notificationPanel.scrolledToTop { - workspace.notificationPanel.scrolledToTop = false + if $0 <= 0.0 && !notificationPanel.scrolledToTop { + notificationPanel.scrolledToTop = true + } else if $0 > 0.0 && notificationPanel.scrolledToTop { + notificationPanel.scrolledToTop = false } } notifications @@ -101,13 +101,13 @@ struct NotificationPanelView: View { .scrollDisabled(!hasOverflow) .coordinateSpace(name: "scroll") .onChange(of: isFocused) { _, newValue in - workspace.notificationPanel.handleFocusChange(isFocused: newValue) + notificationPanel.handleFocusChange(isFocused: newValue) } .onChange(of: geometry.size.height) { _, newValue in updateOverflow(contentHeight: contentHeight, containerHeight: newValue) } - .onChange(of: workspace.notificationPanel.isPresented) { _, isPresented in - if !isPresented && !workspace.notificationPanel.scrolledToTop { + .onChange(of: notificationPanel.isPresented) { _, isPresented in + if !isPresented && !notificationPanel.scrolledToTop { // If scrolled, delay scroll animation until after notifications are hidden DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.easeOut(duration: 0.3)) { @@ -117,8 +117,8 @@ struct NotificationPanelView: View { } } .allowsHitTesting( - workspace.notificationPanel.activeNotifications - .contains { workspace.notificationPanel.isNotificationVisible($0) } + notificationPanel.activeNotifications + .contains { notificationPanel.isNotificationVisible($0) } ) } } @@ -133,16 +133,16 @@ struct NotificationPanelView: View { .focusable() .focusEffectDisabled() .focused($isFocused) - .onChange(of: workspace.notificationPanel.isPresented) { _, isPresented in + .onChange(of: notificationPanel.isPresented) { _, isPresented in if isPresented { isFocused = true } } .onChange(of: controlActiveState) { _, newState in - if newState != .active && newState != .key && workspace.notificationPanel.isPresented { + if newState != .active && newState != .key && notificationPanel.isPresented { // Delay hiding notifications to match animation timing DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - workspace.notificationPanel.toggleNotificationsVisibility() + notificationPanel.toggleNotificationsVisibility() } } } @@ -153,12 +153,12 @@ struct NotificationPanelView: View { .opacity(controlActiveState == .active || controlActiveState == .key ? 1 : 0) .offset( x: (controlActiveState == .active || controlActiveState == .key) && - (workspace.notificationPanel.isPresented || workspace.notificationPanel.scrolledToTop) + (notificationPanel.isPresented || notificationPanel.scrolledToTop) ? 0 : 350 ) - .animation(.easeInOut(duration: 0.3), value: workspace.notificationPanel.isPresented) - .animation(.easeInOut(duration: 0.3), value: workspace.notificationPanel.scrolledToTop) + .animation(.easeInOut(duration: 0.3), value: notificationPanel.isPresented) + .animation(.easeInOut(duration: 0.3), value: notificationPanel.scrolledToTop) .animation(.easeInOut(duration: 0.2), value: controlActiveState) } } diff --git a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift b/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift index ecf8ea94a5..8e7fb47b0e 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift @@ -8,17 +8,17 @@ import SwiftUI struct NotificationToolbarItem: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @ObservedObject private var notificationManager = NotificationManager.shared @Environment(\.controlActiveState) private var controlActiveState var body: some View { - let visibleNotifications = workspace.notificationPanel.visibleNotifications + let visibleNotifications = notificationPanel.visibleNotifications if notificationManager.unreadCount > 0 || !visibleNotifications.isEmpty { Button { - workspace.notificationPanel.toggleNotificationsVisibility() + notificationPanel.toggleNotificationsVisibility() } label: { HStack(spacing: 4) { Image(systemName: "bell.badge.fill") diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift index 0a696432c1..2a191cfdb1 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift @@ -24,7 +24,7 @@ struct OpenQuicklyPreviewView: View { withContentsOf: item.url, ofType: item.contentType?.identifier ?? "public.source-code" ) - self._editorInstance = .init(wrappedValue: EditorInstance(workspace: nil, file: item)) + self._editorInstance = .init(wrappedValue: EditorInstance(searchState: nil, file: item)) self._document = .init(wrappedValue: doc ?? .init()) } diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Find.swift b/CodeEdit/Features/Search/SearchState+Find.swift similarity index 99% rename from CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Find.swift rename to CodeEdit/Features/Search/SearchState+Find.swift index 159a4bf0d7..6c5cd25e06 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Find.swift +++ b/CodeEdit/Features/Search/SearchState+Find.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+Find.swift +// SearchState+Find.swift // CodeEdit // // Created by Tommy Ludwig on 02.01.24. @@ -7,9 +7,9 @@ import Foundation -extension WorkspaceDocument.SearchState: @unchecked Sendable {} +extension SearchState: @unchecked Sendable {} -extension WorkspaceDocument.SearchState { +extension SearchState { /// Creates a search term based on the given query and search mode. /// /// - Parameter query: The original user query string. diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+FindAndReplace.swift b/CodeEdit/Features/Search/SearchState+FindAndReplace.swift similarity index 98% rename from CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+FindAndReplace.swift rename to CodeEdit/Features/Search/SearchState+FindAndReplace.swift index 48ed02b8ce..177f6b9688 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+FindAndReplace.swift +++ b/CodeEdit/Features/Search/SearchState+FindAndReplace.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+FindAndReplace.swift +// SearchState+FindAndReplace.swift // CodeEdit // // Created by Tommy Ludwig on 02.01.24. @@ -8,7 +8,7 @@ import Foundation import AppKit -extension WorkspaceDocument.SearchState { +extension SearchState { /// Performs a search and replace operation in a collection of files based on the provided query. /// /// - Parameters: diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Index.swift b/CodeEdit/Features/Search/SearchState+Index.swift similarity index 96% rename from CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Index.swift rename to CodeEdit/Features/Search/SearchState+Index.swift index e84edceaf7..c29e527fb3 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Index.swift +++ b/CodeEdit/Features/Search/SearchState+Index.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+Index.swift +// SearchState+Index.swift // CodeEdit // // Created by Tommy Ludwig on 02.01.24. @@ -7,12 +7,12 @@ import Foundation -extension WorkspaceDocument.SearchState { +extension SearchState { /// Adds the contents of the current workspace URL to the search index. /// That means that the contents of the workspace will be indexed and searchable. func addProjectToIndex() { guard let indexer = indexer else { return } - guard let url = workspace.fileURL else { return } + let url = workspaceURL indexStatus = .indexing(progress: 0.0) let uuidString = UUID().uuidString diff --git a/CodeEdit/Features/Search/SearchState.swift b/CodeEdit/Features/Search/SearchState.swift new file mode 100644 index 0000000000..5ee412f2ab --- /dev/null +++ b/CodeEdit/Features/Search/SearchState.swift @@ -0,0 +1,79 @@ +// +// SearchState.swift +// CodeEdit +// +// Created by Tom Ludwig on 16.01.24. +// + +import Foundation + +/// Manages the search/find state for a workspace, including indexing, search results, +/// and find-and-replace operations. Extracted from WorkspaceDocument to be independently +/// injectable and testable. +final class SearchState: ObservableObject { + enum IndexStatus: Equatable { + case none + case indexing(progress: Double) + case done + } + + enum FindNavigatorStatus: Equatable { + case none + case searching + case replacing + case found + case replaced(updatedFiles: Int) + case failed(errorMessage: String) + } + + @Published var searchResult: [SearchResultModel] = [] + @Published var searchResultsFileCount: Int = 0 + @Published var searchResultsCount: Int = 0 + /// Stores the user's input, shown when no files are found, and persists across navigation items. + @Published var searchQuery: String = "" + @Published var replaceText: String = "" + + @Published var indexStatus: IndexStatus = .none + + @Published var findNavigatorStatus: FindNavigatorStatus = .none + + @Published var shouldFocusSearchField: Bool = false + + let workspaceURL: URL + var tempSearchResults = [SearchResultModel]() + var caseSensitive: Bool = false + var indexer: SearchIndexer? + var selectedMode: [SearchModeModel] = [ + .Find, + .Text, + .Containing + ] + + init(workspaceURL: URL) { + self.workspaceURL = workspaceURL + self.indexer = SearchIndexer.Memory.create() + addProjectToIndex() + } + + /// Represents the compare options to be used for find and replace. + /// + /// The `replaceOptions` property is a lazy, computed property that dynamically calculates + /// the compare options based on the values of `selectedMode` and `ignoreCase`. It is used + /// for controlling string replacement behavior for the find and replace functions. + /// + /// - Note: This property is implemented as a lazy property in the main class body because + /// extensions cannot contain stored properties directly. + lazy var replaceOptions: NSString.CompareOptions = { + var options: NSString.CompareOptions = [] + + if selectedMode.second == .RegularExpression { + options.insert(.regularExpression) + } + + if !caseSensitive { + options.insert(.caseInsensitive) + } + + return options + }() +} diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift index ad18a67a6c..6bb77179be 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift @@ -12,7 +12,6 @@ struct SourceControlSwitchView: View { private var dismiss @EnvironmentObject var sourceControlManager: SourceControlManager - @EnvironmentObject var workspace: WorkspaceDocument var branch: GitBranch diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 0cc075464a..ca1a2c3c66 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -38,16 +38,16 @@ class UtilityAreaViewModel: ObservableObject { // MARK: - State Restoration - func restoreFromState(_ workspace: WorkspaceDocument) { - isCollapsed = workspace.getFromWorkspaceState(.utilityAreaCollapsed) as? Bool ?? false - currentHeight = workspace.getFromWorkspaceState(.utilityAreaHeight) as? Double ?? 300.0 - isMaximized = workspace.getFromWorkspaceState(.utilityAreaMaximized) as? Bool ?? false + func restoreFromState(_ statePersistence: WorkspaceStatePersistence) { + isCollapsed = statePersistence.get(.utilityAreaCollapsed) as? Bool ?? false + currentHeight = statePersistence.get(.utilityAreaHeight) as? Double ?? 300.0 + isMaximized = statePersistence.get(.utilityAreaMaximized) as? Bool ?? false } - func saveRestorationState(_ workspace: WorkspaceDocument) { - workspace.addToWorkspaceState(key: .utilityAreaCollapsed, value: isCollapsed) - workspace.addToWorkspaceState(key: .utilityAreaHeight, value: currentHeight) - workspace.addToWorkspaceState(key: .utilityAreaMaximized, value: isMaximized) + func saveRestorationState(_ statePersistence: WorkspaceStatePersistence) { + statePersistence.set(key: .utilityAreaCollapsed, value: isCollapsed) + statePersistence.set(key: .utilityAreaHeight, value: currentHeight) + statePersistence.set(key: .utilityAreaMaximized, value: isMaximized) } func togglePanel(animation: Bool = true) { diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 11ca04eae5..84701d7dd7 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -112,7 +112,7 @@ struct WorkspaceView: View { .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { output in if let window = output.object as? NSWindow, self.window == window { - workspace.addToWorkspaceState( + workspace.statePersistence?.set( key: .workspaceWindowSize, value: NSStringFromRect(window.frame) ) diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 34209faea0..26a0dfde62 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -13,7 +13,7 @@ final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_bod private var directory: URL! private var files: [CEWorkspaceFile] = [] private var mockWorkspace: WorkspaceDocument! - private var searchState: WorkspaceDocument.SearchState! + private var searchState: SearchState! private var folder1File: CEWorkspaceFile? private var folder2File: CEWorkspaceFile? diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 61ddfb2bbd..0baed46114 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -12,7 +12,7 @@ final class FindTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] private var mockWorkspace: WorkspaceDocument! - private var searchState: WorkspaceDocument.SearchState! + private var searchState: SearchState! // MARK: - Setup /// A mock WorkspaceDocument is created @@ -137,7 +137,7 @@ final class FindTests: XCTestCase { XCTAssertEqual(searchState.getRegexPattern(query), "\\b@\\(test\\. !\\*#Query\\b") } - /// Tests the search functionality of the `WorkspaceDocument.SearchState` and `SearchIndexer`. + /// Tests the search functionality of the `SearchState` and `SearchIndexer`. func testSearch() async { await searchState.search("Ipsum") // Wait for the first search expectation to be fulfilled diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index e37039e0aa..016978b05b 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -12,7 +12,7 @@ final class WorkspaceDocumentIndexTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] private var mockWorkspace: WorkspaceDocument! - private var searchState: WorkspaceDocument.SearchState! + private var searchState: SearchState! private var folder1File: CEWorkspaceFile? private var folder2File: CEWorkspaceFile? From 2fc7054c4f3936057838be21d356c5e08c517c30 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 10:33:11 +0200 Subject: [PATCH 002/335] Refactor: Replace NSDocument architecture with plain Workspace model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove WorkspaceDocument (NSDocument) and CodeEditDocumentController (NSDocumentController) in favor of a plain Workspace model and WorkspaceWindowManager. The NSDocument pattern was a poor fit for a code editor — WorkspaceDocument's isDocumentEdited always returned false and its write method was a no-op, while EditorManager was already the de facto source of truth for open files. Key changes: - Introduce Workspace as a plain @MainActor ObservableObject - Add WorkspaceWindowManager registered in ServiceContainer to manage workspace lifecycle, window creation, and file routing - Add WorkspaceManaging, WorkspaceStatePersisting, and WorkspaceWindowManaging protocols for testability - Update all ~50 consumers from WorkspaceDocument to Workspace - Move unsaved-changes check into CodeEditWindowController.windowShouldClose - CodeFileDocument remains as NSDocument (it provides genuine value via file I/O, undo, autosave, and presentedItemDidChange) - Fix pre-existing bug in WindowCodeFileView (workspace: → searchState:) --- CodeEdit/AppDelegate.swift | 125 +++++---- CodeEdit/CodeEditApp.swift | 12 +- .../CEWorkspace/Models/CEWorkspaceFile.swift | 4 +- .../CodeFileDocument/CodeFileDocument.swift | 3 +- .../CodeEditDocumentController.swift | 181 ------------ .../CodeEditSplitViewController.swift | 6 +- .../CodeEditWindowController.swift | 19 +- .../CodeEditWindowControllerExtensions.swift | 4 +- .../Protocols/WorkspaceManaging.swift | 33 +++ .../Protocols/WorkspaceStatePersisting.swift | 14 + .../WorkspaceDocument/WorkspaceDocument.swift | 263 ------------------ .../WorkspaceStatePersistence.swift | 4 +- .../Views/EditorJumpBarComponent.swift | 2 +- .../EditorLayout+StateRestoration.swift | 4 +- .../TabBar/Tabs/Tab/EditorTabView.swift | 2 +- .../Editor/TabBar/Tabs/Views/EditorTabs.swift | 2 +- .../Views/EditorTabBarContextMenu.swift | 2 +- .../Editor/Views/WindowCodeFileView.swift | 2 +- .../FileInspector/FileInspectorView.swift | 2 +- .../HistoryInspectorView.swift | 2 +- .../Views/InspectorAreaView.swift | 2 +- .../Features/LSP/Service/LSPService.swift | 2 +- .../FindNavigatorListViewController.swift | 4 +- .../FindNavigatorResultList.swift | 2 +- .../FindNavigator/FindNavigatorView.swift | 4 +- .../OutlineView/StandardTableViewCell.swift | 2 +- .../OutlineView/ProjectNavigatorMenu.swift | 5 +- .../ProjectNavigatorOutlineView.swift | 11 +- .../ProjectNavigatorViewController.swift | 2 +- .../ProjectNavigatorToolbarBottom.swift | 2 +- .../SourceControlNavigatorChangesList.swift | 2 +- .../ChangedFile/GitChangedFileLabel.swift | 6 +- .../ChangedFile/GitChangedFileListView.swift | 2 +- .../SourceControlNavigatorToolbarBottom.swift | 2 +- .../Views/SourceControlNavigatorView.swift | 2 +- .../Views/NavigatorAreaView.swift | 4 +- .../OpenQuickly/Views/OpenQuicklyView.swift | 2 +- .../Features/Search/SearchState+Find.swift | 10 +- CodeEdit/Features/Search/SearchState.swift | 2 +- .../SourceControlGitView.swift | 13 +- .../Views/SourceControlFetchView.swift | 2 +- .../ToolbarItems/StartTaskToolbarItem.swift | 4 +- .../ToolbarItems/StopTaskToolbarItem.swift | 4 +- .../Tasks/Views/StartTaskToolbarButton.swift | 2 +- .../View/UtilityAreaOutputSourcePicker.swift | 2 +- .../UtilityAreaTerminalSidebar.swift | 2 +- .../UtilityAreaTerminalView.swift | 2 +- .../ViewModels/UtilityAreaViewModel.swift | 4 +- .../Features/Welcome/GitCloneButton.swift | 6 +- CodeEdit/Features/Welcome/NewFileButton.swift | 5 +- .../Welcome/OpenFileOrFolderButton.swift | 6 +- .../WindowCommands/FileCommands.swift | 6 +- .../Utils/RecentProjectsMenu.swift | 7 +- .../WindowControllerPropertyWrapper.swift | 1 + .../Features/Workspace/Models/Workspace.swift | 179 ++++++++++++ .../Models/WorkspaceNotificationModel.swift} | 2 +- .../Protocols/WorkspaceWindowManaging.swift | 18 ++ .../Services/WorkspaceWindowManager.swift | 230 +++++++++++++++ CodeEdit/Info.plist | 2 - .../Extensions/URL/URL+FindWorkspace.swift | 11 +- CodeEdit/WorkspaceView.swift | 2 +- .../Documents/DocumentsUnitTests.swift | 2 +- ...ment+SearchState+FindAndReplaceTests.swift | 8 +- ...kspaceDocument+SearchState+FindTests.swift | 8 +- ...spaceDocument+SearchState+IndexTests.swift | 10 +- .../LSP/LanguageServer+CodeFileDocument.swift | 8 +- 66 files changed, 677 insertions(+), 623 deletions(-) delete mode 100644 CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift create mode 100644 CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift create mode 100644 CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift delete mode 100644 CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift create mode 100644 CodeEdit/Features/Workspace/Models/Workspace.swift rename CodeEdit/Features/{Documents/WorkspaceDocument/WorkspaceDocument+Listeners.swift => Workspace/Models/WorkspaceNotificationModel.swift} (85%) create mode 100644 CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift create mode 100644 CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 124e7fb4b5..f0cbea6de8 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -19,12 +19,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { var openWindow @LazyService var lspService: LSPService + @LazyService var windowManager: WorkspaceWindowManager + + private var welcomeWindowObserver: NSObjectProtocol? func applicationDidFinishLaunching(_ notification: Notification) { enableWindowSizeSaveOnQuit() Settings.shared.preferences.general.appAppearance.applyAppearance() checkForFilesToOpen() + // Listen for requests to open the welcome window from non-SwiftUI contexts + welcomeWindowObserver = NotificationCenter.default.addObserver( + forName: .openWelcomeWindow, + object: nil, + queue: .main + ) { [weak self] _ in + self?.openWindow(sceneID: .welcome) + } + NSApp.closeWindow(.welcome, .about) DispatchQueue.main.async { @@ -42,12 +54,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { let path = CommandLine.arguments[index+1] let url = URL(fileURLWithPath: path) - CodeEditDocumentController.shared.reopenDocument( - for: url, - withContentsOf: url, - display: true - ) { document, _, _ in - document?.windowControllers.first?.synchronizeWindowTitleWithDocumentName() + do { + try self.windowManager.openWorkspace(at: url) + } catch { + self.logger.error("Failed to open workspace at \(path): \(error.localizedDescription)") } needToHandleOpen = false @@ -61,7 +71,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } func applicationWillTerminate(_ aNotification: Notification) { - + if let welcomeWindowObserver { + NotificationCenter.default.removeObserver(welcomeWindowObserver) + } } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -93,9 +105,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { openWindow(sceneID: .welcome) } case .openPanel: - CodeEditDocumentController.shared.openDocument(self) + windowManager.openDocumentFromPanel() case .newDocument: - CodeEditDocumentController.shared.newDocument(self) + windowManager.newDocumentFromPanel() } } @@ -107,18 +119,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { let line = file.count > 1 ? Int(file[1]) ?? 0 : 0 let column = file.count > 2 ? Int(file[2]) ?? 1 : 1 - CodeEditDocumentController.shared - .openDocument(withContentsOf: filePath, display: true) { document, _, error in - if let error { - NSAlert(error: error).runModal() - return - } - if line > 0, let document = document as? CodeFileDocument { - document.openOptions = CodeFileDocument.OpenOptions( - cursorPositions: [CursorPosition(line: line, column: column > 0 ? column : 1)] - ) - } + do { + if filePath.isFolder { + try windowManager.openWorkspace(at: filePath) + } else if !windowManager.openFileInWorkspace(url: filePath) { + // Standalone file — open via NSDocumentController (for CodeFileDocument) + NSDocumentController.shared + .openDocument(withContentsOf: filePath, display: true) { document, _, error in + if let error { + NSAlert(error: error).runModal() + return + } + if line > 0, let document = document as? CodeFileDocument { + document.openOptions = CodeFileDocument.OpenOptions( + cursorPositions: [CursorPosition(line: line, column: column > 0 ? column : 1)] + ) + } + } } + } catch { + NSAlert(error: error).runModal() + } } } @@ -127,32 +148,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { /// Defers the application terminate message until we've finished cleanup. /// /// All paths _must_ call `NSApplication.shared.reply(toApplicationShouldTerminate: true)` as soon as possible. - /// - /// The two things needing deferring are: - /// - Language server cancellation - /// - Outstanding document changes. - /// - /// Things that don't need deferring (happen immediately): - /// - Task termination. - /// These are called immediately if no documents need closing, and are called by - /// ``documentController(_:didCloseAll:contextInfo:)`` if there are documents we need to defer for. - /// - /// See ``terminateLanguageServers()`` and ``documentController(_:didCloseAll:contextInfo:)`` for deferring tasks. func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - let projects: [String] = CodeEditDocumentController.shared.documents - .compactMap { ($0 as? WorkspaceDocument)?.fileURL?.path } + let projects: [String] = windowManager.openWorkspaces + .compactMap { $0.fileURL?.path } UserDefaults.standard.set(projects, forKey: AppDelegate.recoverWorkspacesKey) - let areAllDocumentsClean = CodeEditDocumentController.shared.documents.allSatisfy { !$0.isDocumentEdited } - guard areAllDocumentsClean else { - CodeEditDocumentController.shared.closeAllDocuments( - withDelegate: self, - didCloseAllSelector: #selector(documentController(_:didCloseAll:contextInfo:)), - contextInfo: nil - ) - // `documentController(_:didCloseAll:contextInfo:)` will call `terminateLanguageServers()` - return .terminateLater + let hasUnsavedChanges = windowManager.openWorkspaces.contains { $0.hasUnsavedChanges() } + guard !hasUnsavedChanges else { + // Prompt the user to save unsaved changes across all workspaces + var allSaved = true + for workspace in windowManager.openWorkspaces { + if !workspace.promptSaveUnsavedFiles() { + allSaved = false + break + } + } + + if allSaved { + terminateTasks() + terminateLanguageServers() + } + // If not all saved (user cancelled), don't terminate + return allSaved ? .terminateLater : .terminateCancel } terminateTasks() @@ -223,12 +241,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { for filePath in files { let fileURL = URL(fileURLWithPath: String(filePath)) - CodeEditDocumentController.shared.reopenDocument( - for: fileURL, - withContentsOf: fileURL, - display: true - ) { document, _, _ in - document?.windowControllers.first?.synchronizeWindowTitleWithDocumentName() + do { + try windowManager.openWorkspace(at: fileURL) + } catch { + logger.error("Failed to open \(filePath): \(error.localizedDescription)") } } @@ -246,16 +262,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") } - // MARK: NSDocumentController delegate - - @objc - func documentController(_ docController: NSDocumentController, didCloseAll: Bool, contextInfo: Any) { - if didCloseAll { - terminateTasks() - terminateLanguageServers() - } - } - /// Terminates running language servers. Used during app termination to ensure resources are freed. private func terminateLanguageServers() { Task { @MainActor in @@ -295,8 +301,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { isLoading: true ) - let taskManagers = CodeEditDocumentController.shared.documents - .compactMap({ $0 as? WorkspaceDocument }) + let taskManagers = windowManager.openWorkspaces .compactMap({ $0.taskManager }) if taskManagers.reduce(0, { $0 + $1.activeTasks.count }) > 0 { diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 17de696d90..30885fbe33 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -21,8 +21,10 @@ struct CodeEditApp: App { ServiceContainer.register( LSPService() ) + ServiceContainer.register( + WorkspaceWindowManager() + ) - _ = CodeEditDocumentController.shared NSMenuItem.swizzle() NSSplitViewItem.swizzle() } @@ -37,8 +39,14 @@ struct CodeEditApp: App { OpenFileOrFolderButton(dismissWindow: dismissWindow) }, onDrop: { url, dismissWindow in + @Service var windowManager: WorkspaceWindowManager Task { - await CodeEditDocumentController.shared.openDocument(at: url, onCompletion: { dismissWindow() }) + do { + try windowManager.openWorkspace(at: url) + dismissWindow() + } catch { + print("Failed to open workspace: \(error)") + } } } ) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index ce3a4d7c94..32b6f0a337 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -266,11 +266,9 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor return true } - /// Loads the ``fileDocument`` property with a new ``CodeFileDocument`` and registers it with the shared - /// ``CodeEditDocumentController``. + /// Loads the ``fileDocument`` property with a new ``CodeFileDocument``. func loadCodeFile() throws { let codeFile = try CodeFileDocument(contentsOf: resolvedURL, ofType: contentType?.identifier ?? "") - CodeEditDocumentController.shared.addDocument(codeFile) self.fileDocument = codeFile } diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 5a6a3b3b46..e799e3c571 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -332,7 +332,8 @@ final class CodeFileDocument: NSDocument, ObservableObject { ) } - func findWorkspace() -> WorkspaceDocument? { + @MainActor + func findWorkspace() -> Workspace? { fileURL?.findWorkspace() } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift deleted file mode 100644 index e666f7668f..0000000000 --- a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// CodeEditDocumentController.swift -// CodeEdit -// -// Created by Pavel Kasila on 17.03.22. -// - -import Cocoa -import SwiftUI -import WelcomeWindow - -final class CodeEditDocumentController: NSDocumentController { - @Environment(\.openWindow) - private var openWindow - - @Service var lspService: LSPService - - private let fileManager = FileManager.default - - @MainActor - func createAndOpenNewDocument(onCompletion: @escaping () -> Void) { - guard let newDocumentUrl = self.newDocumentUrl else { return } - - let createdFile = self.fileManager.createFile( - atPath: newDocumentUrl.path, - contents: nil, - attributes: [FileAttributeKey.creationDate: Date()] - ) - - guard createdFile else { - print("Failed to create new document") - return - } - - self.openDocument(withContentsOf: newDocumentUrl, display: true) { _, _, _ in - onCompletion() - } - } - - override func newDocument(_ sender: Any?) { - guard let newDocumentUrl = self.newDocumentUrl else { return } - - let createdFile = self.fileManager.createFile( - atPath: newDocumentUrl.path, - contents: nil, - attributes: [FileAttributeKey.creationDate: Date()] - ) - guard createdFile else { - print("Failed to create new document") - return - } - - self.openDocument(withContentsOf: newDocumentUrl, display: true) { _, _, _ in } - } - - private var newDocumentUrl: URL? { - let panel = NSSavePanel() - guard panel.runModal() == .OK else { - return nil - } - - return panel.url - } - - override func openDocument(_ sender: Any?) { - self.openDocument(onCompletion: { document, documentWasAlreadyOpen in - // TODO: handle errors - - guard let document else { - print("Failed to unwrap document") - return - } - - print(document, documentWasAlreadyOpen) - }, onCancel: {}) - } - - override func openDocument( - withContentsOf url: URL, - display displayDocument: Bool, - completionHandler: @escaping (NSDocument?, Bool, Error?) -> Void - ) { - guard !openFileInExistingWorkspace(url: url) else { - return - } - - super.openDocument(withContentsOf: url, display: displayDocument) { document, documentWasAlreadyOpen, error in - MainActor.assumeIsolated { - if let document { - self.addDocument(document) - } else { - let errorMessage = error?.localizedDescription ?? "unknown error" - print("Unable to open document '\(url)': \(errorMessage)") - } - - RecentsStore.documentOpened(at: url) - completionHandler(document, documentWasAlreadyOpen, error) - } - } - } - - /// Attempt to open the file URL in an open workspace, finding the nearest workspace to open it in if possible. - /// - Parameter url: The file URL to open. - /// - Returns: True, if the document was opened in a workspace. - private func openFileInExistingWorkspace(url: URL) -> Bool { - guard !url.isFolder else { return false } - let workspaces = documents.compactMap({ $0 as? WorkspaceDocument }) - - // Check open workspaces for the file being opened. Sorted by shared components with the url so we - // open the nearest workspace possible. - for workspace in workspaces.sorted(by: { - ($0.fileURL?.sharedComponents(url) ?? 0) > ($1.fileURL?.sharedComponents(url) ?? 0) - }) { - // createIfNotFound will still return `nil` if the files don't share a common ancestor. - if let newFile = workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) { - workspace.editorManager?.openTab(item: newFile) - workspace.showWindows() - return true - } - } - return false - } - - override func removeDocument(_ document: NSDocument) { - super.removeDocument(document) - - if let workspace = document as? WorkspaceDocument, let path = workspace.fileURL?.absoluteURL.path() { - lspService.closeWorkspace(path) - } - - if CodeEditDocumentController.shared.documents.isEmpty { - switch Settings[\.general].reopenWindowAfterClose { - case .showWelcomeWindow: - // Opens the welcome window - openWindow(sceneID: .welcome) - case .quit: - // Quits CodeEdit - NSApplication.shared.terminate(nil) - case .doNothing: break - } - } - } -} - -extension NSDocumentController { - final func openDocument(onCompletion: @escaping (NSDocument?, Bool) -> Void, onCancel: @escaping () -> Void) { - let dialog = NSOpenPanel() - - dialog.title = "Open Workspace or File" - dialog.showsResizeIndicator = true - dialog.showsHiddenFiles = false - dialog.canChooseFiles = true - dialog.canChooseDirectories = true - - dialog.begin { result in - if result == NSApplication.ModalResponse.OK, let url = dialog.url { - self.openDocument(withContentsOf: url, display: true) { document, documentWasAlreadyOpen, error in - if let error { - NSAlert(error: error).runModal() - return - } - - guard let document else { - let alert = NSAlert() - alert.messageText = NSLocalizedString( - "Failed to get document", - comment: "Failed to get document" - ) - alert.runModal() - return - } - onCompletion(document, documentWasAlreadyOpen) - print("Document:", document) - print("Was already open?", documentWasAlreadyOpen) - } - } else if result == NSApplication.ModalResponse.cancel { - onCancel() - } - } - } -} diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index f3d543434b..0399371798 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -14,16 +14,16 @@ final class CodeEditSplitViewController: NSSplitViewController { static let snapWidth: CGFloat = 272 static let minSnapWidth: CGFloat = snapWidth - 10 - private weak var workspace: WorkspaceDocument? + private weak var workspace: Workspace? private weak var navigatorViewModel: NavigatorAreaViewModel? private weak var windowRef: NSWindow? - private weak var statePersistence: WorkspaceStatePersistence? + private weak var statePersistence: (any WorkspaceStatePersisting)? private unowned var hapticPerformer: NSHapticFeedbackPerformer // MARK: - Initialization init( - workspace: WorkspaceDocument, + workspace: Workspace, navigatorViewModel: NavigatorAreaViewModel, windowRef: NSWindow, hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index be20a708df..8ce6de29b8 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -24,7 +24,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs var observers: [NSKeyValueObservation] = [] - var workspace: WorkspaceDocument? + var workspace: Workspace? var workspaceSettingsWindow: NSWindow? var quickOpenPanel: SearchPanel? var commandPalettePanel: SearchPanel? @@ -38,7 +38,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs init( window: NSWindow?, - workspace: WorkspaceDocument? + workspace: Workspace? ) { super.init(window: window) window?.delegate = self @@ -88,7 +88,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs fatalError("init(coder:) has not been implemented") } - private func setupSplitView(with workspace: WorkspaceDocument) -> CodeEditSplitViewController? { + private func setupSplitView(with workspace: Workspace) -> CodeEditSplitViewController? { guard let window else { assertionFailure("No window found for this controller. Cannot set up content.") return nil @@ -209,6 +209,13 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } func windowShouldClose(_ sender: NSWindow) -> Bool { + // Check for unsaved changes before closing + if let workspace, workspace.hasUnsavedChanges() { + guard workspace.promptSaveUnsavedFiles() else { + return false // User cancelled + } + } + cancellables.forEach({ $0.cancel() }) cancellables.removeAll() @@ -223,6 +230,12 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs quickOpenPanel = nil commandPalettePanel = nil navigatorSidebarViewModel = nil + + // Notify the window manager to clean up workspace state + if let workspace { + @Service var windowManager: WorkspaceWindowManager + windowManager.closeWorkspace(workspace) + } workspace = nil return true } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index d8cb37450c..9f575a4c29 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -34,7 +34,7 @@ extension CodeEditWindowController { } // Listen to changes in all tabs/files - internal func listenToDocumentEdited(workspace: WorkspaceDocument) { + internal func listenToDocumentEdited(workspace: Workspace) { workspace.editorManager?.$activeEditor .flatMap({ editor in editor.$tabs @@ -70,7 +70,7 @@ extension CodeEditWindowController { } // Recalculate documentEdited by checking if any tab/file is edited - private func updateDocumentEdited(workspace: WorkspaceDocument) { + private func updateDocumentEdited(workspace: Workspace) { let hasEditedDocuments = !(workspace .editorManager? .editorLayout diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift new file mode 100644 index 0000000000..ef1cdad42d --- /dev/null +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -0,0 +1,33 @@ +// +// WorkspaceManaging.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 06.04.26. +// + +import Foundation + +/// Protocol defining the interface that workspace consumers depend on. +/// Enables testability via mock implementations and decouples views from the concrete Workspace type. +protocol WorkspaceManaging: AnyObject, ObservableObject { + var fileURL: URL? { get } + var displayName: String { get } + var workspaceFileManager: CEWorkspaceFileManager? { get } + var editorManager: EditorManager? { get } + var statusBarViewModel: StatusBarViewModel? { get } + var utilityAreaModel: UtilityAreaViewModel? { get } + var searchState: SearchState? { get } + var openQuicklyViewModel: OpenQuicklyViewModel? { get } + var commandsPaletteState: QuickActionsViewModel? { get } + var sourceControlManager: SourceControlManager? { get } + var taskManager: TaskManager? { get } + var workspaceSettingsManager: CEWorkspaceSettings? { get } + var statePersistence: WorkspaceStatePersistence? { get } + var listenerModel: WorkspaceNotificationModel { get } + var undoRegistration: UndoManagerRegistration { get } + var notificationPanel: NotificationPanelViewModel { get } + var taskNotificationHandler: TaskNotificationHandler { get } + var navigatorFilter: String { get set } + var sourceControlFilter: Bool { get set } + var sortFoldersOnTop: Bool { get set } +} diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift new file mode 100644 index 0000000000..d503ea3c21 --- /dev/null +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift @@ -0,0 +1,14 @@ +// +// WorkspaceStatePersisting.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 06.04.26. +// + +import Foundation + +/// Protocol for workspace state persistence, enabling mock implementations for testing. +protocol WorkspaceStatePersisting: AnyObject { + func get(_ key: WorkspaceStateKey) -> Any? + func set(key: WorkspaceStateKey, value: Any?) +} diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift deleted file mode 100644 index c7775f4592..0000000000 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// WorkspaceDocument.swift -// CodeEdit -// -// Created by Pavel Kasila on 17.03.22. -// - -import AppKit -import SwiftUI -import Foundation - -@objc(WorkspaceDocument) -final class WorkspaceDocument: NSDocument, ObservableObject { - @Published var sortFoldersOnTop: Bool = true - /// A string used to filter the displayed files and folders in the project navigator area based on user input. - @Published var navigatorFilter: String = "" - /// Whether the workspace only shows files with changes. - @Published var sourceControlFilter = false - - var workspaceFileManager: CEWorkspaceFileManager? - - var editorManager: EditorManager? = EditorManager() - var statusBarViewModel: StatusBarViewModel? = StatusBarViewModel() - var utilityAreaModel: UtilityAreaViewModel? = UtilityAreaViewModel() - var searchState: SearchState? - var openQuicklyViewModel: OpenQuicklyViewModel? - var commandsPaletteState: QuickActionsViewModel? - var listenerModel: WorkspaceNotificationModel = .init() - var sourceControlManager: SourceControlManager? - - var taskManager: TaskManager? - var workspaceSettingsManager: CEWorkspaceSettings? - var taskNotificationHandler: TaskNotificationHandler = TaskNotificationHandler() - - var statePersistence: WorkspaceStatePersistence? - - var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() - - var notificationPanel = NotificationPanelViewModel() - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: NSDocument - - private let ignoredFilesAndDirectory = [ - ".DS_Store" - ] - - override static var autosavesInPlace: Bool { - false - } - - override var isDocumentEdited: Bool { - false - } - - override func makeWindowControllers() { - let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, - defer: false - ) - // Note For anyone hoping to switch back to a Root-SwiftUI window: - // See Commit 0200c87 for more details and to see what was previously here. - // ----- - // Setting the "min size" like this is hacky, but SwiftUI overrides the contentRect and - // any of the built-in window size functions & autosave stuff. So we have to set it like this. - // SwiftUI also ignores this value, so it just manages to set the initial window size. *Hopefully* this - // is fixed in the future. - // ---- - let windowController = CodeEditWindowController( - window: window, - workspace: self - ) - - if let rectString = statePersistence?.get(.workspaceWindowSize) as? String { - window.setFrame(NSRectFromString(rectString), display: true, animate: false) - } else { - window.setFrame(NSRect(x: 0, y: 0, width: 1400, height: 900), display: true, animate: false) - window.center() - } - - window.setAccessibilityIdentifier("workspace") - window.setAccessibilityDocument(self.fileURL?.absoluteString) - - self.addWindowController(windowController) - notificationPanel.windowController = windowController - - window.makeKeyAndOrderFront(nil) - } - - // MARK: Set Up Workspace - - private func initWorkspaceState(_ url: URL) throws { - // Ensure the URL ends with a "/" to prevent certain URL(filePath:relativeTo) initializers from - // placing the file one directory above our workspace. This quick fix appends a "/" if needed. - var url = url - if !url.absoluteString.hasSuffix("/") { - url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") - } - - self.fileURL = url - self.displayName = url.lastPathComponent - self.statePersistence = WorkspaceStatePersistence(workspaceURL: url) - - let sourceControlManager = SourceControlManager( - workspaceURL: url, - editorManager: editorManager! - ) - - self.workspaceFileManager = .init( - folderUrl: url, - ignoredFilesAndFolders: Set(ignoredFilesAndDirectory), - sourceControlManager: sourceControlManager - ) - self.sourceControlManager = sourceControlManager - sourceControlManager.fileManager = workspaceFileManager - self.searchState = SearchState(workspaceURL: url) - self.openQuicklyViewModel = .init(fileURL: url) - self.commandsPaletteState = .init() - self.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) - if let workspaceSettingsManager { - self.taskManager = TaskManager( - workspaceSettings: workspaceSettingsManager.settings, - workspaceURL: url - ) - } - self.taskNotificationHandler.workspaceURL = url - - workspaceFileManager?.addObserver(undoRegistration) - if let statePersistence { - editorManager?.restoreFromState( - statePersistence: statePersistence, - fileManager: workspaceFileManager, - searchState: searchState - ) - utilityAreaModel?.restoreFromState(statePersistence) - } - } - - override func read(from url: URL, ofType typeName: String) throws { - try initWorkspaceState(url) - } - - override func write(to url: URL, ofType typeName: String) throws {} - - // MARK: Close Workspace - - override func close() { - super.close() - if let statePersistence { - editorManager?.saveRestorationState(statePersistence) - utilityAreaModel?.saveRestorationState(statePersistence) - } - - statusBarViewModel = nil - utilityAreaModel = nil - searchState = nil - editorManager = nil - openQuicklyViewModel = nil - commandsPaletteState = nil - sourceControlManager = nil - workspaceFileManager?.cleanUp() - workspaceFileManager = nil - workspaceSettingsManager?.cleanUp() - workspaceSettingsManager = nil - taskManager = nil - statePersistence = nil - } - - /// Determines the windows should be closed. - /// - /// This method iterates all edited documents If there are any edited documents. - /// - /// A panel giving the user the choice of canceling, discarding changes, or saving is presented while iteration. - /// - /// If the user chooses cancel on the panel, iteration is broken. - /// - /// In the last step, `shouldCloseSelector` is called with true if all documents are clean, otherwise false - /// - /// - Parameters: - /// - windowController: The windowController may be closed. - /// - delegate: The object which is a target of `shouldCloseSelector`. - /// - shouldClose: The callback which receives result of this method. - /// - contextInfo: The additional info which is not used in this method. - override func shouldCloseWindowController( - _ windowController: NSWindowController, - delegate: Any?, - shouldClose shouldCloseSelector: Selector?, - contextInfo: UnsafeMutableRawPointer? - ) { - guard let object = (delegate as? NSObject), - let shouldCloseSelector = shouldCloseSelector, - let contextInfo = contextInfo - else { - super.shouldCloseWindowController( - windowController, - delegate: delegate, - shouldClose: shouldCloseSelector, - contextInfo: contextInfo - ) - return - } - // Save unsaved changes before closing - let editedCodeFiles = editorManager?.editorLayout - .gatherOpenFiles() - .compactMap(\.fileDocument) - .filter(\.isDocumentEdited) ?? [] - - for editedCodeFile in editedCodeFiles { - let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) - shouldClose.initialize(to: true) - defer { - _ = shouldClose.move() - shouldClose.deallocate() - } - // Present a panel giving the user the choice of canceling, discarding changes, or saving. - editedCodeFile.canClose( - withDelegate: self, - shouldClose: #selector(document(_:shouldClose:contextInfo:)), - contextInfo: shouldClose - ) - // pointee becomes false when user select cancel - guard shouldClose.pointee else { - break - } - } - // Invoke shouldCloseSelector at delegate - let implementation = object.method(for: shouldCloseSelector) - let function = unsafeBitCast( - implementation, - to: (@convention(c)(Any, Selector, Any, Bool, UnsafeMutableRawPointer?) -> Void).self - ) - let areAllOpenedCodeFilesClean = editorManager?.editorLayout.gatherOpenFiles() - .compactMap(\.fileDocument) - .allSatisfy { !$0.isDocumentEdited } ?? false - function(object, shouldCloseSelector, self, areAllOpenedCodeFilesClean, contextInfo) - } - - // MARK: NSDocument delegate - - /// Receives result of `canClose` and then, set `shouldClose` to `contextInfo`'s `pointee`. - /// - /// - Parameters: - /// - document: The document may be closed. - /// - shouldClose: The result of user selection. - /// `shouldClose` becomes false if the user selects cancel, otherwise true. - /// - contextInfo: The additional info which will be set `shouldClose`. - /// `contextInfo` must be `UnsafeMutablePointer`. - @objc - func document( - _ document: NSDocument, - shouldClose: Bool, - contextInfo: UnsafeMutableRawPointer - ) { - let opaquePtr = OpaquePointer(contextInfo) - let mutablePointer = UnsafeMutablePointer(opaquePtr) - mutablePointer.pointee = shouldClose - } -} diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift index c6731a95a4..6defadc155 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift @@ -8,8 +8,8 @@ import Foundation /// A standalone service for persisting workspace-specific UI state (window size, collapsed panels, etc.) -/// via UserDefaults. Extracted from WorkspaceDocument to enable independent injection and testing. -final class WorkspaceStatePersistence: ObservableObject { +/// via UserDefaults. Extracted from Workspace to enable independent injection and testing. +final class WorkspaceStatePersistence: ObservableObject, WorkspaceStatePersisting { private let workspaceURL: URL private var workspaceState: [String: Any] { diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift index c4f4ab0aaf..6f3acfd85c 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift @@ -20,7 +20,7 @@ struct EditorJumpBarComponent: View { @Environment(\.controlActiveState) private var activeState - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @State var position: NSPoint? @State var selection: CEWorkspaceFile diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 0cc3cf2f60..43b18e64c3 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -16,7 +16,7 @@ extension EditorManager { /// - fileManager: The file manager to resolve file references. /// - searchState: The search state for editor instances. func restoreFromState( - statePersistence: WorkspaceStatePersistence, + statePersistence: any WorkspaceStatePersisting, fileManager: CEWorkspaceFileManager?, searchState: SearchState? ) { @@ -137,7 +137,7 @@ extension EditorManager { } } - func saveRestorationState(_ statePersistence: WorkspaceStatePersistence) { + func saveRestorationState(_ statePersistence: any WorkspaceStatePersisting) { if let data = try? JSONEncoder().encode( EditorRestorationState(activeEditor: activeEditor.id, groups: editorLayout) ) { diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 0c5112789c..f93d105524 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -21,7 +21,7 @@ struct EditorTabView: View { @Environment(\.isFullscreen) private var isFullscreen - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager @StateObject private var fileObserver: EditorTabFileObserver diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift index c2287c144e..bc2ab11c4d 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift @@ -20,7 +20,7 @@ struct EditorTabs: View { private var colorScheme /// The workspace document. - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editor: Editor diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index d5db182aa5..f2c7f7b4de 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -23,7 +23,7 @@ struct EditorTabBarContextMenu: ViewModifier { self.isTemporary = isTemporary } - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @EnvironmentObject var tabs: Editor diff --git a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift index d53d1682f8..c3f4b749be 100644 --- a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift @@ -18,7 +18,7 @@ struct WindowCodeFileView: View { init(codeFile: CodeFileDocument) { self._editorInstance = .init( wrappedValue: EditorInstance( - workspace: nil, + searchState: nil, file: CEWorkspaceFile(url: codeFile.fileURL ?? URL(fileURLWithPath: "")) ) ) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 8a00ccb4b4..efa9532cd0 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -8,7 +8,7 @@ import SwiftUI import CodeEditLanguages struct FileInspectorView: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 9977683cab..ec40a52928 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -10,7 +10,7 @@ struct HistoryInspectorView: View { @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) var showMergeCommitsPerFileLog - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index 61822a3b60..923ace3fc6 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -8,7 +8,7 @@ import SwiftUI struct InspectorAreaView: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager @ObservedObject private var extensionManager = ExtensionManager.shared @ObservedObject public var viewModel: InspectorAreaViewModel diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 373b951660..d4d7df7aab 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -258,7 +258,7 @@ final class LSPService: ObservableObject { /// Close all language clients for a workspace. /// - /// This is intentionally synchronous so we can exit from the workspace document's ``WorkspaceDocument/close()`` + /// This is intentionally synchronous so we can exit from the workspace document's ``Workspace/close()`` /// method ASAP. /// /// Errors thrown in this method are logged and otherwise not handled. diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index dfec82a1b0..78648183e8 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -9,7 +9,7 @@ import SwiftUI final class FindNavigatorListViewController: NSViewController { - public var workspace: WorkspaceDocument + public var workspace: Workspace public var selectedItem: Any? private var searchItems: [SearchResultModel] = [] @@ -44,7 +44,7 @@ final class FindNavigatorListViewController: NSViewController { self.scrollView.contentView.contentInsets = .init(top: 0, left: 0, bottom: 0, right: 0) } - init(workspace: WorkspaceDocument) { + init(workspace: Workspace) { self.workspace = workspace super.init(nibName: nil, bundle: nil) } diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift index 9dd5ec2567..f13defbc17 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift @@ -10,7 +10,7 @@ import Combine struct FindNavigatorResultList: NSViewControllerRepresentable { - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @AppSettings(\.general.projectNavigatorSize) var projectNavigatorSize diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift index 1db46c42f7..423bbaaf95 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift @@ -8,10 +8,10 @@ import SwiftUI struct FindNavigatorView: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace private var state: SearchState { - // SearchState is always initialized in WorkspaceDocument.initWorkspaceState + // SearchState is always initialized in Workspace.initWorkspaceState // before any views are created, so this is safe to force unwrap. workspace.searchState! } diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift index 3f1becd8d4..bae2778516 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift @@ -10,7 +10,7 @@ import SwiftUI class StandardTableViewCell: NSTableCellView { weak var secondaryLabel: NSTextField? - weak var workspace: WorkspaceDocument? + weak var workspace: Workspace? var secondaryLabelRightAligned: Bool = true { didSet { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 8fd37b7db1..e78029cc61 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -9,13 +9,14 @@ import SwiftUI import UniformTypeIdentifiers /// A subclass of `NSMenu` implementing the contextual menu for the project navigator +@MainActor final class ProjectNavigatorMenu: NSMenu { /// The item to show the contextual menu for var item: CEWorkspaceFile? /// The workspace, for opening the item - var workspace: WorkspaceDocument? + var workspace: Workspace? /// The `ProjectNavigatorViewController` is being called from. /// By sending it, we can access it's variables and functions. @@ -46,7 +47,7 @@ final class ProjectNavigatorMenu: NSMenu { /// Configures the menu based on the current selection in the outline view. /// - Menu items get added depending on the amount of selected items. - private func setupMenu() { // swiftlint:disable:this function_body_length + @MainActor private func setupMenu() { // swiftlint:disable:this function_body_length guard let item else { return } let showInFinder = menuItem("Show in Finder", action: #selector(showInFinder)) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index a072d80c27..ffdbc28695 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -11,7 +11,7 @@ import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @EnvironmentObject var editorManager: EditorManager @StateObject var prefs: Settings = .shared @@ -45,9 +45,11 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { Coordinator(workspace) } + @MainActor class Coordinator: NSObject, CEWorkspaceFileManagerObserver { - init(_ workspace: WorkspaceDocument) { + init(_ workspace: Workspace) { self.workspace = workspace + self.fileManager = workspace.workspaceFileManager super.init() workspace.listenerModel.$highlightedFileItem @@ -78,7 +80,8 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } var cancellables: Set = [] - weak var workspace: WorkspaceDocument? + weak var workspace: Workspace? + weak var fileManager: CEWorkspaceFileManager? weak var controller: ProjectNavigatorViewController? func fileManagerUpdated(updatedItems: Set) { @@ -105,7 +108,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } deinit { - workspace?.workspaceFileManager?.removeObserver(self) + fileManager?.removeObserver(self) } } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index f681705351..282da19b5e 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -35,7 +35,7 @@ final class ProjectNavigatorViewController: NSViewController { var filteredContentChildren: [CEWorkspaceFile: [CEWorkspaceFile]] = [:] var expandedItems: Set = [] - weak var workspace: WorkspaceDocument? + weak var workspace: Workspace? weak var editor: Editor? var iconColor: SettingsData.FileIconStyle = .color { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index bb1e44fb8e..94c0c34f48 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -14,7 +14,7 @@ struct ProjectNavigatorToolbarBottom: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @EnvironmentObject var editorManager: EditorManager @State var recentsFilter: Bool = false diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 5aed2e3ced..b2afda4b79 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -9,7 +9,7 @@ import AppKit import SwiftUI struct SourceControlNavigatorChangesList: View { - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace @EnvironmentObject var sourceControlManager: SourceControlManager @State var selection = Set() diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index 02a8769b7f..a8727f7b64 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -8,7 +8,7 @@ import SwiftUI struct GitChangedFileLabel: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var sourceControlManager: SourceControlManager let file: GitChangedFile @@ -39,7 +39,7 @@ struct GitChangedFileLabel: View { originalFilename: nil )) .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init())) - .environmentObject(WorkspaceDocument()) + .environmentObject(Workspace()) GitChangedFileLabel(file: GitChangedFile( status: .none, @@ -48,6 +48,6 @@ struct GitChangedFileLabel: View { originalFilename: "app2.jsx" )) .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init())) - .environmentObject(WorkspaceDocument()) + .environmentObject(Workspace()) }.padding() } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index 989e43818f..bea8d285ae 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -11,7 +11,7 @@ import SwiftUI struct GitChangedFileListView: View { @AppSettings(\.general.fileIconStyle) private var fileIconStyle - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var sourceControlManager: SourceControlManager @Binding private var changedFile: GitChangedFile diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift index c2c4d60dd9..11c601c66d 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift @@ -8,7 +8,7 @@ import SwiftUI struct SourceControlNavigatorToolbarBottom: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject var sourceControlManager: SourceControlManager @State private var text = "" diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 31363fc913..f9ae981b36 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SourceControlNavigatorView: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @AppSettings(\.sourceControl.general.fetchRefreshServerStatus) var fetchRefreshServerStatus diff --git a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift b/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift index a3fdabf65c..85321b60cd 100644 --- a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift +++ b/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift @@ -8,14 +8,14 @@ import SwiftUI struct NavigatorAreaView: View { - @ObservedObject private var workspace: WorkspaceDocument + @ObservedObject private var workspace: Workspace @ObservedObject private var extensionManager = ExtensionManager.shared @ObservedObject public var viewModel: NavigatorAreaViewModel @AppSettings(\.general.navigatorTabBarPosition) var sidebarPosition: SettingsData.SidebarTabBarPosition - init(workspace: WorkspaceDocument, viewModel: NavigatorAreaViewModel) { + init(workspace: Workspace, viewModel: NavigatorAreaViewModel) { self.workspace = workspace self.viewModel = viewModel diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift index 4525a50835..32b2482313 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift @@ -8,7 +8,7 @@ import SwiftUI struct OpenQuicklyView: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace private let onClose: () -> Void private let openFile: (CEWorkspaceFile) -> Void diff --git a/CodeEdit/Features/Search/SearchState+Find.swift b/CodeEdit/Features/Search/SearchState+Find.swift index 6c5cd25e06..41f1ec894b 100644 --- a/CodeEdit/Features/Search/SearchState+Find.swift +++ b/CodeEdit/Features/Search/SearchState+Find.swift @@ -50,7 +50,7 @@ extension SearchState { /// - Returns: A string representing the regular expression pattern based on the selected search mode. /// /// - Note: This function is creating similar patterns to the - /// ``WorkspaceDocument/SearchState-swift.class/getSearchTerm(_:)`` function, + /// ``Workspace/SearchState-swift.class/getSearchTerm(_:)`` function, /// Except its using the word boundary anchor(\b) instead of the asterisk(\*). /// This is needed to highlight the search results correctly. func getRegexPattern(_ query: String) -> String { @@ -75,14 +75,14 @@ extension SearchState { } /// Searches the entire workspace for the given string, using the - /// ``WorkspaceDocument/SearchState-swift.class/selectedMode`` modifiers + /// ``Workspace/SearchState-swift.class/selectedMode`` modifiers /// to modify the search if needed. This is done by filtering out files with SearchKit and then searching /// within each file for the given string. /// /// This method will update - /// ``WorkspaceDocument/SearchState-swift.class/searchResult``, - /// ``WorkspaceDocument/SearchState-swift.class/searchResultsFileCount`` - /// and ``WorkspaceDocument/SearchState-swift.class/searchResultCount`` with any matched + /// ``Workspace/SearchState-swift.class/searchResult``, + /// ``Workspace/SearchState-swift.class/searchResultsFileCount`` + /// and ``Workspace/SearchState-swift.class/searchResultCount`` with any matched /// search results. See ``SearchResultModel`` and ``SearchResultMatchModel`` /// for more information on search results and matches. /// diff --git a/CodeEdit/Features/Search/SearchState.swift b/CodeEdit/Features/Search/SearchState.swift index 5ee412f2ab..bed26d7ec5 100644 --- a/CodeEdit/Features/Search/SearchState.swift +++ b/CodeEdit/Features/Search/SearchState.swift @@ -8,7 +8,7 @@ import Foundation /// Manages the search/find state for a workspace, including indexing, search results, -/// and find-and-replace operations. Extracted from WorkspaceDocument to be independently +/// and find-and-replace operations. Extracted from Workspace to be independently /// injectable and testable. final class SearchState: ObservableObject { enum IndexStatus: Equatable { diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index 7c9e9e70b7..6c3944ddbe 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -202,14 +202,8 @@ private extension SourceControlGitView { 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)") - } - } + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocument(at: fileURL, onCompletion: {}) } private func openGitIgnoreFile() { @@ -223,7 +217,8 @@ private extension SourceControlGitView { } // Open the file in the editor - try await NSDocumentController.shared.openDocument(withContentsOf: fileURL, display: true) + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocument(at: fileURL, onCompletion: {}) } catch { print("Failed to open document: \(error.localizedDescription)") } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift index 2a43dd8c11..0867d9bd7e 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift @@ -12,7 +12,7 @@ struct SourceControlFetchView: View { private var dismiss @EnvironmentObject var sourceControlManager: SourceControlManager - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace var projectName: String { workspace.workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" diff --git a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift b/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift index 2d2b5d6e1b..a46919e0ec 100644 --- a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift +++ b/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift @@ -9,13 +9,13 @@ import AppKit @available(macOS 26, *) final class StartTaskToolbarItem: NSToolbarItem { - private weak var workspace: WorkspaceDocument? + private weak var workspace: Workspace? private var utilityAreaCollapsed: Bool { workspace?.utilityAreaModel?.isCollapsed ?? true } - init(workspace: WorkspaceDocument) { + init(workspace: Workspace) { self.workspace = workspace super.init(itemIdentifier: NSToolbarItem.Identifier("StartTaskToolbarItem")) diff --git a/CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift b/CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift index eaa5148439..e3e6b6d63b 100644 --- a/CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift +++ b/CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift @@ -10,7 +10,7 @@ import Combine @available(macOS 26, *) final class StopTaskToolbarItem: NSToolbarItem { - private weak var workspace: WorkspaceDocument? + private weak var workspace: Workspace? private var taskManager: TaskManager? { workspace?.taskManager @@ -21,7 +21,7 @@ final class StopTaskToolbarItem: NSToolbarItem { private var statusListener: AnyCancellable? private var otherListeners: Set = [] - init?(workspace: WorkspaceDocument) { + init?(workspace: Workspace) { guard let taskManager = workspace.taskManager else { return nil } self.workspace = workspace diff --git a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift index 01bbb97498..2a31b3cc90 100644 --- a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift +++ b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift @@ -12,7 +12,7 @@ struct StartTaskToolbarButton: View { private var activeState @ObservedObject var taskManager: TaskManager - @EnvironmentObject var workspace: WorkspaceDocument + @EnvironmentObject var workspace: Workspace var utilityAreaCollapsed: Bool { workspace.utilityAreaModel?.isCollapsed ?? true diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index 4d65d39d80..70bc4cf39a 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -10,7 +10,7 @@ import SwiftUI struct UtilityAreaOutputSourcePicker: View { typealias Sources = UtilityAreaOutputView.Sources - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @AppSettings(\.developerSettings.showInternalDevelopmentInspector) var showInternalDevelopmentInspector diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index 1a9fb53ffe..8f7b45d755 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -10,7 +10,7 @@ import SwiftUI /// The view that displays the list of available terminals in the utility area. /// See ``UtilityAreaTerminalView`` for use. struct UtilityAreaTerminalSidebar: View { - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel var body: some View { diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 9e6d047ca3..8536aad15e 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -25,7 +25,7 @@ struct UtilityAreaTerminalView: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index ca1a2c3c66..6806ca11ac 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -38,13 +38,13 @@ class UtilityAreaViewModel: ObservableObject { // MARK: - State Restoration - func restoreFromState(_ statePersistence: WorkspaceStatePersistence) { + func restoreFromState(_ statePersistence: any WorkspaceStatePersisting) { isCollapsed = statePersistence.get(.utilityAreaCollapsed) as? Bool ?? false currentHeight = statePersistence.get(.utilityAreaHeight) as? Double ?? 300.0 isMaximized = statePersistence.get(.utilityAreaMaximized) as? Bool ?? false } - func saveRestorationState(_ statePersistence: WorkspaceStatePersistence) { + func saveRestorationState(_ statePersistence: any WorkspaceStatePersisting) { statePersistence.set(key: .utilityAreaCollapsed, value: isCollapsed) statePersistence.set(key: .utilityAreaHeight, value: currentHeight) statePersistence.set(key: .utilityAreaMaximized, value: isMaximized) diff --git a/CodeEdit/Features/Welcome/GitCloneButton.swift b/CodeEdit/Features/Welcome/GitCloneButton.swift index 08293aafcb..3565803e07 100644 --- a/CodeEdit/Features/Welcome/GitCloneButton.swift +++ b/CodeEdit/Features/Welcome/GitCloneButton.swift @@ -29,7 +29,8 @@ struct GitCloneButton: View { showCheckoutBranchItem = url }, openDocument: { url in - CodeEditDocumentController.shared.openDocument(at: url, onCompletion: { dismissWindow() }) + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocument(at: url, onCompletion: { dismissWindow() }) } ) } @@ -37,7 +38,8 @@ struct GitCloneButton: View { GitCheckoutBranchView( repoLocalPath: url, openDocument: { url in - CodeEditDocumentController.shared.openDocument(at: url, onCompletion: { dismissWindow() }) + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocument(at: url, onCompletion: { dismissWindow() }) } ) } diff --git a/CodeEdit/Features/Welcome/NewFileButton.swift b/CodeEdit/Features/Welcome/NewFileButton.swift index 75261faee5..00ceed5227 100644 --- a/CodeEdit/Features/Welcome/NewFileButton.swift +++ b/CodeEdit/Features/Welcome/NewFileButton.swift @@ -17,8 +17,9 @@ struct NewFileButton: View { iconName: "plus.square", title: "Create New File...", action: { - let documentController = CodeEditDocumentController() - documentController.createAndOpenNewDocument(onCompletion: { dismissWindow() }) + @Service var windowManager: WorkspaceWindowManager + windowManager.newDocumentFromPanel() + dismissWindow() } ) } diff --git a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift index 78a8b9467e..8814983038 100644 --- a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift +++ b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift @@ -20,8 +20,10 @@ struct OpenFileOrFolderButton: View { iconName: "folder", title: "Open File or Folder...", action: { - CodeEditDocumentController.shared.openDocumentWithDialog( - configuration: .init(canChooseFiles: true, canChooseDirectories: true), + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocumentWithDialog( + canChooseFiles: true, + canChooseDirectories: true, onDialogPresented: { dismissWindow() }, onCancel: { openWindow(id: DefaultSceneID.welcome) } ) diff --git a/CodeEdit/Features/WindowCommands/FileCommands.swift b/CodeEdit/Features/WindowCommands/FileCommands.swift index 130ce0e5b1..8be55ca358 100644 --- a/CodeEdit/Features/WindowCommands/FileCommands.swift +++ b/CodeEdit/Features/WindowCommands/FileCommands.swift @@ -21,12 +21,14 @@ struct FileCommands: Commands { CommandGroup(replacing: .newItem) { Group { Button("New") { - NSDocumentController.shared.newDocument(nil) + @Service var windowManager: WorkspaceWindowManager + windowManager.newDocumentFromPanel() } .keyboardShortcut("n") Button("Open...") { - NSDocumentController.shared.openDocument(nil) + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocumentFromPanel() } .keyboardShortcut("o") diff --git a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift index 67ad05cc80..d2fec045c2 100644 --- a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift +++ b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift @@ -126,11 +126,8 @@ final class RecentProjectsMenu: NSObject, NSMenuDelegate { @objc private func recentProjectItemClicked(_ sender: NSMenuItem) { guard let projectURL = sender.representedObject as? URL else { return } - CodeEditDocumentController.shared.openDocument( - withContentsOf: projectURL, - display: true, - completionHandler: { _, _, _ in } - ) + @Service var windowManager: WorkspaceWindowManager + windowManager.openDocument(at: projectURL, onCompletion: {}) } @objc diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index ecd717e111..701cdc0e16 100644 --- a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -33,6 +33,7 @@ struct UpdatingWindowController: DynamicProperty { box.controller } + @MainActor class WindowControllerBox: ObservableObject { public private(set) weak var controller: CodeEditWindowController? diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift new file mode 100644 index 0000000000..6c81d6831a --- /dev/null +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -0,0 +1,179 @@ +// +// Workspace.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 06.04.26. +// + +import AppKit +import SwiftUI +import Foundation + +/// A plain model representing an open workspace (folder). +/// Replaces `WorkspaceDocument` (NSDocument) with no framework coupling. +@MainActor +final class Workspace: ObservableObject, WorkspaceManaging { + @Published var sortFoldersOnTop: Bool = true + @Published var navigatorFilter: String = "" + @Published var sourceControlFilter = false + + private(set) var fileURL: URL? + private(set) var displayName: String = "" + + var workspaceFileManager: CEWorkspaceFileManager? + var editorManager: EditorManager? = EditorManager() + var statusBarViewModel: StatusBarViewModel? = StatusBarViewModel() + var utilityAreaModel: UtilityAreaViewModel? = UtilityAreaViewModel() + var searchState: SearchState? + var openQuicklyViewModel: OpenQuicklyViewModel? + var commandsPaletteState: QuickActionsViewModel? + var listenerModel: WorkspaceNotificationModel = .init() + var sourceControlManager: SourceControlManager? + + var taskManager: TaskManager? + var workspaceSettingsManager: CEWorkspaceSettings? + var taskNotificationHandler: TaskNotificationHandler = TaskNotificationHandler() + + var statePersistence: WorkspaceStatePersistence? + + var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() + + var notificationPanel = NotificationPanelViewModel() + + private let ignoredFilesAndDirectory = [ + ".DS_Store" + ] + + // MARK: - Initialization + + init(url: URL) throws { + try initWorkspaceState(url) + } + + /// Minimal initializer for testing. Does not set up workspace state. + internal init() {} + + private func initWorkspaceState(_ url: URL) throws { + var url = url + if !url.absoluteString.hasSuffix("/") { + url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") + } + + self.fileURL = url + self.displayName = url.lastPathComponent + self.statePersistence = WorkspaceStatePersistence(workspaceURL: url) + + let sourceControlManager = SourceControlManager( + workspaceURL: url, + editorManager: editorManager! + ) + + self.workspaceFileManager = .init( + folderUrl: url, + ignoredFilesAndFolders: Set(ignoredFilesAndDirectory), + sourceControlManager: sourceControlManager + ) + self.sourceControlManager = sourceControlManager + sourceControlManager.fileManager = workspaceFileManager + self.searchState = SearchState(workspaceURL: url) + self.openQuicklyViewModel = .init(fileURL: url) + self.commandsPaletteState = .init() + self.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) + if let workspaceSettingsManager { + self.taskManager = TaskManager( + workspaceSettings: workspaceSettingsManager.settings, + workspaceURL: url + ) + } + self.taskNotificationHandler.workspaceURL = url + + workspaceFileManager?.addObserver(undoRegistration) + if let statePersistence { + editorManager?.restoreFromState( + statePersistence: statePersistence, + fileManager: workspaceFileManager, + searchState: searchState + ) + utilityAreaModel?.restoreFromState(statePersistence) + } + } + + // MARK: - Tear Down + + func tearDown() { + if let statePersistence { + editorManager?.saveRestorationState(statePersistence) + utilityAreaModel?.saveRestorationState(statePersistence) + } + + statusBarViewModel = nil + utilityAreaModel = nil + searchState = nil + editorManager = nil + openQuicklyViewModel = nil + commandsPaletteState = nil + sourceControlManager = nil + workspaceFileManager?.cleanUp() + workspaceFileManager = nil + workspaceSettingsManager?.cleanUp() + workspaceSettingsManager = nil + taskManager = nil + statePersistence = nil + } + + // MARK: - Unsaved Changes + + func hasUnsavedChanges() -> Bool { + let editedFiles = editorManager?.editorLayout + .gatherOpenFiles() + .compactMap(\.fileDocument) + .filter(\.isDocumentEdited) ?? [] + return !editedFiles.isEmpty + } + + /// Prompts the user to save any unsaved files before closing. + /// Returns `true` if all files are clean and the workspace can close, `false` if the user cancelled. + func promptSaveUnsavedFiles() -> Bool { + let editedCodeFiles = editorManager?.editorLayout + .gatherOpenFiles() + .compactMap(\.fileDocument) + .filter(\.isDocumentEdited) ?? [] + + for editedCodeFile in editedCodeFiles { + let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) + shouldClose.initialize(to: true) + defer { + _ = shouldClose.move() + shouldClose.deallocate() + } + editedCodeFile.canClose( + withDelegate: self, + shouldClose: #selector(document(_:shouldClose:contextInfo:)), + contextInfo: shouldClose + ) + guard shouldClose.pointee else { + return false + } + } + + let areAllClean = editorManager?.editorLayout.gatherOpenFiles() + .compactMap(\.fileDocument) + .allSatisfy { !$0.isDocumentEdited } ?? true + return areAllClean + } + + @objc + func document( + _ document: NSDocument, + shouldClose: Bool, + contextInfo: UnsafeMutableRawPointer + ) { + let opaquePtr = OpaquePointer(contextInfo) + let mutablePointer = UnsafeMutablePointer(opaquePtr) + mutablePointer.pointee = shouldClose + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Listeners.swift b/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift similarity index 85% rename from CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Listeners.swift rename to CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift index 70bc18e78f..795e7bc3a4 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument+Listeners.swift +++ b/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+CommandListeners.swift +// WorkspaceNotificationModel.swift // CodeEdit // // Created by Khan Winter on 6/5/22. diff --git a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift new file mode 100644 index 0000000000..1f6be962db --- /dev/null +++ b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift @@ -0,0 +1,18 @@ +// +// WorkspaceWindowManaging.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 06.04.26. +// + +import Foundation + +/// Protocol for workspace window lifecycle management. +/// Enables testability by abstracting the window manager. +protocol WorkspaceWindowManaging: AnyObject { + var openWorkspaces: [Workspace] { get } + func openWorkspace(at url: URL) throws + func closeWorkspace(_ workspace: Workspace) + func workspace(containing url: URL) -> Workspace? + func openFileInWorkspace(url: URL) -> Bool +} diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift new file mode 100644 index 0000000000..2a8a13da75 --- /dev/null +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -0,0 +1,230 @@ +// +// WorkspaceWindowManager.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 06.04.26. +// + +import AppKit +import SwiftUI +import WelcomeWindow + +extension Notification.Name { + static let openWelcomeWindow = Notification.Name("CodeEdit.openWelcomeWindow") +} + +/// Manages the lifecycle of workspace windows, replacing NSDocumentController for workspace management. +@MainActor +final class WorkspaceWindowManager: WorkspaceWindowManaging { + + @LazyService var lspService: LSPService + + /// All currently open workspaces. + private(set) var openWorkspaces: [Workspace] = [] + + /// Maps workspaces to their window controllers for lookup. + private var windowControllers: [ObjectIdentifier: CodeEditWindowController] = [:] + + // MARK: - Open Workspace + + func openWorkspace(at url: URL) throws { + // Check if this workspace is already open + if let existing = openWorkspaces.first(where: { $0.fileURL?.standardizedFileURL == url.standardizedFileURL }) { + focusWorkspace(existing) + return + } + + let workspace = try Workspace(url: url) + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + + let windowController = CodeEditWindowController( + window: window, + workspace: workspace + ) + + if let rectString = workspace.statePersistence?.get(.workspaceWindowSize) as? String { + window.setFrame(NSRectFromString(rectString), display: true, animate: false) + } else { + window.setFrame(NSRect(x: 0, y: 0, width: 1400, height: 900), display: true, animate: false) + window.center() + } + + window.setAccessibilityIdentifier("workspace") + window.setAccessibilityDocument(workspace.fileURL?.absoluteString) + + openWorkspaces.append(workspace) + windowControllers[ObjectIdentifier(workspace)] = windowController + workspace.notificationPanel.windowController = windowController + + window.makeKeyAndOrderFront(nil) + + RecentsStore.documentOpened(at: url) + } + + // MARK: - Close Workspace + + func closeWorkspace(_ workspace: Workspace) { + if let path = workspace.fileURL?.absoluteURL.path() { + lspService.closeWorkspace(path) + } + + workspace.tearDown() + + let id = ObjectIdentifier(workspace) + windowControllers.removeValue(forKey: id) + openWorkspaces.removeAll { $0 === workspace } + + if openWorkspaces.isEmpty { + handleLastWorkspaceClosed() + } + } + + // MARK: - Query + + func workspace(containing url: URL) -> Workspace? { + openWorkspaces.first { workspace in + workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) != nil + } + } + + /// Attempts to open a file URL in an existing workspace, finding the nearest workspace. + /// Returns `true` if the file was opened in a workspace. + func openFileInWorkspace(url: URL) -> Bool { + guard !url.isFolder else { return false } + + for workspace in openWorkspaces.sorted(by: { + ($0.fileURL?.sharedComponents(url) ?? 0) > ($1.fileURL?.sharedComponents(url) ?? 0) + }) { + if let newFile = workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) { + workspace.editorManager?.openTab(item: newFile) + focusWorkspace(workspace) + return true + } + } + return false + } + + // MARK: - Window Controller Access + + func windowController(for workspace: Workspace) -> CodeEditWindowController? { + windowControllers[ObjectIdentifier(workspace)] + } + + // MARK: - Open Panel + + func openDocumentFromPanel() { + let dialog = NSOpenPanel() + dialog.title = "Open Workspace or File" + dialog.showsResizeIndicator = true + dialog.showsHiddenFiles = false + dialog.canChooseFiles = true + dialog.canChooseDirectories = true + + dialog.begin { [weak self] result in + guard let self, result == .OK, let url = dialog.url else { return } + self.openDocument(at: url, onCompletion: {}) + } + } + + func newDocumentFromPanel() { + let panel = NSSavePanel() + guard panel.runModal() == .OK, let url = panel.url else { return } + + let created = FileManager.default.createFile( + atPath: url.path, + contents: nil, + attributes: [FileAttributeKey.creationDate: Date()] + ) + guard created else { + print("Failed to create new document") + return + } + + if !openFileInWorkspace(url: url) { + NSDocumentController.shared.openDocument( + withContentsOf: url, + display: true + ) { _, _, error in + if let error { NSAlert(error: error).runModal() } + } + } + } + + // MARK: - Convenience Openers + + /// Opens a workspace or file at the given URL, calling the completion handler on success. + func openDocument(at url: URL, onCompletion: @escaping () -> Void) { + do { + if url.isFolder { + try openWorkspace(at: url) + onCompletion() + } else if openFileInWorkspace(url: url) { + onCompletion() + } else { + NSDocumentController.shared.openDocument( + withContentsOf: url, display: true + ) { _, _, error in + if error == nil { onCompletion() } + } + } + } catch { + NSAlert(error: error).runModal() + } + } + + /// Opens a dialog to choose a file or folder, with optional configuration. + func openDocumentWithDialog( + canChooseFiles: Bool = true, + canChooseDirectories: Bool = true, + onDialogPresented: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil + ) { + let dialog = NSOpenPanel() + dialog.title = "Open Workspace or File" + dialog.showsResizeIndicator = true + dialog.showsHiddenFiles = false + dialog.canChooseFiles = canChooseFiles + dialog.canChooseDirectories = canChooseDirectories + + onDialogPresented?() + + dialog.begin { [weak self] result in + guard let self else { return } + if result == .OK, let url = dialog.url { + self.openDocument(at: url, onCompletion: {}) + } else if result == .cancel { + onCancel?() + } + } + } + + // MARK: - Private + + private func focusWorkspace(_ workspace: Workspace) { + if let controller = windowControllers[ObjectIdentifier(workspace)] { + controller.window?.makeKeyAndOrderFront(nil) + } + } + + private func handleLastWorkspaceClosed() { + switch Settings[\.general].reopenWindowAfterClose { + case .showWelcomeWindow: + if let welcomeWindow = NSApp.findWindow(.welcome) { + welcomeWindow.makeKeyAndOrderFront(nil) + } else { + // Post notification for AppDelegate to open the welcome window via SwiftUI's openWindow + NotificationCenter.default.post(name: .openWelcomeWindow, object: nil) + } + case .quit: + NSApplication.shared.terminate(nil) + case .doNothing: + break + } + } +} diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index 56f0b08b94..cbb70151bf 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -1247,8 +1247,6 @@ public.folder - NSDocumentClass - WorkspaceDocument CFBundleExecutable diff --git a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift index a3259ef6bd..9c5d65d918 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift @@ -9,12 +9,9 @@ import Foundation extension URL { /// Finds a workspace that contains the url. - func findWorkspace() -> WorkspaceDocument? { - CodeEditDocumentController.shared.documents.first(where: { doc in - guard let workspace = doc as? WorkspaceDocument else { return false } - // createIfNotFound is safe here because it will still exit if the file and the workspace - // do not share a path prefix - return workspace.workspaceFileManager?.getFile(absolutePath, createIfNotFound: true) != nil - }) as? WorkspaceDocument + @MainActor + func findWorkspace() -> Workspace? { + @Service var windowManager: WorkspaceWindowManager + return windowManager.workspace(containing: self) } } diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 84701d7dd7..78c57fd804 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -23,7 +23,7 @@ struct WorkspaceView: View { @AppSettings(\.sourceControl.general.sourceControlIsEnabled) var sourceControlIsEnabled - @EnvironmentObject private var workspace: WorkspaceDocument + @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 39f47901b8..46c03c0a33 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -15,7 +15,7 @@ final class DocumentsUnitTests: XCTestCase { private var hapticFeedbackPerformerMock: NSHapticFeedbackPerformerMock! private var navigatorViewModel: NavigatorAreaViewModel! private var window: NSWindow! - private var workspace = WorkspaceDocument() + private var workspace = Workspace() // MARK: - Lifecycle diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 26a0dfde62..dd49f6ae08 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+SearchState+FindAndReplaceTests.swift +// Workspace+SearchState+FindAndReplaceTests.swift // CodeEditTests // // Created by Tommy Ludwig on 26.01.24. @@ -12,14 +12,14 @@ import XCTest final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_body_length private var directory: URL! private var files: [CEWorkspaceFile] = [] - private var mockWorkspace: WorkspaceDocument! + private var mockWorkspace: Workspace! private var searchState: SearchState! private var folder1File: CEWorkspaceFile? private var folder2File: CEWorkspaceFile? // MARK: - Setup - /// A mock WorkspaceDocument is created + /// A mock Workspace is created /// 3 mock files are added to the index /// which will be removed in the teardown function override func setUp() async throws { @@ -34,7 +34,7 @@ final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_bod try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - mockWorkspace = try WorkspaceDocument(for: directory, withContentsOf: directory, ofType: "") + mockWorkspace = try Workspace(for: directory, withContentsOf: directory, ofType: "") searchState = mockWorkspace.searchState // Add a few files diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 0baed46114..b878bacd90 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+SearchState+FindTests.swift +// Workspace+SearchState+FindTests.swift // CodeEditTests // // Created by Tommy Ludwig on 26.01.24. @@ -11,11 +11,11 @@ import XCTest final class FindTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] - private var mockWorkspace: WorkspaceDocument! + private var mockWorkspace: Workspace! private var searchState: SearchState! // MARK: - Setup - /// A mock WorkspaceDocument is created + /// A mock Workspace is created /// 3 mock files are added to the index /// which will be removed in the teardown function override func setUp() async throws { @@ -30,7 +30,7 @@ final class FindTests: XCTestCase { try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - mockWorkspace = try await WorkspaceDocument(for: directory, withContentsOf: directory, ofType: "") + mockWorkspace = try await Workspace(for: directory, withContentsOf: directory, ofType: "") searchState = await mockWorkspace.searchState // Add a few files diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index 016978b05b..570af4b60a 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -1,5 +1,5 @@ // -// WorkspaceDocument+SearchState+IndexTests.swift +// Workspace+SearchState+IndexTests.swift // CodeEditTests // // Created by Tommy Ludwig on 26.01.24. @@ -8,17 +8,17 @@ import XCTest @testable import CodeEdit -final class WorkspaceDocumentIndexTests: XCTestCase { +final class WorkspaceIndexTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] - private var mockWorkspace: WorkspaceDocument! + private var mockWorkspace: Workspace! private var searchState: SearchState! private var folder1File: CEWorkspaceFile? private var folder2File: CEWorkspaceFile? // MARK: - Setup - /// A mock WorkspaceDocument is created + /// A mock Workspace is created /// 3 mock files are added to the index /// which will be removed in the teardown function override func setUp() async throws { @@ -33,7 +33,7 @@ final class WorkspaceDocumentIndexTests: XCTestCase { try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - mockWorkspace = try await WorkspaceDocument(for: directory, withContentsOf: directory, ofType: "") + mockWorkspace = try await Workspace(for: directory, withContentsOf: directory, ofType: "") searchState = await mockWorkspace.searchState // Add a few files diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 7112ccec8a..532e103cad 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -78,8 +78,8 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { return (connection: bufferingConnection, server: server) } - func makeTestWorkspace() throws -> (WorkspaceDocument, CEWorkspaceFileManager) { - let workspace = WorkspaceDocument() + func makeTestWorkspace() throws -> (Workspace, CEWorkspaceFileManager) { + let workspace = Workspace() try workspace.read(from: tempTestDir, ofType: "") guard let fileManager = workspace.workspaceFileManager else { XCTFail("No File Manager") @@ -151,7 +151,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { // Set up workspace let (workspace, fileManager) = try makeTestWorkspace() - CodeEditDocumentController.shared.addDocument(workspace) + WorkspaceWindowManager.shared.addDocument(workspace) // Add a CEWorkspaceFile _ = try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") @@ -167,7 +167,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { ofType: "public.swift-source" ) file.fileDocument = codeFile - CodeEditDocumentController.shared.addDocument(codeFile) + WorkspaceWindowManager.shared.addDocument(codeFile) await waitForClientState( ( From bd6e14b78591b3129183a70f5c25510ccd901199 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 12:54:50 +0200 Subject: [PATCH 003/335] Refactor: Extract WorkspaceFactory to decouple manager wiring from Workspace Move all manager construction, dependency ordering, cross-wiring, and state restoration logic from Workspace.initWorkspaceState() into a dedicated WorkspaceFactory.populate() method. This keeps Workspace as a plain state container while the factory owns the object graph setup. --- .../Features/Workspace/Models/Workspace.swift | 57 +----------- .../Services/WorkspaceWindowManager.swift | 2 +- .../Features/Workspace/WorkspaceFactory.swift | 86 +++++++++++++++++++ 3 files changed, 91 insertions(+), 54 deletions(-) create mode 100644 CodeEdit/Features/Workspace/WorkspaceFactory.swift diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 6c81d6831a..02c57f0ee7 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -17,8 +17,8 @@ final class Workspace: ObservableObject, WorkspaceManaging { @Published var navigatorFilter: String = "" @Published var sourceControlFilter = false - private(set) var fileURL: URL? - private(set) var displayName: String = "" + internal(set) var fileURL: URL? + internal(set) var displayName: String = "" var workspaceFileManager: CEWorkspaceFileManager? var editorManager: EditorManager? = EditorManager() @@ -40,64 +40,15 @@ final class Workspace: ObservableObject, WorkspaceManaging { var notificationPanel = NotificationPanelViewModel() - private let ignoredFilesAndDirectory = [ - ".DS_Store" - ] - // MARK: - Initialization - init(url: URL) throws { - try initWorkspaceState(url) + init(url: URL) { + WorkspaceFactory.populate(self, url: url) } /// Minimal initializer for testing. Does not set up workspace state. internal init() {} - private func initWorkspaceState(_ url: URL) throws { - var url = url - if !url.absoluteString.hasSuffix("/") { - url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") - } - - self.fileURL = url - self.displayName = url.lastPathComponent - self.statePersistence = WorkspaceStatePersistence(workspaceURL: url) - - let sourceControlManager = SourceControlManager( - workspaceURL: url, - editorManager: editorManager! - ) - - self.workspaceFileManager = .init( - folderUrl: url, - ignoredFilesAndFolders: Set(ignoredFilesAndDirectory), - sourceControlManager: sourceControlManager - ) - self.sourceControlManager = sourceControlManager - sourceControlManager.fileManager = workspaceFileManager - self.searchState = SearchState(workspaceURL: url) - self.openQuicklyViewModel = .init(fileURL: url) - self.commandsPaletteState = .init() - self.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) - if let workspaceSettingsManager { - self.taskManager = TaskManager( - workspaceSettings: workspaceSettingsManager.settings, - workspaceURL: url - ) - } - self.taskNotificationHandler.workspaceURL = url - - workspaceFileManager?.addObserver(undoRegistration) - if let statePersistence { - editorManager?.restoreFromState( - statePersistence: statePersistence, - fileManager: workspaceFileManager, - searchState: searchState - ) - utilityAreaModel?.restoreFromState(statePersistence) - } - } - // MARK: - Tear Down func tearDown() { diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 2a8a13da75..4784fe09d1 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -34,7 +34,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { return } - let workspace = try Workspace(url: url) + let workspace = Workspace(url: url) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift new file mode 100644 index 0000000000..1e0141feb7 --- /dev/null +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -0,0 +1,86 @@ +// +// WorkspaceFactory.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 07.04.26. +// + +import Foundation + +/// Constructs and wires the manager/service object graph for a ``Workspace``. +/// +/// This factory encapsulates the dependency ordering and cross-wiring +/// required when opening a workspace, keeping `Workspace` itself a plain state container. +enum WorkspaceFactory { + + private static let ignoredFilesAndDirectories: Set = [".DS_Store"] + + /// Populates all manager properties on `workspace` for the given workspace URL. + /// + /// - Parameters: + /// - workspace: The workspace to populate. Its `editorManager`, + /// `statusBarViewModel`, `utilityAreaModel`, `listenerModel`, + /// `taskNotificationHandler`, `undoRegistration`, and `notificationPanel` + /// must already be initialized (they are set at declaration time). + /// - url: The root URL of the workspace folder. + @MainActor + static func populate(_ workspace: Workspace, url: URL) { + // Normalize the URL to always end with "/" + var url = url + if !url.absoluteString.hasSuffix("/") { + url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") + } + + workspace.fileURL = url + workspace.displayName = url.lastPathComponent + workspace.statePersistence = WorkspaceStatePersistence(workspaceURL: url) + + // --- Phase 1: Source control + file manager (dependency chain) --- + guard let editorManager = workspace.editorManager else { + assertionFailure("EditorManager must be initialized before calling populate") + return + } + + let sourceControlManager = SourceControlManager( + workspaceURL: url, + editorManager: editorManager + ) + + let workspaceFileManager = CEWorkspaceFileManager( + folderUrl: url, + ignoredFilesAndFolders: ignoredFilesAndDirectories, + sourceControlManager: sourceControlManager + ) + + sourceControlManager.fileManager = workspaceFileManager + + workspace.sourceControlManager = sourceControlManager + workspace.workspaceFileManager = workspaceFileManager + + // --- Phase 2: Independent managers --- + workspace.searchState = SearchState(workspaceURL: url) + workspace.openQuicklyViewModel = OpenQuicklyViewModel(fileURL: url) + workspace.commandsPaletteState = QuickActionsViewModel() + workspace.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) + if let workspaceSettingsManager = workspace.workspaceSettingsManager { + workspace.taskManager = TaskManager( + workspaceSettings: workspaceSettingsManager.settings, + workspaceURL: url + ) + } + workspace.taskNotificationHandler.workspaceURL = url + + // --- Phase 3: Observer registration --- + workspaceFileManager.addObserver(workspace.undoRegistration) + + // --- Phase 4: State restoration --- + if let statePersistence = workspace.statePersistence { + editorManager.restoreFromState( + statePersistence: statePersistence, + fileManager: workspaceFileManager, + searchState: workspace.searchState + ) + workspace.utilityAreaModel?.restoreFromState(statePersistence) + } + } +} From 8f8346da73105ec9249686d4ff4203e1582e71f1 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 13:38:53 +0200 Subject: [PATCH 004/335] Refactor: Decompose SearchState+Find into focused extensions Split SearchState+Find.swift (381 lines) into three focused files: - SearchState+QueryProcessing.swift: query transformation (shared by Find and FindAndReplace) - SearchState+Find.swift: search orchestration only (now 113 lines) - SearchState+MatchExtraction.swift: file evaluation and match context extraction --- .../Features/Search/SearchState+Find.swift | 266 ------------------ .../Search/SearchState+MatchExtraction.swift | 212 ++++++++++++++ .../Search/SearchState+QueryProcessing.swift | 74 +++++ 3 files changed, 286 insertions(+), 266 deletions(-) create mode 100644 CodeEdit/Features/Search/SearchState+MatchExtraction.swift create mode 100644 CodeEdit/Features/Search/SearchState+QueryProcessing.swift diff --git a/CodeEdit/Features/Search/SearchState+Find.swift b/CodeEdit/Features/Search/SearchState+Find.swift index 41f1ec894b..f25e2d5c32 100644 --- a/CodeEdit/Features/Search/SearchState+Find.swift +++ b/CodeEdit/Features/Search/SearchState+Find.swift @@ -10,70 +10,6 @@ import Foundation extension SearchState: @unchecked Sendable {} extension SearchState { - /// Creates a search term based on the given query and search mode. - /// - /// - Parameter query: The original user query string. - /// - /// - Returns: A modified search term according to the specified search mode. - func getSearchTerm(_ query: String) -> String { - let newQuery = stripSpecialCharacters(from: (caseSensitive ? query : query.lowercased())) - guard let mode = selectedMode.third else { - return newQuery - } - - switch mode { - case .Containing: - return "*\(newQuery)*" - case .StartingWith: - return "\(newQuery)*" - case .EndingWith: - return "*\(newQuery)" - default: - return newQuery - } - } - - func stripSpecialCharacters(from string: String) -> String { - let regex = try? NSRegularExpression(pattern: "[^a-zA-Z0-9]+", options: .caseInsensitive) - return regex!.stringByReplacingMatches( - in: string, - options: [], - range: NSRange(location: 0, length: string.utf16.count), - withTemplate: "*" - ) - } - - /// Generates a regular expression pattern based on the specified query and search mode. - /// - /// - Parameter query: The original user query string. - /// - /// - Returns: A string representing the regular expression pattern based on the selected search mode. - /// - /// - Note: This function is creating similar patterns to the - /// ``Workspace/SearchState-swift.class/getSearchTerm(_:)`` function, - /// Except its using the word boundary anchor(\b) instead of the asterisk(\*). - /// This is needed to highlight the search results correctly. - func getRegexPattern(_ query: String) -> String { - let newQuery = NSRegularExpression.escapedPattern(for: query.trimmingCharacters(in: .whitespacesAndNewlines)) - - guard let mode = selectedMode.third else { - return newQuery - } - - switch mode { - case .Containing: - return "\(newQuery)" - case .StartingWith: - return "\\b\(newQuery)" - case .EndingWith: - return "\(newQuery)\\b" - case .MatchingWord: - return "\\b\(newQuery)\\b" - default: - return newQuery - } - } - /// Searches the entire workspace for the given string, using the /// ``Workspace/SearchState-swift.class/selectedMode`` modifiers /// to modify the search if needed. This is done by filtering out files with SearchKit and then searching @@ -167,183 +103,6 @@ extension SearchState { self.tempSearchResults = [] } - /// Evaluates a search query within the content of a file and updates - /// the provided `SearchResultModel` with matching occurrences. - /// - /// - Parameters: - /// - query: The search query to be evaluated, potentially containing a regular expression. - /// - searchResult: The `SearchResultModel` object to be updated with the matching occurrences. - /// - /// This function retrieves the content of a file specified in the `searchResult` parameter - /// and applies a search query using a regular expression. - /// It then iterates over the matches found in the file content, - /// creating `SearchResultMatchModel` instances for each match. - /// The resulting matches are appended to the `lineMatches` property of the `searchResult`. - /// Line matches are the preview lines that are shown in the search results. - /// - /// # Example Usage - /// ```swift - /// var resultModel = SearchResultModel() - /// await evaluateFile(query: "example", searchResult: &resultModel) - /// ``` - private func evaluateFile(query: String, searchResult: inout SearchResultModel) async { - guard let data = try? Data(contentsOf: searchResult.file.url) else { - return - } - guard let fileContent = String(bytes: data, encoding: .utf8) else { - await setStatus(.failed(errorMessage: "Failed to decode file content.")) - return - } - - // Attempt to create a regular expression from the provided query - guard let regex = try? NSRegularExpression( - pattern: query, - options: caseSensitive ? [] : .caseInsensitive - ) else { - await setStatus(.failed(errorMessage: "Invalid regular expression.")) - return - } - - // Find all matches of the query within the file content using the regular expression - let matches = regex.matches(in: fileContent, range: NSRange(location: 0, length: fileContent.utf16.count)) - - var newMatches = [SearchResultMatchModel]() - - // Process each match and add it to the array of `newMatches` - for match in matches { - if let matchRange = Range(match.range, in: fileContent) { - let matchWordLength = match.range.length - let matchModel = createMatchModel( - from: matchRange, - fileContent: fileContent, - file: searchResult.file, - matchWordLength: matchWordLength - ) - newMatches.append(matchModel) - } - } - - searchResult.lineMatches = newMatches - } - - /// Creates a `SearchResultMatchModel` instance based on the provided parameters, - /// representing a matching occurrence within a file. - /// - /// - Parameters: - /// - matchRange: The range of the matched substring within the entire file content. - /// - fileContent: The content of the file where the match was found. - /// - file: The `CEWorkspaceFile` object representing the file containing the match. - /// - matchWordLength: The length of the matched substring. - /// - /// - Returns: A `SearchResultMatchModel` instance representing the matching occurrence. - /// - /// This function is responsible for constructing a `SearchResultMatchModel` - /// based on the provided parameters. It extracts the relevant portions of the file content, - /// including the lines before and after the match, and combines them into a final line. - /// The resulting model includes information about the match's range within the file, - /// the file itself, the content of the line containing the match, - /// and the range of the matched keyword within that line. - private func createMatchModel( - from matchRange: Range, - fileContent: String, - file: CEWorkspaceFile, - matchWordLength: Int - ) -> SearchResultMatchModel { - let preLine = extractPreLine(from: matchRange, fileContent: fileContent) - let keywordRange = extractKeywordRange(from: preLine, matchWordLength: matchWordLength) - let postLine = extractPostLine(from: matchRange, fileContent: fileContent) - - let finalLine = preLine + postLine - - return SearchResultMatchModel( - rangeWithinFile: matchRange, - file: file, - lineContent: finalLine, - keywordRange: keywordRange - ) - } - - /// Extracts the line preceding a matching occurrence within a file. - /// - /// - Parameters: - /// - matchRange: The range of the matched substring within the entire file content. - /// - fileContent: The content of the file where the match was found. - /// - /// - Returns: A string representing the line preceding the match. - /// - /// This function retrieves the line preceding a matching occurrence within the provided file content. - /// It considers a context of up to 60 characters before the match and clips the result to the last - /// occurrence of a newline character, ensuring that only the line containing the search term is displayed. - /// The extracted line is then trimmed of leading and trailing whitespaces and - /// newline characters before being returned. - private func extractPreLine(from matchRange: Range, fileContent: String) -> String { - let preRangeStart = fileContent.index( - matchRange.lowerBound, - offsetBy: -60, - limitedBy: fileContent.startIndex - ) ?? fileContent.startIndex - - let preRangeEnd = matchRange.upperBound - let preRange = preRangeStart.. Range { - let keywordLowerBound = preLine.index( - preLine.endIndex, - offsetBy: -matchWordLength, - limitedBy: preLine.startIndex - ) ?? preLine.endIndex - let keywordUpperBound = preLine.endIndex - - return keywordLowerBound.., fileContent: String) -> String { - let postRangeStart = matchRange.upperBound - let postRangeEnd = fileContent.index( - matchRange.upperBound, - offsetBy: 60, - limitedBy: fileContent.endIndex - ) ?? fileContent.endIndex - - let postRange = postRangeStart.. SearchResultModel? { - var newResult = SearchResultModel( - file: CEWorkspaceFile(url: fileURL), - score: fileScore - ) - - await evaluateFile(query: regexPattern, searchResult: &newResult) - - return newResult.lineMatches.isEmpty ? nil : newResult - } } diff --git a/CodeEdit/Features/Search/SearchState+MatchExtraction.swift b/CodeEdit/Features/Search/SearchState+MatchExtraction.swift new file mode 100644 index 0000000000..fccfa229c0 --- /dev/null +++ b/CodeEdit/Features/Search/SearchState+MatchExtraction.swift @@ -0,0 +1,212 @@ +// +// SearchState+MatchExtraction.swift +// CodeEdit +// +// Created by Tommy Ludwig on 02.01.24. +// + +import Foundation + +extension SearchState { + /// Evaluates a matched file to determine if it contains any search matches. + /// Requires a file score from the search model. + /// + /// Evaluates the file's contents asynchronously. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to evaluate. + /// - fileScore: The file's score from a ``SearchIndexer`` + /// - regexPattern: The pattern to evaluate against the file's contents. + /// - Returns: `nil` if there are no relevant search matches, or a search result if matches are found. + func evaluateSearchResult( + fileURL: URL, + fileScore: Float, + regexPattern: String + ) async -> SearchResultModel? { + var newResult = SearchResultModel( + file: CEWorkspaceFile(url: fileURL), + score: fileScore + ) + + await evaluateFile(query: regexPattern, searchResult: &newResult) + + return newResult.lineMatches.isEmpty ? nil : newResult + } + + /// Evaluates a search query within the content of a file and updates + /// the provided `SearchResultModel` with matching occurrences. + /// + /// - Parameters: + /// - query: The search query to be evaluated, potentially containing a regular expression. + /// - searchResult: The `SearchResultModel` object to be updated with the matching occurrences. + /// + /// This function retrieves the content of a file specified in the `searchResult` parameter + /// and applies a search query using a regular expression. + /// It then iterates over the matches found in the file content, + /// creating `SearchResultMatchModel` instances for each match. + /// The resulting matches are appended to the `lineMatches` property of the `searchResult`. + /// Line matches are the preview lines that are shown in the search results. + /// + /// # Example Usage + /// ```swift + /// var resultModel = SearchResultModel() + /// await evaluateFile(query: "example", searchResult: &resultModel) + /// ``` + private func evaluateFile(query: String, searchResult: inout SearchResultModel) async { + guard let data = try? Data(contentsOf: searchResult.file.url) else { + return + } + guard let fileContent = String(bytes: data, encoding: .utf8) else { + await setStatus(.failed(errorMessage: "Failed to decode file content.")) + return + } + + // Attempt to create a regular expression from the provided query + guard let regex = try? NSRegularExpression( + pattern: query, + options: caseSensitive ? [] : .caseInsensitive + ) else { + await setStatus(.failed(errorMessage: "Invalid regular expression.")) + return + } + + // Find all matches of the query within the file content using the regular expression + let matches = regex.matches(in: fileContent, range: NSRange(location: 0, length: fileContent.utf16.count)) + + var newMatches = [SearchResultMatchModel]() + + // Process each match and add it to the array of `newMatches` + for match in matches { + if let matchRange = Range(match.range, in: fileContent) { + let matchWordLength = match.range.length + let matchModel = createMatchModel( + from: matchRange, + fileContent: fileContent, + file: searchResult.file, + matchWordLength: matchWordLength + ) + newMatches.append(matchModel) + } + } + + searchResult.lineMatches = newMatches + } + + /// Creates a `SearchResultMatchModel` instance based on the provided parameters, + /// representing a matching occurrence within a file. + /// + /// - Parameters: + /// - matchRange: The range of the matched substring within the entire file content. + /// - fileContent: The content of the file where the match was found. + /// - file: The `CEWorkspaceFile` object representing the file containing the match. + /// - matchWordLength: The length of the matched substring. + /// + /// - Returns: A `SearchResultMatchModel` instance representing the matching occurrence. + /// + /// This function is responsible for constructing a `SearchResultMatchModel` + /// based on the provided parameters. It extracts the relevant portions of the file content, + /// including the lines before and after the match, and combines them into a final line. + /// The resulting model includes information about the match's range within the file, + /// the file itself, the content of the line containing the match, + /// and the range of the matched keyword within that line. + private func createMatchModel( + from matchRange: Range, + fileContent: String, + file: CEWorkspaceFile, + matchWordLength: Int + ) -> SearchResultMatchModel { + let preLine = extractPreLine(from: matchRange, fileContent: fileContent) + let keywordRange = extractKeywordRange(from: preLine, matchWordLength: matchWordLength) + let postLine = extractPostLine(from: matchRange, fileContent: fileContent) + + let finalLine = preLine + postLine + + return SearchResultMatchModel( + rangeWithinFile: matchRange, + file: file, + lineContent: finalLine, + keywordRange: keywordRange + ) + } + + /// Extracts the line preceding a matching occurrence within a file. + /// + /// - Parameters: + /// - matchRange: The range of the matched substring within the entire file content. + /// - fileContent: The content of the file where the match was found. + /// + /// - Returns: A string representing the line preceding the match. + /// + /// This function retrieves the line preceding a matching occurrence within the provided file content. + /// It considers a context of up to 60 characters before the match and clips the result to the last + /// occurrence of a newline character, ensuring that only the line containing the search term is displayed. + /// The extracted line is then trimmed of leading and trailing whitespaces and + /// newline characters before being returned. + private func extractPreLine(from matchRange: Range, fileContent: String) -> String { + let preRangeStart = fileContent.index( + matchRange.lowerBound, + offsetBy: -60, + limitedBy: fileContent.startIndex + ) ?? fileContent.startIndex + + let preRangeEnd = matchRange.upperBound + let preRange = preRangeStart.. Range { + let keywordLowerBound = preLine.index( + preLine.endIndex, + offsetBy: -matchWordLength, + limitedBy: preLine.startIndex + ) ?? preLine.endIndex + let keywordUpperBound = preLine.endIndex + + return keywordLowerBound.., fileContent: String) -> String { + let postRangeStart = matchRange.upperBound + let postRangeEnd = fileContent.index( + matchRange.upperBound, + offsetBy: 60, + limitedBy: fileContent.endIndex + ) ?? fileContent.endIndex + + let postRange = postRangeStart.. String { + let newQuery = stripSpecialCharacters(from: (caseSensitive ? query : query.lowercased())) + guard let mode = selectedMode.third else { + return newQuery + } + + switch mode { + case .Containing: + return "*\(newQuery)*" + case .StartingWith: + return "\(newQuery)*" + case .EndingWith: + return "*\(newQuery)" + default: + return newQuery + } + } + + func stripSpecialCharacters(from string: String) -> String { + let regex = try? NSRegularExpression(pattern: "[^a-zA-Z0-9]+", options: .caseInsensitive) + return regex!.stringByReplacingMatches( + in: string, + options: [], + range: NSRange(location: 0, length: string.utf16.count), + withTemplate: "*" + ) + } + + /// Generates a regular expression pattern based on the specified query and search mode. + /// + /// - Parameter query: The original user query string. + /// + /// - Returns: A string representing the regular expression pattern based on the selected search mode. + /// + /// - Note: This function is creating similar patterns to the + /// ``Workspace/SearchState-swift.class/getSearchTerm(_:)`` function, + /// Except its using the word boundary anchor(\b) instead of the asterisk(\*). + /// This is needed to highlight the search results correctly. + func getRegexPattern(_ query: String) -> String { + let newQuery = NSRegularExpression.escapedPattern(for: query.trimmingCharacters(in: .whitespacesAndNewlines)) + + guard let mode = selectedMode.third else { + return newQuery + } + + switch mode { + case .Containing: + return "\(newQuery)" + case .StartingWith: + return "\\b\(newQuery)" + case .EndingWith: + return "\(newQuery)\\b" + case .MatchingWord: + return "\\b\(newQuery)\\b" + default: + return newQuery + } + } +} From 9538075861fb501f8774588445b35eb40ddc8f7b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 14:05:08 +0200 Subject: [PATCH 005/335] Refactor: Extract and decompose git event handling from DirectoryEvents Move handleGitEvents from CEWorkspaceFileManager+DirectoryEvents into a dedicated +GitEvents file. Split the monolithic function into focused methods, each handling one git event type (index, stash, branches, HEAD, config, .git folder) for improved readability and testability. --- ...WorkspaceFileManager+DirectoryEvents.swift | 77 ----------------- .../CEWorkspaceFileManager+GitEvents.swift | 84 +++++++++++++++++++ 2 files changed, 84 insertions(+), 77 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift index 25715b51c2..a40b6b4c59 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift @@ -52,83 +52,6 @@ extension CEWorkspaceFileManager { } } - func handleGitEvents(events: [DirectoryEventStream.Event]) { - // Changes excluding .git folder - let notGitChanges = events.filter({ !$0.path.contains(".git/") }) - - // .git folder was changed - let gitFolderChange = events.first(where: { - $0.path == "\(self.folderUrl.relativePath)/.git" - }) - - // Change made to git index file, staged/unstaged files - let gitIndexChange = events.first(where: { - $0.path == "\(self.folderUrl.relativePath)/.git/index" - }) - - // Change made to git stash - let gitStashChange = events.first(where: { - $0.path == "\(self.folderUrl.relativePath)/.git/refs/stash" - }) - - // Changes made to git branches - let gitBranchChange = events.first(where: { - $0.path.contains("\(self.folderUrl.relativePath)/.git/refs/heads") - }) - - // Changes made to git HEAD - current branch changed - let gitHeadChange = events.first(where: { - $0.path.contains("\(self.folderUrl.relativePath)/.git/HEAD") - }) - - // Change made to remotes by looking at .git/config - let gitConfigChange = events.first(where: { - $0.path == "\(self.folderUrl.relativePath)/.git/config" - }) - - // If changes were made to project OR files were staged, refresh changes - if !notGitChanges.isEmpty || gitIndexChange != nil { - Task { - await self.sourceControlManager?.refreshAllChangedFiles() - } - } - - // If changes were stashed, refresh stashed entries - if gitStashChange != nil { - Task { - try await self.sourceControlManager?.refreshStashEntries() - } - } - - // If branches were added or removed, refresh branches - if gitBranchChange != nil { - Task { - await self.sourceControlManager?.refreshBranches() - } - } - - // If HEAD was changed, refresh the current branch - if gitHeadChange != nil { - Task { - await self.sourceControlManager?.refreshCurrentBranch() - } - } - - // If git config changed, refresh remotes - if gitConfigChange != nil { - Task { - try await self.sourceControlManager?.refreshRemotes() - } - } - - // If .git folder was added or removed, check if repository is valid - if gitFolderChange != nil { - Task { - try await self.sourceControlManager?.validate() - } - } - } - /// Creates or deletes children of the ``CEWorkspaceFile`` so that they are accurate with the file system, /// instead of creating an entirely new ``CEWorkspaceFile``. Can optionally run a deep rebuild. /// diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift new file mode 100644 index 0000000000..0d984dd441 --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift @@ -0,0 +1,84 @@ +// +// CEWorkspaceFileManager+GitEvents.swift +// CodeEdit +// +// Created by Axel Martinez on 5/8/24. +// + +import Foundation + +/// Handles git-specific file system events by detecting changes to git internals +/// and dispatching targeted refreshes to `SourceControlManager`. +extension CEWorkspaceFileManager { + func handleGitEvents(events: [DirectoryEventStream.Event]) { + refreshChangedFilesIfNeeded(events: events) + refreshStashIfNeeded(events: events) + refreshBranchesIfNeeded(events: events) + refreshCurrentBranchIfNeeded(events: events) + refreshRemotesIfNeeded(events: events) + validateRepositoryIfNeeded(events: events) + } + + /// If changes were made to project files or the git index, refresh the changed files list. + private func refreshChangedFilesIfNeeded(events: [DirectoryEventStream.Event]) { + let hasNonGitChanges = events.contains(where: { !$0.path.contains(".git/") }) + let hasIndexChange = events.contains(where: { + $0.path == "\(self.folderUrl.relativePath)/.git/index" + }) + + guard hasNonGitChanges || hasIndexChange else { return } + Task { + await self.sourceControlManager?.refreshAllChangedFiles() + } + } + + /// If changes were stashed, refresh stash entries. + private func refreshStashIfNeeded(events: [DirectoryEventStream.Event]) { + guard events.contains(where: { + $0.path == "\(self.folderUrl.relativePath)/.git/refs/stash" + }) else { return } + Task { + try await self.sourceControlManager?.refreshStashEntries() + } + } + + /// If branches were added or removed, refresh the branches list. + private func refreshBranchesIfNeeded(events: [DirectoryEventStream.Event]) { + guard events.contains(where: { + $0.path.contains("\(self.folderUrl.relativePath)/.git/refs/heads") + }) else { return } + Task { + await self.sourceControlManager?.refreshBranches() + } + } + + /// If HEAD was changed, refresh the current branch. + private func refreshCurrentBranchIfNeeded(events: [DirectoryEventStream.Event]) { + guard events.contains(where: { + $0.path.contains("\(self.folderUrl.relativePath)/.git/HEAD") + }) else { return } + Task { + await self.sourceControlManager?.refreshCurrentBranch() + } + } + + /// If .git/config changed, refresh remotes. + private func refreshRemotesIfNeeded(events: [DirectoryEventStream.Event]) { + guard events.contains(where: { + $0.path == "\(self.folderUrl.relativePath)/.git/config" + }) else { return } + Task { + try await self.sourceControlManager?.refreshRemotes() + } + } + + /// If the .git folder was added or removed, validate the repository. + private func validateRepositoryIfNeeded(events: [DirectoryEventStream.Event]) { + guard events.contains(where: { + $0.path == "\(self.folderUrl.relativePath)/.git" + }) else { return } + Task { + try await self.sourceControlManager?.validate() + } + } +} From 3cce192c470f4ba66b51c18e6f75b504dfdfa9af Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 14:30:19 +0200 Subject: [PATCH 006/335] Refactor: Decompose EditorTabs view into focused files Extract the drag gesture logic and drop delegate from EditorTabs.swift (444 lines) into dedicated files: - EditorTabs+DragGesture.swift: tab reordering drag gesture - EditorTabOnDropDelegate.swift: drop delegate for external drags Removes SwiftLint file_length/type_body_length exemptions from the main file, which is now 261 lines of state, geometry, and layout. --- .../Tabs/Views/EditorTabOnDropDelegate.swift | 67 ++++++ .../Tabs/Views/EditorTabs+DragGesture.swift | 131 +++++++++++ .../Editor/TabBar/Tabs/Views/EditorTabs.swift | 203 +----------------- 3 files changed, 208 insertions(+), 193 deletions(-) create mode 100644 CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift create mode 100644 CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift new file mode 100644 index 0000000000..6035b548f6 --- /dev/null +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift @@ -0,0 +1,67 @@ +// +// EditorTabOnDropDelegate.swift +// CodeEdit +// +// Created by Austin Condiff on 9/7/23. +// + +import SwiftUI + +struct EditorTabOnDropDelegate: DropDelegate { + typealias TabID = CEWorkspaceFile.ID + + private let currentTabId: TabID + @Binding private var openedTabs: [TabID] + @Binding private var onDragTabId: TabID? + @Binding private var onDragLastLocation: CGPoint? + @Binding private var isOnDragOverTabs: Bool + @Binding private var tabWidth: [TabID: CGFloat] + + public init( + currentTabId: TabID, + openedTabs: Binding<[TabID]>, + onDragTabId: Binding, + onDragLastLocation: Binding, + isOnDragOverTabs: Binding, + tabWidth: Binding<[TabID: CGFloat]> + ) { + self.currentTabId = currentTabId + self._openedTabs = openedTabs + self._onDragTabId = onDragTabId + self._onDragLastLocation = onDragLastLocation + self._isOnDragOverTabs = isOnDragOverTabs + self._tabWidth = tabWidth + } + + func dropEntered(info: DropInfo) { + isOnDragOverTabs = true + guard let onDragTabId, + currentTabId != onDragTabId, + let from = openedTabs.firstIndex(of: onDragTabId), + let toIndex = openedTabs.firstIndex(of: currentTabId) + else { return } + if openedTabs[toIndex] != onDragTabId { + withAnimation { + openedTabs.move( + fromOffsets: IndexSet(integer: from), + toOffset: toIndex > from ? toIndex + 1 : toIndex + ) + } + } + } + + func dropExited(info: DropInfo) { + // Do nothing. + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + return DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + isOnDragOverTabs = false + onDragTabId = nil + onDragLastLocation = nil + return true + } +} diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift new file mode 100644 index 0000000000..e9c7b51ab1 --- /dev/null +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift @@ -0,0 +1,131 @@ +// +// EditorTabs+DragGesture.swift +// CodeEdit +// +// Created by Austin Condiff on 9/7/23. +// + +import SwiftUI + +// Disable the rule because this function is implementing the drag gesture and its animations. +// It is fairly complicated, so ignore the function body length limitation for now. +// swiftlint:disable function_body_length cyclomatic_complexity + +extension EditorTabs { + func makeTabDragGesture(id: TabID) -> some Gesture { + return DragGesture(minimumDistance: 2, coordinateSpace: .global) + .onChanged({ value in + if closeButtonGestureActive { + return + } + + if draggingTabId != id { + shouldOnDrag = false + draggingTabId = id + draggingStartLocation = value.startLocation.x + draggingLastLocation = value.location.x + } + // TODO: Enable this code snippet when re-enabling dragging-out behavior. + // I disabled (1 == 0) this behavior for now as dragging-out behavior isn't allowed. + if 1 == 0 && abs(value.location.y - value.startLocation.y) > EditorTabBarView.height { + shouldOnDrag = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + shouldOnDrag = false + draggingStartLocation = nil + draggingLastLocation = nil + draggingTabId = nil + withAnimation(.easeInOut(duration: 0.25)) { + // Clean the tab offsets. + tabOffsets = [:] + } + }) + return + } + // Get the current cursor location. + let currentLocation = value.location.x + guard let startLocation = draggingStartLocation, + let currentIndex = openedTabs.firstIndex(of: id), + let currentTabWidth = tabWidth[id], + let lastLocation = draggingLastLocation + else { return } + let dragDifference = currentLocation - lastLocation + let previousIndex = currentIndex > 0 ? currentIndex - 1 : nil + let nextIndex = currentIndex < openedTabs.count - 1 ? currentIndex + 1 : nil + tabOffsets[id] = currentLocation - startLocation + // Interacting with the previous tab. + if previousIndex != nil && dragDifference < 0 { + // Wrap `previousTabIndex` because it may be `nil`. + guard let previousTabIndex = previousIndex, + let previousTabLocation = tabLocations[openedTabs[previousTabIndex]], + let previousTabWidth = tabWidth[openedTabs[previousTabIndex]] + else { return } + if currentLocation < max( + previousTabLocation.maxX - previousTabWidth * 0.1, + previousTabLocation.minX + currentTabWidth * 0.9 + ) { + let changing = previousTabWidth - 1 // One offset for overlapping divider. + draggingStartLocation! -= changing + withAnimation { + tabOffsets[id]! += changing + openedTabs.move( + fromOffsets: IndexSet(integer: previousTabIndex), + toOffset: currentIndex + 1 + ) + } + return + } + } + // Interacting with the next tab. + if nextIndex != nil && dragDifference > 0 { + // Wrap `previousTabIndex` because it may be `nil`. + guard let nextTabIndex = nextIndex, + let nextTabLocation = tabLocations[openedTabs[nextTabIndex]], + let nextTabWidth = tabWidth[openedTabs[nextTabIndex]] + else { return } + if currentLocation > min( + nextTabLocation.minX + nextTabWidth * 0.1, + nextTabLocation.maxX - currentTabWidth * 0.9 + ) { + let changing = nextTabWidth - 1 // One offset for overlapping divider. + draggingStartLocation! += changing + withAnimation { + tabOffsets[id]! -= changing + openedTabs.move( + fromOffsets: IndexSet(integer: nextTabIndex), + toOffset: currentIndex + ) + } + return + } + } + // Only update the last dragging location when there is enough offset. + if draggingLastLocation == nil || abs(value.location.x - draggingLastLocation!) >= 10 { + draggingLastLocation = value.location.x + } + }) + .onEnded({ _ in + shouldOnDrag = false + draggingStartLocation = nil + draggingLastLocation = nil + withAnimation(.easeInOut(duration: 0.25)) { + tabOffsets = [:] + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + draggingTabId = nil + } + // Sync the workspace's `openedTabs` 150ms after animation is finished. + // In order to avoid the lag due to the update of workspace state. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.40) { + if draggingStartLocation == nil { + editor.tabs = .init(openedTabs.compactMap { id in + editor.tabs.first { $0.file.id == id } + }) + // workspace.reorderedTabs(openedTabs: openedTabs) + // TODO: Fix save state + } + } + }) + } +} + +// swiftlint:enable function_body_length cyclomatic_complexity diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift index bc2ab11c4d..7704121920 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift @@ -7,10 +7,6 @@ import SwiftUI -// Disable the rule because the tab bar view is fairly complicated. -// It has the gesture implementation and its animations. -// I am now also disabling `file_length` rule because the dragging algorithm (with UX) is complex. -// swiftlint:disable file_length type_body_length // - TODO: EditorTabView drop-outside event handler. struct EditorTabs: View { @@ -22,12 +18,12 @@ struct EditorTabs: View { /// The workspace document. @EnvironmentObject private var workspace: Workspace - @EnvironmentObject private var editor: Editor + @EnvironmentObject var editor: Editor /// The tab id of current dragging tab. /// /// It will be `nil` when there is no tab dragged currently. - @State private var draggingTabId: TabID? + @State var draggingTabId: TabID? @State private var onDragTabId: TabID? @@ -35,38 +31,38 @@ struct EditorTabs: View { /// /// When there is no tab being dragged, it will be `nil`. /// - TODO: Check if I can use `value.startLocation` trustfully. - @State private var draggingStartLocation: CGFloat? + @State var draggingStartLocation: CGFloat? /// The last location of dragging. /// /// This is used to determine the dragging direction. /// - TODO: Check if I can use `value.translation` instead. - @State private var draggingLastLocation: CGFloat? + @State var draggingLastLocation: CGFloat? /// Current opened tabs. /// /// This is a copy of `workspace.selectionState.openedTabs`. /// I am making a copy of it because using state will hugely improve the dragging performance. /// Updating ObservedObject too often will generate lags. - @State private var openedTabs: [TabID] = [] + @State var openedTabs: [TabID] = [] /// A map of tab width. /// /// All width are measured dynamically (so it can also fit the Xcode tab bar style). /// This is used to be added on the offset of current dragging tab in order to make a smooth /// dragging experience. - @State private var tabWidth: [TabID: CGFloat] = [:] + @State var tabWidth: [TabID: CGFloat] = [:] /// A map of tab location (CGRect). /// /// All locations are measured dynamically. /// This is used to compute when we should swap two tabs based on current cursor location. - @State private var tabLocations: [TabID: CGRect] = [:] + @State var tabLocations: [TabID: CGRect] = [:] /// A map of tab offsets. /// /// This is used to determine the tab offset of every tab (by their tab id) while dragging. - @State private var tabOffsets: [TabID: CGFloat] = [:] + @State var tabOffsets: [TabID: CGFloat] = [:] /// This state is used to detect if the mouse is hovering over tabs. /// If it is true, then we do not update the expected tab width immediately. @@ -74,7 +70,7 @@ struct EditorTabs: View { /// This state is used to detect if the dragging type should be changed from DragGesture to OnDrag. /// It is basically switched when vertical displacement is exceeding the threshold. - @State private var shouldOnDrag: Bool = false + @State var shouldOnDrag: Bool = false /// Is current `onDrag` over tabs? /// @@ -89,130 +85,12 @@ struct EditorTabs: View { /// It can be used on reordering algorithm of `onDrag` (detecting when should we switch two tabs). @State private var onDragLastLocation: CGPoint? - @State private var closeButtonGestureActive: Bool = false + @State var closeButtonGestureActive: Bool = false @State private var scrollOffset: CGFloat = 0 @State private var scrollTrailingOffset: CGFloat? = 0 - // Disable the rule because this function is implementing the drag gesture and its animations. - // It is fairly complicated, so ignore the function body length limitation for now. - // swiftlint:disable function_body_length cyclomatic_complexity - private func makeTabDragGesture(id: TabID) -> some Gesture { - return DragGesture(minimumDistance: 2, coordinateSpace: .global) - .onChanged({ value in - if closeButtonGestureActive { - return - } - - if draggingTabId != id { - shouldOnDrag = false - draggingTabId = id - draggingStartLocation = value.startLocation.x - draggingLastLocation = value.location.x - } - // TODO: Enable this code snippet when re-enabling dragging-out behavior. - // I disabled (1 == 0) this behavior for now as dragging-out behavior isn't allowed. - if 1 == 0 && abs(value.location.y - value.startLocation.y) > EditorTabBarView.height { - shouldOnDrag = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { - shouldOnDrag = false - draggingStartLocation = nil - draggingLastLocation = nil - draggingTabId = nil - withAnimation(.easeInOut(duration: 0.25)) { - // Clean the tab offsets. - tabOffsets = [:] - } - }) - return - } - // Get the current cursor location. - let currentLocation = value.location.x - guard let startLocation = draggingStartLocation, - let currentIndex = openedTabs.firstIndex(of: id), - let currentTabWidth = tabWidth[id], - let lastLocation = draggingLastLocation - else { return } - let dragDifference = currentLocation - lastLocation - let previousIndex = currentIndex > 0 ? currentIndex - 1 : nil - let nextIndex = currentIndex < openedTabs.count - 1 ? currentIndex + 1 : nil - tabOffsets[id] = currentLocation - startLocation - // Interacting with the previous tab. - if previousIndex != nil && dragDifference < 0 { - // Wrap `previousTabIndex` because it may be `nil`. - guard let previousTabIndex = previousIndex, - let previousTabLocation = tabLocations[openedTabs[previousTabIndex]], - let previousTabWidth = tabWidth[openedTabs[previousTabIndex]] - else { return } - if currentLocation < max( - previousTabLocation.maxX - previousTabWidth * 0.1, - previousTabLocation.minX + currentTabWidth * 0.9 - ) { - let changing = previousTabWidth - 1 // One offset for overlapping divider. - draggingStartLocation! -= changing - withAnimation { - tabOffsets[id]! += changing - openedTabs.move( - fromOffsets: IndexSet(integer: previousTabIndex), - toOffset: currentIndex + 1 - ) - } - return - } - } - // Interacting with the next tab. - if nextIndex != nil && dragDifference > 0 { - // Wrap `previousTabIndex` because it may be `nil`. - guard let nextTabIndex = nextIndex, - let nextTabLocation = tabLocations[openedTabs[nextTabIndex]], - let nextTabWidth = tabWidth[openedTabs[nextTabIndex]] - else { return } - if currentLocation > min( - nextTabLocation.minX + nextTabWidth * 0.1, - nextTabLocation.maxX - currentTabWidth * 0.9 - ) { - let changing = nextTabWidth - 1 // One offset for overlapping divider. - draggingStartLocation! += changing - withAnimation { - tabOffsets[id]! -= changing - openedTabs.move( - fromOffsets: IndexSet(integer: nextTabIndex), - toOffset: currentIndex - ) - } - return - } - } - // Only update the last dragging location when there is enough offset. - if draggingLastLocation == nil || abs(value.location.x - draggingLastLocation!) >= 10 { - draggingLastLocation = value.location.x - } - }) - .onEnded({ _ in - shouldOnDrag = false - draggingStartLocation = nil - draggingLastLocation = nil - withAnimation(.easeInOut(duration: 0.25)) { - tabOffsets = [:] - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - draggingTabId = nil - } - // Sync the workspace's `openedTabs` 150ms after animation is finished. - // In order to avoid the lag due to the update of workspace state. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.40) { - if draggingStartLocation == nil { - editor.tabs = .init(openedTabs.compactMap { id in - editor.tabs.first { $0.file.id == id } - }) - // workspace.reorderedTabs(openedTabs: openedTabs) - // TODO: Fix save state - } - } - }) - } - private func makeTabItemGeometryReader(id: TabID) -> some View { GeometryReader { tabItemGeoReader in Rectangle() @@ -241,8 +119,6 @@ struct EditorTabs: View { openedTabs = editor.tabs.map(\.file.id) } - // swiftlint:enable function_body_length cyclomatic_complexity - var body: some View { GeometryReader { geometryProxy in TrackableScrollView( @@ -382,63 +258,4 @@ struct EditorTabs: View { } } } - - private struct EditorTabOnDropDelegate: DropDelegate { - private let currentTabId: TabID - @Binding private var openedTabs: [TabID] - @Binding private var onDragTabId: TabID? - @Binding private var onDragLastLocation: CGPoint? - @Binding private var isOnDragOverTabs: Bool - @Binding private var tabWidth: [TabID: CGFloat] - - public init( - currentTabId: TabID, - openedTabs: Binding<[TabID]>, - onDragTabId: Binding, - onDragLastLocation: Binding, - isOnDragOverTabs: Binding, - tabWidth: Binding<[TabID: CGFloat]> - ) { - self.currentTabId = currentTabId - self._openedTabs = openedTabs - self._onDragTabId = onDragTabId - self._onDragLastLocation = onDragLastLocation - self._isOnDragOverTabs = isOnDragOverTabs - self._tabWidth = tabWidth - } - - func dropEntered(info: DropInfo) { - isOnDragOverTabs = true - guard let onDragTabId, - currentTabId != onDragTabId, - let from = openedTabs.firstIndex(of: onDragTabId), - let toIndex = openedTabs.firstIndex(of: currentTabId) - else { return } - if openedTabs[toIndex] != onDragTabId { - withAnimation { - openedTabs.move( - fromOffsets: IndexSet(integer: from), - toOffset: toIndex > from ? toIndex + 1 : toIndex - ) - } - } - } - - func dropExited(info: DropInfo) { - // Do nothing. - } - - func dropUpdated(info: DropInfo) -> DropProposal? { - return DropProposal(operation: .move) - } - - func performDrop(info: DropInfo) -> Bool { - isOnDragOverTabs = false - onDragTabId = nil - onDragLastLocation = nil - return true - } - } } - -// swiftlint:enable file_length type_body_length From 96bd69b3182bca2c4f7c216a3d916224bf63e2f4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 15:10:14 +0200 Subject: [PATCH 007/335] Refactor: Split SourceControlManager by domain and extract GitClientProtocol Split the monolithic SourceControlManager+GitClient.swift (300 lines) into focused domain extensions: BranchOperations, StashOperations, RemoteOperations, FileOperations, Repository, and Alerts. Extract GitClientProtocol to decouple SourceControlManager from the concrete GitClient class. The manager now depends on the protocol abstraction, enabling mock-based testing without hitting the shell. --- .../HistoryInspectorModel.swift | 1 + .../SourceControlNavigatorHistoryView.swift | 2 + .../SourceControl/Client/GitClient.swift | 2 +- .../Client/GitClientProtocol.swift | 63 ++++ .../SourceControlManager+Alerts.swift | 53 ++++ ...ourceControlManager+BranchOperations.swift | 57 ++++ .../SourceControlManager+FileOperations.swift | 134 ++++++++ .../SourceControlManager+GitClient.swift | 300 ------------------ ...ourceControlManager+RemoteOperations.swift | 90 ++++++ .../SourceControlManager+Repository.swift | 24 ++ ...SourceControlManager+StashOperations.swift | 39 +++ .../SourceControl/SourceControlManager.swift | 64 ++-- 12 files changed, 482 insertions(+), 347 deletions(-) create mode 100644 CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift delete mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+GitClient.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index bb5ff4b8b3..a1c639b405 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -41,6 +41,7 @@ final class HistoryInspectorModel: ObservableObject { let commitHistory = try await sourceControlManager .gitClient .getCommitHistory( + branchName: nil, maxCount: 40, fileLocalPath: fileURL, showMergeCommits: Settings.shared.preferences.sourceControl.git.showMergeCommitsPerFileLog diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index 50898899f3..482a379945 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -33,6 +33,8 @@ struct SourceControlNavigatorHistoryView: View { .gitClient .getCommitHistory( branchName: sourceControlManager.currentBranch?.name, + maxCount: nil, + fileLocalPath: nil, showMergeCommits: Settings.shared.preferences.sourceControl.git.showMergeCommitsPerFileLog ) await MainActor.run { diff --git a/CodeEdit/Features/SourceControl/Client/GitClient.swift b/CodeEdit/Features/SourceControl/Client/GitClient.swift index eb1643cfbf..aa7178cb0b 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient.swift @@ -9,7 +9,7 @@ import Combine import Foundation import OSLog -class GitClient { +class GitClient: GitClientProtocol { enum GitClientError: Error { case outputError(String) case notGitRepository diff --git a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift b/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift new file mode 100644 index 0000000000..5b61e6a6c4 --- /dev/null +++ b/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift @@ -0,0 +1,63 @@ +// +// GitClientProtocol.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 07.04.26. +// + +import Foundation + +/// Abstraction over git operations used by ``SourceControlManager``. +/// +/// This protocol decouples the manager from the concrete ``GitClient`` class, +/// enabling mock-based testing without hitting the shell. +protocol GitClientProtocol { + // MARK: - Repository + + func validate() async -> Bool + func initiate() async throws + + // MARK: - Branches + + func getBranches(remote: String?) async throws -> [GitBranch] + func getCurrentBranch() async throws -> GitBranch? + func checkoutBranch(_ branch: GitBranch, forceLocal: Bool, newName: String?) async throws + func renameBranch(oldName: String, newName: String) async throws + func deleteBranch(_ branch: GitBranch) async throws + + // MARK: - Stash + + func stash(message: String?) async throws + func stashList() async throws -> [GitStashEntry] + func applyStashEntry(_ index: Int?) async throws + func deleteStashEntry(_ index: Int) async throws + + // MARK: - History + + func getCommitHistory( + branchName: String?, + maxCount: Int?, + fileLocalPath: String?, + showMergeCommits: Bool + ) async throws -> [GitCommit] + + // MARK: - Status & Files + + func getStatus() async throws -> GitClient.Status + func commit(message: String, details: String?) async throws + func add(_ files: [URL]) async throws + func reset(_ files: [URL]) async throws + func numberOfUnsyncedCommits() async throws -> (ahead: Int, behind: Int) + func getCommitChangedFiles(commitSHA: String) async throws -> [GitChangedFile] + func discardChanges(for file: URL) async throws + func discardAllChanges() async throws + + // MARK: - Remotes + + func getRemotes() async throws -> [GitRemote] + func addRemote(name: String, location: String) async throws + func removeRemote(name: String) async throws + func fetchFromRemote() async throws + func pullFromRemote(remote: String?, branch: String?, rebase: Bool) async throws + func pushToRemote(remote: String?, branch: String?, setUpstream: Bool?, force: Bool?, tags: Bool?) async throws +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift b/CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift new file mode 100644 index 0000000000..d62ee54a7a --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift @@ -0,0 +1,53 @@ +// +// SourceControlManager+Alerts.swift +// CodeEdit +// +// Created by Nanashi Li on 2022/05/20. +// + +import AppKit + +/// Alert presentation helpers for source control error handling. +extension SourceControlManager { + /// Show alert for error + func showAlertForError(title: String, error: Error) async { + if let error = error as? GitClient.GitClientError { + await showAlert(title: title, message: error.description) + return + } + + if let error = error as? LocalizedError { + var description = error.errorDescription ?? "" + if let failureReason = error.failureReason { + if description.isEmpty { + description += failureReason + } else { + description += "\n\n" + failureReason + } + } + + if let recoverySuggestion = error.recoverySuggestion { + if description.isEmpty { + description += recoverySuggestion + } else { + description += "\n\n" + recoverySuggestion + } + } + + await showAlert(title: title, message: description) + } else { + await showAlert(title: title, message: error.localizedDescription) + } + } + + func showAlert(title: String, message: String) async { + await MainActor.run { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + alert.runModal() + } + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift new file mode 100644 index 0000000000..0f8ca53b4d --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift @@ -0,0 +1,57 @@ +// +// SourceControlManager+BranchOperations.swift +// CodeEdit +// +// Created by Austin Condiff on 7/2/24. +// + +import Foundation + +/// Branch-related git operations. +extension SourceControlManager { + /// Refresh current branch + func refreshCurrentBranch() async { + let currentBranch = try? await gitClient.getCurrentBranch() + await MainActor.run { + self.currentBranch = currentBranch + } + } + + /// Refresh branches + func refreshBranches() async { + let branches = (try? await gitClient.getBranches(remote: nil)) ?? [] + await MainActor.run { + self.branches = branches + } + } + + /// Checkout branch + func checkoutBranch(branch: GitBranch) async throws { + try await gitClient.checkoutBranch(branch, forceLocal: false, newName: nil) + await refreshBranches() + await refreshCurrentBranch() + } + + /// Create new branch, can be created only from local branch + func newBranch(name: String, from: GitBranch) async throws { + try await gitClient.checkoutBranch(from, forceLocal: false, newName: name) + await refreshBranches() + await refreshCurrentBranch() + } + + /// Rename branch + func renameBranch(oldName: String, newName: String) async throws { + try await gitClient.renameBranch(oldName: oldName, newName: newName) + await refreshBranches() + } + + /// Delete branch if it's local and not current + func deleteBranch(branch: GitBranch) async throws { + if !branch.isLocal || branch == currentBranch { + return + } + + try await gitClient.deleteBranch(branch) + await refreshBranches() + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift new file mode 100644 index 0000000000..2e4d6f330c --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift @@ -0,0 +1,134 @@ +// +// SourceControlManager+FileOperations.swift +// CodeEdit +// +// Created by Austin Condiff on 7/2/24. +// + +import Foundation + +/// File status, staging, committing, and discard operations. +extension SourceControlManager { + /// Refresh all changed files and refresh status in file manager + func refreshAllChangedFiles() async { + do { + let status = try await gitClient.getStatus() + + // TODO: Unmerged changes + // status.unmergedChanges + + await setChangedFiles(status.changedFiles + status.untrackedFiles) + await refreshStatusInFileManager() + } catch GitClient.GitClientError.notGitRepository { + await setChangedFiles([]) + } catch { + logger.error("Error fetching git status: \(error)") + await setChangedFiles([]) + } + } + + /// Get all changed files for a commit + func getCommitChangedFiles(commitSHA: String) async -> [GitChangedFile] { + do { + return try await gitClient.getCommitChangedFiles(commitSHA: commitSHA) + } catch { + logger.error("Error committing changed files: \(error)") + return [] + } + } + + /// Commit files selected by user + func commit(message: String, details: String? = nil) async throws { + try await gitClient.commit(message: message, details: details) + + await self.refreshAllChangedFiles() + await self.refreshNumberOfUnsyncedCommits() + } + + /// Adds the given URLs to the staged changes. + /// - Parameter files: The files to stage. + func add(_ files: [URL]) async throws { + try await gitClient.add(files) + } + + /// Removes the given URLs from the staged changes. + /// - Parameter files: The URLs to un-stage. + func reset(_ files: [URL]) async throws { + try await gitClient.reset(files) + } + + /// Refresh number of unsynced commits + func refreshNumberOfUnsyncedCommits() async { + let numberOfUnpushedCommits = (try? await gitClient.numberOfUnsyncedCommits()) ?? (ahead: 0, behind: 0) + + await MainActor.run { + self.numberOfUnsyncedCommits = numberOfUnpushedCommits + } + } + + /// Discard changes for file + func discardChanges(for file: URL) { + Task { + do { + try await gitClient.discardChanges(for: file) + // TODO: Refresh content of active and unmodified document, + // requires CodeEditSourceEditor changes + } catch { + logger.error("Failed to discard changes for file (\(file.lastPathComponent): \(error)") + await showAlertForError(title: "Failed to discard changes", error: error) + } + } + } + + /// Discard changes for repository + func discardAllChanges() { + Task { + do { + try await gitClient.discardAllChanges() + // TODO: Refresh content of active and unmodified document, + // requires CodeEditSourceEditor changes + } catch { + logger.error("Failed to discard changes: \(error)") + await showAlertForError(title: "Failed to discard changes", error: error) + } + } + } + + /// Set changed files on main actor + @MainActor + private func setChangedFiles(_ files: [GitChangedFile]) { + self.changedFiles = files + } + + /// Refresh git status for files in project navigator + @MainActor + private func refreshStatusInFileManager() { + guard let fileManager = fileManager else { + return + } + + var updatedStatusFor: Set = [] + // Refresh status of file manager files + for changedFile in changedFiles { + guard let file = fileManager.getFile(changedFile.ceFileKey) else { + continue + } + if file.gitStatus != changedFile.anyStatus() { + file.gitStatus = changedFile.anyStatus() + } + updatedStatusFor.insert(file) + } + + for (_, file) in fileManager.flattenedFileItems + where !updatedStatusFor.contains(file) && file.gitStatus != nil { + file.gitStatus = nil + updatedStatusFor.insert(file) + } + + if updatedStatusFor.isEmpty { + return + } + + fileManager.notifyObservers(updatedItems: updatedStatusFor) + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+GitClient.swift b/CodeEdit/Features/SourceControl/SourceControlManager+GitClient.swift deleted file mode 100644 index d912cfa0df..0000000000 --- a/CodeEdit/Features/SourceControl/SourceControlManager+GitClient.swift +++ /dev/null @@ -1,300 +0,0 @@ -// -// SourceControlManager+GitClient.swift -// CodeEdit -// -// Created by Austin Condiff on 7/2/24. -// - -import Foundation - -extension SourceControlManager { - /// Validate repository - func validate() async throws { - let isGitRepository = await gitClient.validate() - await MainActor.run { - self.isGitRepository = isGitRepository - } - } - - /// Fetch from remote - func fetch() async throws { - try await gitClient.fetchFromRemote() - await self.refreshNumberOfUnsyncedCommits() - } - - /// Refresh current branch - func refreshCurrentBranch() async { - let currentBranch = try? await gitClient.getCurrentBranch() - await MainActor.run { - self.currentBranch = currentBranch - } - } - - /// Refresh branches - func refreshBranches() async { - let branches = (try? await gitClient.getBranches()) ?? [] - await MainActor.run { - self.branches = branches - } - } - - /// Checkout branch - func checkoutBranch(branch: GitBranch) async throws { - try await gitClient.checkoutBranch(branch) - await refreshBranches() - await refreshCurrentBranch() - } - - /// Create new branch, can be created only from local branch - func newBranch(name: String, from: GitBranch) async throws { - try await gitClient.checkoutBranch(from, newName: name) - await refreshBranches() - await refreshCurrentBranch() - } - - /// Rename branch - func renameBranch(oldName: String, newName: String) async throws { - try await gitClient.renameBranch(oldName: oldName, newName: newName) - await refreshBranches() - } - - /// Delete branch if it's local and not current - func deleteBranch(branch: GitBranch) async throws { - if !branch.isLocal || branch == currentBranch { - return - } - - try await gitClient.deleteBranch(branch) - await refreshBranches() - } - - /// Delete stash entry - func deleteStashEntry(stashEntry: GitStashEntry) async throws { - try await gitClient.deleteStashEntry(stashEntry.index) - try await refreshStashEntries() - } - - /// Apply stash entry - func applyStashEntry(stashEntry: GitStashEntry) async throws { - try await gitClient.applyStashEntry(stashEntry.index) - try await refreshStashEntries() - await refreshAllChangedFiles() - } - - /// Stash changes - func stashChanges(message: String?) async throws { - try await gitClient.stash(message: message) - try await refreshStashEntries() - await refreshAllChangedFiles() - } - - /// Delete remote - func deleteRemote(remote: GitRemote) async throws { - try await gitClient.removeRemote(name: remote.name) - try await refreshRemotes() - } - - /// Discard changes for file - func discardChanges(for file: URL) { - Task { - do { - try await gitClient.discardChanges(for: file) - // TODO: Refresh content of active and unmodified document, - // requires CodeEditSourceEditor changes - } catch { - logger.error("Failed to discard changes for file (\(file.lastPathComponent): \(error)") - await showAlertForError(title: "Failed to discard changes", error: error) - } - } - } - - /// Discard changes for repository - func discardAllChanges() { - Task { - do { - try await gitClient.discardAllChanges() - // TODO: Refresh content of active and unmodified document, - // requires CodeEditSourceEditor changes - } catch { - logger.error("Failed to discard changes: \(error)") - await showAlertForError(title: "Failed to discard changes", error: error) - } - } - } - - /// Set changed files on main actor - @MainActor - private func setChangedFiles(_ files: [GitChangedFile]) { - self.changedFiles = files - } - - /// Refresh git status for files in project navigator - @MainActor - private func refreshStatusInFileManager() { - guard let fileManager = fileManager else { - return - } - - var updatedStatusFor: Set = [] - // Refresh status of file manager files - for changedFile in changedFiles { - guard let file = fileManager.getFile(changedFile.ceFileKey) else { - continue - } - if file.gitStatus != changedFile.anyStatus() { - file.gitStatus = changedFile.anyStatus() - } - updatedStatusFor.insert(file) - } - - for (_, file) in fileManager.flattenedFileItems - where !updatedStatusFor.contains(file) && file.gitStatus != nil { - file.gitStatus = nil - updatedStatusFor.insert(file) - } - - if updatedStatusFor.isEmpty { - return - } - - fileManager.notifyObservers(updatedItems: updatedStatusFor) - } - - /// Refresh all changed files and refresh status in file manager - func refreshAllChangedFiles() async { - do { - let status = try await gitClient.getStatus() - - // TODO: Unmerged changes - // status.unmergedChanges - - await setChangedFiles(status.changedFiles + status.untrackedFiles) - await refreshStatusInFileManager() - } catch GitClient.GitClientError.notGitRepository { - await setChangedFiles([]) - } catch { - logger.error("Error fetching git status: \(error)") - await setChangedFiles([]) - } - } - - /// Get all changed files for a commit - func getCommitChangedFiles(commitSHA: String) async -> [GitChangedFile] { - do { - return try await gitClient.getCommitChangedFiles(commitSHA: commitSHA) - } catch { - logger.error("Error committing changed files: \(error)") - return [] - } - } - - /// Commit files selected by user - func commit(message: String, details: String? = nil) async throws { - try await gitClient.commit(message: message, details: details) - - await self.refreshAllChangedFiles() - await self.refreshNumberOfUnsyncedCommits() - } - - /// Adds the given URLs to the staged changes. - /// - Parameter files: The files to stage. - func add(_ files: [URL]) async throws { - try await gitClient.add(files) - } - - /// Removes the given URLs from the staged changes. - /// - Parameter files: The URLs to un-stage. - func reset(_ files: [URL]) async throws { - try await gitClient.reset(files) - } - - /// Refresh number of unsynced commits - func refreshNumberOfUnsyncedCommits() async { - let numberOfUnpushedCommits = (try? await gitClient.numberOfUnsyncedCommits()) ?? (ahead: 0, behind: 0) - - await MainActor.run { - self.numberOfUnsyncedCommits = numberOfUnpushedCommits - } - } - - /// Add existing remote to git - func addRemote(name: String, location: String) async throws { - try await gitClient.addRemote(name: name, location: location) - try await refreshRemotes() - } - - /// Get all remotes - func refreshRemotes() async throws { - let remotes = (try? await gitClient.getRemotes()) ?? [] - await MainActor.run { - self.remotes = remotes - } - if !remotes.isEmpty { - try await self.refreshAllRemotesBranches() - } - } - - /// Refresh branches for all remotes - func refreshAllRemotesBranches() async throws { - for remote in remotes { - try await refreshRemoteBranches(remote: remote) - } - } - - /// Refresh branches for a specific remote - func refreshRemoteBranches(remote: GitRemote) async throws { - let branches = try await getRemoteBranches(remote: remote.name) - if let index = remotes.firstIndex(of: remote) { - await MainActor.run { - remotes[index].branches = branches - } - } - - } - - /// Get branches for a specific remote - func getRemoteBranches(remote: String) async throws -> [GitBranch] { - try await gitClient.getBranches(remote: remote) - } - - func refreshStashEntries() async throws { - let stashEntries = (try? await gitClient.stashList()) ?? [] - await MainActor.run { - self.stashEntries = stashEntries - } - } - - /// Pull changes from remote - func pull(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { - try await gitClient.pullFromRemote(remote: remote, branch: branch, rebase: rebase) - - await self.refreshNumberOfUnsyncedCommits() - } - - /// Push changes to remote - func push( - remote: String? = nil, - branch: String? = nil, - setUpstream: Bool = false, - force: Bool = false, - tags: Bool = false - ) async throws { - guard currentBranch != nil else { return } - - try await gitClient.pushToRemote( - remote: remote, - branch: branch, - setUpstream: setUpstream, - force: force, - tags: tags - ) - - await refreshCurrentBranch() - await self.refreshNumberOfUnsyncedCommits() - } - - /// Initiate repository - func initiate() async throws { - try await gitClient.initiate() - } -} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift new file mode 100644 index 0000000000..7075381be3 --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift @@ -0,0 +1,90 @@ +// +// SourceControlManager+RemoteOperations.swift +// CodeEdit +// +// Created by Austin Condiff on 7/2/24. +// + +import Foundation + +/// Remote, fetch, pull, and push operations. +extension SourceControlManager { + /// Fetch from remote + func fetch() async throws { + try await gitClient.fetchFromRemote() + await self.refreshNumberOfUnsyncedCommits() + } + + /// Pull changes from remote + func pull(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { + try await gitClient.pullFromRemote(remote: remote, branch: branch, rebase: rebase) + await self.refreshNumberOfUnsyncedCommits() + } + + /// Push changes to remote + func push( + remote: String? = nil, + branch: String? = nil, + setUpstream: Bool = false, + force: Bool = false, + tags: Bool = false + ) async throws { + guard currentBranch != nil else { return } + + try await gitClient.pushToRemote( + remote: remote, + branch: branch, + setUpstream: setUpstream, + force: force, + tags: tags + ) + + await refreshCurrentBranch() + await self.refreshNumberOfUnsyncedCommits() + } + + /// Get all remotes + func refreshRemotes() async throws { + let remotes = (try? await gitClient.getRemotes()) ?? [] + await MainActor.run { + self.remotes = remotes + } + if !remotes.isEmpty { + try await self.refreshAllRemotesBranches() + } + } + + /// Refresh branches for all remotes + func refreshAllRemotesBranches() async throws { + for remote in remotes { + try await refreshRemoteBranches(remote: remote) + } + } + + /// Refresh branches for a specific remote + func refreshRemoteBranches(remote: GitRemote) async throws { + let branches = try await getRemoteBranches(remote: remote.name) + if let index = remotes.firstIndex(of: remote) { + await MainActor.run { + remotes[index].branches = branches + } + } + } + + /// Get branches for a specific remote + func getRemoteBranches(remote: String) async throws -> [GitBranch] { + try await gitClient.getBranches(remote: remote) + } + + /// Add existing remote to git + func addRemote(name: String, location: String) async throws { + try await gitClient.addRemote(name: name, location: location) + try await refreshRemotes() + } + + /// Delete remote + func deleteRemote(remote: GitRemote) async throws { + try await gitClient.removeRemote(name: remote.name) + try await refreshRemotes() + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift b/CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift new file mode 100644 index 0000000000..860c9b58c8 --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift @@ -0,0 +1,24 @@ +// +// SourceControlManager+Repository.swift +// CodeEdit +// +// Created by Austin Condiff on 7/2/24. +// + +import Foundation + +/// Repository-level git operations. +extension SourceControlManager { + /// Validate repository + func validate() async throws { + let isGitRepository = await gitClient.validate() + await MainActor.run { + self.isGitRepository = isGitRepository + } + } + + /// Initiate repository + func initiate() async throws { + try await gitClient.initiate() + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift new file mode 100644 index 0000000000..d770e9253b --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift @@ -0,0 +1,39 @@ +// +// SourceControlManager+StashOperations.swift +// CodeEdit +// +// Created by Austin Condiff on 7/2/24. +// + +import Foundation + +/// Stash-related git operations. +extension SourceControlManager { + /// Refresh stash entries + func refreshStashEntries() async throws { + let stashEntries = (try? await gitClient.stashList()) ?? [] + await MainActor.run { + self.stashEntries = stashEntries + } + } + + /// Stash changes + func stashChanges(message: String?) async throws { + try await gitClient.stash(message: message) + try await refreshStashEntries() + await refreshAllChangedFiles() + } + + /// Apply stash entry + func applyStashEntry(stashEntry: GitStashEntry) async throws { + try await gitClient.applyStashEntry(stashEntry.index) + try await refreshStashEntries() + await refreshAllChangedFiles() + } + + /// Delete stash entry + func deleteStashEntry(stashEntry: GitStashEntry) async throws { + try await gitClient.deleteStashEntry(stashEntry.index) + try await refreshStashEntries() + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index 9a42cf7f1c..4da16056ae 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -6,15 +6,21 @@ // import Foundation -import AppKit import OSLog -/// This class is used to perform git functions such as fetch, pull, add/remove of changes, commit, push, etc. -/// It also stores remotes, branches, current changes, stashes, and commits +/// Stores git state for the workspace and delegates operations to ``GitClient``. +/// +/// Git operations are organized across domain-specific extensions: +/// - `+BranchOperations`: checkout, create, rename, delete branches +/// - `+StashOperations`: stash, apply, delete stash entries +/// - `+RemoteOperations`: fetch, pull, push, remote management +/// - `+FileOperations`: status, staging, commit, discard +/// - `+Repository`: validate, initiate +/// - `+Alerts`: error presentation helpers final class SourceControlManager: ObservableObject { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "SourceControlManager") - let gitClient: GitClient + let gitClient: GitClientProtocol /// The base URL of the workspace let workspaceURL: URL @@ -22,6 +28,8 @@ final class SourceControlManager: ObservableObject { let editorManager: EditorManager weak var fileManager: CEWorkspaceFileManager? + // MARK: - Git State + /// A list of changed files @Published var changedFiles: [GitChangedFile] = [] @@ -43,6 +51,8 @@ final class SourceControlManager: ObservableObject { /// Is project a git repository @Published var isGitRepository: Bool = false + // MARK: - UI Presentation State + /// Is the push sheet presented @Published var pushSheetIsPresented: Bool = false { didSet { @@ -105,6 +115,8 @@ final class SourceControlManager: ObservableObject { /// Is no changes to discard alert presented @Published var noChangesToDiscardAlertIsPresented: Bool = false + // MARK: - Computed Properties + var orderedLocalBranches: [GitBranch] { var orderedBranches: [GitBranch] = [currentBranch].compactMap { $0 } let otherBranches = branches.filter { $0.isLocal && $0 != currentBranch } @@ -113,6 +125,8 @@ final class SourceControlManager: ObservableObject { return orderedBranches } + // MARK: - Initialization + init( workspaceURL: URL, editorManager: EditorManager @@ -121,46 +135,4 @@ final class SourceControlManager: ObservableObject { self.editorManager = editorManager gitClient = GitClient(directoryURL: workspaceURL, shellClient: currentWorld.shellClient) } - - /// Show alert for error - func showAlertForError(title: String, error: Error) async { - if let error = error as? GitClient.GitClientError { - await showAlert(title: title, message: error.description) - return - } - - if let error = error as? LocalizedError { - var description = error.errorDescription ?? "" - if let failureReason = error.failureReason { - if description.isEmpty { - description += failureReason - } else { - description += "\n\n" + failureReason - } - } - - if let recoverySuggestion = error.recoverySuggestion { - if description.isEmpty { - description += recoverySuggestion - } else { - description += "\n\n" + recoverySuggestion - } - } - - await showAlert(title: title, message: description) - } else { - await showAlert(title: title, message: error.localizedDescription) - } - } - - private func showAlert(title: String, message: String) async { - await MainActor.run { - let alert = NSAlert() - alert.messageText = title - alert.informativeText = message - alert.addButton(withTitle: "OK") - alert.alertStyle = .warning - alert.runModal() - } - } } From 68b0ddacf6785bf72fb19749447591f8787a2193 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 15:19:38 +0200 Subject: [PATCH 008/335] Refactor: Decompose NotificationPanelViewModel into focused extensions Split NotificationPanelViewModel.swift (313 lines) into focused files: - +TimerManagement: auto-hide scheduling, pause/resume - +Visibility: panel show/hide transitions, focus handling - +NotificationHandling: insertion, dismissal, event handling - +Toolbar: dynamic toolbar item management Main file retains only properties and lifecycle (73 lines). --- ...nPanelViewModel+NotificationHandling.swift | 117 ++++++++ ...cationPanelViewModel+TimerManagement.swift | 58 ++++ .../NotificationPanelViewModel+Toolbar.swift | 35 +++ ...otificationPanelViewModel+Visibility.swift | 80 ++++++ .../NotificationPanelViewModel.swift | 271 ++---------------- 5 files changed, 306 insertions(+), 255 deletions(-) create mode 100644 CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift create mode 100644 CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift create mode 100644 CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift create mode 100644 CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift new file mode 100644 index 0000000000..4757c11233 --- /dev/null +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift @@ -0,0 +1,117 @@ +// +// NotificationPanelViewModel+NotificationHandling.swift +// CodeEdit +// +// Created by Austin Condiff on 2/14/24. +// + +import SwiftUI + +/// Notification insertion, dismissal, and event handling. +extension NotificationPanelViewModel { + /// Inserts a notification in the correct position (sticky notifications on top) + func insertNotification(_ notification: CENotification) { + if notification.isSticky { + // Find the first sticky notification (to insert before it) + if let firstStickyIndex = activeNotifications.firstIndex(where: { $0.isSticky }) { + // Insert at the very start of sticky group + activeNotifications.insert(notification, at: firstStickyIndex) + } else { + // No sticky notifications yet, insert at the start + activeNotifications.insert(notification, at: 0) + } + } else { + // Find the first non-sticky notification + if let firstNonStickyIndex = activeNotifications.firstIndex(where: { !$0.isSticky }) { + // Insert at the start of non-sticky group + activeNotifications.insert(notification, at: firstNonStickyIndex) + } else { + // No non-sticky notifications yet, append at the end + activeNotifications.append(notification) + } + } + } + + /// Handles a new notification being added + func handleNewNotification(_ notification: CENotification) { + let operation = { + self.insertNotification(notification) + self.hiddenNotificationIds.remove(notification.id) + if !self.isPresented && !notification.isSticky { + self.startHideTimer(for: notification) + } + } + + if #available(macOS 26, *) { + withAnimation(.easeInOut(duration: 0.3), operation) { + self.updateToolbarItem() + } + } else { + withAnimation(.easeInOut(duration: 0.3), operation) + } + } + + /// Dismisses a specific notification + func dismissNotification(_ notification: CENotification, disableAnimation: Bool = false) { + // Clean up timers + timers[notification.id]?.invalidate() + timers[notification.id] = nil + hiddenNotificationIds.remove(notification.id) + + // Mark as being dismissed for animation + if let index = activeNotifications.firstIndex(where: { $0.id == notification.id }) { + if disableAnimation { + self.activeNotifications.removeAll(where: { $0.id == notification.id }) + NotificationManager.shared.markAsRead(notification) + NotificationManager.shared.dismissNotification(notification) + return + } + + var dismissingNotification = activeNotifications[index] + dismissingNotification.isBeingDismissed = true + activeNotifications[index] = dismissingNotification + + // Wait for fade animation before removing + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + withAnimation(.easeOut(duration: 0.2)) { + self.activeNotifications.removeAll(where: { $0.id == notification.id }) + if self.activeNotifications.isEmpty && self.isPresented { + self.isPresented = false + } + } + + NotificationManager.shared.markAsRead(notification) + NotificationManager.shared.dismissNotification(notification) + } + } + } + + @objc + func handleNewNotificationAdded(_ notification: Notification) { + guard let ceNotification = notification.object as? CENotification else { return } + handleNewNotification(ceNotification) + } + + @objc + func handleNotificationRemoved(_ notification: Notification) { + guard let ceNotification = notification.object as? CENotification else { return } + + let operation: () -> Void = { + self.activeNotifications.removeAll(where: { $0.id == ceNotification.id }) + + // If this was the last notification and they were manually shown, hide the panel + if self.activeNotifications.isEmpty && self.isPresented { + self.isPresented = false + } + } + + // Just remove from active notifications without triggering global state changes + if #available(macOS 26, *) { + withAnimation(.easeOut(duration: 0.2), operation) { + self.updateToolbarItem() + } + } else { + withAnimation(.easeOut(duration: 0.2), operation) + } + } +} diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift new file mode 100644 index 0000000000..2be98ac4cb --- /dev/null +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift @@ -0,0 +1,58 @@ +// +// NotificationPanelViewModel+TimerManagement.swift +// CodeEdit +// +// Created by Austin Condiff on 2/14/24. +// + +import SwiftUI + +/// Auto-hide timer scheduling, pausing, and resuming. +extension NotificationPanelViewModel { + /// Starts the timer to automatically hide a notification + func startHideTimer(for notification: CENotification) { + guard !notification.isSticky && !isPresented else { return } + + timers[notification.id]?.invalidate() + timers[notification.id] = nil + + guard !isPaused else { return } + + timers[notification.id] = Timer.scheduledTimer( + withTimeInterval: displayDuration, + repeats: false + ) { [weak self] _ in + guard let self = self else { return } + self.timers[notification.id] = nil + + // Ensure we're on the main thread and animate the change + DispatchQueue.main.async { + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.3 + context.allowsImplicitAnimation = true + + withAnimation(.easeInOut(duration: 0.3)) { + var newHiddenIds = self.hiddenNotificationIds + newHiddenIds.insert(notification.id) + self.hiddenNotificationIds = newHiddenIds + } + } + } + } + } + + /// Pauses all auto-hide timers + func pauseTimer() { + isPaused = true + timers.values.forEach { $0.invalidate() } + } + + /// Resumes all auto-hide timers + func resumeTimer() { + isPaused = false + // Only restart timers for notifications that are currently visible + activeNotifications + .filter { !$0.isSticky && isNotificationVisible($0) } + .forEach { startHideTimer(for: $0) } + } +} diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift new file mode 100644 index 0000000000..19e6565430 --- /dev/null +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift @@ -0,0 +1,35 @@ +// +// NotificationPanelViewModel+Toolbar.swift +// CodeEdit +// +// Created by Austin Condiff on 2/14/24. +// + +import AppKit + +/// Dynamic toolbar item management for the notification badge. +extension NotificationPanelViewModel { + func updateToolbarItem() { + if #available(macOS 15.0, *) { + guard let windowController, let toolbar = windowController.window?.toolbar else { + return + } + + let shouldShow = !self.visibleNotifications.isEmpty || NotificationManager.shared.unreadCount > 0 + if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { + guard let activityItemIdx = toolbar.items + .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { + return + } + toolbar.insertItem(withItemIdentifier: .space, at: activityItemIdx + 1) + toolbar.insertItem(withItemIdentifier: .notificationItem, at: activityItemIdx + 2) + } + + if !shouldShow, let index = toolbar.items + .firstIndex(where: { $0.itemIdentifier == .notificationItem }) { + toolbar.removeItem(at: index) + toolbar.removeItem(at: index) + } + } + } +} diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift new file mode 100644 index 0000000000..ababa01abe --- /dev/null +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift @@ -0,0 +1,80 @@ +// +// NotificationPanelViewModel+Visibility.swift +// CodeEdit +// +// Created by Austin Condiff on 2/14/24. +// + +import SwiftUI + +/// Panel visibility control, focus handling, and show/hide transitions. +extension NotificationPanelViewModel { + /// Whether a notification should be visible in the panel + func isNotificationVisible(_ notification: CENotification) -> Bool { + if notification.isBeingDismissed { + return true // Always show notifications being dismissed + } + if notification.isSticky { + return true // Always show sticky notifications + } + if isPresented { + return true // Show all notifications when manually shown + } + return !hiddenNotificationIds.contains(notification.id) + } + + /// Handles focus changes for the notification panel + func handleFocusChange(isFocused: Bool) { + if !isFocused { + // Only hide if manually shown and focus is completely lost + if isPresented { + toggleNotificationsVisibility() + } + } + } + + /// Toggles visibility of notifications in the panel + func toggleNotificationsVisibility() { + if isPresented { + if !scrolledToTop { + // Just set isPresented to false to trigger the offset animation + withAnimation(.easeInOut(duration: 0.3)) { + isPresented = false + } + + // After the slide-out animation, hide notifications + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + // Hide non-sticky notifications + self.activeNotifications + .filter { !$0.isSticky } + .forEach { self.hiddenNotificationIds.insert($0.id) } + self.objectWillChange.send() + + // After notifications are hidden, reset scroll position + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.scrolledToTop = true + } + } + } else { + // At top, just hide normally + hideNotifications() + } + } else { + withAnimation(.easeInOut(duration: 0.3)) { + isPresented = true + hiddenNotificationIds.removeAll() + objectWillChange.send() + } + } + } + + func hideNotifications() { + withAnimation(.easeInOut(duration: 0.3)) { + self.isPresented = false + self.activeNotifications + .filter { !$0.isSticky } + .forEach { self.hiddenNotificationIds.insert($0.id) } + self.objectWillChange.send() + } + } +} diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift index c07f0f2a3b..a1f037588b 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -7,28 +7,35 @@ import SwiftUI +/// Coordinates notification display, auto-hide timers, panel visibility, and toolbar integration. +/// +/// Methods are organized across focused extensions: +/// - `+TimerManagement`: auto-hide scheduling, pause/resume +/// - `+Visibility`: panel show/hide, focus handling +/// - `+NotificationHandling`: insertion, dismissal, event handling +/// - `+Toolbar`: dynamic toolbar item management final class NotificationPanelViewModel: ObservableObject { /// Currently displayed notifications in the panel - @Published private(set) var activeNotifications: [CENotification] = [] + @Published internal(set) var activeNotifications: [CENotification] = [] /// Whether notifications panel was manually shown via toolbar - @Published private(set) var isPresented: Bool = false + @Published internal(set) var isPresented: Bool = false /// Set of hidden notification IDs - @Published private(set) var hiddenNotificationIds: Set = [] + @Published internal(set) var hiddenNotificationIds: Set = [] + + @Published var scrolledToTop: Bool = true /// Timers for notifications - private var timers: [UUID: Timer] = [:] + var timers: [UUID: Timer] = [:] /// Display duration for notifications - private let displayDuration: TimeInterval = 5.0 + let displayDuration: TimeInterval = 5.0 /// Whether notifications are paused - private var isPaused: Bool = false + var isPaused: Bool = false - private var notificationManager = NotificationManager.shared - - @Published var scrolledToTop: Bool = true + var notificationManager = NotificationManager.shared /// A filtered list of active notifications. var visibleNotifications: [CENotification] { @@ -37,223 +44,6 @@ final class NotificationPanelViewModel: ObservableObject { weak var windowController: NSWindowController? - /// Whether a notification should be visible in the panel - func isNotificationVisible(_ notification: CENotification) -> Bool { - if notification.isBeingDismissed { - return true // Always show notifications being dismissed - } - if notification.isSticky { - return true // Always show sticky notifications - } - if isPresented { - return true // Show all notifications when manually shown - } - return !hiddenNotificationIds.contains(notification.id) - } - - /// Handles focus changes for the notification panel - func handleFocusChange(isFocused: Bool) { - if !isFocused { - // Only hide if manually shown and focus is completely lost - if isPresented { - toggleNotificationsVisibility() - } - } - } - - /// Toggles visibility of notifications in the panel - func toggleNotificationsVisibility() { - if isPresented { - if !scrolledToTop { - // Just set isPresented to false to trigger the offset animation - withAnimation(.easeInOut(duration: 0.3)) { - isPresented = false - } - - // After the slide-out animation, hide notifications - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - // Hide non-sticky notifications - self.activeNotifications - .filter { !$0.isSticky } - .forEach { self.hiddenNotificationIds.insert($0.id) } - self.objectWillChange.send() - - // After notifications are hidden, reset scroll position - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.scrolledToTop = true - } - } - } else { - // At top, just hide normally - hideNotifications() - } - } else { - withAnimation(.easeInOut(duration: 0.3)) { - isPresented = true - hiddenNotificationIds.removeAll() - objectWillChange.send() - } - } - } - - private func hideNotifications() { - withAnimation(.easeInOut(duration: 0.3)) { - self.isPresented = false - self.activeNotifications - .filter { !$0.isSticky } - .forEach { self.hiddenNotificationIds.insert($0.id) } - self.objectWillChange.send() - } - } - - /// Starts the timer to automatically hide a notification - func startHideTimer(for notification: CENotification) { - guard !notification.isSticky && !isPresented else { return } - - timers[notification.id]?.invalidate() - timers[notification.id] = nil - - guard !isPaused else { return } - - timers[notification.id] = Timer.scheduledTimer( - withTimeInterval: displayDuration, - repeats: false - ) { [weak self] _ in - guard let self = self else { return } - self.timers[notification.id] = nil - - // Ensure we're on the main thread and animate the change - DispatchQueue.main.async { - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.3 - context.allowsImplicitAnimation = true - - withAnimation(.easeInOut(duration: 0.3)) { - var newHiddenIds = self.hiddenNotificationIds - newHiddenIds.insert(notification.id) - self.hiddenNotificationIds = newHiddenIds - } - } - } - } - } - - /// Pauses all auto-hide timers - func pauseTimer() { - isPaused = true - timers.values.forEach { $0.invalidate() } - } - - /// Resumes all auto-hide timers - func resumeTimer() { - isPaused = false - // Only restart timers for notifications that are currently visible - activeNotifications - .filter { !$0.isSticky && isNotificationVisible($0) } - .forEach { startHideTimer(for: $0) } - } - - /// Inserts a notification in the correct position (sticky notifications on top) - private func insertNotification(_ notification: CENotification) { - if notification.isSticky { - // Find the first sticky notification (to insert before it) - if let firstStickyIndex = activeNotifications.firstIndex(where: { $0.isSticky }) { - // Insert at the very start of sticky group - activeNotifications.insert(notification, at: firstStickyIndex) - } else { - // No sticky notifications yet, insert at the start - activeNotifications.insert(notification, at: 0) - } - } else { - // Find the first non-sticky notification - if let firstNonStickyIndex = activeNotifications.firstIndex(where: { !$0.isSticky }) { - // Insert at the start of non-sticky group - activeNotifications.insert(notification, at: firstNonStickyIndex) - } else { - // No non-sticky notifications yet, append at the end - activeNotifications.append(notification) - } - } - } - - /// Handles a new notification being added - func handleNewNotification(_ notification: CENotification) { - let operation = { - self.insertNotification(notification) - self.hiddenNotificationIds.remove(notification.id) - if !self.isPresented && !notification.isSticky { - self.startHideTimer(for: notification) - } - } - - if #available(macOS 26, *) { - withAnimation(.easeInOut(duration: 0.3), operation) { - self.updateToolbarItem() - } - } else { - withAnimation(.easeInOut(duration: 0.3), operation) - } - } - - /// Dismisses a specific notification - func dismissNotification(_ notification: CENotification, disableAnimation: Bool = false) { - // Clean up timers - timers[notification.id]?.invalidate() - timers[notification.id] = nil - hiddenNotificationIds.remove(notification.id) - - // Mark as being dismissed for animation - if let index = activeNotifications.firstIndex(where: { $0.id == notification.id }) { - if disableAnimation { - self.activeNotifications.removeAll(where: { $0.id == notification.id }) - NotificationManager.shared.markAsRead(notification) - NotificationManager.shared.dismissNotification(notification) - return - } - - var dismissingNotification = activeNotifications[index] - dismissingNotification.isBeingDismissed = true - activeNotifications[index] = dismissingNotification - - // Wait for fade animation before removing - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - withAnimation(.easeOut(duration: 0.2)) { - self.activeNotifications.removeAll(where: { $0.id == notification.id }) - if self.activeNotifications.isEmpty && self.isPresented { - self.isPresented = false - } - } - - NotificationManager.shared.markAsRead(notification) - NotificationManager.shared.dismissNotification(notification) - } - } - } - - func updateToolbarItem() { - if #available(macOS 15.0, *) { - guard let windowController, let toolbar = windowController.window?.toolbar else { - return - } - - let shouldShow = !self.visibleNotifications.isEmpty || NotificationManager.shared.unreadCount > 0 - if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { - guard let activityItemIdx = toolbar.items - .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { - return - } - toolbar.insertItem(withItemIdentifier: .space, at: activityItemIdx + 1) - toolbar.insertItem(withItemIdentifier: .notificationItem, at: activityItemIdx + 2) - } - - if !shouldShow, let index = toolbar.items - .firstIndex(where: { $0.itemIdentifier == .notificationItem }) { - toolbar.removeItem(at: index) - toolbar.removeItem(at: index) - } - } - } - init() { // Observe new notifications NotificationCenter.default.addObserver( @@ -280,33 +70,4 @@ final class NotificationPanelViewModel: ObservableObject { deinit { NotificationCenter.default.removeObserver(self) } - - @objc - private func handleNewNotificationAdded(_ notification: Notification) { - guard let ceNotification = notification.object as? CENotification else { return } - handleNewNotification(ceNotification) - } - - @objc - private func handleNotificationRemoved(_ notification: Notification) { - guard let ceNotification = notification.object as? CENotification else { return } - - let operation: () -> Void = { - self.activeNotifications.removeAll(where: { $0.id == ceNotification.id }) - - // If this was the last notification and they were manually shown, hide the panel - if self.activeNotifications.isEmpty && self.isPresented { - self.isPresented = false - } - } - - // Just remove from active notifications without triggering global state changes - if #available(macOS 26, *) { - withAnimation(.easeOut(duration: 0.2), operation) { - self.updateToolbarItem() - } - } else { - withAnimation(.easeOut(duration: 0.2), operation) - } - } } From a60956cacc1a175f6663ca41bdacccbe74070f9f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 17:02:35 +0200 Subject: [PATCH 009/335] Refactor: Add CodableDefault property wrapper to eliminate decoder boilerplate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce @CodableDefault property wrapper that automatically falls back to a default value when a key is missing from JSON. This eliminates the need for hand-written init(from:) decoders in settings models. Convert TerminalSettings as proof of concept — removes 15-line custom decoder entirely. Remaining settings models can be converted following the same pattern. --- .../Models/CodableDefault+Providers.swift | 32 ++++++++++ .../Settings/Models/CodableDefault.swift | 62 +++++++++++++++++++ .../Models/TerminalSettings.swift | 39 ++++-------- 3 files changed, 105 insertions(+), 28 deletions(-) create mode 100644 CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift create mode 100644 CodeEdit/Features/Settings/Models/CodableDefault.swift diff --git a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift new file mode 100644 index 0000000000..ebd6f284c7 --- /dev/null +++ b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift @@ -0,0 +1,32 @@ +// +// CodableDefault+Providers.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 07.04.26. +// + +import AppKit + +// MARK: - Bool Defaults + +enum DefaultTrue: DefaultValueProvider { + static let defaultValue = true +} + +enum DefaultFalse: DefaultValueProvider { + static let defaultValue = false +} + +// MARK: - Terminal Defaults + +enum DefaultTerminalShell: DefaultValueProvider { + static let defaultValue = SettingsData.TerminalShell.system +} + +enum DefaultTerminalCursorStyle: DefaultValueProvider { + static let defaultValue = SettingsData.TerminalCursorStyle.block +} + +enum DefaultTerminalFont: DefaultValueProvider { + static let defaultValue = SettingsData.TerminalFont() +} diff --git a/CodeEdit/Features/Settings/Models/CodableDefault.swift b/CodeEdit/Features/Settings/Models/CodableDefault.swift new file mode 100644 index 0000000000..e20f664bbc --- /dev/null +++ b/CodeEdit/Features/Settings/Models/CodableDefault.swift @@ -0,0 +1,62 @@ +// +// CodableDefault.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 07.04.26. +// + +import Foundation + +/// A type that provides a default value for a ``CodableDefault`` property wrapper. +/// +/// Conform to this protocol to define a default value that will be used +/// when decoding fails or the key is missing from the JSON. +protocol DefaultValueProvider { + associatedtype Value: Codable & Hashable + static var defaultValue: Value { get } +} + +/// A property wrapper that provides a default value when decoding from JSON +/// and the key is missing or the value cannot be decoded. +/// +/// Usage: +/// ```swift +/// struct MySettings: Codable, Hashable { +/// @CodableDefault var isEnabled: Bool +/// @CodableDefault var isHidden: Bool +/// } +/// ``` +/// +/// With this wrapper, you no longer need a custom `init(from:)` for handling +/// missing keys — Swift's auto-synthesized decoder handles it automatically. +@propertyWrapper +struct CodableDefault: Codable, Hashable { + var wrappedValue: Provider.Value + + init(wrappedValue: Provider.Value) { + self.wrappedValue = wrappedValue + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = (try? container.decode(Provider.Value.self)) ?? Provider.defaultValue + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} + +// MARK: - KeyedDecodingContainer Support + +/// When a key is missing from the JSON, return the provider's default value +/// instead of throwing a `DecodingError.keyNotFound`. +extension KeyedDecodingContainer { + func decode( + _ type: CodableDefault

.Type, + forKey key: Key + ) throws -> CodableDefault

{ + (try? decodeIfPresent(type, forKey: key)) ?? CodableDefault(wrappedValue: P.defaultValue) + } +} diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift index 38f18bb316..d0844f03c6 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift @@ -28,57 +28,40 @@ extension SettingsData { } /// If true terminal will use editor theme. - var useEditorTheme: Bool = true + @CodableDefault var useEditorTheme = true /// If true terminal appearance will always be `dark`. Otherwise it adapts to the system setting. - var darkAppearance: Bool = false + @CodableDefault var darkAppearance = false /// If true, the terminal uses the background color of the theme, otherwise it is clear - var useThemeBackground: Bool = true + @CodableDefault var useThemeBackground = true /// If true, the terminal treats the `Option` key as the `Meta` key - var optionAsMeta: Bool = false + @CodableDefault var optionAsMeta = false /// The selected shell to use. - var shell: TerminalShell = .system + @CodableDefault var shell: TerminalShell = .system /// The font to use in terminal. - var font: TerminalFont = .init() + @CodableDefault var font: TerminalFont = .init() // The cursor style to use in terminal - var cursorStyle: TerminalCursorStyle = .block + @CodableDefault var cursorStyle: TerminalCursorStyle = .block // Toggle for blinking cursor or not - var cursorBlink: Bool = false + @CodableDefault var cursorBlink = false // Use font settings from Text Editing - var useTextEditorFont: Bool = true + @CodableDefault var useTextEditorFont = true /// If `true`, use injection scripts for terminal features like automatic tab title. - var useShellIntegration: Bool = true + @CodableDefault var useShellIntegration = true /// If `true`, use a login shell. - var useLoginShell: Bool = true + @CodableDefault var useLoginShell = true /// 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.darkAppearance = try container.decodeIfPresent(Bool.self, forKey: .darkAppearance) ?? false - self.optionAsMeta = try container.decodeIfPresent(Bool.self, forKey: .optionAsMeta) ?? false - self.shell = try container.decodeIfPresent(TerminalShell.self, forKey: .shell) ?? .system - self.font = try container.decodeIfPresent(TerminalFont.self, forKey: .font) ?? .init() - self.cursorStyle = try container.decodeIfPresent( - TerminalCursorStyle.self, - forKey: .cursorStyle - ) ?? .block - self.cursorBlink = try container.decodeIfPresent(Bool.self, forKey: .cursorBlink) ?? false - self.useTextEditorFont = try container.decodeIfPresent(Bool.self, forKey: .useTextEditorFont) ?? true - self.useShellIntegration = try container.decodeIfPresent(Bool.self, forKey: .useShellIntegration) ?? true - self.useLoginShell = try container.decodeIfPresent(Bool.self, forKey: .useLoginShell) ?? true - } } /// The shell options. From dd38ac74fb5706f5987cdd41b81ad791dfbaab4b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 18:04:34 +0200 Subject: [PATCH 010/335] Refactor: Convert 7 settings models to use @CodableDefault Apply CodableDefault property wrapper to eliminate hand-written init(from:) decoders in GeneralSettings (74 lines removed), NavigationSettings, SearchSettings, DeveloperSettings, AccountsSettings, GitAccounts, and LanguageServerSettings. Add default value providers for all enum/struct types used across these models. --- .../Models/CodableDefault+Providers.swift | 76 ++++++++++++ .../Models/AccountsSettings.swift | 22 +--- .../Models/DeveloperSettings.swift | 19 +-- .../Models/LanguageServerSettings.swift | 14 +-- .../Models/GeneralSettings.swift | 115 +++--------------- .../Models/NavigationSettings.swift | 10 +- .../Models/SearchSettings.swift | 12 +- 7 files changed, 104 insertions(+), 164 deletions(-) diff --git a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift index ebd6f284c7..048af86c1f 100644 --- a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift +++ b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift @@ -30,3 +30,79 @@ enum DefaultTerminalCursorStyle: DefaultValueProvider { enum DefaultTerminalFont: DefaultValueProvider { static let defaultValue = SettingsData.TerminalFont() } + +// MARK: - Navigation Defaults + +enum DefaultNavigationStyle: DefaultValueProvider { + static let defaultValue = SettingsData.NavigationStyle.openInTabs +} + +// MARK: - Collection Defaults + +enum DefaultEmptyGlobPatterns: DefaultValueProvider { + static let defaultValue: [GlobPattern] = [] +} + +enum DefaultEmptyStringDictionary: DefaultValueProvider { + static let defaultValue: [String: String] = [:] +} + +enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { + static let defaultValue: [String: SettingsData.InstalledLanguageServer] = [:] +} + +// MARK: - Account Defaults + +enum DefaultGitAccounts: DefaultValueProvider { + static let defaultValue = SettingsData.GitAccounts() +} + +enum DefaultEmptySourceControlAccounts: DefaultValueProvider { + static let defaultValue: [SourceControlAccount] = [] +} + +enum DefaultEmptyString: DefaultValueProvider { + static let defaultValue = "" +} + +// MARK: - General Settings Defaults + +enum DefaultAppearance: DefaultValueProvider { + static let defaultValue = SettingsData.Appearances.system +} + +enum DefaultIssues: DefaultValueProvider { + static let defaultValue = SettingsData.Issues.inline +} + +enum DefaultFileExtensionsVisibility: DefaultValueProvider { + static let defaultValue = SettingsData.FileExtensionsVisibility.showAll +} + +enum DefaultFileExtensions: DefaultValueProvider { + static let defaultValue = SettingsData.FileExtensions.default +} + +enum DefaultFileIconStyle: DefaultValueProvider { + static let defaultValue = SettingsData.FileIconStyle.color +} + +enum DefaultSidebarTabBarPositionTop: DefaultValueProvider { + static let defaultValue = SettingsData.SidebarTabBarPosition.top +} + +enum DefaultReopenBehavior: DefaultValueProvider { + static let defaultValue = SettingsData.ReopenBehavior.welcome +} + +enum DefaultReopenWindowBehavior: DefaultValueProvider { + static let defaultValue = SettingsData.ReopenWindowBehavior.doNothing +} + +enum DefaultProjectNavigatorSize: DefaultValueProvider { + static let defaultValue = SettingsData.ProjectNavigatorSize.medium +} + +enum DefaultNavigatorDetail: DefaultValueProvider { + static let defaultValue = SettingsData.NavigatorDetail.upTo3 +} diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift index ef8922187a..18ff529e6d 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift @@ -12,7 +12,7 @@ extension SettingsData { /// The global settings for source control accounts struct AccountsSettings: Codable, Hashable, SearchableSettingsPage { /// The list of git accounts the user has saved - var sourceControlAccounts: GitAccounts = .init() + @CodableDefault var sourceControlAccounts: GitAccounts = .init() /// The search keys var searchKeys: [String] { @@ -26,29 +26,15 @@ extension SettingsData { /// 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.sourceControlAccounts = try container.decodeIfPresent( - GitAccounts.self, - forKey: .sourceControlAccounts - ) ?? .init() - } } struct GitAccounts: Codable, Hashable { /// This id will store the account name as the identifiable - var gitAccounts: [SourceControlAccount] = [] + @CodableDefault var gitAccounts: [SourceControlAccount] = [] + + @CodableDefault var sshKey = "" - var sshKey: 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.gitAccounts = try container.decodeIfPresent([SourceControlAccount].self, forKey: .gitAccounts) ?? [] - self.sshKey = try container.decodeIfPresent(String.self, forKey: .sshKey) ?? "" - } } } diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift index f52f075385..2b6eb9d645 100644 --- a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift @@ -22,27 +22,12 @@ extension SettingsData { } /// A dictionary that stores a file type and a path to an LSP binary - var lspBinaries: [String: String] = [:] + @CodableDefault var lspBinaries: [String: String] = [:] /// Toggle for showing the internal development inspector - var showInternalDevelopmentInspector: Bool = false + @CodableDefault var showInternalDevelopmentInspector = 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.lspBinaries = try container.decodeIfPresent( - [String: String].self, - forKey: .lspBinaries - ) ?? [:] - - self.showInternalDevelopmentInspector = try container.decodeIfPresent( - Bool.self, - forKey: .showInternalDevelopmentInspector - ) ?? false - } } } diff --git a/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift b/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift index 9e65691a98..60a0765309 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift @@ -24,21 +24,11 @@ extension SettingsData { } /// Stores the currently installed language servers. The key is the name of the language server. + @CodableDefault var installedLanguageServers: [String: InstalledLanguageServer] = [:] /// Default initializer - init() { - self.installedLanguageServers = [:] - } - - /// 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.installedLanguageServers = try container.decodeIfPresent( - [String: InstalledLanguageServer].self, - forKey: .installedLanguageServers - ) ?? [:] - } + init() {} } struct InstalledLanguageServer: Codable, Hashable { diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift b/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift index 12e2375505..1e67727763 100644 --- a/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift +++ b/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift @@ -13,13 +13,13 @@ extension SettingsData { struct GeneralSettings: Codable, Hashable, SearchableSettingsPage { /// The appearance of the app - var appAppearance: Appearances = .system + @CodableDefault var appAppearance: Appearances = .system /// The show issues behavior of the app - var showIssues: Issues = .inline + @CodableDefault var showIssues: Issues = .inline /// The show live issues behavior of the app - var showLiveIssues: Bool = true + @CodableDefault var showLiveIssues = true /// The search keys var searchKeys: [String] { @@ -52,131 +52,52 @@ extension SettingsData { } /// Show editor jump bar - var showEditorJumpBar: Bool = true + @CodableDefault var showEditorJumpBar = true /// Dims editors without focus - var dimEditorsWithoutFocus: Bool = false + @CodableDefault var dimEditorsWithoutFocus = false /// The show file extensions behavior of the app - var fileExtensionsVisibility: FileExtensionsVisibility = .showAll + @CodableDefault var fileExtensionsVisibility: FileExtensionsVisibility = .showAll /// The file extensions collection to display - var shownFileExtensions: FileExtensions = .default + @CodableDefault var shownFileExtensions: FileExtensions = .default /// The file extensions collection to hide - var hiddenFileExtensions: FileExtensions = .default + @CodableDefault var hiddenFileExtensions: FileExtensions = .default /// The style for file icons - var fileIconStyle: FileIconStyle = .color + @CodableDefault var fileIconStyle: FileIconStyle = .color /// The position for the navigator sidebar tab bar - var navigatorTabBarPosition: SidebarTabBarPosition = .top + @CodableDefault var navigatorTabBarPosition: SidebarTabBarPosition = .top /// The position for the inspector sidebar tab bar - var inspectorTabBarPosition: SidebarTabBarPosition = .top + @CodableDefault var inspectorTabBarPosition: SidebarTabBarPosition = .top /// The reopen behavior of the app - var reopenBehavior: ReopenBehavior = .welcome + @CodableDefault var reopenBehavior: ReopenBehavior = .welcome /// Decides what the app does after a workspace is closed - var reopenWindowAfterClose: ReopenWindowBehavior = .doNothing + @CodableDefault var reopenWindowAfterClose: ReopenWindowBehavior = .doNothing /// The size of the project navigator - var projectNavigatorSize: ProjectNavigatorSize = .medium + @CodableDefault var projectNavigatorSize: ProjectNavigatorSize = .medium /// The Find Navigator Detail line limit - var findNavigatorDetail: NavigatorDetail = .upTo3 + @CodableDefault var findNavigatorDetail: NavigatorDetail = .upTo3 /// The Issue Navigator Detail line limit - var issueNavigatorDetail: NavigatorDetail = .upTo3 + @CodableDefault var issueNavigatorDetail: NavigatorDetail = .upTo3 /// The reveal file in navigator when focus changes behavior of the app. - var revealFileOnFocusChange: Bool = false + @CodableDefault var revealFileOnFocusChange = false /// Auto save behavior toggle - var isAutoSaveOn: Bool = true + @CodableDefault var isAutoSaveOn = true /// Default initializer init() {} - - // swiftlint:disable function_body_length - /// 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.appAppearance = try container.decodeIfPresent( - Appearances.self, - forKey: .appAppearance - ) ?? .system - self.showIssues = try container.decodeIfPresent( - Issues.self, - forKey: .showIssues - ) ?? .inline - self.showLiveIssues = try container.decodeIfPresent( - Bool.self, - forKey: .showLiveIssues - ) ?? true - self.showEditorJumpBar = try container.decodeIfPresent( - Bool.self, - forKey: .showEditorJumpBar - ) ?? true - self.dimEditorsWithoutFocus = try container.decodeIfPresent( - Bool.self, - forKey: .dimEditorsWithoutFocus - ) ?? false - self.fileExtensionsVisibility = try container.decodeIfPresent( - FileExtensionsVisibility.self, - forKey: .fileExtensionsVisibility - ) ?? .showAll - self.shownFileExtensions = try container.decodeIfPresent( - FileExtensions.self, - forKey: .shownFileExtensions - ) ?? .default - self.hiddenFileExtensions = try container.decodeIfPresent( - FileExtensions.self, - forKey: .hiddenFileExtensions - ) ?? .default - self.fileIconStyle = try container.decodeIfPresent( - FileIconStyle.self, - forKey: .fileIconStyle - ) ?? .color - self.navigatorTabBarPosition = try container.decodeIfPresent( - SidebarTabBarPosition.self, - forKey: .navigatorTabBarPosition - ) ?? .top - self.inspectorTabBarPosition = try container.decodeIfPresent( - SidebarTabBarPosition.self, - forKey: .inspectorTabBarPosition - ) ?? .top - self.reopenBehavior = try container.decodeIfPresent( - ReopenBehavior.self, - forKey: .reopenBehavior - ) ?? .welcome - self.reopenWindowAfterClose = try container.decodeIfPresent( - ReopenWindowBehavior.self, - forKey: .reopenWindowAfterClose - ) ?? .doNothing - self.projectNavigatorSize = try container.decodeIfPresent( - ProjectNavigatorSize.self, - forKey: .projectNavigatorSize - ) ?? .medium - self.findNavigatorDetail = try container.decodeIfPresent( - NavigatorDetail.self, - forKey: .findNavigatorDetail - ) ?? .upTo3 - self.issueNavigatorDetail = try container.decodeIfPresent( - NavigatorDetail.self, - forKey: .issueNavigatorDetail - ) ?? .upTo3 - self.revealFileOnFocusChange = try container.decodeIfPresent( - Bool.self, - forKey: .revealFileOnFocusChange - ) ?? false - self.isAutoSaveOn = try container.decodeIfPresent( - Bool.self, - forKey: .isAutoSaveOn - ) ?? true - } - // swiftlint:enable function_body_length } /// The appearance of the app diff --git a/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift b/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift index fa95f98d37..55a4f39e71 100644 --- a/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift +++ b/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift @@ -21,18 +21,10 @@ extension SettingsData { } /// Navigation style used - var navigationStyle: NavigationStyle = .openInTabs + @CodableDefault var navigationStyle: NavigationStyle = .openInTabs /// 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.navigationStyle = try container.decodeIfPresent( - NavigationStyle.self, forKey: .navigationStyle - ) ?? .openInTabs - } } enum NavigationStyle: String, Codable, Hashable { diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift index 6510d418c0..cc93d0bb1d 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift +++ b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift @@ -20,19 +20,9 @@ extension SettingsData { } /// List of Glob Patterns that determine which files or directories to ignore - var ignoreGlobPatterns: [GlobPattern] = .init() + @CodableDefault var ignoreGlobPatterns: [GlobPattern] = [] /// 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.ignoreGlobPatterns = try container.decodeIfPresent( - [GlobPattern].self, - forKey: .ignoreGlobPatterns - ) ?? [] - } } } From 558218037e1975ad089c93213202e9fde2d10060 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Apr 2026 18:48:55 +0200 Subject: [PATCH 011/335] Refactor: Separate Theme system into data model, repository, and UI layers Extract ThemeRepository for all file I/O operations (load, save, delete, rename, duplicate) from ThemeModel+CRUD. Extract color conversions (swiftColor, nsColor, editorTheme) from Theme.swift into Theme+Color. Extract export dialogs into ThemeModel+Export. ThemeModel+CRUD drops from 290 to 124 lines, now purely orchestrating repository calls with state updates. Theme.swift is now a pure data model with no UI framework imports. --- .../ThemeSettings/Models/Theme+Color.swift | 77 ++++++ .../Pages/ThemeSettings/Models/Theme.swift | 65 +---- .../Models/ThemeModel+CRUD.swift | 244 +++--------------- .../Models/ThemeModel+Export.swift | 63 +++++ .../ThemeSettings/Models/ThemeModel.swift | 58 +---- .../Models/ThemeRepository.swift | 186 +++++++++++++ 6 files changed, 374 insertions(+), 319 deletions(-) create mode 100644 CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift create mode 100644 CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift create mode 100644 CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift new file mode 100644 index 0000000000..42eb3be90a --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift @@ -0,0 +1,77 @@ +// +// Theme+Color.swift +// CodeEdit +// +// Created by Lukas Pistrol on 31.03.22. +// + +import SwiftUI +import CodeEditSourceEditor + +/// Color conversion extensions for Theme types. +/// These bridge between the hex string storage format and SwiftUI/AppKit color types. + +extension Theme.Attributes { + /// The `SwiftUI` color of ``color`` + var swiftColor: Color { + get { + Color(hex: color) + } + set { + self.color = newValue.hexString + } + } + + /// The `NSColor` of ``color`` + var nsColor: NSColor { + get { + NSColor(hex: color) + } + set { + self.color = newValue.hexString + } + } +} + +extension Theme.EditorColors { + var editorTheme: EditorTheme { + get { + .init( + text: .init(color: text.nsColor), + insertionPoint: insertionPoint.nsColor, + invisibles: .init(color: invisibles.nsColor), + background: background.nsColor, + lineHighlight: lineHighlight.nsColor, + selection: selection.nsColor, + keywords: .init(color: keywords.nsColor), + commands: .init(color: commands.nsColor), + types: .init(color: types.nsColor), + attributes: .init(color: attributes.nsColor), + variables: .init(color: variables.nsColor), + values: .init(color: values.nsColor), + numbers: .init(color: numbers.nsColor), + strings: .init(color: strings.nsColor), + characters: .init(color: characters.nsColor), + comments: .init(color: comments.nsColor) + ) + } + set { + self.text.nsColor = newValue.text.color + self.insertionPoint.nsColor = newValue.insertionPoint + self.invisibles.nsColor = newValue.invisibles.color + self.background.nsColor = newValue.background + self.lineHighlight.nsColor = newValue.lineHighlight + self.selection.nsColor = newValue.selection + self.keywords.nsColor = newValue.keywords.color + self.commands.nsColor = newValue.commands.color + self.types.nsColor = newValue.types.color + self.attributes.nsColor = newValue.attributes.color + self.variables.nsColor = newValue.variables.color + self.values.nsColor = newValue.values.color + self.numbers.nsColor = newValue.numbers.color + self.strings.nsColor = newValue.strings.color + self.characters.nsColor = newValue.characters.color + self.comments.nsColor = newValue.comments.color + } + } +} diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift index 749d1c1afd..77fc9bb86f 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift @@ -5,8 +5,7 @@ // Created by Lukas Pistrol on 31.03.22. // -import SwiftUI -import CodeEditSourceEditor +import Foundation // swiftlint:disable file_length @@ -146,74 +145,12 @@ extension Theme { case bold case italic } - - /// The `SwiftUI` of ``color`` - var swiftColor: Color { - get { - Color(hex: color) - } - set { - self.color = newValue.hexString - } - } - - /// The `NSColor` of ``color`` - var nsColor: NSColor { - get { - NSColor(hex: color) - } - set { - self.color = newValue.hexString - } - } } } extension Theme { /// The editor colors of the theme struct EditorColors: Codable, Hashable, Loopable { - - var editorTheme: EditorTheme { - get { - .init( - text: .init(color: text.nsColor), - insertionPoint: insertionPoint.nsColor, - invisibles: .init(color: invisibles.nsColor), - background: background.nsColor, - lineHighlight: lineHighlight.nsColor, - selection: selection.nsColor, - keywords: .init(color: keywords.nsColor), - commands: .init(color: commands.nsColor), - types: .init(color: types.nsColor), - attributes: .init(color: attributes.nsColor), - variables: .init(color: variables.nsColor), - values: .init(color: values.nsColor), - numbers: .init(color: numbers.nsColor), - strings: .init(color: strings.nsColor), - characters: .init(color: characters.nsColor), - comments: .init(color: comments.nsColor) - ) - } - set { - self.text.nsColor = newValue.text.color - self.insertionPoint.nsColor = newValue.insertionPoint - self.invisibles.nsColor = newValue.invisibles.color - self.background.nsColor = newValue.background - self.lineHighlight.nsColor = newValue.lineHighlight - self.selection.nsColor = newValue.selection - self.keywords.nsColor = newValue.keywords.color - self.commands.nsColor = newValue.commands.color - self.types.nsColor = newValue.types.color - self.attributes.nsColor = newValue.attributes.color - self.variables.nsColor = newValue.variables.color - self.values.nsColor = newValue.values.color - self.numbers.nsColor = newValue.numbers.color - self.strings.nsColor = newValue.strings.color - self.characters.nsColor = newValue.characters.color - self.comments.nsColor = newValue.comments.color - } - } - var text: Attributes var insertionPoint: Attributes var invisibles: Attributes diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 3c9e5e0936..16d66467e5 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -9,124 +9,32 @@ import SwiftUI import UniformTypeIdentifiers extension ThemeModel { - /// Loads a theme from a given url and appends it to ``themes``. - /// - Parameter url: The URL of the theme - /// - Returns: A ``Theme`` - private func load(from url: URL) throws -> Theme? { - do { - // get the data from the provided file - let json = try Data(contentsOf: url) - // decode the json into ``Theme`` - let theme = try JSONDecoder().decode(Theme.self, from: json) - return theme - } catch { - print(error) - return nil - } - } - - /// Loads all available themes from `~/Library/Application Support/CodeEdit/Themes/` - /// - /// If no themes are available, it will create a default theme and save - /// it to the location mentioned above. - /// - /// When overrides are found in `~/Library/Application Support/CodeEdit/settings.json` - /// they are applied to the loaded themes without altering the original - /// the files in `~/Library/Application Support/CodeEdit/Themes/`. + /// Loads all available themes from disk, applies overrides, and selects the initial theme. func loadThemes() throws { // swiftlint:disable:this function_body_length - if let bundledThemesURL = bundledThemesURL { - // remove all themes from memory - themes.removeAll() - - var isDir: ObjCBool = false - - // check if a themes directory exists, otherwise create one - if !filemanager.fileExists(atPath: themesURL.path, isDirectory: &isDir) { - try filemanager.createDirectory(at: themesURL, withIntermediateDirectories: true) - } - - // get all URLs in users themes folder that end with `.cetheme` - let userDefinedThemeFilenames = try filemanager.contentsOfDirectory(atPath: themesURL.path).filter { - $0.contains(".cetheme") - } - let userDefinedThemeURLs = userDefinedThemeFilenames.map { - themesURL.appending(path: $0) - } - - // get all bundled theme URLs - let bundledThemeFilenames = try filemanager.contentsOfDirectory(atPath: bundledThemesURL.path).filter { - $0.contains(".cetheme") - } - let bundledThemeURLs = bundledThemeFilenames.map { - bundledThemesURL.appending(path: $0) - } - - // combine user theme URLs with bundled theme URLs - let themeURLs = userDefinedThemeURLs + bundledThemeURLs - - let prefs = Settings.shared.preferences - - // load each theme from disk and store in memory - try themeURLs.forEach { fileURL in - if var theme = try load(from: fileURL) { - - // get all properties of terminal and editor colors - guard let terminalColors = try theme.terminal.allProperties() as? [String: Theme.Attributes], - let editorColors = try theme.editor.allProperties() as? [String: Theme.Attributes] - else { - print("error") - // TODO: Throw a proper error - throw NSError() // swiftlint:disable:this discouraged_direct_init - } - - // check if there are any overrides in `settings.json` - if let overrides = prefs.theme.overrides[theme.name]?["terminal"] { - terminalColors.forEach { (key, _) in - if let attributes = overrides[key] { - theme.terminal[key] = attributes - } - } - } - - if let overrides = prefs.theme.overrides[theme.name]?["editor"] { - editorColors.forEach { (key, _) in - if let attributes = overrides[key] { - theme.editor[key] = attributes - } - } - } - - theme.isBundled = fileURL.path.contains(bundledThemesURL.path) - - theme.fileURL = fileURL - - // add the theme to themes array - self.themes.append(theme) - - // if there already is a selected theme in `settings.json` select this theme - // otherwise take the first in the list - self.selectedDarkTheme = self.darkThemes.first { - $0.name == prefs.theme.selectedDarkTheme - } ?? self.darkThemes.first - - self.selectedLightTheme = self.lightThemes.first { - $0.name == prefs.theme.selectedLightTheme - } ?? self.lightThemes.first - - // For selecting the default theme, doing it correctly on startup requires some more logic - let userSelectedTheme = self.themes.first { $0.name == prefs.theme.selectedTheme } - let systemAppearance = NSAppearance.currentDrawing().name - - if userSelectedTheme != nil { - self.selectedTheme = userSelectedTheme - } else { - if systemAppearance == .darkAqua { - self.selectedTheme = self.selectedDarkTheme - } else { - self.selectedTheme = self.selectedLightTheme - } - } - } + themes.removeAll() + + let prefs = Settings.shared.preferences + themes = try repository.loadAllThemes(overrides: prefs.theme.overrides) + + // Select initial themes based on preferences + self.selectedDarkTheme = self.darkThemes.first { + $0.name == prefs.theme.selectedDarkTheme + } ?? self.darkThemes.first + + self.selectedLightTheme = self.lightThemes.first { + $0.name == prefs.theme.selectedLightTheme + } ?? self.lightThemes.first + + let userSelectedTheme = self.themes.first { $0.name == prefs.theme.selectedTheme } + let systemAppearance = NSAppearance.currentDrawing().name + + if userSelectedTheme != nil { + self.selectedTheme = userSelectedTheme + } else { + if systemAppearance == .darkAqua { + self.selectedTheme = self.selectedDarkTheme + } else { + self.selectedTheme = self.selectedLightTheme } } } @@ -153,53 +61,17 @@ extension ThemeModel { func duplicate(_ url: URL) { do { self.isAdding = true - // Construct the destination file URL - var destinationFileURL = self.themesURL.appending(path: url.lastPathComponent) - - // Extract the base filename and extension - let fileExtension = destinationFileURL.pathExtension - - var fileName = destinationFileURL.deletingPathExtension().lastPathComponent - var newFileName = fileName - - var iterator = 1 - let isBundled = url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") + let isBundledURL = bundledThemesURL?.absoluteString ?? "" let isImporting = - !url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") + !url.absoluteString.hasPrefix(isBundledURL) && !url.absoluteString.hasPrefix(themesURL.absoluteString) - if isBundled { - newFileName = "\(fileName) \(iterator)" - destinationFileURL = self.themesURL - .appending(path: newFileName) - .appendingPathExtension(fileExtension) - } - - // Check if the file already exists - while FileManager.default.fileExists(atPath: destinationFileURL.path) { - fileName = destinationFileURL.deletingPathExtension().lastPathComponent - - // Remove any existing iterator - if let range = fileName.range(of: " \\d+$", options: .regularExpression) { - fileName = String(fileName[.. Theme { + let json = try Data(contentsOf: url) + return try JSONDecoder().decode(Theme.self, from: json) + } + + /// Discovers and loads all themes from user and bundled directories. + /// Applies overrides from settings and marks bundled themes. + func loadAllThemes( + overrides: [String: [String: [String: Theme.Attributes]]] + ) throws -> [Theme] { + ensureThemesDirectoryExists() + + let userURLs = themeFileURLs(in: themesURL) + let bundledURLs = bundledThemesURL.map { themeFileURLs(in: $0) } ?? [] + let allURLs = userURLs + bundledURLs + + var themes: [Theme] = [] + for url in allURLs { + guard var theme = try? loadTheme(from: url) else { continue } + + applyOverrides(to: &theme, overrides: overrides) + theme.isBundled = bundledThemesURL.map { url.path.contains($0.path) } ?? false + theme.fileURL = url + themes.append(theme) + } + + return themes + } + + // MARK: - Saving + + /// Saves a theme to its file URL as pretty-printed JSON. + func save(_ theme: Theme) throws { + guard let fileURL = theme.fileURL else { return } + + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let data = try encoder.encode(theme) + let json = try JSONSerialization.jsonObject(with: data) + let prettyJSON = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) + try prettyJSON.write(to: fileURL, options: .atomic) + } + + // MARK: - Delete + + /// Removes a theme file from disk. + func delete(_ theme: Theme) throws { + guard let url = theme.fileURL else { return } + try fileManager.removeItem(at: url) + } + + // MARK: - Rename + + /// Moves a theme file to a new name, resolving conflicts. + /// Returns the new file URL. + func rename(_ theme: Theme, to newName: String, existingNames: [String]) throws -> URL { + guard let oldURL = theme.fileURL else { + throw NSError( + domain: "ThemeRepository", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Theme file URL not found"] + ) + } + + var finalName = newName + var finalURL = themesURL.appending(path: finalName).appendingPathExtension("cetheme") + var iterator = 1 + + while existingNames.contains(where: { $0 == finalName && finalName != theme.displayName }) { + finalName = "\(newName) \(iterator)" + finalURL = themesURL.appending(path: finalName).appendingPathExtension("cetheme") + iterator += 1 + } + + try fileManager.moveItem(at: oldURL, to: finalURL) + return finalURL + } + + // MARK: - Duplicate / Import + + /// Copies a theme file to the themes directory, resolving filename conflicts. + /// Returns the destination URL and resolved filename. + func duplicateFile(from sourceURL: URL) throws -> (url: URL, name: String) { + var destinationURL = themesURL.appending(path: sourceURL.lastPathComponent) + let fileExtension = destinationURL.pathExtension + var fileName = destinationURL.deletingPathExtension().lastPathComponent + var newFileName = fileName + var iterator = 1 + + let isBundled = bundledThemesURL.map { sourceURL.absoluteString.hasPrefix($0.absoluteString) } ?? false + + if isBundled { + newFileName = "\(fileName) \(iterator)" + destinationURL = themesURL + .appending(path: newFileName) + .appendingPathExtension(fileExtension) + } + + while fileManager.fileExists(atPath: destinationURL.path) { + fileName = destinationURL.deletingPathExtension().lastPathComponent + if let range = fileName.range(of: " \\d+$", options: .regularExpression) { + fileName = String(fileName[.. [URL] { + let filenames = (try? fileManager.contentsOfDirectory(atPath: directory.path)) ?? [] + return filenames + .filter { $0.hasSuffix(".cetheme") } + .map { directory.appending(path: $0) } + } + + private func applyOverrides( + to theme: inout Theme, + overrides: [String: [String: [String: Theme.Attributes]]] + ) { + guard let terminalColors = try? theme.terminal.allProperties() as? [String: Theme.Attributes], + let editorColors = try? theme.editor.allProperties() as? [String: Theme.Attributes] + else { return } + + if let terminalOverrides = overrides[theme.name]?["terminal"] { + for key in terminalColors.keys { + if let attributes = terminalOverrides[key] { + theme.terminal[key] = attributes + } + } + } + + if let editorOverrides = overrides[theme.name]?["editor"] { + for key in editorColors.keys { + if let attributes = editorOverrides[key] { + theme.editor[key] = attributes + } + } + } + } +} From 0360dbf09caee926c81f3f841d1ac3089a4f1024 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 11 Apr 2026 15:01:29 +0200 Subject: [PATCH 012/335] Refactored code to use the Factory dependency, for dependency injection, instead of the "in-house" depenency container for. That has been removed --- CodeEdit.xcodeproj/project.pbxproj | 25 +++++++ .../xcshareddata/swiftpm/Package.resolved | 11 +++- CodeEdit/AppDelegate.swift | 8 ++- CodeEdit/CodeEditApp.swift | 11 +--- CodeEdit/CodeEditContainer.swift | 18 +++++ .../CodeEditWindowController.swift | 3 +- .../SourceControlGitView.swift | 5 +- .../View/UtilityAreaOutputSourcePicker.swift | 3 +- .../Features/Welcome/GitCloneButton.swift | 5 +- CodeEdit/Features/Welcome/NewFileButton.swift | 3 +- .../Welcome/OpenFileOrFolderButton.swift | 3 +- .../WindowCommands/FileCommands.swift | 5 +- .../Utils/RecentProjectsMenu.swift | 3 +- .../Services/WorkspaceWindowManager.swift | 4 +- .../LazyServiceWrapper.swift | 36 ---------- .../ServiceContainer.swift | 65 ------------------- .../DependencyInjection/ServiceType.swift | 16 ----- .../DependencyInjection/ServiceWrapper.swift | 26 -------- .../Extensions/URL/URL+FindWorkspace.swift | 3 +- 19 files changed, 85 insertions(+), 168 deletions(-) create mode 100644 CodeEdit/CodeEditContainer.swift delete mode 100644 CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift delete mode 100644 CodeEdit/Utils/DependencyInjection/ServiceContainer.swift delete mode 100644 CodeEdit/Utils/DependencyInjection/ServiceType.swift delete mode 100644 CodeEdit/Utils/DependencyInjection/ServiceWrapper.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 2b2a39b04a..8f51960877 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; + 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; + 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5E4485602DF600D9008BBE69 /* AboutWindow */; }; @@ -192,6 +194,7 @@ 6C73A6D32D4F1E550012D95C /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, + 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */, 5EACE6222DF4BF08005E08B8 /* WelcomeWindow in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, @@ -205,6 +208,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */, 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -337,6 +341,7 @@ 6C76D6D32E15B91E00EF52C3 /* CodeEditSourceEditor */, 6CCF6DD22E26D48F00B94F75 /* SwiftTerm */, 6CCF73CF2E26DE3200B94F75 /* SwiftTerm */, + 58CF9F392F86D64F009F4AA7 /* Factory */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -361,6 +366,7 @@ name = CodeEditTests; packageProductDependencies = ( 583E529B29361BAB001AB554 /* SnapshotTesting */, + 58CF9F412F86D981009F4AA7 /* FactoryTesting */, ); productName = CodeEditTests; productReference = B658FB3D27DA9E1000EA4DBD /* CodeEditTests.xctest */; @@ -443,6 +449,7 @@ 5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */, 6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6CCF73CE2E26DE3200B94F75 /* XCRemoteSwiftPackageReference "SwiftTerm" */, + 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */, ); preferredProjectObjectVersion = 55; productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; @@ -1742,6 +1749,14 @@ minimumVersion = 1.14.2; }; }; + 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/hmlongco/Factory"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.5.3; + }; + }; 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; @@ -1874,6 +1889,16 @@ package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; productName = SnapshotTesting; }; + 58CF9F392F86D64F009F4AA7 /* Factory */ = { + isa = XCSwiftPackageProductDependency; + package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; + productName = Factory; + }; + 58CF9F412F86D981009F4AA7 /* FactoryTesting */ = { + isa = XCSwiftPackageProductDependency; + package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; + productName = FactoryTesting; + }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 835319d36b..63b20a4ac0 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "01191ca9685501db65981a6fd21ab2d11c32196633d4cb776b5bb25908ed212f", + "originHash" : "c4368adf5cf7353593e131deab35e74464b98a4f7755aea8cdd711a421976b22", "pins" : [ { "identity" : "aboutwindow", @@ -82,6 +82,15 @@ "version" : "0.4.2" } }, + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory", + "state" : { + "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", + "version" : "2.5.3" + } + }, { "identity" : "fseventswrapper", "kind" : "remoteSourceControl", diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index f0cbea6de8..d15ad87cfa 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory import CodeEditSymbols import CodeEditSourceEditor import OSLog @@ -18,8 +19,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @Environment(\.openWindow) var openWindow - @LazyService var lspService: LSPService - @LazyService var windowManager: WorkspaceWindowManager + @LazyInjected(\.lspService) + var lspService + + @LazyInjected(\.workspaceWindowManager) + var windowManager private var welcomeWindowObserver: NSObjectProtocol? diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 30885fbe33..0eadde5056 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory import WelcomeWindow import AboutWindow @@ -17,14 +18,6 @@ struct CodeEditApp: App { let updater: SoftwareUpdater = SoftwareUpdater() init() { - // Register singleton services before anything else - ServiceContainer.register( - LSPService() - ) - ServiceContainer.register( - WorkspaceWindowManager() - ) - NSMenuItem.swizzle() NSSplitViewItem.swizzle() } @@ -39,7 +32,7 @@ struct CodeEditApp: App { OpenFileOrFolderButton(dismissWindow: dismissWindow) }, onDrop: { url, dismissWindow in - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() Task { do { try windowManager.openWorkspace(at: url) diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift new file mode 100644 index 0000000000..6071ff958f --- /dev/null +++ b/CodeEdit/CodeEditContainer.swift @@ -0,0 +1,18 @@ +// +// CodeEditContainer.swift +// CodeEdit +// +// Created by CodeEdit Contributors on 08.04.26. +// + +import Factory + +extension Container { + var lspService: Factory { + self { @MainActor in LSPService() }.singleton + } + + var workspaceWindowManager: Factory { + self { @MainActor in WorkspaceWindowManager() }.singleton + } +} diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 8ce6de29b8..d529dd4a85 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -7,6 +7,7 @@ import Cocoa import SwiftUI +import Factory import Combine final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject, NSWindowDelegate { @@ -233,7 +234,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs // Notify the window manager to clean up workspace state if let workspace { - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.closeWorkspace(workspace) } workspace = nil diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index 6c3944ddbe..c4068af1fc 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct SourceControlGitView: View { @AppSettings(\.sourceControl.git) @@ -202,7 +203,7 @@ private extension SourceControlGitView { FileManager.default.createFile(atPath: fileURL.path, contents: nil) } - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: fileURL, onCompletion: {}) } @@ -217,7 +218,7 @@ private extension SourceControlGitView { } // Open the file in the editor - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: fileURL, onCompletion: {}) } catch { print("Failed to open document: \(error.localizedDescription)") diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index 70bc4cf39a..bdbdeee416 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct UtilityAreaOutputSourcePicker: View { typealias Sources = UtilityAreaOutputView.Sources @@ -19,7 +20,7 @@ struct UtilityAreaOutputSourcePicker: View { @ObservedObject var extensionManager = ExtensionManager.shared - @Service var lspService: LSPService + @Injected(\.lspService) var lspService @State private var updater: UUID = UUID() @State private var languageServerClients: [LSPService.LanguageServerType] = [] diff --git a/CodeEdit/Features/Welcome/GitCloneButton.swift b/CodeEdit/Features/Welcome/GitCloneButton.swift index 3565803e07..721ab9fe97 100644 --- a/CodeEdit/Features/Welcome/GitCloneButton.swift +++ b/CodeEdit/Features/Welcome/GitCloneButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory import WelcomeWindow struct GitCloneButton: View { @@ -29,7 +30,7 @@ struct GitCloneButton: View { showCheckoutBranchItem = url }, openDocument: { url in - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: url, onCompletion: { dismissWindow() }) } ) @@ -38,7 +39,7 @@ struct GitCloneButton: View { GitCheckoutBranchView( repoLocalPath: url, openDocument: { url in - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: url, onCompletion: { dismissWindow() }) } ) diff --git a/CodeEdit/Features/Welcome/NewFileButton.swift b/CodeEdit/Features/Welcome/NewFileButton.swift index 00ceed5227..a5cd1dfe0e 100644 --- a/CodeEdit/Features/Welcome/NewFileButton.swift +++ b/CodeEdit/Features/Welcome/NewFileButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory import WelcomeWindow struct NewFileButton: View { @@ -17,7 +18,7 @@ struct NewFileButton: View { iconName: "plus.square", title: "Create New File...", action: { - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.newDocumentFromPanel() dismissWindow() } diff --git a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift index 8814983038..9537328b96 100644 --- a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift +++ b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory import WelcomeWindow struct OpenFileOrFolderButton: View { @@ -20,7 +21,7 @@ struct OpenFileOrFolderButton: View { iconName: "folder", title: "Open File or Folder...", action: { - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocumentWithDialog( canChooseFiles: true, canChooseDirectories: true, diff --git a/CodeEdit/Features/WindowCommands/FileCommands.swift b/CodeEdit/Features/WindowCommands/FileCommands.swift index 8be55ca358..b1b923d95e 100644 --- a/CodeEdit/Features/WindowCommands/FileCommands.swift +++ b/CodeEdit/Features/WindowCommands/FileCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct FileCommands: Commands { static let recentProjectsMenu = RecentProjectsMenu() @@ -21,13 +22,13 @@ struct FileCommands: Commands { CommandGroup(replacing: .newItem) { Group { Button("New") { - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.newDocumentFromPanel() } .keyboardShortcut("n") Button("Open...") { - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocumentFromPanel() } .keyboardShortcut("o") diff --git a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift index d2fec045c2..5cc0cc1d76 100644 --- a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift +++ b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift @@ -6,6 +6,7 @@ // import AppKit +import Factory import WelcomeWindow @MainActor @@ -126,7 +127,7 @@ final class RecentProjectsMenu: NSObject, NSMenuDelegate { @objc private func recentProjectItemClicked(_ sender: NSMenuItem) { guard let projectURL = sender.representedObject as? URL else { return } - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: projectURL, onCompletion: {}) } diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 4784fe09d1..297fa11eaf 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -7,6 +7,7 @@ import AppKit import SwiftUI +import Factory import WelcomeWindow extension Notification.Name { @@ -17,7 +18,8 @@ extension Notification.Name { @MainActor final class WorkspaceWindowManager: WorkspaceWindowManaging { - @LazyService var lspService: LSPService + @LazyInjected(\.lspService) + var lspService /// All currently open workspaces. private(set) var openWorkspaces: [Workspace] = [] diff --git a/CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift b/CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift deleted file mode 100644 index b64dc7f61c..0000000000 --- a/CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// LazyServiceWrapper.swift -// CodeEdit -// -// Created by Khan Winter on 9/9/24. -// - -/// A property wrapper that provides lazily-loaded access to a service instance. -/// -/// Using this wrapper, the service is only resolved when the property is first accessed. -@propertyWrapper -struct LazyService { - private let type: ServiceType - private var service: Service? - - init(_ type: ServiceType = .singleton) { - self.type = type - } - - var wrappedValue: Service { - mutating get { - if let service { - return service - } else { - guard let resolvedService = ServiceContainer.resolve(type, Service.self) else { - let serviceName = String(describing: Service.self) - fatalError("No service of type \(serviceName) registered!") - } - self.service = resolvedService - return resolvedService - } - } mutating set { - self.service = newValue - } - } -} diff --git a/CodeEdit/Utils/DependencyInjection/ServiceContainer.swift b/CodeEdit/Utils/DependencyInjection/ServiceContainer.swift deleted file mode 100644 index 21234637a8..0000000000 --- a/CodeEdit/Utils/DependencyInjection/ServiceContainer.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// ServiceContainer.swift -// CodeEdit -// -// Created by Abe Malla on 4/3/24. -// - -import Foundation - -/// A service container that manages the registration and resolution of services. -enum ServiceContainer { - /// A dictionary storing the closures for creating service instances. - private static var factories: [ObjectIdentifier: () -> Any] = [:] - /// A dictionary storing the cached service instances. - private static var cache: [ObjectIdentifier: Any] = [:] - /// A dispatch queue used for synchronizing access to the factories and cache. - private static let queue = DispatchQueue(label: "ServiceContainerQueue") - - /// Registers a factory closure for creating instances of a service type. - /// - /// - Parameter factory: An autoclosure that returns an instance of the service type. - static func register(_ factory: @autoclosure @escaping () -> Service) { - queue.sync { - let key = ObjectIdentifier(Service.Type.self) - factories[key] = factory - } - } - - /// Resolves an instance of a service type based on the specified resolution type. - /// - /// - Parameters: - /// - resolveType: The type of resolution to use for the service. Defaults to `.singleton`. - /// - type: The type of the service to resolve. - /// - Returns: An instance of the resolved service type, or `nil` if the service is not registered. - static func resolve(_ resolveType: ServiceType = .singleton, _ type: Service.Type) -> Service? { - let serviceId = ObjectIdentifier(Service.Type.self) - - return queue.sync { - switch resolveType { - case .singleton: - if let service = cache[serviceId] as? Service { - return service - } else { - let service = factories[serviceId]?() as? Service - - if let service = service { - cache[serviceId] = service - } - - return service - } - case .newSingleton: - let service = factories[serviceId]?() as? Service - - if let service = service { - cache[serviceId] = service - } - - return service - case .new: - return factories[serviceId]?() as? Service - } - } - } -} diff --git a/CodeEdit/Utils/DependencyInjection/ServiceType.swift b/CodeEdit/Utils/DependencyInjection/ServiceType.swift deleted file mode 100644 index 536e89db06..0000000000 --- a/CodeEdit/Utils/DependencyInjection/ServiceType.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ServiceType.swift -// CodeEdit -// -// Created by Abe Malla on 4/3/24. -// - -/// Defines the type of service instantiation strategy. -enum ServiceType { - /// Returns a new singleton on the first call, then returns a cached one every other time - case singleton - /// Creates a new singleton reference each time and caches it, returning the newer singleton - case newSingleton - /// Creates a new singleton - case new -} diff --git a/CodeEdit/Utils/DependencyInjection/ServiceWrapper.swift b/CodeEdit/Utils/DependencyInjection/ServiceWrapper.swift deleted file mode 100644 index 3f5d7824b1..0000000000 --- a/CodeEdit/Utils/DependencyInjection/ServiceWrapper.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ServiceWrapper.swift -// CodeEdit -// -// Created by Abe Malla on 4/3/24. -// - -/// A property wrapper that provides access to a service instance. -@propertyWrapper -struct Service { - var service: Service - - init(_ type: ServiceType = .singleton) { - guard let service = ServiceContainer.resolve(type, Service.self) else { - let serviceName = String(describing: Service.self) - fatalError("No service of type \(serviceName) registered!") - } - - self.service = service - } - - var wrappedValue: Service { - get { self.service } - mutating set { service = newValue } - } -} diff --git a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift index 9c5d65d918..a834fcdd3c 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift @@ -6,12 +6,13 @@ // import Foundation +import Factory extension URL { /// Finds a workspace that contains the url. @MainActor func findWorkspace() -> Workspace? { - @Service var windowManager: WorkspaceWindowManager + let windowManager = Container.shared.workspaceWindowManager() return windowManager.workspace(containing: self) } } From 36131458a9750881e796b075f6c43a15cc58394d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 11 Apr 2026 17:29:54 +0200 Subject: [PATCH 013/335] Refactor: Wrap app-level services behind protocols and register in Factory Eliminate all global singletons (.shared, currentWorld) by introducing protocol abstractions and Factory DI registration for 6 services: ShellClient, CommandManager, KeybindingManager, NotificationManager, LSPService, and RegistryManager. Also introduces the Services/ layer and moves ShellClient there as the first service to occupy it. Deletes World.swift. --- CodeEdit/CodeEditContainer.swift | 22 ++++++++- .../ViewModels/QuickActionsViewModel.swift | 7 +-- .../Commands/Views/QuickActionsView.swift | 3 +- .../CodeEditWindowControllerExtensions.swift | 8 ++-- ...InternalDevelopmentNotificationsView.swift | 9 ++-- .../Features/Keybindings/CommandManager.swift | 6 +-- .../Keybindings/KeybindingManager.swift | 7 +-- .../Protocols/CommandManaging.swift | 15 ++++++ .../Protocols/KeybindingManaging.swift | 15 ++++++ .../LSP/Registry/PackageManagerProtocol.swift | 2 +- .../PackageManagerInstallOperation.swift | 3 +- .../Install/PackageManagerProgressModel.swift | 7 +-- .../Sources/CargoPackageManager.swift | 5 +- .../Sources/GithubPackageManager.swift | 5 +- .../Sources/GolangPackageManager.swift | 5 +- .../Sources/NPMPackageManager.swift | 5 +- .../Sources/PipPackageManager.swift | 5 +- .../Registry/Protocols/RegistryManaging.swift | 24 ++++++++++ .../LSP/Registry/RegistryManager.swift | 6 +-- .../Features/LSP/Service/LSPService.swift | 7 +-- .../LSP/Service/LSPServiceProtocol.swift | 20 ++++++++ .../ChangedFile/GitChangedFileLabel.swift | 5 +- .../Notifications/NotificationManager.swift | 4 +- .../Protocols/NotificationManaging.swift | 47 ++++++++++++++++++ ...nPanelViewModel+NotificationHandling.swift | 9 ++-- .../NotificationPanelViewModel+Toolbar.swift | 3 +- .../NotificationPanelViewModel.swift | 3 +- .../Views/NotificationBannerView.swift | 3 +- .../Views/NotificationPanelView.swift | 3 +- .../Views/NotificationToolbarItem.swift | 3 +- .../Extensions/LanguageServersView.swift | 3 +- .../Models/KeybindingsSettings.swift | 7 +-- .../Models/IgnorePatternModel.swift | 3 +- .../SourceControlGeneralView.swift | 3 +- .../SourceControlGitView.swift | 2 +- .../Models/TextEditingSettings.swift | 3 +- .../SourceControl/Client/GitClient.swift | 4 +- .../Client/GitConfigClient.swift | 4 +- .../GitCheckoutBranchViewModel.swift | 3 +- .../Clone/ViewModels/GitCloneViewModel.swift | 3 +- .../SourceControl/SourceControlManager.swift | 5 +- .../Views/SourceControlPullView.swift | 3 +- .../StatusBarToggleUtilityAreaButton.swift | 5 +- .../ToolbarItems/StartTaskToolbarItem.swift | 3 +- .../Tasks/Views/StartTaskToolbarButton.swift | 3 +- .../WindowCommands/ViewCommands.swift | 3 +- .../Features/Workspace/WorkspaceFactory.swift | 7 ++- .../ShellClient}/ShellClient.swift | 18 +++---- .../ShellClient/ShellClientProtocol.swift | 48 +++++++++++++++++++ CodeEdit/WorkspaceView.swift | 2 - CodeEdit/World.swift | 6 --- 51 files changed, 304 insertions(+), 100 deletions(-) create mode 100644 CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift create mode 100644 CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift create mode 100644 CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift create mode 100644 CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift create mode 100644 CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift rename CodeEdit/{Utils/ShellClient/Models => Services/ShellClient}/ShellClient.swift (90%) create mode 100644 CodeEdit/Services/ShellClient/ShellClientProtocol.swift delete mode 100644 CodeEdit/World.swift diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 6071ff958f..088b11a396 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -2,7 +2,7 @@ // CodeEditContainer.swift // CodeEdit // -// Created by CodeEdit Contributors on 08.04.26. +// Created by Matthijs Eikelenboom on 08.04.26. // import Factory @@ -15,4 +15,24 @@ extension Container { var workspaceWindowManager: Factory { self { @MainActor in WorkspaceWindowManager() }.singleton } + + var shellClient: Factory { + self { ShellClient() as ShellClientProtocol }.singleton + } + + var commandManager: Factory { + self { CommandManager() }.singleton + } + + var keybindingManager: Factory { + self { KeybindingManager() }.singleton + } + + var notificationManager: Factory { + self { NotificationManager() }.singleton + } + + var registryManager: Factory { + self { @MainActor in RegistryManager() }.singleton + } } diff --git a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift index a796c2aaa7..f2194cdd05 100644 --- a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift +++ b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory /// Simple state class for command palette view. Contains currently selected command, /// query text and list of filtered commands @@ -24,15 +25,15 @@ final class QuickActionsViewModel: ObservableObject { func reset() { commandQuery = "" selected = nil - filteredCommands = CommandManager.shared.commands + filteredCommands = Container.shared.commandManager().commands } func fetchMatchingCommands(val: String) { if val == "" { - self.filteredCommands = CommandManager.shared.commands + self.filteredCommands = Container.shared.commandManager().commands return } - self.filteredCommands = CommandManager.shared.commands.filter { $0.title.localizedCaseInsensitiveContains(val) } + self.filteredCommands = Container.shared.commandManager().commands.filter { $0.title.localizedCaseInsensitiveContains(val) } self.selected = self.filteredCommands.first } diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/Views/QuickActionsView.swift index 12d60e1148..7d05abc407 100644 --- a/CodeEdit/Features/Commands/Views/QuickActionsView.swift +++ b/CodeEdit/Features/Commands/Views/QuickActionsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory /// Quick actions view struct QuickActionsView: View { @@ -15,7 +16,7 @@ struct QuickActionsView: View { @ObservedObject private var state: QuickActionsViewModel - @ObservedObject private var commandManager: CommandManager = .shared + @ObservedObject private var commandManager: CommandManager = Container.shared.commandManager() @State private var monitor: Any? diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 9f575a4c29..fab6c6cf31 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -6,26 +6,28 @@ // import SwiftUI +import Factory import Combine extension CodeEditWindowController { /// These are example items that added as commands to command palette func registerCommands() { - CommandManager.shared.addCommand( + let commandManager = Container.shared.commandManager() + commandManager.addCommand( name: "Quick Open", title: "Quick Open", id: "quick_open", command: { [weak self] in self?.openQuickly(nil) } ) - CommandManager.shared.addCommand( + commandManager.addCommand( name: "Toggle Navigator", title: "Toggle Navigator", id: "toggle_left_sidebar", command: { [weak self] in self?.toggleFirstPanel() } ) - CommandManager.shared.addCommand( + commandManager.addCommand( name: "Toggle Inspector", title: "Toggle Inspector", id: "toggle_right_sidebar", diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift index 5334880996..4a962a5a11 100644 --- a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct InternalDevelopmentNotificationsView: View { enum IconType: String, CaseIterable { @@ -129,7 +130,7 @@ struct InternalDevelopmentNotificationsView: View { let iconSymbol = selectedSymbol ?? availableSymbols.randomElement() ?? "bell.fill" let iconColor = selectedColor ?? availableColors.randomElement()?.1 ?? .blue - NotificationManager.shared.post( + Container.shared.notificationManager().post( iconSymbol: iconSymbol, iconColor: iconColor, title: notificationTitle, @@ -143,7 +144,7 @@ struct InternalDevelopmentNotificationsView: View { case .image: let imageName = selectedImage ?? availableImages.randomElement() ?? "GitHubIcon" - NotificationManager.shared.post( + Container.shared.notificationManager().post( iconImage: Image(imageName), title: notificationTitle, description: notificationDescription, @@ -157,7 +158,7 @@ struct InternalDevelopmentNotificationsView: View { let text = selectedText ?? randomLetter() let iconColor = selectedColor ?? availableColors.randomElement()?.1 ?? .blue - NotificationManager.shared.post( + Container.shared.notificationManager().post( iconText: text, iconTextColor: .white, iconColor: iconColor, @@ -173,7 +174,7 @@ struct InternalDevelopmentNotificationsView: View { let emoji = selectedEmoji ?? availableEmojis.randomElement() ?? "🔔" let iconColor = selectedColor ?? availableColors.randomElement()?.1 ?? .blue - NotificationManager.shared.post( + Container.shared.notificationManager().post( iconText: emoji, iconTextColor: .white, iconColor: iconColor, diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/Features/Keybindings/CommandManager.swift index f21f2ad81d..e3b26efdf1 100644 --- a/CodeEdit/Features/Keybindings/CommandManager.swift +++ b/CodeEdit/Features/Keybindings/CommandManager.swift @@ -22,15 +22,13 @@ mgr.executeCommand("test") ``` */ -final class CommandManager: ObservableObject { +final class CommandManager: CommandManaging { @Published private var commandsList: [String: Command] - private init() { + init() { commandsList = [:] } - static let shared: CommandManager = .init() - func addCommand(name: String, title: String, id: String, command: @escaping () -> Void) { let command = Command.init(id: name, title: title, closureWrapper: command) commandsList[id] = command diff --git a/CodeEdit/Features/Keybindings/KeybindingManager.swift b/CodeEdit/Features/Keybindings/KeybindingManager.swift index 8c3b1bfebc..95bc15804d 100644 --- a/CodeEdit/Features/Keybindings/KeybindingManager.swift +++ b/CodeEdit/Features/Keybindings/KeybindingManager.swift @@ -7,17 +7,14 @@ import Foundation import SwiftUI -final class KeybindingManager { +final class KeybindingManager: KeybindingManaging { /// Array which contains all available keyboard shortcuts var keyboardShortcuts = [String: KeyboardShortcutWrapper]() - private init() { + init() { loadKeybindings() } - /// Static method to access singleton - static let shared: KeybindingManager = .init() - // We need this fallback shortcut because optional shortcuts available only from 12.3, while we have target of 12.0x var fallbackShortcut = KeyboardShortcutWrapper( name: "?", diff --git a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift new file mode 100644 index 0000000000..24f8e61273 --- /dev/null +++ b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift @@ -0,0 +1,15 @@ +// +// CommandManaging.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 11/04/26. +// + +import Foundation + +/// Protocol for managing application commands (command palette). +protocol CommandManaging: AnyObject, ObservableObject { + var commands: [Command] { get } + func addCommand(name: String, title: String, id: String, command: @escaping () -> Void) + func executeCommand(_ id: String) +} diff --git a/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift b/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift new file mode 100644 index 0000000000..da6dc841d6 --- /dev/null +++ b/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift @@ -0,0 +1,15 @@ +// +// KeybindingManaging.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 11/04/26. +// + +import Foundation + +/// Protocol for managing keyboard shortcuts. +protocol KeybindingManaging: AnyObject { + var keyboardShortcuts: [String: KeyboardShortcutWrapper] { get } + func addNewShortcut(shortcut: KeyboardShortcutWrapper, name: String) + func named(with name: String) -> KeyboardShortcutWrapper +} diff --git a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift b/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift index c7398e0edc..481030464f 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift @@ -9,7 +9,7 @@ import Foundation /// The protocol each package manager conforms to for creating ``PackageManagerInstallOperation``s. protocol PackageManagerProtocol { - var shellClient: ShellClient { get } + var shellClient: ShellClientProtocol { get } /// Calls the shell commands to install a package func install(method installationMethod: InstallationMethod) throws -> [PackageManagerInstallStep] diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift index 03e866adbb..695ee70e7f 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift @@ -6,6 +6,7 @@ // import Foundation +import Factory import Combine /// An executable install operation for installing a ``RegistryItem``. @@ -64,7 +65,7 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { /// If non-nil, indicates that this operation has halted and requires confirmation. @Published public private(set) var waitingForConfirmation: String? - private let shellClient: ShellClient = .live() + private let shellClient: ShellClientProtocol = Container.shared.shellClient() private var operationTask: Task? private var confirmationContinuation: CheckedContinuation? private var outputIdx = 0 diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift index 7451ef22e8..b0d80714c7 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift @@ -5,8 +5,9 @@ // Created by Khan Winter on 8/8/25. // -import Foundation import Combine +import Factory +import Foundation /// This model is injected into each ``PackageManagerInstallStep`` when executing a ``PackageManagerInstallOperation``. /// A single model is used for each step. Output is collected by the ``PackageManagerInstallOperation``. @@ -23,10 +24,10 @@ final class PackageManagerProgressModel: ObservableObject { let outputStream: AsyncStream @Published var progress: Progress - private let shellClient: ShellClient + private let shellClient: ShellClientProtocol private let outputContinuation: AsyncStream.Continuation - init(shellClient: ShellClient) { + init(shellClient: ShellClientProtocol) { self.shellClient = shellClient self.progress = Progress(totalUnitCount: 1) (outputStream, outputContinuation) = AsyncStream.makeStream() diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift index 8eef86c149..c91fb99223 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift @@ -5,16 +5,17 @@ // Created by Abe Malla on 2/3/25. // +import Factory import Foundation final class CargoPackageManager: PackageManagerProtocol { private let installationDirectory: URL - let shellClient: ShellClient + let shellClient: ShellClientProtocol init(installationDirectory: URL) { self.installationDirectory = installationDirectory - self.shellClient = .live() + self.shellClient = Container.shared.shellClient() } func install(method installationMethod: InstallationMethod) throws -> [PackageManagerInstallStep] { diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift index de06433c1d..31e5e6cbb7 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift @@ -5,16 +5,17 @@ // Created by Abe Malla on 3/10/25. // +import Factory import Foundation final class GithubPackageManager: PackageManagerProtocol { private let installationDirectory: URL - let shellClient: ShellClient + let shellClient: ShellClientProtocol init(installationDirectory: URL) { self.installationDirectory = installationDirectory - self.shellClient = .live() + self.shellClient = Container.shared.shellClient() } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift index 574cdd2e39..0b2f4a4a40 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift @@ -5,16 +5,17 @@ // Created by Abe Malla on 2/3/25. // +import Factory import Foundation final class GolangPackageManager: PackageManagerProtocol { private let installationDirectory: URL - let shellClient: ShellClient + let shellClient: ShellClientProtocol init(installationDirectory: URL) { self.installationDirectory = installationDirectory - self.shellClient = .live() + self.shellClient = Container.shared.shellClient() } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift index e5988429b3..0b7c7062c3 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift @@ -5,16 +5,17 @@ // Created by Abe Malla on 2/2/25. // +import Factory import Foundation final class NPMPackageManager: PackageManagerProtocol { private let installationDirectory: URL - let shellClient: ShellClient + let shellClient: ShellClientProtocol init(installationDirectory: URL) { self.installationDirectory = installationDirectory - self.shellClient = .live() + self.shellClient = Container.shared.shellClient() } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift index b7840e46aa..5aa87782d6 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift @@ -5,16 +5,17 @@ // Created by Abe Malla on 2/3/25. // +import Factory import Foundation final class PipPackageManager: PackageManagerProtocol { private let installationDirectory: URL - let shellClient: ShellClient + let shellClient: ShellClientProtocol init(installationDirectory: URL) { self.installationDirectory = installationDirectory - self.shellClient = .live() + self.shellClient = Container.shared.shellClient() } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift new file mode 100644 index 0000000000..6dd212ff54 --- /dev/null +++ b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift @@ -0,0 +1,24 @@ +// +// RegistryManaging.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 11/04/26. +// + +import Foundation + +/// Protocol for managing the language server registry. +/// +/// Note: `@Published` properties are not included because consumers +/// need the concrete type for SwiftUI observation. Use `RegistryManager` directly in views. +@MainActor +protocol RegistryManaging: AnyObject, ObservableObject { + var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] { get } + var isInstalling: Bool { get } + + func setPackageEnabled(packageName: String, enabled: Bool) + func removeLanguageServer(packageName: String) async throws + func installOperation(package: RegistryItem) throws -> PackageManagerInstallOperation + func startInstallation(operation: PackageManagerInstallOperation) throws + func cancelInstallation() +} diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index c287f85519..d946695bb2 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -9,10 +9,10 @@ import OSLog import Foundation import ZIPFoundation import Combine +import Factory @MainActor -final class RegistryManager: ObservableObject { - static let shared = RegistryManager() +final class RegistryManager: ObservableObject, RegistryManaging { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RegistryManager") let installPath = Settings.shared.baseURL.appending(path: "Language Servers") @@ -180,7 +180,7 @@ final class RegistryManager: ObservableObject { fail failed: Bool ) { if failed { - NotificationManager.shared.post( + Container.shared.notificationManager().post( iconSymbol: "xmark.circle", iconColor: .clear, title: "Could not install \(activityName)", diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index d4d7df7aab..c15720e61f 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -9,6 +9,7 @@ import os.log import JSONRPC import SwiftUI import Foundation +import Factory import LanguageClient import LanguageServerProtocol import CodeEditLanguages @@ -99,7 +100,7 @@ import CodeEditLanguages /// } /// ``` @MainActor -final class LSPService: ObservableObject { +final class LSPService: ObservableObject, LSPServiceProtocol { typealias LanguageServerType = LanguageServer let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") @@ -340,11 +341,11 @@ extension LSPService { let lspLanguageTitle = lspLanguage.rawValue.capitalized let notificationTitle = "Install \(lspLanguageTitle) Language Server" // Make sure the user doesn't have the same existing notification - guard !NotificationManager.shared.notifications.contains(where: { $0.title == notificationTitle }) else { + guard !Container.shared.notificationManager().notifications.contains(where: { $0.title == notificationTitle }) else { return } - NotificationManager.shared.post( + Container.shared.notificationManager().post( iconSymbol: "arrow.down.circle", iconColor: .clear, title: notificationTitle, diff --git a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift b/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift new file mode 100644 index 0000000000..3b718a47e6 --- /dev/null +++ b/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift @@ -0,0 +1,20 @@ +// +// LSPServiceProtocol.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 11/04/26. +// + +import Foundation + +/// Protocol for managing Language Server Protocol services. +/// +/// Note: `languageClients` is not included here because consumers that need +/// reactive observation of `@Published` properties require the concrete type. +/// Use `LSPService` directly in those cases. +@MainActor +protocol LSPServiceProtocol: AnyObject { + func closeWorkspace(_ workspacePath: String) + func stopAllServers() async + func killAllServers() +} diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index a8727f7b64..880aaaa470 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct GitChangedFileLabel: View { @EnvironmentObject private var workspace: Workspace @@ -38,7 +39,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: nil )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init(), shellClient: Container.shared.shellClient())) .environmentObject(Workspace()) GitChangedFileLabel(file: GitChangedFile( @@ -47,7 +48,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: "app2.jsx" )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init(), shellClient: Container.shared.shellClient())) .environmentObject(Workspace()) }.padding() } diff --git a/CodeEdit/Features/Notifications/NotificationManager.swift b/CodeEdit/Features/Notifications/NotificationManager.swift index e270514189..a2cfd1d0d4 100644 --- a/CodeEdit/Features/Notifications/NotificationManager.swift +++ b/CodeEdit/Features/Notifications/NotificationManager.swift @@ -14,9 +14,7 @@ import UserNotifications /// - Managing notification persistence /// - Tracking notification read status /// - Broadcasting notifications to workspaces -final class NotificationManager: NSObject, ObservableObject { - /// Shared instance for accessing the notification manager - static let shared = NotificationManager() +final class NotificationManager: NSObject, NotificationManaging { /// Collection of all notifications, both read and unread @Published private(set) var notifications: [CENotification] = [] diff --git a/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift b/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift new file mode 100644 index 0000000000..375c6f67ca --- /dev/null +++ b/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift @@ -0,0 +1,47 @@ +// +// NotificationManaging.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 11/04/26. +// + +import SwiftUI + +/// Protocol for managing application notifications. +protocol NotificationManaging: AnyObject, ObservableObject { + var notifications: [CENotification] { get } + var unreadCount: Int { get } + + func post( + iconSymbol: String, + iconColor: Color?, + title: String, + description: String, + actionButtonTitle: String, + action: @escaping () -> Void, + isSticky: Bool + ) + + func post( + iconImage: Image, + title: String, + description: String, + actionButtonTitle: String, + action: @escaping () -> Void, + isSticky: Bool + ) + + func post( + iconText: String, + iconTextColor: Color?, + iconColor: Color?, + title: String, + description: String, + actionButtonTitle: String, + action: @escaping () -> Void, + isSticky: Bool + ) + + func dismissNotification(_ notification: CENotification) + func markAsRead(_ notification: CENotification) +} diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift index 4757c11233..b8582a9ad9 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory /// Notification insertion, dismissal, and event handling. extension NotificationPanelViewModel { @@ -62,8 +63,8 @@ extension NotificationPanelViewModel { if let index = activeNotifications.firstIndex(where: { $0.id == notification.id }) { if disableAnimation { self.activeNotifications.removeAll(where: { $0.id == notification.id }) - NotificationManager.shared.markAsRead(notification) - NotificationManager.shared.dismissNotification(notification) + Container.shared.notificationManager().markAsRead(notification) + Container.shared.notificationManager().dismissNotification(notification) return } @@ -80,8 +81,8 @@ extension NotificationPanelViewModel { } } - NotificationManager.shared.markAsRead(notification) - NotificationManager.shared.dismissNotification(notification) + Container.shared.notificationManager().markAsRead(notification) + Container.shared.notificationManager().dismissNotification(notification) } } } diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift index 19e6565430..51e1d3f002 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift @@ -6,6 +6,7 @@ // import AppKit +import Factory /// Dynamic toolbar item management for the notification badge. extension NotificationPanelViewModel { @@ -15,7 +16,7 @@ extension NotificationPanelViewModel { return } - let shouldShow = !self.visibleNotifications.isEmpty || NotificationManager.shared.unreadCount > 0 + let shouldShow = !self.visibleNotifications.isEmpty || Container.shared.notificationManager().unreadCount > 0 if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { guard let activityItemIdx = toolbar.items .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift index a1f037588b..4d55f8713f 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory /// Coordinates notification display, auto-hide timers, panel visibility, and toolbar integration. /// @@ -35,7 +36,7 @@ final class NotificationPanelViewModel: ObservableObject { /// Whether notifications are paused var isPaused: Bool = false - var notificationManager = NotificationManager.shared + var notificationManager = Container.shared.notificationManager() /// A filtered list of active notifications. var visibleNotifications: [CENotification] { diff --git a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift index f11bedb933..5972e6fb98 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift @@ -6,13 +6,14 @@ // import SwiftUI +import Factory struct NotificationBannerView: View { @Environment(\.colorScheme) private var colorScheme @EnvironmentObject private var notificationPanel: NotificationPanelViewModel - @ObservedObject private var notificationManager = NotificationManager.shared + @ObservedObject private var notificationManager = Container.shared.notificationManager() let notification: CENotification let onDismiss: () -> Void diff --git a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift b/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift index a5d234a743..7ea18e6ea4 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift @@ -6,13 +6,14 @@ // import SwiftUI +import Factory struct NotificationPanelView: View { @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @Environment(\.controlActiveState) private var controlActiveState - @ObservedObject private var notificationManager = NotificationManager.shared + @ObservedObject private var notificationManager = Container.shared.notificationManager() @FocusState private var isFocused: Bool // ID for the top anchor diff --git a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift b/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift index 8e7fb47b0e..24d9b5506d 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift @@ -6,10 +6,11 @@ // import SwiftUI +import Factory struct NotificationToolbarItem: View { @EnvironmentObject private var notificationPanel: NotificationPanelViewModel - @ObservedObject private var notificationManager = NotificationManager.shared + @ObservedObject private var notificationManager = Container.shared.notificationManager() @Environment(\.controlActiveState) private var controlActiveState diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 44cd02aafb..122b30539a 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -6,10 +6,11 @@ // import SwiftUI +import Factory /// Displays a searchable list of packages from the ``RegistryManager``. struct LanguageServersView: View { - @StateObject var registryManager: RegistryManager = .shared + @ObservedObject var registryManager: RegistryManager = Container.shared.registryManager() @StateObject private var searchModel = FuzzySearchUIModel() @State private var searchText: String = "" @State private var selectedInstall: PackageManagerInstallOperation? diff --git a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift index 883ac3a175..ddeba525f0 100644 --- a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift @@ -6,6 +6,7 @@ // import Foundation +import Factory extension SettingsData { @@ -17,7 +18,7 @@ extension SettingsData { /// Default initializer init() { - self.keybindings = KeybindingManager.shared.keyboardShortcuts + self.keybindings = Container.shared.keybindingManager().keyboardShortcuts } /// Explicit decoder init for setting default values when key is not present in `JSON` @@ -33,10 +34,10 @@ extension SettingsData { /// Adds new keybindings if they were added to default_keybindings.json. /// To ensure users will get new keybindings with new app version releases private mutating func appendNew() { - let newKeybindings = KeybindingManager.shared + let newKeybindings = Container.shared.keybindingManager() .keyboardShortcuts.filter { !keybindings.keys.contains($0.key) } for keybinding in newKeybindings { - self.keybindings[keybinding.key] = KeybindingManager.shared.named(with: keybinding.key) + self.keybindings[keybinding.key] = Container.shared.keybindingManager().named(with: keybinding.key) } } } diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift index 01c8d7aaba..04d8800401 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift @@ -6,6 +6,7 @@ // import Foundation +import Factory /// A model to manage Git ignore patterns for a file, including loading, saving, and monitoring changes. @MainActor @@ -28,7 +29,7 @@ class IgnorePatternModel: ObservableObject { @Published var selection: Set = [] /// A client for interacting with the Git configuration. - private let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + private let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) /// A file system monitor for detecting changes to the Git ignore file. private var fileMonitor: DispatchSourceFileSystemObject? diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index 652094d7e4..e2b03620d7 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -6,12 +6,13 @@ // import SwiftUI +import Factory struct SourceControlGeneralView: View { @AppSettings(\.sourceControl.general) var settings - let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) var body: some View { Group { diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index c4068af1fc..57f780f0b9 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -12,7 +12,7 @@ struct SourceControlGitView: View { @AppSettings(\.sourceControl.git) var git - let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) @State private var authorName: String = "" @State private var authorEmail: String = "" diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift index b5b5abb5a4..fa55d6433c 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift @@ -6,6 +6,7 @@ // import AppKit +import Factory import Foundation extension SettingsData { @@ -166,7 +167,7 @@ extension SettingsData { /// Adds toggle-able preferences to the command palette via shared `CommandManager` private func populateCommands() { - let mgr = CommandManager.shared + let mgr = Container.shared.commandManager() mgr.addCommand( name: "Toggle Type-Over Completion", diff --git a/CodeEdit/Features/SourceControl/Client/GitClient.swift b/CodeEdit/Features/SourceControl/Client/GitClient.swift index aa7178cb0b..57b7417c04 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient.swift @@ -36,11 +36,11 @@ class GitClient: GitClientProtocol { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "GitClient") internal let directoryURL: URL - internal let shellClient: ShellClient + internal let shellClient: ShellClientProtocol private let configClient: GitConfigClient - init(directoryURL: URL, shellClient: ShellClient) { + init(directoryURL: URL, shellClient: ShellClientProtocol) { self.directoryURL = directoryURL self.shellClient = shellClient self.configClient = GitConfigClient(projectURL: directoryURL, shellClient: shellClient) diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift b/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift index 7323715e46..b660445162 100644 --- a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift +++ b/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift @@ -12,13 +12,13 @@ import Foundation /// project and global levels. class GitConfigClient { private let projectURL: URL? - private let shellClient: ShellClient + private let shellClient: ShellClientProtocol /// 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) { + init(projectURL: URL? = nil, shellClient: ShellClientProtocol) { self.projectURL = projectURL self.shellClient = shellClient } diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift index df72ef2246..77d6c811d4 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift @@ -6,6 +6,7 @@ // import Foundation +import Factory class GitCheckoutBranchViewModel: ObservableObject { @Published var selectedBranch: GitBranch? @@ -16,7 +17,7 @@ class GitCheckoutBranchViewModel: ObservableObject { init(repoPath: URL) { self.repoPath = repoPath - gitClient = .init(directoryURL: repoPath, shellClient: .live()) + gitClient = .init(directoryURL: repoPath, shellClient: Container.shared.shellClient()) } func loadBranches() async { diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift index a82330ed1f..aeeac03654 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift @@ -6,6 +6,7 @@ // import Foundation +import Factory import AppKit class GitCloneViewModel: ObservableObject { @@ -108,7 +109,7 @@ class GitCloneViewModel: ObservableObject { return } - gitClient = GitClient(directoryURL: localPath, shellClient: .live()) + gitClient = GitClient(directoryURL: localPath, shellClient: Container.shared.shellClient()) self.cloningTask = Task(priority: .background) { await processCloning( diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index 4da16056ae..64ccd02996 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -129,10 +129,11 @@ final class SourceControlManager: ObservableObject { init( workspaceURL: URL, - editorManager: EditorManager + editorManager: EditorManager, + shellClient: ShellClientProtocol ) { self.workspaceURL = workspaceURL self.editorManager = editorManager - gitClient = GitClient(directoryURL: workspaceURL, shellClient: currentWorld.shellClient) + gitClient = GitClient(directoryURL: workspaceURL, shellClient: shellClient) } } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift index 45d0a8f970..87e4b92bc3 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct SourceControlPullView: View { @Environment(\.dismiss) @@ -13,7 +14,7 @@ struct SourceControlPullView: View { @EnvironmentObject var sourceControlManager: SourceControlManager - let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient) + let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) @State var loading: Bool = false diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift index c035241596..442762c0f6 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory internal struct StatusBarToggleUtilityAreaButton: View { @Environment(\.controlActiveState) @@ -25,7 +26,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { .onHover { isHovering($0) } .onChange(of: controlActiveState) { _, newValue in if newValue == .key { - CommandManager.shared.addCommand( + Container.shared.commandManager().addCommand( name: "Toggle Utility Area", title: "Toggle Utility Area", id: "open.drawer", @@ -34,7 +35,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { } } .onAppear { - CommandManager.shared.addCommand( + Container.shared.commandManager().addCommand( name: "Toggle Utility Area", title: "Toggle Utility Area", id: "open.drawer", diff --git a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift b/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift index a46919e0ec..6d532ff3fc 100644 --- a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift +++ b/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift @@ -6,6 +6,7 @@ // import AppKit +import Factory @available(macOS 26, *) final class StartTaskToolbarItem: NSToolbarItem { @@ -36,7 +37,7 @@ final class StartTaskToolbarItem: NSToolbarItem { taskManager.executeActiveTask() if utilityAreaCollapsed { - CommandManager.shared.executeCommand("open.drawer") + Container.shared.commandManager().executeCommand("open.drawer") } workspace?.utilityAreaModel?.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID diff --git a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift index 2a31b3cc90..68d464f4db 100644 --- a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift +++ b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory struct StartTaskToolbarButton: View { @Environment(\.controlActiveState) @@ -22,7 +23,7 @@ struct StartTaskToolbarButton: View { Button { taskManager.executeActiveTask() if utilityAreaCollapsed { - CommandManager.shared.executeCommand("open.drawer") + Container.shared.commandManager().executeCommand("open.drawer") } workspace.utilityAreaModel?.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index c72ccb0710..7dcfadf20e 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory import Combine struct ViewCommands: Commands { @@ -134,7 +135,7 @@ extension ViewCommands { .keyboardShortcut("i", modifiers: [.control, .command]) Button("\(utilityAreaCollapsed ? "Show" : "Hide") Utility Area") { - CommandManager.shared.executeCommand("open.drawer") + Container.shared.commandManager().executeCommand("open.drawer") } .disabled(windowController == nil) .keyboardShortcut("y", modifiers: [.shift, .command]) diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 1e0141feb7..5cef65cf68 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -2,10 +2,11 @@ // WorkspaceFactory.swift // CodeEdit // -// Created by CodeEdit Contributors on 07.04.26. +// Created by Matthijs Eikelenboom on 07.04.26. // import Foundation +import Factory /// Constructs and wires the manager/service object graph for a ``Workspace``. /// @@ -41,9 +42,11 @@ enum WorkspaceFactory { return } + let shellClient = Container.shared.shellClient() let sourceControlManager = SourceControlManager( workspaceURL: url, - editorManager: editorManager + editorManager: editorManager, + shellClient: shellClient ) let workspaceFileManager = CEWorkspaceFileManager( diff --git a/CodeEdit/Utils/ShellClient/Models/ShellClient.swift b/CodeEdit/Services/ShellClient/ShellClient.swift similarity index 90% rename from CodeEdit/Utils/ShellClient/Models/ShellClient.swift rename to CodeEdit/Services/ShellClient/ShellClient.swift index 37dcfe513b..16c91fdd3d 100644 --- a/CodeEdit/Utils/ShellClient/Models/ShellClient.swift +++ b/CodeEdit/Services/ShellClient/ShellClient.swift @@ -16,11 +16,11 @@ enum ShellClientError: Error { /// Shell Client /// Run commands in shell -class ShellClient { +final class ShellClient: ShellClientProtocol { /// Generate a process and pipe to run commands /// - Parameter args: commands to run /// - Returns: command output - func generateProcessAndPipe(_ args: [String]) -> (Process, Pipe) { + private func generateProcessAndPipe(_ args: [String]) -> (Process, Pipe) { // Run in an 'interactive' login shell. Because we're passing -c here it won't actually be // interactive but it will source the user's zshrc file as well as the zshprofile. var arguments = ["-lic"] @@ -35,13 +35,13 @@ class ShellClient { } /// Cancellable tasks - var cancellables: [UUID: AnyCancellable] = [:] + private var cancellables: [UUID: AnyCancellable] = [:] /// Run a command /// - Parameter args: command to run /// - Returns: command output @discardableResult - func run(_ args: String...) throws -> String { + func run(_ args: [String]) throws -> String { let (task, pipe) = generateProcessAndPipe(args) try task.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() @@ -55,7 +55,7 @@ class ShellClient { /// - Parameter args: command to run /// - Returns: command output @discardableResult - func runLive(_ args: String...) -> AnyPublisher { + func runLive(_ args: [String]) -> AnyPublisher { let subject = PassthroughSubject() let (task, pipe) = generateProcessAndPipe(args) let outputHandler = pipe.fileHandleForReading @@ -91,7 +91,7 @@ class ShellClient { /// Run a command with AsyncStream /// - Parameter args: command to run /// - Returns: async stream of command output - func runAsync(_ args: String...) -> AsyncThrowingStream { + func runAsync(_ args: [String]) -> AsyncThrowingStream { let (task, pipe) = generateProcessAndPipe(args) return AsyncThrowingStream { continuation in @@ -126,10 +126,4 @@ class ShellClient { } } } - - /// Shell client - /// - Returns: description - static func live() -> ShellClient { - return ShellClient() - } } diff --git a/CodeEdit/Services/ShellClient/ShellClientProtocol.swift b/CodeEdit/Services/ShellClient/ShellClientProtocol.swift new file mode 100644 index 0000000000..f983944fc4 --- /dev/null +++ b/CodeEdit/Services/ShellClient/ShellClientProtocol.swift @@ -0,0 +1,48 @@ +// +// ShellClientProtocol.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 11/04/26. +// + +import Combine +import Foundation + +/// Protocol for executing shell commands. +protocol ShellClientProtocol: Sendable { + /// Run a command synchronously. + /// - Parameter args: Arguments passed to the shell. + /// - Returns: The command output. + @discardableResult + func run(_ args: [String]) throws -> String + + /// Run a command with a Combine publisher for live output. + /// - Parameter args: Arguments passed to the shell. + /// - Returns: A publisher that emits output lines. + @discardableResult + func runLive(_ args: [String]) -> AnyPublisher + + /// Run a command with an async stream for live output. + /// - Parameter args: Arguments passed to the shell. + /// - Returns: An async stream that yields output lines. + func runAsync(_ args: [String]) -> AsyncThrowingStream +} + +extension ShellClientProtocol { + /// Convenience variadic overload for `run`. + @discardableResult + func run(_ args: String...) throws -> String { + try run(args) + } + + /// Convenience variadic overload for `runLive`. + @discardableResult + func runLive(_ args: String...) -> AnyPublisher { + runLive(args) + } + + /// Convenience variadic overload for `runAsync`. + func runAsync(_ args: String...) -> AsyncThrowingStream { + runAsync(args) + } +} diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 78c57fd804..59aa4c502d 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -37,8 +37,6 @@ struct WorkspaceView: View { private let statusbarHeight: CGFloat = 29 - private var keybindings: KeybindingManager = .shared - var body: some View { if workspace.workspaceFileManager != nil, let sourceControlManager = workspace.sourceControlManager { VStack { diff --git a/CodeEdit/World.swift b/CodeEdit/World.swift deleted file mode 100644 index 107f4dce09..0000000000 --- a/CodeEdit/World.swift +++ /dev/null @@ -1,6 +0,0 @@ -var currentWorld: World = .init() - -// Inspired by: https://vimeo.com/291588126 -struct World { - var shellClient: ShellClient = .live() -} From 25369ddc919215c5defa2d9e30adebf96b1efbec Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Apr 2026 16:47:44 +0200 Subject: [PATCH 014/335] Refactor: Extract CodeEditDomain as a local Swift package Move 24 pure value types (structs/enums with no UI framework imports) into a separate Swift package at Packages/CodeEditDomain/. This enforces at compile time that domain types stay framework-free and lays the foundation for CodeEditKit extension support. Domain types organized into: Git (7 types), Registry (6 types), Editor (1), Search (3), Commands (1), Tasks (1). Split types that reference app-layer code (InstallationMethod.packageManager, RegistryItem.installMethod, FuzzySearchable conformance) remain as extensions in the app target. --- CodeEdit.xcodeproj/project.pbxproj | 18 ++- CodeEdit/AppDelegate.swift | 1 + CodeEdit/CodeEdit.entitlements | 11 +- .../Models/TaskNotificationModel.swift | 17 --- .../TaskNotificationHandler.swift | 1 + .../Notifications/TaskNotificationView.swift | 1 + .../CEWorkspace/Models/CEWorkspaceFile.swift | 1 + .../Views/ToolbarBranchPicker.swift | 1 + .../ViewModels/QuickActionsViewModel.swift | 1 + .../Commands/Views/QuickActionsView.swift | 1 + .../Protocols/WorkspaceManaging.swift | 2 +- .../Protocols/WorkspaceStatePersisting.swift | 2 +- .../WorkspaceStatePersistence.swift | 2 +- .../Tab/Models/EditorTabRepresentable.swift | 1 + .../HistoryInspectorItemView.swift | 1 + .../HistoryInspectorModel.swift | 1 + .../HistoryInspectorView.swift | 1 + .../HistoryInspector/HistoryPopoverView.swift | 1 + .../Features/Keybindings/CommandManager.swift | 21 +--- .../Protocols/CommandManaging.swift | 1 + .../InstallationMethod+PackageManager.swift | 31 +++++ .../Model/RegistryItem+AppExtensions.swift | 35 ++++++ .../LSP/Registry/PackageManagerProtocol.swift | 1 + .../PackageManagerInstallOperation.swift | 1 + .../Sources/CargoPackageManager.swift | 1 + .../Sources/GithubPackageManager.swift | 1 + .../Sources/GolangPackageManager.swift | 1 + .../Sources/NPMPackageManager.swift | 1 + .../Sources/PipPackageManager.swift | 1 + .../PackageSourceParser+Cargo.swift | 2 + .../PackageSourceParser+Gem.swift | 2 + .../PackageSourceParser+Golang.swift | 2 + .../PackageSourceParser+NPM.swift | 2 + .../PackageSourceParser+PYPI.swift | 2 + .../PackageSourceParser.swift | 1 + .../Registry/Protocols/RegistryManaging.swift | 1 + .../Registry/RegistryItemTemplateParser.swift | 1 + .../RegistryManager+HandleRegistryFile.swift | 1 + .../LSP/Registry/RegistryManager.swift | 1 + .../FindNavigator/FindModePicker.swift | 1 + .../FindNavigator/FindNavigatorForm.swift | 1 + ...ViewController+NSOutlineViewDelegate.swift | 1 + .../ProjectNavigatorViewController.swift | 1 + .../SourceControlNavigatorChangesList.swift | 1 + .../Views/CommitDetailsHeaderView.swift | 1 + .../History/Views/CommitDetailsView.swift | 1 + .../History/Views/CommitListItemView.swift | 1 + .../SourceControlNavigatorHistoryView.swift | 1 + .../Models/RepoOutlineGroupItem.swift | 1 + ...lNavigatorRepositoryView+contextMenu.swift | 1 + ...SourceControlNavigatorRepositoryView.swift | 1 + .../ChangedFile/GitChangedFileLabel.swift | 1 + .../ChangedFile/GitChangedFileListView.swift | 1 + .../FuzzySearch/Collection+FuzzySearch.swift | 1 + .../FuzzySearch/FuzzySearchModels.swift | 26 ---- .../Search/FuzzySearch/FuzzySearchable.swift | 1 + .../String+LengthOfMatchingPrefix.swift | 1 + .../Search/FuzzySearch/String+Normalise.swift | 1 + CodeEdit/Features/Search/SearchState.swift | 1 + .../Models/CodableDefault+Providers.swift | 2 +- .../Settings/Models/CodableDefault.swift | 2 +- .../Extensions/LanguageServerRowView.swift | 1 + .../Extensions/LanguageServersView.swift | 1 + .../Models/ThemeRepository.swift | 2 +- .../Client/GitClient+Branches.swift | 1 + .../Client/GitClient+Commit.swift | 1 + .../Client/GitClient+CommitHistory.swift | 1 + .../Client/GitClient+Remote.swift | 1 + .../Client/GitClient+Stash.swift | 1 + .../Client/GitClient+Status.swift | 1 + .../Client/GitClientProtocol.swift | 3 +- .../Clone/GitCheckoutBranchView.swift | 1 + .../GitCheckoutBranchViewModel.swift | 1 + .../SourceControl/Models/GitBranch.swift | 30 ----- .../Models/GitBranchesGroup.swift | 16 --- .../SourceControl/Models/GitRemote.swift | 15 --- .../SourceControl/Models/GitStashEntry.swift | 14 --- ...ourceControlManager+BranchOperations.swift | 1 + .../SourceControlManager+FileOperations.swift | 1 + ...ourceControlManager+RemoteOperations.swift | 1 + ...SourceControlManager+StashOperations.swift | 1 + .../SourceControl/SourceControlManager.swift | 1 + .../Views/RemoteBranchPicker.swift | 1 + .../Views/SourceControlNewBranchView.swift | 1 + .../Views/SourceControlRenameBranchView.swift | 1 + .../Views/SourceControlSwitchView.swift | 1 + .../Features/Workspace/Models/Workspace.swift | 2 +- .../Protocols/WorkspaceWindowManaging.swift | 2 +- .../Services/WorkspaceWindowManager.swift | 2 +- CodeEdit/WorkspaceSheets.swift | 1 + Packages/CodeEditDomain/Package.swift | 15 +++ .../CodeEditDomain/Commands/Command.swift | 34 ++++++ .../CodeEditDomain/Editor}/EditorItemID.swift | 6 +- .../CodeEditDomain/Git/GitBranch.swift | 44 +++++++ .../CodeEditDomain/Git/GitBranchesGroup.swift | 24 ++++ .../CodeEditDomain/Git}/GitChangedFile.swift | 35 ++++-- .../CodeEditDomain/Git}/GitCommit.swift | 62 +++++++--- .../CodeEditDomain/Git/GitRemote.swift | 27 +++++ .../CodeEditDomain/Git/GitStashEntry.swift | 24 ++++ .../CodeEditDomain/Git}/GitStatus.swift | 6 +- .../Registry}/InstallationMethod.swift | 32 +---- .../Registry}/PackageManagerType.swift | 4 +- .../Registry}/PackageSource.swift | 22 ++-- .../Registry}/RegistryItem+Source.swift | 112 ++++++++++++------ .../Registry}/RegistryItem.swift | 72 ++++++----- .../Registry}/RegistryManagerError.swift | 6 +- .../Search/FuzzySearchModels.swift | 40 +++++++ .../Search}/SearchModeModel.swift | 52 ++++---- .../Tasks/TaskNotificationModel.swift | 25 ++++ 109 files changed, 647 insertions(+), 324 deletions(-) delete mode 100644 CodeEdit/Features/ActivityViewer/Models/TaskNotificationModel.swift create mode 100644 CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift create mode 100644 CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift delete mode 100644 CodeEdit/Features/Search/FuzzySearch/FuzzySearchModels.swift delete mode 100644 CodeEdit/Features/SourceControl/Models/GitBranch.swift delete mode 100644 CodeEdit/Features/SourceControl/Models/GitBranchesGroup.swift delete mode 100644 CodeEdit/Features/SourceControl/Models/GitRemote.swift delete mode 100644 CodeEdit/Features/SourceControl/Models/GitStashEntry.swift create mode 100644 Packages/CodeEditDomain/Package.swift create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Commands/Command.swift rename {CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models => Packages/CodeEditDomain/Sources/CodeEditDomain/Editor}/EditorItemID.swift (84%) create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranch.swift create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranchesGroup.swift rename {CodeEdit/Features/SourceControl/Models => Packages/CodeEditDomain/Sources/CodeEditDomain/Git}/GitChangedFile.swift (56%) rename {CodeEdit/Features/SourceControl/Models => Packages/CodeEditDomain/Sources/CodeEditDomain/Git}/GitCommit.swift (59%) create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitRemote.swift create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStashEntry.swift rename {CodeEdit/Features/SourceControl/Models => Packages/CodeEditDomain/Sources/CodeEditDomain/Git}/GitStatus.swift (88%) rename {CodeEdit/Features/LSP/Registry/Model => Packages/CodeEditDomain/Sources/CodeEditDomain/Registry}/InstallationMethod.swift (64%) rename {CodeEdit/Features/LSP/Registry/Model => Packages/CodeEditDomain/Sources/CodeEditDomain/Registry}/PackageManagerType.swift (91%) rename {CodeEdit/Features/LSP/Registry/Model => Packages/CodeEditDomain/Sources/CodeEditDomain/Registry}/PackageSource.swift (74%) rename {CodeEdit/Features/LSP/Registry/Model => Packages/CodeEditDomain/Sources/CodeEditDomain/Registry}/RegistryItem+Source.swift (71%) rename {CodeEdit/Features/LSP/Registry/Model => Packages/CodeEditDomain/Sources/CodeEditDomain/Registry}/RegistryItem.swift (50%) rename {CodeEdit/Features/LSP/Registry/Errors => Packages/CodeEditDomain/Sources/CodeEditDomain/Registry}/RegistryManagerError.swift (90%) create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Search/FuzzySearchModels.swift rename {CodeEdit/Features/Search/Model => Packages/CodeEditDomain/Sources/CodeEditDomain/Search}/SearchModeModel.swift (50%) create mode 100644 Packages/CodeEditDomain/Sources/CodeEditDomain/Tasks/TaskNotificationModel.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 8f51960877..d26984b8cc 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; + 58CFC49B2F8BE799009F4AA7 /* CodeEditDomain in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5E4485602DF600D9008BBE69 /* AboutWindow */; }; @@ -185,6 +186,7 @@ 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, + 58CFC49B2F8BE799009F4AA7 /* CodeEditDomain in Frameworks */, 6CCF6DD32E26D48F00B94F75 /* SwiftTerm in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */, @@ -342,6 +344,7 @@ 6CCF6DD22E26D48F00B94F75 /* SwiftTerm */, 6CCF73CF2E26DE3200B94F75 /* SwiftTerm */, 58CF9F392F86D64F009F4AA7 /* Factory */, + 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -450,6 +453,7 @@ 6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6CCF73CE2E26DE3200B94F75 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */, + 58CFC4992F8BE78F009F4AA7 /* XCLocalSwiftPackageReference "Packages/CodeEditDomain" */, ); preferredProjectObjectVersion = 55; productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; @@ -1421,7 +1425,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = CodeEdit/CodeEdit.entitlements; CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 47; @@ -1676,6 +1680,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 58CFC4992F8BE78F009F4AA7 /* XCLocalSwiftPackageReference "Packages/CodeEditDomain" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Packages/CodeEditDomain; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = { isa = XCRemoteSwiftPackageReference; @@ -1899,6 +1910,11 @@ package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; productName = FactoryTesting; }; + 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */ = { + isa = XCSwiftPackageProductDependency; + package = 58CFC4992F8BE78F009F4AA7 /* XCLocalSwiftPackageReference "Packages/CodeEditDomain" */; + productName = CodeEditDomain; + }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index d15ad87cfa..857ae964fb 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import CodeEditDomain import CodeEditSymbols import CodeEditSourceEditor import OSLog diff --git a/CodeEdit/CodeEdit.entitlements b/CodeEdit/CodeEdit.entitlements index 5c1489ef3b..83a78156ea 100644 --- a/CodeEdit/CodeEdit.entitlements +++ b/CodeEdit/CodeEdit.entitlements @@ -4,16 +4,13 @@ com.apple.security.app-sandbox - com.apple.security.files.user-selected.read-write - + com.apple.security.application-groups + com.apple.security.files.bookmarks.app-scope + com.apple.security.files.user-selected.read-write + com.apple.security.network.client - com.apple.security.application-groups - - app.codeedit.CodeEdit.shared - $(TeamIdentifierPrefix) - diff --git a/CodeEdit/Features/ActivityViewer/Models/TaskNotificationModel.swift b/CodeEdit/Features/ActivityViewer/Models/TaskNotificationModel.swift deleted file mode 100644 index a92b618bd2..0000000000 --- a/CodeEdit/Features/ActivityViewer/Models/TaskNotificationModel.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TaskNotificationModel.swift -// CodeEdit -// -// Created by Tommy Ludwig on 21.06.24. -// - -import Foundation - -/// Represents a notifications or tasks, that are displayed in the activity viewer -struct TaskNotificationModel: Equatable { - var id: String - var title: String - var message: String? - var percentage: Double? - var isLoading: Bool = false -} diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift index 655a66eab5..1673d8ebc7 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift @@ -7,6 +7,7 @@ import Foundation import Combine +import CodeEditDomain /// Manages task-related notifications. /// diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift index c73d8f229d..0b36891540 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct TaskNotificationView: View { @Environment(\.controlActiveState) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index 32b6f0a337..ff134953df 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import UniformTypeIdentifiers import Combine +import CodeEditDomain /// An object containing all necessary information and actions for a specific file in the workspace /// diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index 7993da1802..7992315e4a 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain import CodeEditSymbols import Combine diff --git a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift index f2194cdd05..0f1dd5f272 100644 --- a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift +++ b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import CodeEditDomain /// Simple state class for command palette view. Contains currently selected command, /// query text and list of filtered commands diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/Views/QuickActionsView.swift index 7d05abc407..6d3eb05be9 100644 --- a/CodeEdit/Features/Commands/Views/QuickActionsView.swift +++ b/CodeEdit/Features/Commands/Views/QuickActionsView.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import CodeEditDomain /// Quick actions view struct QuickActionsView: View { diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index ef1cdad42d..c007228a74 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -2,7 +2,7 @@ // WorkspaceManaging.swift // CodeEdit // -// Created by CodeEdit Contributors on 06.04.26. +// Created by Matthijs Eikelenboom on 06.04.26. // import Foundation diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift index d503ea3c21..54063fc1c6 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift @@ -2,7 +2,7 @@ // WorkspaceStatePersisting.swift // CodeEdit // -// Created by CodeEdit Contributors on 06.04.26. +// Created by Matthijs Eikelenboom on 06.04.26. // import Foundation diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift index 6defadc155..d85f1adfad 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift @@ -2,7 +2,7 @@ // WorkspaceStatePersistence.swift // CodeEdit // -// Created by CodeEdit Contributors on 25.03.26. +// Created by Matthijs Eikelenboom on 25.03.26. // import Foundation diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift index 771de2067d..4fb522ca9f 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain /// Protocol for data passed to EditorTabView to conform to protocol EditorTabRepresentable { diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift index 96bf256a28..88b500ca42 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct HistoryInspectorItemView: View { var commit: GitCommit diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index a1c639b405..7fe19786a5 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain final class HistoryInspectorModel: ObservableObject { private(set) var sourceControlManager: SourceControlManager? diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index ec40a52928..97e750e7f0 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI +import CodeEditDomain struct HistoryInspectorView: View { @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift index 704eddb2c9..af61a79354 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct HistoryPopoverView: View { diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/Features/Keybindings/CommandManager.swift index e3b26efdf1..c0351f20e9 100644 --- a/CodeEdit/Features/Keybindings/CommandManager.swift +++ b/CodeEdit/Features/Keybindings/CommandManager.swift @@ -5,6 +5,7 @@ // import Foundation +import CodeEditDomain /** The object of this class intended to be a hearth of command palette. This object only exists as singleton. @@ -42,23 +43,3 @@ final class CommandManager: CommandManaging { commandsList[id]?.closureWrapper() } } - -/// Command struct uses as a wrapper for command. Used by command palette to call selected commands. -struct Command: Identifiable, Hashable { - - static func == (lhs: Command, rhs: Command) -> Bool { - return lhs.id == rhs.id - } - - static func < (lhs: Command, rhs: Command) -> Bool { - return false - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - let id: String - let title: String - let closureWrapper: () -> Void -} diff --git a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift index 24f8e61273..1a49dd18e4 100644 --- a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift +++ b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Protocol for managing application commands (command palette). protocol CommandManaging: AnyObject, ObservableObject { diff --git a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift b/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift new file mode 100644 index 0000000000..09205a257e --- /dev/null +++ b/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift @@ -0,0 +1,31 @@ +// +// InstallationMethod+PackageManager.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/04/26. +// + +import Foundation +import CodeEditDomain + +extension InstallationMethod { + func packageManager(installPath: URL) -> PackageManagerProtocol? { + switch packageManagerType { + case .npm: + return NPMPackageManager(installationDirectory: installPath) + case .cargo: + return CargoPackageManager(installationDirectory: installPath) + case .pip: + return PipPackageManager(installationDirectory: installPath) + case .golang: + return GolangPackageManager(installationDirectory: installPath) + case .github, .sourceBuild: + return GithubPackageManager(installationDirectory: installPath) + case .nuget, .opam, .gem, .composer: + // TODO: IMPLEMENT OTHER PACKAGE MANAGERS + return nil + default: + return nil + } + } +} diff --git a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift b/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift new file mode 100644 index 0000000000..7de2f2171f --- /dev/null +++ b/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift @@ -0,0 +1,35 @@ +// +// RegistryItem+AppExtensions.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/04/26. +// + +import Foundation +import CodeEditDomain + +extension RegistryItem: FuzzySearchable { + var searchableString: String { name } +} + +extension RegistryItem { + /// The method for installation, parsed from this item's ``source`` parameter. + var installMethod: InstallationMethod? { + let sourceId = source.id + if sourceId.hasPrefix("pkg:cargo/") { + return PackageSourceParser.parseCargoPackage(self) + } else if sourceId.hasPrefix("pkg:npm/") { + return PackageSourceParser.parseNpmPackage(self) + } else if sourceId.hasPrefix("pkg:pypi/") { + return PackageSourceParser.parsePythonPackage(self) + } else if sourceId.hasPrefix("pkg:gem/") { + return PackageSourceParser.parseRubyGem(self) + } else if sourceId.hasPrefix("pkg:golang/") { + return PackageSourceParser.parseGolangPackage(self) + } else if sourceId.hasPrefix("pkg:github/") { + return PackageSourceParser.parseGithubPackage(self) + } else { + return nil + } + } +} diff --git a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift b/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift index 481030464f..d2e5b69747 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// The protocol each package manager conforms to for creating ``PackageManagerInstallOperation``s. protocol PackageManagerProtocol { diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift index 695ee70e7f..488b779390 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift @@ -8,6 +8,7 @@ import Foundation import Factory import Combine +import CodeEditDomain /// An executable install operation for installing a ``RegistryItem``. /// diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift index c91fb99223..55ac352abb 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift @@ -7,6 +7,7 @@ import Factory import Foundation +import CodeEditDomain final class CargoPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift index 31e5e6cbb7..c5be18db0b 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift @@ -7,6 +7,7 @@ import Factory import Foundation +import CodeEditDomain final class GithubPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift index 0b2f4a4a40..0d062f8326 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift @@ -7,6 +7,7 @@ import Factory import Foundation +import CodeEditDomain final class GolangPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift index 0b7c7062c3..e406618930 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift @@ -7,6 +7,7 @@ import Factory import Foundation +import CodeEditDomain final class NPMPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift index 5aa87782d6..d6ac971be5 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift @@ -7,6 +7,7 @@ import Factory import Foundation +import CodeEditDomain final class PipPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift index 0b81d6a97a..242464bf29 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift @@ -5,6 +5,8 @@ // Created by Abe Malla on 3/12/25. // +import CodeEditDomain + extension PackageSourceParser { static func parseCargoPackage(_ entry: RegistryItem) -> InstallationMethod { // Format: pkg:cargo/PACKAGE@VERSION?PARAMS diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift index 1c2c7734af..810c199629 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift @@ -5,6 +5,8 @@ // Created by Abe Malla on 3/12/25. // +import CodeEditDomain + extension PackageSourceParser { static func parseRubyGem(_ entry: RegistryItem) -> InstallationMethod { // Format: pkg:gem/PACKAGE@VERSION?PARAMS diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift index d75bf49700..871cb04228 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift @@ -5,6 +5,8 @@ // Created by Abe Malla on 3/12/25. // +import CodeEditDomain + extension PackageSourceParser { static func parseGolangPackage(_ entry: RegistryItem) -> InstallationMethod { // Format: pkg:golang/PACKAGE@VERSION#SUBPATH?PARAMS diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift index b5a63bb9e9..b098a7e64e 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift @@ -5,6 +5,8 @@ // Created by Abe Malla on 3/12/25. // +import CodeEditDomain + extension PackageSourceParser { static func parseNpmPackage(_ entry: RegistryItem) -> InstallationMethod { // Format: pkg:npm/PACKAGE@VERSION?PARAMS diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift index bb41d7dc55..af31fcdb9f 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift @@ -5,6 +5,8 @@ // Created by Abe Malla on 3/12/25. // +import CodeEditDomain + extension PackageSourceParser { static func parsePythonPackage(_ entry: RegistryItem) -> InstallationMethod { // Format: pkg:pypi/PACKAGE@VERSION?PARAMS diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift index 803d061fa1..2b74015963 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Parser for package source IDs enum PackageSourceParser { diff --git a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift index 6dd212ff54..c8c63f0f27 100644 --- a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift +++ b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Protocol for managing the language server registry. /// diff --git a/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift b/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift index 16d171b62f..90a1de9c31 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// This parser is used to parse expressions that may be included in a field of a registry item. /// diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift b/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift index 86ec7ec2fe..c303a47b51 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension RegistryManager { /// Downloads the latest registry diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index d946695bb2..7028a79a1e 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -10,6 +10,7 @@ import Foundation import ZIPFoundation import Combine import Factory +import CodeEditDomain @MainActor final class RegistryManager: ObservableObject, RegistryManaging { diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift index bb3b09bf70..0a11cdc0e6 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift @@ -7,6 +7,7 @@ import SwiftUI import Combine +import CodeEditDomain struct FindModePicker: View { var modes: [SearchModeModel] diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift index cbf2807401..b409080969 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct FindNavigatorForm: View { @ObservedObject private var state: SearchState diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 9256c3e3e1..b78eb09fa3 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditDomain extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView( diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 282da19b5e..d960fa10ff 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -8,6 +8,7 @@ import AppKit import SwiftUI import OSLog +import CodeEditDomain /// A `NSViewController` that handles the **ProjectNavigatorView** in the **NavigatorArea**. /// diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index b2afda4b79..8793d28932 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -7,6 +7,7 @@ import AppKit import SwiftUI +import CodeEditDomain struct SourceControlNavigatorChangesList: View { @EnvironmentObject var workspace: Workspace diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift index c5993fa51e..455174d2db 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct CommitDetailsHeaderView: View { var commit: GitCommit diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift index ed63905c1e..96c71e0ca1 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct CommitDetailsView: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift index 5314cebbec..f119b290a1 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct CommitListItemView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index 482a379945..2f57c71205 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain import CodeEditSymbols struct SourceControlNavigatorHistoryView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift index a0e4549092..23fb1364ff 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct RepoOutlineGroupItem: Hashable, Identifiable { enum ImageType: Hashable { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift index b52ce306f1..132f42097e 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain extension SourceControlNavigatorRepositoryView { func handleDelete(_ item: RepoOutlineGroupItem) { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift index ecdbdea79b..f9ca8b8474 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain import CodeEditSymbols struct SourceControlNavigatorRepositoryView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index 880aaaa470..e95ec67bd5 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import CodeEditDomain struct GitChangedFileLabel: View { @EnvironmentObject private var workspace: Workspace diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index bea8d285ae..2748b0bcee 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain /// A view to display a changed file's information in a list view. Optionally displays the staged status. struct GitChangedFileListView: View { diff --git a/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift b/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift index 828866e391..7ce8a68969 100644 --- a/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift +++ b/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift @@ -7,6 +7,7 @@ import Foundation import CollectionConcurrencyKit +import CodeEditDomain extension Collection where Iterator.Element: FuzzySearchable { /// Asynchronously performs a fuzzy search on a collection of elements conforming to FuzzySearchable. diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchModels.swift b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchModels.swift deleted file mode 100644 index 9f5511a701..0000000000 --- a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchModels.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// FuzzySearchModels.swift -// CodeEdit -// -// Created by Tommy Ludwig on 03.02.24. -// - -import Foundation - -/// FuzzySearchCharacters is used to normalise strings -struct FuzzySearchCharacter { - let content: String - // normalised content is referring to a string that is case- and accent-insensitive - let normalisedContent: String -} - -/// FuzzySearchString is just made up by multiple characters, similar to a string, but also with normalised characters -struct FuzzySearchString { - var characters: [FuzzySearchCharacter] -} - -/// FuzzySearchMatchResult represents an object that has undergone a fuzzy search using the fuzzyMatch function. -struct FuzzySearchMatchResult { - let weight: Int - let matchedParts: [NSRange] -} diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift index a961c2ad88..0151b5efc7 100644 --- a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift +++ b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// A protocol defining the requirements for an object that can be searched using fuzzy matching. protocol FuzzySearchable { diff --git a/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift b/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift index 4533ca8415..df7fd78cf7 100644 --- a/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift +++ b/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension String { /// Returns the length of the matching prefix content or normalised content at the specified index. diff --git a/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift b/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift index 40e9ee13ae..dc2cb5bc9f 100644 --- a/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift +++ b/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension String { /// Normalises the characters of the string by converting them to ASCII representation. diff --git a/CodeEdit/Features/Search/SearchState.swift b/CodeEdit/Features/Search/SearchState.swift index bed26d7ec5..2ac9700634 100644 --- a/CodeEdit/Features/Search/SearchState.swift +++ b/CodeEdit/Features/Search/SearchState.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Manages the search/find state for a workspace, including indexing, search results, /// and find-and-replace operations. Extracted from Workspace to be independently diff --git a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift index 048af86c1f..6ea0f2cc0b 100644 --- a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift +++ b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift @@ -2,7 +2,7 @@ // CodableDefault+Providers.swift // CodeEdit // -// Created by CodeEdit Contributors on 07.04.26. +// Created by Matthijs Eikelenboom on 07.04.26. // import AppKit diff --git a/CodeEdit/Features/Settings/Models/CodableDefault.swift b/CodeEdit/Features/Settings/Models/CodableDefault.swift index e20f664bbc..ba3752aef8 100644 --- a/CodeEdit/Features/Settings/Models/CodableDefault.swift +++ b/CodeEdit/Features/Settings/Models/CodableDefault.swift @@ -2,7 +2,7 @@ // CodableDefault.swift // CodeEdit // -// Created by CodeEdit Contributors on 07.04.26. +// Created by Matthijs Eikelenboom on 07.04.26. // import Foundation diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift index 02f423f8a0..4fb1d09105 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain private let iconSize: CGFloat = 26 diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 122b30539a..853ea09968 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import CodeEditDomain /// Displays a searchable list of packages from the ``RegistryManager``. struct LanguageServersView: View { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift index e4150f8045..39d6672dc2 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift @@ -2,7 +2,7 @@ // ThemeRepository.swift // CodeEdit // -// Created by CodeEdit Contributors on 07.04.26. +// Created by Matthijs Eikelenboom on 07.04.26. // import Foundation diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift index 328ff81422..1891bfed66 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension GitClient { /// Get branches diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift index 9a8dc0c9a3..e51921b609 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift @@ -7,6 +7,7 @@ import Foundation import RegexBuilder +import CodeEditDomain extension GitClient { /// Commit files diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift b/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift index 6245fe7739..c49398ba28 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension GitClient { /// Gets the commit history log for the specified branch or file diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift index 04307430b3..55bf456c7d 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension GitClient { /// Gets all remotes diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift index 69a0a82116..417cdcbded 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain extension GitClient { /// Add uncommited changes to stash diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift index 5883f7782c..01da617b7a 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Methods for parsing git's porcelain v2 format and returning the info in a ``GitClient/Status`` struct. /// diff --git a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift b/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift index 5b61e6a6c4..70e4c078bb 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift @@ -2,10 +2,11 @@ // GitClientProtocol.swift // CodeEdit // -// Created by CodeEdit Contributors on 07.04.26. +// Created by Matthijs Eikelenboom on 07.04.26. // import Foundation +import CodeEditDomain /// Abstraction over git operations used by ``SourceControlManager``. /// diff --git a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift b/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift index c3a4c3b001..8b7dbae036 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift +++ b/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import CodeEditDomain struct GitCheckoutBranchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift index 77d6c811d4..771037e291 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Factory +import CodeEditDomain class GitCheckoutBranchViewModel: ObservableObject { @Published var selectedBranch: GitBranch? diff --git a/CodeEdit/Features/SourceControl/Models/GitBranch.swift b/CodeEdit/Features/SourceControl/Models/GitBranch.swift deleted file mode 100644 index 1118baf873..0000000000 --- a/CodeEdit/Features/SourceControl/Models/GitBranch.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// GitBranch.swift -// CodeEdit -// -// Created by Albert Vinizhanau on 10/20/23. -// - -import Foundation - -struct GitBranch: Hashable, Identifiable { - let name: String - let longName: String - let upstream: String? - let ahead: Int - let behind: Int - - var id: String { - longName - } - - /// Is local branch - var isLocal: Bool { - return longName.hasPrefix("refs/heads/") - } - - /// Is remote branch - var isRemote: Bool { - return longName.hasPrefix("refs/remotes/") - } -} diff --git a/CodeEdit/Features/SourceControl/Models/GitBranchesGroup.swift b/CodeEdit/Features/SourceControl/Models/GitBranchesGroup.swift deleted file mode 100644 index 6c05097f6b..0000000000 --- a/CodeEdit/Features/SourceControl/Models/GitBranchesGroup.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// GitBranchesGroup.swift -// CodeEdit -// -// Created by Federico Zivolo on 22/01/24. -// - -import Foundation - -struct GitBranchesGroup: Hashable { - let name: String - var branches: [GitBranch] - var shouldNest: Bool { - branches.first?.name.hasPrefix(name + "/") ?? false - } -} diff --git a/CodeEdit/Features/SourceControl/Models/GitRemote.swift b/CodeEdit/Features/SourceControl/Models/GitRemote.swift deleted file mode 100644 index 0edde776e8..0000000000 --- a/CodeEdit/Features/SourceControl/Models/GitRemote.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// GitRemote.swift -// CodeEdit -// -// Created by Austin Condiff on 11/17/23. -// - -import Foundation - -struct GitRemote: Hashable { - let name: String - let pushLocation: String - let fetchLocation: String - var branches: [GitBranch] = [] -} diff --git a/CodeEdit/Features/SourceControl/Models/GitStashEntry.swift b/CodeEdit/Features/SourceControl/Models/GitStashEntry.swift deleted file mode 100644 index cbec68520d..0000000000 --- a/CodeEdit/Features/SourceControl/Models/GitStashEntry.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// GitStashEntry.swift -// CodeEdit -// -// Created by Austin Condiff on 11/20/23. -// - -import Foundation - -struct GitStashEntry: Hashable { - let index: Int - let message: String - let date: Date -} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift index 0f8ca53b4d..897af31436 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Branch-related git operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift index 2e4d6f330c..8e611c0a56 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// File status, staging, committing, and discard operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift index 7075381be3..d9df459320 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Remote, fetch, pull, and push operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift index d770e9253b..a6b153deaa 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDomain /// Stash-related git operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index 64ccd02996..3f8c036177 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -7,6 +7,7 @@ import Foundation import OSLog +import CodeEditDomain /// Stores git state for the workspace and delegates operations to ``GitClient``. /// diff --git a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift b/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift index 165b5d3220..65064c2a94 100644 --- a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift +++ b/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct RemoteBranchPicker: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift index 88fa176a9b..ca90a052b1 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct SourceControlNewBranchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift index 65165caa4f..239fee1721 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct SourceControlRenameBranchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift index 6bb77179be..af57c67526 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct SourceControlSwitchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 02c57f0ee7..b88492ce78 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -2,7 +2,7 @@ // Workspace.swift // CodeEdit // -// Created by CodeEdit Contributors on 06.04.26. +// Created by Matthijs Eikelenboom on 06.04.26. // import AppKit diff --git a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift index 1f6be962db..2680880903 100644 --- a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift +++ b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift @@ -2,7 +2,7 @@ // WorkspaceWindowManaging.swift // CodeEdit // -// Created by CodeEdit Contributors on 06.04.26. +// Created by Matthijs Eikelenboom on 06.04.26. // import Foundation diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 297fa11eaf..d672f06f9a 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -2,7 +2,7 @@ // WorkspaceWindowManager.swift // CodeEdit // -// Created by CodeEdit Contributors on 06.04.26. +// Created by Matthijs Eikelenboom on 06.04.26. // import AppKit diff --git a/CodeEdit/WorkspaceSheets.swift b/CodeEdit/WorkspaceSheets.swift index 131d5bbf7b..ab99f470e8 100644 --- a/CodeEdit/WorkspaceSheets.swift +++ b/CodeEdit/WorkspaceSheets.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDomain struct WorkspaceSheets: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/Packages/CodeEditDomain/Package.swift b/Packages/CodeEditDomain/Package.swift new file mode 100644 index 0000000000..18800ce118 --- /dev/null +++ b/Packages/CodeEditDomain/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CodeEditDomain", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CodeEditDomain", targets: ["CodeEditDomain"]) + ], + targets: [ + .target(name: "CodeEditDomain"), + .testTarget(name: "CodeEditDomainTests", dependencies: ["CodeEditDomain"]) + ] +) diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Commands/Command.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Commands/Command.swift new file mode 100644 index 0000000000..91dadd7a1d --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Commands/Command.swift @@ -0,0 +1,34 @@ +// +// Command.swift +// CodeEditDomain +// +// Created by Matthijs Eikelenboom on 12/04/26. +// + +import Foundation + +/// Command struct uses as a wrapper for command. Used by command palette to call selected commands. +public struct Command: Identifiable, Hashable { + + public static func == (lhs: Command, rhs: Command) -> Bool { + return lhs.id == rhs.id + } + + public static func < (lhs: Command, rhs: Command) -> Bool { + return false + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public let id: String + public let title: String + public let closureWrapper: () -> Void + + public init(id: String, title: String, closureWrapper: @escaping () -> Void) { + self.id = id + self.title = title + self.closureWrapper = closureWrapper + } +} diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorItemID.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Editor/EditorItemID.swift similarity index 84% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorItemID.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Editor/EditorItemID.swift index 27cff36941..39b4f82d96 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorItemID.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Editor/EditorItemID.swift @@ -1,6 +1,6 @@ // // EditorTabID.swift -// +// // // Created by Pavel Kasila on 30.04.22. // @@ -8,8 +8,8 @@ import Foundation /// Enum to represent item's ID to tab bar -enum EditorTabID: Codable, Identifiable, Hashable { - var id: String { +public enum EditorTabID: Codable, Identifiable, Hashable { + public var id: String { switch self { case .codeEditor(let path): return "codeEditor_\(path)" diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranch.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranch.swift new file mode 100644 index 0000000000..ff72401cb1 --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranch.swift @@ -0,0 +1,44 @@ +// +// GitBranch.swift +// CodeEdit +// +// Created by Albert Vinizhanau on 10/20/23. +// + +import Foundation + +public struct GitBranch: Hashable, Identifiable { + public let name: String + public let longName: String + public let upstream: String? + public let ahead: Int + public let behind: Int + + public var id: String { + longName + } + + /// Is local branch + public var isLocal: Bool { + return longName.hasPrefix("refs/heads/") + } + + /// Is remote branch + public var isRemote: Bool { + return longName.hasPrefix("refs/remotes/") + } + + public init( + name: String, + longName: String, + upstream: String?, + ahead: Int, + behind: Int + ) { + self.name = name + self.longName = longName + self.upstream = upstream + self.ahead = ahead + self.behind = behind + } +} diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranchesGroup.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranchesGroup.swift new file mode 100644 index 0000000000..91bdf92b4a --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranchesGroup.swift @@ -0,0 +1,24 @@ +// +// GitBranchesGroup.swift +// CodeEdit +// +// Created by Federico Zivolo on 22/01/24. +// + +import Foundation + +public struct GitBranchesGroup: Hashable { + public let name: String + public var branches: [GitBranch] + public var shouldNest: Bool { + branches.first?.name.hasPrefix(name + "/") ?? false + } + + public init( + name: String, + branches: [GitBranch] + ) { + self.name = name + self.branches = branches + } +} diff --git a/CodeEdit/Features/SourceControl/Models/GitChangedFile.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitChangedFile.swift similarity index 56% rename from CodeEdit/Features/SourceControl/Models/GitChangedFile.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitChangedFile.swift index 641b8d3370..587c306efb 100644 --- a/CodeEdit/Features/SourceControl/Models/GitChangedFile.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitChangedFile.swift @@ -1,47 +1,58 @@ // // ChangedFile.swift -// +// // // Created by Nanashi Li on 2022/05/20. // import Foundation -import SwiftUI /// Represents a single changed file in the working tree. -struct GitChangedFile: Identifiable, Hashable { - var id: String { fileURL.relativePath } +public struct GitChangedFile: Identifiable, Hashable { + public var id: String { fileURL.relativePath } /// The status of the file. - let status: GitStatus + public let status: GitStatus /// The staged status of the file. A non-`none` value here and in ``status`` may indicate a file that was added /// but has since been changed and needs to be re-added before committing. - let stagedStatus: GitStatus + public let stagedStatus: GitStatus /// URL of the file - let fileURL: URL + public let fileURL: URL /// The original file name if ``status`` or ``stagedStatus`` is `renamed` or `copied` - let originalFilename: String? + public let originalFilename: String? /// Returns the user-facing status, if ``status`` is `none`, returns ``stagedStatus``. - func anyStatus() -> GitStatus { + public func anyStatus() -> GitStatus { if case .none = status { return stagedStatus } return status } - var isStaged: Bool { + public var isStaged: Bool { stagedStatus != .none } /// Use this string to find matching `CEWorkspaceFile`s in the workspace file manager. - var ceFileKey: String { + public var ceFileKey: String { fileURL.absoluteURL.path(percentEncoded: false) } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(fileURL) } + + public init( + status: GitStatus, + stagedStatus: GitStatus, + fileURL: URL, + originalFilename: String? + ) { + self.status = status + self.stagedStatus = stagedStatus + self.fileURL = fileURL + self.originalFilename = originalFilename + } } diff --git a/CodeEdit/Features/SourceControl/Models/GitCommit.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitCommit.swift similarity index 59% rename from CodeEdit/Features/SourceControl/Models/GitCommit.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitCommit.swift index da2b95063a..59980eddc4 100644 --- a/CodeEdit/Features/SourceControl/Models/GitCommit.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitCommit.swift @@ -8,22 +8,22 @@ import Foundation.NSDate /// Model class to help map commit history log data -struct GitCommit: Equatable, Hashable, Identifiable { - var id = UUID() - let hash: String - let commitHash: String - let message: String - let author: String - let authorEmail: String - let committer: String - let committerEmail: String - let body: String - let refs: [String] - let tag: String - let remoteURL: URL? - let date: Date +public struct GitCommit: Equatable, Hashable, Identifiable { + public var id: UUID + public let hash: String + public let commitHash: String + public let message: String + public let author: String + public let authorEmail: String + public let committer: String + public let committerEmail: String + public let body: String + public let refs: [String] + public let tag: String + public let remoteURL: URL? + public let date: Date - var commitBaseURL: URL? { + public var commitBaseURL: URL? { if let remoteURL { if remoteURL.absoluteString.contains("github") { return parsedRemoteUrl(domain: "https://github.com", remote: remoteURL) @@ -51,7 +51,7 @@ struct GitCommit: Equatable, Hashable, Identifiable { return formattedRemote.deletingPathExtension().appending(path: "commit") } - var remoteString: String { + public var remoteString: String { if let remoteURL { if remoteURL.absoluteString.contains("github") { return "GitHub" @@ -65,4 +65,34 @@ struct GitCommit: Equatable, Hashable, Identifiable { } return "Remote" } + + public init( + id: UUID = UUID(), + hash: String, + commitHash: String, + message: String, + author: String, + authorEmail: String, + committer: String, + committerEmail: String, + body: String, + refs: [String], + tag: String, + remoteURL: URL?, + date: Date + ) { + self.id = id + self.hash = hash + self.commitHash = commitHash + self.message = message + self.author = author + self.authorEmail = authorEmail + self.committer = committer + self.committerEmail = committerEmail + self.body = body + self.refs = refs + self.tag = tag + self.remoteURL = remoteURL + self.date = date + } } diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitRemote.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitRemote.swift new file mode 100644 index 0000000000..730e92fffb --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitRemote.swift @@ -0,0 +1,27 @@ +// +// GitRemote.swift +// CodeEdit +// +// Created by Austin Condiff on 11/17/23. +// + +import Foundation + +public struct GitRemote: Hashable { + public let name: String + public let pushLocation: String + public let fetchLocation: String + public var branches: [GitBranch] + + public init( + name: String, + pushLocation: String, + fetchLocation: String, + branches: [GitBranch] = [] + ) { + self.name = name + self.pushLocation = pushLocation + self.fetchLocation = fetchLocation + self.branches = branches + } +} diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStashEntry.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStashEntry.swift new file mode 100644 index 0000000000..2e87098ebe --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStashEntry.swift @@ -0,0 +1,24 @@ +// +// GitStashEntry.swift +// CodeEdit +// +// Created by Austin Condiff on 11/20/23. +// + +import Foundation + +public struct GitStashEntry: Hashable { + public let index: Int + public let message: String + public let date: Date + + public init( + index: Int, + message: String, + date: Date + ) { + self.index = index + self.message = message + self.date = date + } +} diff --git a/CodeEdit/Features/SourceControl/Models/GitStatus.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStatus.swift similarity index 88% rename from CodeEdit/Features/SourceControl/Models/GitStatus.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStatus.swift index a5904cae82..251c34e05a 100644 --- a/CodeEdit/Features/SourceControl/Models/GitStatus.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStatus.swift @@ -1,13 +1,13 @@ // // GitType.swift -// +// // // Created by Nanashi Li on 2022/05/20. // import Foundation -enum GitStatus: String, Codable { +public enum GitStatus: String, Codable { case none = "." case modified = "M" case untracked = "?" @@ -18,7 +18,7 @@ enum GitStatus: String, Codable { case copied = "C" case unmerged = "U" - var description: String { + public var description: String { switch self { case .modified: return "M" case .untracked: return "U" diff --git a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/InstallationMethod.swift similarity index 64% rename from CodeEdit/Features/LSP/Registry/Model/InstallationMethod.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/InstallationMethod.swift index 87c5c0d2bd..56b8f6852c 100644 --- a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/InstallationMethod.swift @@ -8,7 +8,7 @@ import Foundation /// Installation method enum with all supported types -enum InstallationMethod: Equatable { +public enum InstallationMethod: Equatable { /// For standard package manager installations case standardPackage(source: PackageSource) /// For packages that need to be built from source with custom build steps @@ -18,7 +18,7 @@ enum InstallationMethod: Equatable { /// For installations that aren't recognized case unknown - var packageName: String? { + public var packageName: String? { switch self { case .standardPackage(let source), .sourceBuild(let source, _), @@ -29,7 +29,7 @@ enum InstallationMethod: Equatable { } } - var version: String? { + public var version: String? { switch self { case .standardPackage(let source), .sourceBuild(let source, _), @@ -40,7 +40,7 @@ enum InstallationMethod: Equatable { } } - var packageManagerType: PackageManagerType? { + public var packageManagerType: PackageManagerType? { switch self { case .standardPackage(let source), .sourceBuild(let source, _), @@ -51,27 +51,7 @@ enum InstallationMethod: Equatable { } } - func packageManager(installPath: URL) -> PackageManagerProtocol? { - switch packageManagerType { - case .npm: - return NPMPackageManager(installationDirectory: installPath) - case .cargo: - return CargoPackageManager(installationDirectory: installPath) - case .pip: - return PipPackageManager(installationDirectory: installPath) - case .golang: - return GolangPackageManager(installationDirectory: installPath) - case .github, .sourceBuild: - return GithubPackageManager(installationDirectory: installPath) - case .nuget, .opam, .gem, .composer: - // TODO: IMPLEMENT OTHER PACKAGE MANAGERS - return nil - default: - return nil - } - } - - var installerDescription: String { + public var installerDescription: String { guard let packageManagerType else { return "Unknown" } switch packageManagerType { case .npm, .cargo, .golang, .pip, .sourceBuild, .github: @@ -81,7 +61,7 @@ enum InstallationMethod: Equatable { } } - var packageDescription: String? { + public var packageDescription: String? { guard let packageName else { return nil } if let version { return "\(packageName)@\(version)" diff --git a/CodeEdit/Features/LSP/Registry/Model/PackageManagerType.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageManagerType.swift similarity index 91% rename from CodeEdit/Features/LSP/Registry/Model/PackageManagerType.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageManagerType.swift index 28ebacee34..2a3d8ee859 100644 --- a/CodeEdit/Features/LSP/Registry/Model/PackageManagerType.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageManagerType.swift @@ -6,7 +6,7 @@ // /// Package manager types supported by the system -enum PackageManagerType: String, Codable { +public enum PackageManagerType: String, Codable { /// JavaScript case npm /// Rust @@ -28,7 +28,7 @@ enum PackageManagerType: String, Codable { /// Binary download case github - var userDescription: String { + public var userDescription: String { switch self { case .npm: "NPM" diff --git a/CodeEdit/Features/LSP/Registry/Model/PackageSource.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageSource.swift similarity index 74% rename from CodeEdit/Features/LSP/Registry/Model/PackageSource.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageSource.swift index 0df959fb80..b2a0b4edd6 100644 --- a/CodeEdit/Features/LSP/Registry/Model/PackageSource.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageSource.swift @@ -7,25 +7,25 @@ /// Generic package source information that applies to all installation methods. /// Takes all the necessary information from `RegistryItem`. -struct PackageSource: Equatable, Codable { +public struct PackageSource: Equatable, Codable { /// The raw source ID string from the registry - let sourceId: String + public let sourceId: String /// The type of the package manager - let type: PackageManagerType + public let type: PackageManagerType /// Package name - let pkgName: String + public let pkgName: String /// The name in the registry.json file. Used for the folder name when saved. - let entryName: String + public let entryName: String /// Package version - let version: String + public let version: String /// URL for repository or download link - let repositoryUrl: String? + public let repositoryUrl: String? /// Git reference type if this is a git based package - let gitReference: GitReference? + public let gitReference: GitReference? /// Additional possible options - var options: [String: String] + public var options: [String: String] - init( + public init( sourceId: String, type: PackageManagerType, pkgName: String, @@ -45,7 +45,7 @@ struct PackageSource: Equatable, Codable { self.options = options } - enum GitReference: Equatable, Codable { + public enum GitReference: Equatable, Codable { case tag(String) case revision(String) } diff --git a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+Source.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem+Source.swift similarity index 71% rename from CodeEdit/Features/LSP/Registry/Model/RegistryItem+Source.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem+Source.swift index 5a1b3e8261..3f2de9290a 100644 --- a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+Source.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem+Source.swift @@ -6,19 +6,31 @@ // extension RegistryItem { - struct Source: Codable { - let id: String - let asset: AssetContainer? - let build: BuildContainer? - let versionOverrides: [VersionOverride]? + public struct Source: Codable { + public let id: String + public let asset: AssetContainer? + public let build: BuildContainer? + public let versionOverrides: [VersionOverride]? - enum AssetContainer: Codable { + public init( + id: String, + asset: AssetContainer?, + build: BuildContainer?, + versionOverrides: [VersionOverride]? + ) { + self.id = id + self.asset = asset + self.build = build + self.versionOverrides = versionOverrides + } + + public enum AssetContainer: Codable { case single(Asset) case multiple([Asset]) case simpleFile(String) case none - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { if let container = try? decoder.singleValueContainer() { if let singleValue = try? container.decode(Asset.self) { self = .single(singleValue) @@ -37,7 +49,7 @@ extension RegistryItem { self = .none } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .single(let value): @@ -51,7 +63,7 @@ extension RegistryItem { } } - func getDarwinFileName() -> String? { + public func getDarwinFileName() -> String? { switch self { case .single(let asset): if asset.target.isDarwinTarget() { @@ -73,12 +85,12 @@ extension RegistryItem { } } - enum BuildContainer: Codable { + public enum BuildContainer: Codable { case single(Build) case multiple([Build]) case none - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { if let container = try? decoder.singleValueContainer() { if let singleValue = try? container.decode(Build.self) { self = .single(singleValue) @@ -91,7 +103,7 @@ extension RegistryItem { self = .none } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .single(let value): @@ -103,7 +115,7 @@ extension RegistryItem { } } - func getUnixBuildCommand() -> String? { + public func getUnixBuildCommand() -> String? { switch self { case .single(let build): return build.run @@ -121,31 +133,53 @@ extension RegistryItem { } } - struct Build: Codable { - let target: Target? - let run: String - let env: [String: String]? - let bin: BinContainer? + public struct Build: Codable { + public let target: Target? + public let run: String + public let env: [String: String]? + public let bin: BinContainer? + + public init( + target: Target?, + run: String, + env: [String: String]?, + bin: BinContainer? + ) { + self.target = target + self.run = run + self.env = env + self.bin = bin + } } - struct Asset: Codable { - let target: Target - let file: String? - let bin: BinContainer? + public struct Asset: Codable { + public let target: Target + public let file: String? + public let bin: BinContainer? - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.target = try container.decode(Target.self, forKey: .target) self.file = try container.decodeIfPresent(String.self, forKey: .file) self.bin = try container.decodeIfPresent(BinContainer.self, forKey: .bin) } + + public init( + target: Target, + file: String?, + bin: BinContainer? + ) { + self.target = target + self.file = file + self.bin = bin + } } - enum Target: Codable { + public enum Target: Codable { case single(String) case multiple([String]) - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let singleValue = try? container.decode(String.self) { self = .single(singleValue) @@ -162,7 +196,7 @@ extension RegistryItem { } } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .single(let value): @@ -172,7 +206,7 @@ extension RegistryItem { } } - func isDarwinTarget() -> Bool { + public func isDarwinTarget() -> Bool { switch self { case .single(let value): #if arch(arm64) @@ -194,11 +228,11 @@ extension RegistryItem { } } - enum BinContainer: Codable { + public enum BinContainer: Codable { case single(String) case multiple([String: String]) - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let singleValue = try? container.decode(String.self) { self = .single(singleValue) @@ -215,7 +249,7 @@ extension RegistryItem { } } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .single(let value): @@ -226,10 +260,20 @@ extension RegistryItem { } } - struct VersionOverride: Codable { - let constraint: String - let id: String - let asset: AssetContainer? + public struct VersionOverride: Codable { + public let constraint: String + public let id: String + public let asset: AssetContainer? + + public init( + constraint: String, + id: String, + asset: AssetContainer? + ) { + self.constraint = constraint + self.id = id + self.asset = asset + } } } } diff --git a/CodeEdit/Features/LSP/Registry/Model/RegistryItem.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem.swift similarity index 50% rename from CodeEdit/Features/LSP/Registry/Model/RegistryItem.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem.swift index 763c5b2e80..554c3a5239 100644 --- a/CodeEdit/Features/LSP/Registry/Model/RegistryItem.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem.swift @@ -8,17 +8,37 @@ import Foundation /// A `RegistryItem` represents an entry in the Registry that saves language servers, DAPs, linters and formatters. -struct RegistryItem: Codable { - let name: String - let description: String - let homepage: String - let licenses: [String] - let languages: [String] - let categories: [String] - let source: Source - let bin: [String: String]? +public struct RegistryItem: Codable { + public let name: String + public let description: String + public let homepage: String + public let licenses: [String] + public let languages: [String] + public let categories: [String] + public let source: Source + public let bin: [String: String]? - var sanitizedName: String { + public init( + name: String, + description: String, + homepage: String, + licenses: [String], + languages: [String], + categories: [String], + source: Source, + bin: [String: String]? + ) { + self.name = name + self.description = description + self.homepage = homepage + self.licenses = licenses + self.languages = languages + self.categories = categories + self.source = source + self.bin = bin + } + + public var sanitizedName: String { name.replacingOccurrences(of: "-", with: " ") .replacingOccurrences(of: "_", with: " ") .split(separator: " ") @@ -33,43 +53,23 @@ struct RegistryItem: Codable { .joined(separator: " ") } - var sanitizedDescription: String { + public var sanitizedDescription: String { description.replacingOccurrences(of: "\n", with: " ") } - var homepageURL: URL? { + public var homepageURL: URL? { URL(string: homepage) } /// A pretty version of the homepage URL. /// Removes the schema (eg https) and leaves the path and domain. - var homepagePretty: String { + public var homepagePretty: String { guard let homepageURL else { return homepage } return (homepageURL.host(percentEncoded: false) ?? "") + homepageURL.path(percentEncoded: false) } - /// The method for installation, parsed from this item's ``source-swift.property`` parameter. - var installMethod: InstallationMethod? { - let sourceId = source.id - if sourceId.hasPrefix("pkg:cargo/") { - return PackageSourceParser.parseCargoPackage(self) - } else if sourceId.hasPrefix("pkg:npm/") { - return PackageSourceParser.parseNpmPackage(self) - } else if sourceId.hasPrefix("pkg:pypi/") { - return PackageSourceParser.parsePythonPackage(self) - } else if sourceId.hasPrefix("pkg:gem/") { - return PackageSourceParser.parseRubyGem(self) - } else if sourceId.hasPrefix("pkg:golang/") { - return PackageSourceParser.parseGolangPackage(self) - } else if sourceId.hasPrefix("pkg:github/") { - return PackageSourceParser.parseGithubPackage(self) - } else { - return nil - } - } - /// Serializes back to JSON format - func toDictionary() throws -> [String: Any] { + public func toDictionary() throws -> [String: Any] { let data = try JSONEncoder().encode(self) let jsonObject = try JSONSerialization.jsonObject(with: data) guard let dictionary = jsonObject as? [String: Any] else { @@ -78,7 +78,3 @@ struct RegistryItem: Codable { return dictionary } } - -extension RegistryItem: FuzzySearchable { - var searchableString: String { name } -} diff --git a/CodeEdit/Features/LSP/Registry/Errors/RegistryManagerError.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryManagerError.swift similarity index 90% rename from CodeEdit/Features/LSP/Registry/Errors/RegistryManagerError.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryManagerError.swift index 68c1006e4e..44a8df6453 100644 --- a/CodeEdit/Features/LSP/Registry/Errors/RegistryManagerError.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryManagerError.swift @@ -7,7 +7,7 @@ import Foundation -enum RegistryManagerError: Error, LocalizedError { +public enum RegistryManagerError: Error, LocalizedError { case installationRunning case invalidResponse(statusCode: Int) case downloadFailed(url: URL, error: Error) @@ -15,7 +15,7 @@ enum RegistryManagerError: Error, LocalizedError { case writeFailed(error: Error) case failedToSaveRegistryCache - var errorDescription: String? { + public var errorDescription: String? { switch self { case .installationRunning: "A package is already being installed." @@ -32,7 +32,7 @@ enum RegistryManagerError: Error, LocalizedError { } } - var failureReason: String? { + public var failureReason: String? { switch self { case .installationRunning, .invalidResponse, .failedToSaveRegistryCache: return nil diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/FuzzySearchModels.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/FuzzySearchModels.swift new file mode 100644 index 0000000000..27d2ef3887 --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/FuzzySearchModels.swift @@ -0,0 +1,40 @@ +// +// FuzzySearchModels.swift +// CodeEdit +// +// Created by Tommy Ludwig on 03.02.24. +// + +import Foundation + +/// FuzzySearchCharacters is used to normalise strings +public struct FuzzySearchCharacter { + public let content: String + // normalised content is referring to a string that is case- and accent-insensitive + public let normalisedContent: String + + public init(content: String, normalisedContent: String) { + self.content = content + self.normalisedContent = normalisedContent + } +} + +/// FuzzySearchString is just made up by multiple characters, similar to a string, but also with normalised characters +public struct FuzzySearchString { + public var characters: [FuzzySearchCharacter] + + public init(characters: [FuzzySearchCharacter]) { + self.characters = characters + } +} + +/// FuzzySearchMatchResult represents an object that has undergone a fuzzy search using the fuzzyMatch function. +public struct FuzzySearchMatchResult { + public let weight: Int + public let matchedParts: [NSRange] + + public init(weight: Int, matchedParts: [NSRange]) { + self.weight = weight + self.matchedParts = matchedParts + } +} diff --git a/CodeEdit/Features/Search/Model/SearchModeModel.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/SearchModeModel.swift similarity index 50% rename from CodeEdit/Features/Search/Model/SearchModeModel.swift rename to Packages/CodeEditDomain/Sources/CodeEditDomain/Search/SearchModeModel.swift index 007651727c..e9c8e0f71d 100644 --- a/CodeEdit/Features/Search/Model/SearchModeModel.swift +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/SearchModeModel.swift @@ -8,75 +8,87 @@ import Foundation // TODO: DOCS (Ziyuan Zhao) -struct SearchModeModel: Hashable { - let title: String - let children: [SearchModeModel] - let needSelectionHighlight: Bool +public struct SearchModeModel: Hashable, Sendable { + public let title: String + public let children: [SearchModeModel] + public let needSelectionHighlight: Bool - static let Containing = SearchModeModel(title: "Containing", children: [], needSelectionHighlight: false) - static let MatchingWord = SearchModeModel( + public init(title: String, children: [SearchModeModel], needSelectionHighlight: Bool) { + self.title = title + self.children = children + self.needSelectionHighlight = needSelectionHighlight + } + + public static let Containing = SearchModeModel(title: "Containing", children: [], needSelectionHighlight: false) + public static let MatchingWord = SearchModeModel( title: "Matching Word", children: [], needSelectionHighlight: true ) - static let StartingWith = SearchModeModel( + public static let StartingWith = SearchModeModel( title: "Starting With", children: [], needSelectionHighlight: true ) - static let EndingWith = SearchModeModel(title: "Ending With", children: [], needSelectionHighlight: true) + public static let EndingWith = SearchModeModel( + title: "Ending With", + children: [], + needSelectionHighlight: true + ) - static let Text = SearchModeModel( + public static let Text = SearchModeModel( title: "Text", children: [.Containing, .MatchingWord, .StartingWith, .EndingWith], needSelectionHighlight: false ) - static let References = SearchModeModel( + public static let References = SearchModeModel( title: "References", children: [.Containing, .MatchingWord, .StartingWith, .EndingWith], needSelectionHighlight: true ) - static let Definitions = SearchModeModel( + public static let Definitions = SearchModeModel( title: "Definitions", children: [.Containing, .MatchingWord, .StartingWith, .EndingWith], needSelectionHighlight: true ) - static let RegularExpression = SearchModeModel( + public static let RegularExpression = SearchModeModel( title: "Regular Expression", children: [], needSelectionHighlight: true ) - static let CallHierarchy = SearchModeModel( + public static let CallHierarchy = SearchModeModel( title: "Call Hierarchy", children: [], needSelectionHighlight: true ) - static let Find = SearchModeModel( + public static let Find = SearchModeModel( title: "Find", children: [.Text, .References, .Definitions, .RegularExpression, .CallHierarchy], needSelectionHighlight: false ) - static let Replace = SearchModeModel( + public static let Replace = SearchModeModel( title: "Replace", children: [.Text, .RegularExpression], needSelectionHighlight: true ) - static let TextMatchingModes: [SearchModeModel] = [.Containing, .MatchingWord, .StartingWith, .EndingWith] - static let FindModes: [SearchModeModel] = [ + public static let TextMatchingModes: [SearchModeModel] = [ + .Containing, .MatchingWord, .StartingWith, .EndingWith + ] + public static let FindModes: [SearchModeModel] = [ .Text, .References, .Definitions, .RegularExpression, .CallHierarchy ] - static let ReplaceModes: [SearchModeModel] = [.Text, .RegularExpression] - static let SearchModes: [SearchModeModel] = [.Find, .Replace] + public static let ReplaceModes: [SearchModeModel] = [.Text, .RegularExpression] + public static let SearchModes: [SearchModeModel] = [.Find, .Replace] } extension SearchModeModel: Equatable { - static func == (lhs: SearchModeModel, rhs: SearchModeModel) -> Bool { + public static func == (lhs: SearchModeModel, rhs: SearchModeModel) -> Bool { lhs.title == rhs.title && lhs.children == rhs.children && lhs.needSelectionHighlight == rhs.needSelectionHighlight diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Tasks/TaskNotificationModel.swift b/Packages/CodeEditDomain/Sources/CodeEditDomain/Tasks/TaskNotificationModel.swift new file mode 100644 index 0000000000..ca85f0f946 --- /dev/null +++ b/Packages/CodeEditDomain/Sources/CodeEditDomain/Tasks/TaskNotificationModel.swift @@ -0,0 +1,25 @@ +// +// TaskNotificationModel.swift +// CodeEdit +// +// Created by Tommy Ludwig on 21.06.24. +// + +import Foundation + +/// Represents a notifications or tasks, that are displayed in the activity viewer +public struct TaskNotificationModel: Equatable { + public var id: String + public var title: String + public var message: String? + public var percentage: Double? + public var isLoading: Bool = false + + public init(id: String, title: String, message: String? = nil, percentage: Double? = nil, isLoading: Bool = false) { + self.id = id + self.title = title + self.message = message + self.percentage = percentage + self.isLoading = isLoading + } +} From c6bd7aa3503884e3eb20757086c33d226a8601c8 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Apr 2026 22:13:03 +0200 Subject: [PATCH 015/335] Refactor: Introduce UseCases for cross-service orchestration Extract orchestration logic from WorkspaceWindowManager and AppDelegate into dedicated UseCase types: - OpenWorkspaceUseCase: workspace + window creation and geometry restore - CloseWorkspaceUseCase: LSP cleanup and workspace teardown - ShutdownApplicationUseCase: workspace path saving, unsaved changes prompting, and task termination Managers now handle state tracking and framework callbacks while UseCases own the multi-service coordination logic. --- CodeEdit/AppDelegate.swift | 51 ++------------- .../Services/WorkspaceWindowManager.swift | 43 +++--------- .../UseCases/CloseWorkspaceUseCase.swift | 24 +++++++ .../UseCases/OpenWorkspaceUseCase.swift | 48 ++++++++++++++ .../UseCases/ShutdownApplicationUseCase.swift | 65 +++++++++++++++++++ 5 files changed, 150 insertions(+), 81 deletions(-) create mode 100644 CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift create mode 100644 CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift create mode 100644 CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 857ae964fb..a60f710bed 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -22,10 +22,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @LazyInjected(\.lspService) var lspService - + @LazyInjected(\.workspaceWindowManager) var windowManager + private let shutdownUseCase = ShutdownApplicationUseCase() + private var welcomeWindowObserver: NSObjectProtocol? func applicationDidFinishLaunching(_ notification: Notification) { @@ -154,31 +156,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { /// /// All paths _must_ call `NSApplication.shared.reply(toApplicationShouldTerminate: true)` as soon as possible. func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - let projects: [String] = windowManager.openWorkspaces - .compactMap { $0.fileURL?.path } - - UserDefaults.standard.set(projects, forKey: AppDelegate.recoverWorkspacesKey) - - let hasUnsavedChanges = windowManager.openWorkspaces.contains { $0.hasUnsavedChanges() } - guard !hasUnsavedChanges else { - // Prompt the user to save unsaved changes across all workspaces - var allSaved = true - for workspace in windowManager.openWorkspaces { - if !workspace.promptSaveUnsavedFiles() { - allSaved = false - break - } - } - - if allSaved { - terminateTasks() - terminateLanguageServers() - } - // If not all saved (user cancelled), don't terminate - return allSaved ? .terminateLater : .terminateCancel + guard shutdownUseCase.execute() else { + return .terminateCancel } - terminateTasks() terminateLanguageServers() return .terminateLater } @@ -297,28 +278,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } } - /// Terminates all running tasks. Used during app termination to ensure resources are freed. - private func terminateTasks() { - let task = TaskNotificationModel( - id: "appdelegate.terminate_tasks", - title: "Terminating Tasks", - message: "Interrupting all running tasks before quitting...", - isLoading: true - ) - - let taskManagers = windowManager.openWorkspaces - .compactMap({ $0.taskManager }) - - if taskManagers.reduce(0, { $0 + $1.activeTasks.count }) > 0 { - TaskNotificationHandler.postTask(action: .create, model: task) - } - - taskManagers.forEach { manager in - manager.stopAllTasks() - } - - TaskNotificationHandler.postTask(action: .delete, model: task) - } } extension AppDelegate { diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index d672f06f9a..ac5f9258a0 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -7,7 +7,6 @@ import AppKit import SwiftUI -import Factory import WelcomeWindow extension Notification.Name { @@ -18,8 +17,8 @@ extension Notification.Name { @MainActor final class WorkspaceWindowManager: WorkspaceWindowManaging { - @LazyInjected(\.lspService) - var lspService + private let openWorkspaceUseCase = OpenWorkspaceUseCase() + private let closeWorkspaceUseCase = CloseWorkspaceUseCase() /// All currently open workspaces. private(set) var openWorkspaces: [Workspace] = [] @@ -36,35 +35,13 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { return } - let workspace = Workspace(url: url) + let result = openWorkspaceUseCase.execute(url: url) - let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], - backing: .buffered, - defer: false - ) - - let windowController = CodeEditWindowController( - window: window, - workspace: workspace - ) - - if let rectString = workspace.statePersistence?.get(.workspaceWindowSize) as? String { - window.setFrame(NSRectFromString(rectString), display: true, animate: false) - } else { - window.setFrame(NSRect(x: 0, y: 0, width: 1400, height: 900), display: true, animate: false) - window.center() - } + openWorkspaces.append(result.workspace) + windowControllers[ObjectIdentifier(result.workspace)] = result.windowController + result.workspace.notificationPanel.windowController = result.windowController - window.setAccessibilityIdentifier("workspace") - window.setAccessibilityDocument(workspace.fileURL?.absoluteString) - - openWorkspaces.append(workspace) - windowControllers[ObjectIdentifier(workspace)] = windowController - workspace.notificationPanel.windowController = windowController - - window.makeKeyAndOrderFront(nil) + result.window.makeKeyAndOrderFront(nil) RecentsStore.documentOpened(at: url) } @@ -72,11 +49,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { // MARK: - Close Workspace func closeWorkspace(_ workspace: Workspace) { - if let path = workspace.fileURL?.absoluteURL.path() { - lspService.closeWorkspace(path) - } - - workspace.tearDown() + closeWorkspaceUseCase.execute(workspace: workspace) let id = ObjectIdentifier(workspace) windowControllers.removeValue(forKey: id) diff --git a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift new file mode 100644 index 0000000000..453af77be8 --- /dev/null +++ b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift @@ -0,0 +1,24 @@ +// +// CloseWorkspaceUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/04/26. +// + +import Foundation +import Factory + +/// Coordinates cleanup when a workspace is closed (LSP shutdown + workspace teardown). +@MainActor +final class CloseWorkspaceUseCase { + + @LazyInjected(\.lspService) + private var lspService + + func execute(workspace: Workspace) { + if let path = workspace.fileURL?.absoluteURL.path() { + lspService.closeWorkspace(path) + } + workspace.tearDown() + } +} diff --git a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift new file mode 100644 index 0000000000..2c30517878 --- /dev/null +++ b/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift @@ -0,0 +1,48 @@ +// +// OpenWorkspaceUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/04/26. +// + +import AppKit + +/// Creates and configures a workspace, window, and window controller for a given URL. +@MainActor +final class OpenWorkspaceUseCase { + + struct Result { + let workspace: Workspace + let window: NSWindow + let windowController: CodeEditWindowController + } + + func execute(url: URL) -> Result { + let workspace = Workspace(url: url) + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + + let windowController = CodeEditWindowController( + window: window, + workspace: workspace + ) + + // Restore saved window geometry, or use default centered frame + if let rectString = workspace.statePersistence?.get(.workspaceWindowSize) as? String { + window.setFrame(NSRectFromString(rectString), display: true, animate: false) + } else { + window.setFrame(NSRect(x: 0, y: 0, width: 1400, height: 900), display: true, animate: false) + window.center() + } + + window.setAccessibilityIdentifier("workspace") + window.setAccessibilityDocument(workspace.fileURL?.absoluteString) + + return Result(workspace: workspace, window: window, windowController: windowController) + } +} diff --git a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift new file mode 100644 index 0000000000..ff31fbf1eb --- /dev/null +++ b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift @@ -0,0 +1,65 @@ +// +// ShutdownApplicationUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/04/26. +// + +import Foundation +import Factory +import CodeEditDomain + +/// Orchestrates application shutdown: saves workspace paths, checks for unsaved changes, +/// prompts the user to save, and terminates running tasks. +/// +/// Returns `true` if shutdown should proceed, `false` if the user cancelled. +/// Language server shutdown is handled separately by the caller (AppDelegate) +/// since it's async and tied to the NSApplication reply lifecycle. +@MainActor +final class ShutdownApplicationUseCase { + + @LazyInjected(\.workspaceWindowManager) + private var windowManager + + /// - Returns: `true` if the app should proceed with termination, `false` if the user cancelled. + func execute() -> Bool { + let workspaces = windowManager.openWorkspaces + + // Save workspace paths for recovery on next launch + let projects: [String] = workspaces.compactMap { $0.fileURL?.path } + UserDefaults.standard.set(projects, forKey: AppDelegate.recoverWorkspacesKey) + + // Check for unsaved changes and prompt the user + let hasUnsavedChanges = workspaces.contains { $0.hasUnsavedChanges() } + if hasUnsavedChanges { + for workspace in workspaces { + if !workspace.promptSaveUnsavedFiles() { + return false // User cancelled + } + } + } + + // Terminate all running tasks across workspaces + terminateTasks(in: workspaces) + + return true + } + + private func terminateTasks(in workspaces: [Workspace]) { + let taskManagers = workspaces.compactMap { $0.taskManager } + + if taskManagers.reduce(0, { $0 + $1.activeTasks.count }) > 0 { + let task = TaskNotificationModel( + id: "appdelegate.terminate_tasks", + title: "Terminating Tasks", + message: "Interrupting all running tasks before quitting...", + isLoading: true + ) + TaskNotificationHandler.postTask(action: .create, model: task) + + taskManagers.forEach { $0.stopAllTasks() } + + TaskNotificationHandler.postTask(action: .delete, model: task) + } + } +} From 34d82bb02dcf0c13a194ad08511670a2ee46d2ce Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Apr 2026 19:45:12 +0200 Subject: [PATCH 016/335] Refactor: Finish UseCase extraction (5 more) Extract orchestration logic into dedicated UseCases, especially out of view controllers and view models where it doesn't belong: - RestoreEditorStateUseCase: editor state decode, validation, and file reference resolution (was in EditorManager.restoreFromState) - OpenDocumentUseCase: routes URLs to workspace/file/standalone openers (was in WorkspaceWindowManager.openDocument) - MoveFileUseCase: file move + tab close + reopen (was in ProjectNavigatorViewController.moveFile) - AcceptDroppedFilesUseCase: drag-and-drop URL resolution and replace conflict handling (was in ProjectNavigatorViewController.outlineView(_:acceptDrop:)) - CloneRepositoryUseCase: git installation check, URL parsing, directory creation, and clone streaming (was in GitCloneViewModel) UI concerns (NSAlert, NSSavePanel, outline view manipulation) stay at the boundary; UseCases stay UI-agnostic via callbacks where needed. --- .../UseCases/AcceptDroppedFilesUseCase.swift | 58 +++++++++ .../UseCases/MoveFileUseCase.swift | 33 +++++ .../EditorLayout+StateRestoration.swift | 116 ++--------------- .../UseCases/RestoreEditorStateUseCase.swift | 114 ++++++++++++++++ ...ewController+NSOutlineViewDataSource.swift | 59 ++++----- ...troller+OutlineTableViewCellDelegate.swift | 12 +- .../SourceControl/Clone/GitCloneView.swift | 3 +- .../Clone/ViewModels/GitCloneViewModel.swift | 123 +++++------------- .../UseCases/CloneRepositoryUseCase.swift | 97 ++++++++++++++ .../Services/WorkspaceWindowManager.swift | 18 +-- .../UseCases/OpenDocumentUseCase.swift | 38 ++++++ 11 files changed, 415 insertions(+), 256 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift create mode 100644 CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift create mode 100644 CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift create mode 100644 CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift create mode 100644 CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift diff --git a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift new file mode 100644 index 0000000000..a796df58c9 --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift @@ -0,0 +1,58 @@ +// +// AcceptDroppedFilesUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/04/26. +// + +import Foundation + +/// Resolves dropped file URLs into copy/move operations, handling source resolution and replace conflicts. +@MainActor +final class AcceptDroppedFilesUseCase { + + struct Operation { + let source: CEWorkspaceFile + let destination: URL + let isCopy: Bool + } + + /// Returns the operations that should be executed for the given dropped URLs. + /// Filters out drops onto self/parent. Asks `confirmReplace` (and removes the existing file) + /// when the destination already exists. + func execute( + urls: [URL], + destinationParent: CEWorkspaceFile, + isCopyOperation: Bool, + in workspace: Workspace, + confirmReplace: (String) -> Bool + ) throws -> [Operation] { + let destParentURL = destinationParent.url + var operations: [Operation] = [] + + for url in urls { + let destURL = destParentURL.appending(path: url.lastPathComponent) + + // Cancel dropping a file on itself or in its own parent directory + if url == destURL || url == destParentURL { + continue + } + + // Resolve the source: either an existing workspace file, or treat as external + let source = workspace.workspaceFileManager?.getFile(url.path) + ?? CEWorkspaceFile(url: URL(fileURLWithPath: url.path)) + + // Handle existing destination via the supplied confirmation closure + if CEWorkspaceFile.fileManager.fileExists(atPath: destURL.path) { + guard confirmReplace(url.lastPathComponent) else { + continue + } + try CEWorkspaceFile.fileManager.removeItem(at: destURL) + } + + operations.append(Operation(source: source, destination: destURL, isCopy: isCopyOperation)) + } + + return operations + } +} diff --git a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift new file mode 100644 index 0000000000..e7cbad551d --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift @@ -0,0 +1,33 @@ +// +// MoveFileUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/04/26. +// + +import Foundation + +/// Moves a file within a workspace, closing any open tabs for it and reopening the new location. +/// +/// Returns the resolved new file (for non-folder moves) so the caller can update its UI. +@MainActor +final class MoveFileUseCase { + + func execute(file: CEWorkspaceFile, to destination: URL, in workspace: Workspace) throws -> CEWorkspaceFile? { + guard let newFile = try workspace.workspaceFileManager?.move(file: file, to: destination) else { + return nil + } + + guard !newFile.isFolder else { + return newFile + } + + if !file.isFolder { + workspace.editorManager?.editorLayout.closeAllTabs(of: file) + } + workspace.listenerModel.highlightedFileItem = newFile + workspace.editorManager?.openTab(item: newFile) + + return newFile + } +} diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 43b18e64c3..460e56a4b4 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -28,112 +28,20 @@ extension EditorManager { } } - do { - guard let data = statePersistence.get(.openTabs) as? Data else { - return - } - - let state = try JSONDecoder().decode(EditorRestorationState.self, from: data) - - guard !state.groups.isEmpty else { - logger.warning("Empty Editor State found, restoring to clean editor state.") - initCleanState() - return - } - - guard let activeEditor = state.groups.find( - editor: state.activeEditor - ) ?? state.groups.findSomeEditor() else { - logger.warning("Editor state could not restore active editor.") - initCleanState() - return - } - - try fixRestoredEditorLayout(state.groups, fileManager: fileManager, searchState: searchState) - - self.editorLayout = state.groups + let useCase = RestoreEditorStateUseCase() + switch useCase.execute( + statePersistence: statePersistence, + fileManager: fileManager, + searchState: searchState + ) { + case .restored(let layout, let activeEditor): + self.editorLayout = layout self.activeEditor = activeEditor switchToActiveEditor() - } catch { - logger.warning( - "Could not restore editor state from saved data: \(error.localizedDescription, privacy: .public)" - ) - } - } - - /// Fix any hanging files after restoring from saved state. - /// - /// After decoding the state, we're left with `CEWorkspaceFile`s that don't exist in the file manager - /// so this function maps all those to 'real' files. Works recursively on all the tab groups. - /// - Parameters: - /// - group: The tab group to fix. - /// - fileManager: The file manager to use to map files. - private func fixRestoredEditorLayout( - _ group: EditorLayout, - fileManager: CEWorkspaceFileManager?, - searchState: SearchState? - ) throws { - switch group { - case let .one(data): - try fixEditor(data, fileManager: fileManager, searchState: searchState) - case let .vertical(splitData): - try splitData.editorLayouts.forEach { group in - try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) - } - case let .horizontal(splitData): - try splitData.editorLayouts.forEach { group in - try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) - } - } - } - - private func findEditorLayout(group: EditorLayout, searchFor id: UUID) throws -> Editor? { - switch group { - case let .one(data): - return data.id == id ? data : nil - case let .vertical(splitData): - return try splitData.editorLayouts.compactMap { try findEditorLayout(group: $0, searchFor: id) }.first - case let .horizontal(splitData): - return try splitData.editorLayouts.compactMap { try findEditorLayout(group: $0, searchFor: id) }.first - } - } - - /// Fixes any hanging files after restoring from saved state. - /// - /// Resolves all file references with the workspace's file manager to ensure any referenced files use their shared - /// object representation. - /// - /// - Parameters: - /// - data: The tab group to fix. - /// - fileManager: The file manager to use to map files.a - private func fixEditor( - _ editor: Editor, - fileManager: CEWorkspaceFileManager?, - searchState: SearchState? - ) throws { - guard let fileManager else { return } - let resolvedTabs = editor - .tabs - .compactMap({ fileManager.getFile($0.file.url.path(percentEncoded: false), createIfNotFound: true) }) - .map({ EditorInstance(searchState: searchState, file: $0) }) - - for tab in resolvedTabs { - try tab.file.loadCodeFile() - } - - editor.searchState = searchState - editor.isAttachedToWorkspace = true - editor.tabs = OrderedSet(resolvedTabs) - - if let selectedTab = editor.selectedTab { - if let resolvedFile = fileManager.getFile( - selectedTab.file.url.path(percentEncoded: false), - createIfNotFound: true - ) { - editor.setSelectedTab(resolvedFile) - } else { - editor.setSelectedTab(nil) - } + case .shouldInitCleanState: + initCleanState() + case .noChange: + break } } diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift new file mode 100644 index 0000000000..a2c9e3420a --- /dev/null +++ b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -0,0 +1,114 @@ +// +// RestoreEditorStateUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/04/26. +// + +import Foundation +import OSLog +import OrderedCollections + +/// Restores an editor layout from persisted state, resolving file references against the current file manager. +final class RestoreEditorStateUseCase { + + enum Outcome { + /// Persisted state was loaded and resolved successfully. + case restored(layout: EditorLayout, activeEditor: Editor) + /// Persisted state was found but is empty/invalid; caller should initialize a clean state. + case shouldInitCleanState + /// No persisted state exists, or decoding failed; caller should leave the editor as-is. + case noChange + } + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RestoreEditorStateUseCase") + + /// Decodes persisted editor state, validates it, and resolves file references. + func execute( + statePersistence: any WorkspaceStatePersisting, + fileManager: CEWorkspaceFileManager?, + searchState: SearchState? + ) -> Outcome { + guard let data = statePersistence.get(.openTabs) as? Data else { + return .noChange + } + + do { + let state = try JSONDecoder().decode(EditorRestorationState.self, from: data) + + guard !state.groups.isEmpty else { + logger.warning("Empty Editor State found, restoring to clean editor state.") + return .shouldInitCleanState + } + + guard let activeEditor = state.groups.find( + editor: state.activeEditor + ) ?? state.groups.findSomeEditor() else { + logger.warning("Editor state could not restore active editor.") + return .shouldInitCleanState + } + + try fixRestoredEditorLayout(state.groups, fileManager: fileManager, searchState: searchState) + + return .restored(layout: state.groups, activeEditor: activeEditor) + } catch { + logger.warning( + "Could not restore editor state from saved data: \(error.localizedDescription, privacy: .public)" + ) + return .noChange + } + } + + /// Recursively maps decoded `CEWorkspaceFile` references to their shared file-manager-owned representations. + private func fixRestoredEditorLayout( + _ group: EditorLayout, + fileManager: CEWorkspaceFileManager?, + searchState: SearchState? + ) throws { + switch group { + case let .one(data): + try fixEditor(data, fileManager: fileManager, searchState: searchState) + case let .vertical(splitData): + try splitData.editorLayouts.forEach { group in + try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) + } + case let .horizontal(splitData): + try splitData.editorLayouts.forEach { group in + try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) + } + } + } + + /// Resolves all file references on a single editor with the workspace's file manager + /// and loads each tab's underlying code file. + private func fixEditor( + _ editor: Editor, + fileManager: CEWorkspaceFileManager?, + searchState: SearchState? + ) throws { + guard let fileManager else { return } + let resolvedTabs = editor + .tabs + .compactMap({ fileManager.getFile($0.file.url.path(percentEncoded: false), createIfNotFound: true) }) + .map({ EditorInstance(searchState: searchState, file: $0) }) + + for tab in resolvedTabs { + try tab.file.loadCodeFile() + } + + editor.searchState = searchState + editor.isAttachedToWorkspace = true + editor.tabs = OrderedSet(resolvedTabs) + + if let selectedTab = editor.selectedTab { + if let resolvedFile = fileManager.getFile( + selectedTab.file.url.path(percentEncoded: false), + createIfNotFound: true + ) { + editor.setSelectedTab(resolvedFile) + } else { + editor.setSelectedTab(nil) + } + } + } +} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 2cc45c4b19..8857da9c5a 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -95,45 +95,34 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { guard let pasteboardItems = info.draggingPasteboard.readObjects(forClasses: [NSURL.self]) else { return false } let fileItemURLS = pasteboardItems.compactMap { $0 as? URL } - guard let fileItemDestination = item as? CEWorkspaceFile else { return false } - let destParentURL = fileItemDestination.url - - for fileItemURL in fileItemURLS { - let destURL = destParentURL.appending(path: fileItemURL.lastPathComponent) - // cancel dropping file item on self or in parent directory - if fileItemURL == destURL || fileItemURL == destParentURL { - return false - } - - // Needs to come before call to .removeItem or else race condition occurs - var srcFileItem: CEWorkspaceFile? = workspace?.workspaceFileManager?.getFile(fileItemURL.path) - // If srcFileItem is nil, fileItemUrl is an external file url. - if srcFileItem == nil { - srcFileItem = CEWorkspaceFile(url: URL(fileURLWithPath: fileItemURL.path)) - } - - guard let srcFileItem else { - return false - } - - if CEWorkspaceFile.fileManager.fileExists(atPath: destURL.path) { - let shouldReplace = replaceFileDialog(fileName: fileItemURL.lastPathComponent) - guard shouldReplace else { - return false + guard let fileItemDestination = item as? CEWorkspaceFile, + let workspace else { return false } + + let useCase = AcceptDroppedFilesUseCase() + let isCopy = info.draggingSourceOperationMask == .copy + + do { + let operations = try useCase.execute( + urls: fileItemURLS, + destinationParent: fileItemDestination, + isCopyOperation: isCopy, + in: workspace, + confirmReplace: { [weak self] fileName in + self?.replaceFileDialog(fileName: fileName) ?? false } - do { - try CEWorkspaceFile.fileManager.removeItem(at: destURL) - } catch { - fatalError(error.localizedDescription) + ) + + for operation in operations { + if operation.isCopy { + self.copyFile(file: operation.source, to: operation.destination) + } else { + self.moveFile(file: operation.source, to: operation.destination) } } - if info.draggingSourceOperationMask == .copy { - self.copyFile(file: srcFileItem, to: destURL) - } else { - self.moveFile(file: srcFileItem, to: destURL) - } + return !operations.isEmpty + } catch { + fatalError(error.localizedDescription) } - return true } func replaceFileDialog(fileName: String) -> Bool { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index 91d68b42e2..11278121e7 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -12,17 +12,11 @@ import AppKit extension ProjectNavigatorViewController: OutlineTableViewCellDelegate { func moveFile(file: CEWorkspaceFile, to destination: URL) { + guard let workspace else { return } do { - guard let newFile = try workspace?.workspaceFileManager?.move(file: file, to: destination), - !newFile.isFolder else { - return - } + let useCase = MoveFileUseCase() + _ = try useCase.execute(file: file, to: destination, in: workspace) outlineView.reloadItem(file.parent, reloadChildren: true) - if !file.isFolder { - workspace?.editorManager?.editorLayout.closeAllTabs(of: file) - } - workspace?.listenerModel.highlightedFileItem = newFile - workspace?.editorManager?.openTab(item: newFile) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") diff --git a/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift b/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift index 279032c4e2..82ddf741c4 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift +++ b/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift @@ -7,6 +7,7 @@ import SwiftUI import Foundation +import Factory import Combine struct GitCloneView: View { @@ -99,7 +100,7 @@ struct GitCloneView: View { viewModel.cloneRepository { localPath in dismiss() - guard let gitClient = viewModel.gitClient else { return } + let gitClient = GitClient(directoryURL: localPath, shellClient: Container.shared.shellClient()) Task { let branches = ((try? await gitClient.getBranches()) ?? []) diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift index aeeac03654..7809cc4037 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift @@ -6,7 +6,6 @@ // import Foundation -import Factory import AppKit class GitCloneViewModel: ObservableObject { @@ -14,9 +13,10 @@ class GitCloneViewModel: ObservableObject { @Published var isCloning: Bool = false @Published var cloningProgress: GitClient.CloneProgress = .init(progress: 0, state: .initialState) - var gitClient: GitClient? var cloningTask: Task? + private let useCase = CloneRepositoryUseCase() + /// Check if url is valid /// - Parameter url: Url to check /// - Returns: True if url is valid @@ -32,22 +32,6 @@ class GitCloneViewModel: ObservableObject { } return false } - /// Check if Git is installed - /// - Returns: True if Git is found by running "which git" command - func isGitInstalled() -> Bool { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/which") - process.arguments = ["git"] - let pipe = Pipe() - process.standardOutput = pipe - do { - try process.run() - process.waitUntilExit() - return process.terminationStatus == 0 - } catch { - return false - } - } /// Check if clipboard contains git url func checkClipboard() { @@ -60,123 +44,80 @@ class GitCloneViewModel: ObservableObject { /// Clone repository func cloneRepository(completionHandler: @escaping (URL) -> Void) { - if !isGitInstalled() { - showAlert( - alertMsg: "Git installation not found.", - infoText: "Ensure Git is installed on your system and try again." - ) - return - } - if repoUrlStr == "" { - showAlert( - alertMsg: "Url cannot be empty", - infoText: "You must specify a repository to clone" - ) - return - } - - // Parsing repo name - guard let remoteUrl = URL(string: repoUrlStr) else { + do { + try useCase.verifyGitInstalled() + } catch { + showAlert(alertMsg: "Git installation not found.", infoText: error.localizedDescription) return } - var repoName = remoteUrl.lastPathComponent - - // Strip .git from name if it has it. - // Cloning repository without .git also works - if repoName.contains(".git") { - repoName.removeLast(4) - } - - guard let localPath = getPath(saveName: repoName) else { + let parsed: (remoteUrl: URL, suggestedName: String) + do { + parsed = try useCase.parse(repoUrl: repoUrlStr) + } catch { + showAlert(alertMsg: "Invalid URL", infoText: error.localizedDescription) return } - var isDir: ObjCBool = true - if FileManager.default.fileExists(atPath: localPath.relativePath, isDirectory: &isDir) { - showAlert(alertMsg: "Error", infoText: "Directory already exists") + guard let localPath = getPath(saveName: parsed.suggestedName) else { return } + let progressStream: AsyncThrowingMapSequence do { - try FileManager.default.createDirectory( - atPath: localPath.relativePath, - withIntermediateDirectories: true, - attributes: nil - ) + progressStream = try useCase.execute(remoteUrl: parsed.remoteUrl, localPath: localPath) } catch { - showAlert(alertMsg: "Failed to create folder", infoText: "\(error)") + showAlert(alertMsg: "Failed to clone", infoText: error.localizedDescription) return } - gitClient = GitClient(directoryURL: localPath, shellClient: Container.shared.shellClient()) - - self.cloningTask = Task(priority: .background) { - await processCloning( - remoteUrl: remoteUrl, + cloningTask = Task(priority: .background) { [weak self] in + await self?.consumeProgress( + stream: progressStream, localPath: localPath, completionHandler: completionHandler ) } } - /// Process cloning - /// - Parameters: - /// - remoteUrl: Path to remote repository - /// - localPath: Path to local folder - /// - completionHandler: Completion handler if cloning is successful - private func processCloning( - remoteUrl: URL, + @MainActor + private func consumeProgress( + stream: AsyncThrowingMapSequence, localPath: URL, completionHandler: @escaping (URL) -> Void ) async { - guard let gitClient else { return } - - await setIsCloning(true) + isCloning = true + defer { isCloning = false } do { - for try await progress in gitClient.cloneRepository(remoteUrl: remoteUrl, localPath: localPath) { - await MainActor.run { - self.cloningProgress = progress - } + for try await progress in stream { + self.cloningProgress = progress } if Task.isCancelled { - await MainActor.run { - deleteTemporaryFolder(localPath: localPath) - } + deleteTemporaryFolder(localPath: localPath) return } completionHandler(localPath) } catch { - await MainActor.run { - if let error = error as? GitClient.GitClientError { - showAlert(alertMsg: "Failed to clone", infoText: error.description) - } else { - showAlert(alertMsg: "Failed to clone", infoText: error.localizedDescription) - } - deleteTemporaryFolder(localPath: localPath) + if let error = error as? GitClient.GitClientError { + showAlert(alertMsg: "Failed to clone", infoText: error.description) + } else { + showAlert(alertMsg: "Failed to clone", infoText: error.localizedDescription) } + deleteTemporaryFolder(localPath: localPath) } - - await setIsCloning(false) } private func deleteTemporaryFolder(localPath: URL) { do { - try FileManager.default.removeItem(atPath: localPath.relativePath) + try useCase.cleanup(localPath: localPath) } catch { showAlert(alertMsg: "Failed to delete folder", infoText: "\(error)") - return } } - @MainActor - private func setIsCloning(_ newValue: Bool) { - self.isCloning = newValue - } - private func getPath(saveName: String) -> URL? { let dialog = NSSavePanel() dialog.showsResizeIndicator = true diff --git a/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift b/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift new file mode 100644 index 0000000000..30f1937278 --- /dev/null +++ b/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift @@ -0,0 +1,97 @@ +// +// CloneRepositoryUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/04/26. +// + +import Foundation +import Factory + +/// Validates and orchestrates a `git clone` operation, streaming progress to the caller. +final class CloneRepositoryUseCase { + + enum Failure: Error, LocalizedError { + case gitNotInstalled + case invalidUrl + case directoryExists + case directoryCreationFailed(Error) + + var errorDescription: String? { + switch self { + case .gitNotInstalled: + return "Git installation not found. Ensure Git is installed on your system and try again." + case .invalidUrl: + return "Invalid repository URL." + case .directoryExists: + return "Directory already exists at the destination." + case .directoryCreationFailed(let error): + return "Failed to create folder: \(error.localizedDescription)" + } + } + } + + /// Parses and sanitizes the user-entered URL string, returning the remote URL and the suggested repo folder name. + func parse(repoUrl: String) throws -> (remoteUrl: URL, suggestedName: String) { + guard !repoUrl.isEmpty, let remoteUrl = URL(string: repoUrl) else { + throw Failure.invalidUrl + } + + var name = remoteUrl.lastPathComponent + if name.hasSuffix(".git") { + name.removeLast(4) + } + + return (remoteUrl, name) + } + + /// Verifies Git is available on the system. + func verifyGitInstalled() throws { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/which") + process.arguments = ["git"] + let pipe = Pipe() + process.standardOutput = pipe + do { + try process.run() + process.waitUntilExit() + if process.terminationStatus != 0 { + throw Failure.gitNotInstalled + } + } catch let error as Failure { + throw error + } catch { + throw Failure.gitNotInstalled + } + } + + /// Creates the local directory and starts the clone, streaming progress. + /// On error or task cancellation, the caller is responsible for cleanup via ``cleanup(localPath:)``. + func execute( + remoteUrl: URL, + localPath: URL + ) throws -> AsyncThrowingMapSequence { + var isDir: ObjCBool = true + if FileManager.default.fileExists(atPath: localPath.relativePath, isDirectory: &isDir) { + throw Failure.directoryExists + } + + do { + try FileManager.default.createDirectory( + atPath: localPath.relativePath, + withIntermediateDirectories: true, + attributes: nil + ) + } catch { + throw Failure.directoryCreationFailed(error) + } + + let gitClient = GitClient(directoryURL: localPath, shellClient: Container.shared.shellClient()) + return gitClient.cloneRepository(remoteUrl: remoteUrl, localPath: localPath) + } + + /// Removes a partially-cloned directory after a failure or cancellation. + func cleanup(localPath: URL) throws { + try FileManager.default.removeItem(atPath: localPath.relativePath) + } +} diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index ac5f9258a0..992ad8cce9 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -19,6 +19,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { private let openWorkspaceUseCase = OpenWorkspaceUseCase() private let closeWorkspaceUseCase = CloseWorkspaceUseCase() + private lazy var openDocumentUseCase = OpenDocumentUseCase(windowManager: self) /// All currently open workspaces. private(set) var openWorkspaces: [Workspace] = [] @@ -135,22 +136,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { /// Opens a workspace or file at the given URL, calling the completion handler on success. func openDocument(at url: URL, onCompletion: @escaping () -> Void) { - do { - if url.isFolder { - try openWorkspace(at: url) - onCompletion() - } else if openFileInWorkspace(url: url) { - onCompletion() - } else { - NSDocumentController.shared.openDocument( - withContentsOf: url, display: true - ) { _, _, error in - if error == nil { onCompletion() } - } - } - } catch { - NSAlert(error: error).runModal() - } + openDocumentUseCase.execute(url: url, onCompletion: onCompletion) } /// Opens a dialog to choose a file or folder, with optional configuration. diff --git a/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift b/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift new file mode 100644 index 0000000000..f147ef7cbb --- /dev/null +++ b/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift @@ -0,0 +1,38 @@ +// +// OpenDocumentUseCase.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/04/26. +// + +import AppKit + +/// Routes a URL to the appropriate opener: a workspace (folder), an existing workspace's file, +/// or a standalone document via NSDocumentController. +@MainActor +final class OpenDocumentUseCase { + private let windowManager: WorkspaceWindowManaging + + init(windowManager: WorkspaceWindowManaging) { + self.windowManager = windowManager + } + + func execute(url: URL, onCompletion: @escaping () -> Void) { + do { + if url.isFolder { + try windowManager.openWorkspace(at: url) + onCompletion() + } else if windowManager.openFileInWorkspace(url: url) { + onCompletion() + } else { + NSDocumentController.shared.openDocument( + withContentsOf: url, display: true + ) { _, _, error in + if error == nil { onCompletion() } + } + } + } catch { + NSAlert(error: error).runModal() + } + } +} From e0a396d65fbfb986e3d45aa9d91b90f5f9f231ab Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 1 Jul 2026 20:42:12 +0200 Subject: [PATCH 017/335] Refactor: Convert to Xcode workspace with CodeEditDomain as workspace package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the XCLocalSwiftPackageReference in the project with a proper Xcode workspace (CodeEdit.xcworkspace). CodeEditDomain is now a workspace-level package, which scales cleanly as more local packages are added (e.g. CodeEditCore). Also removes the abandoned CodeEditUI/src/ folder — a leftover from an unrealised package split that was never wired into the project. --- CodeEdit.xcodeproj/project.pbxproj | 9 - CodeEdit.xcworkspace/contents.xcworkspacedata | 10 + .../xcshareddata/swiftpm/Package.resolved | 321 ++++++++++++++++++ .../Preferences/ViewOffsetPreferenceKey.swift | 10 - Packages/CodeEditDomain/Package.swift | 3 +- 5 files changed, 332 insertions(+), 21 deletions(-) create mode 100644 CodeEdit.xcworkspace/contents.xcworkspacedata create mode 100644 CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 CodeEditUI/src/Preferences/ViewOffsetPreferenceKey.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index d26984b8cc..64dd535590 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -453,7 +453,6 @@ 6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6CCF73CE2E26DE3200B94F75 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */, - 58CFC4992F8BE78F009F4AA7 /* XCLocalSwiftPackageReference "Packages/CodeEditDomain" */, ); preferredProjectObjectVersion = 55; productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; @@ -1680,13 +1679,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - 58CFC4992F8BE78F009F4AA7 /* XCLocalSwiftPackageReference "Packages/CodeEditDomain" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = Packages/CodeEditDomain; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCRemoteSwiftPackageReference section */ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = { isa = XCRemoteSwiftPackageReference; @@ -1912,7 +1904,6 @@ }; 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */ = { isa = XCSwiftPackageProductDependency; - package = 58CFC4992F8BE78F009F4AA7 /* XCLocalSwiftPackageReference "Packages/CodeEditDomain" */; productName = CodeEditDomain; }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a2d361704c --- /dev/null +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..63b20a4ac0 --- /dev/null +++ b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,321 @@ +{ + "originHash" : "c4368adf5cf7353593e131deab35e74464b98a4f7755aea8cdd711a421976b22", + "pins" : [ + { + "identity" : "aboutwindow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/AboutWindow", + "state" : { + "revision" : "79c7c01fb739d024a3ca07fe153a068339213baf", + "version" : "1.0.0" + } + }, + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + }, + { + "identity" : "codeeditkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditKit.git", + "state" : { + "revision" : "ad28213a968586abb0cb21a8a56a3587227895f1", + "version" : "0.1.2" + } + }, + { + "identity" : "codeeditlanguages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", + "state" : { + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" + } + }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "state" : { + "revision" : "ee0c00a2343903df9d6ef45ce53228aca8637369", + "version" : "0.15.1" + } + }, + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", + "version" : "0.12.1" + } + }, + { + "identity" : "collectionconcurrencykit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/johnsundell/collectionconcurrencykit", + "state" : { + "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version" : "0.2.0" + } + }, + { + "identity" : "concurrencyplus", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ConcurrencyPlus", + "state" : { + "revision" : "8dc56499412a373d617d50d059116bccf44b9874", + "version" : "0.4.2" + } + }, + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory", + "state" : { + "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", + "version" : "2.5.3" + } + }, + { + "identity" : "fseventswrapper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Frizlab/FSEventsWrapper", + "state" : { + "revision" : "70bbea4b108221fcabfce8dbced8502831c0ae04", + "version" : "2.1.0" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "2cf6c756e1e5ef6901ebae16576a7e4e4b834622", + "version" : "6.29.3" + } + }, + { + "identity" : "jsonrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/JSONRPC", + "state" : { + "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", + "version" : "0.9.0" + } + }, + { + "identity" : "languageclient", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageClient", + "state" : { + "revision" : "4f28cc3cad7512470275f65ca2048359553a86f5", + "version" : "0.8.2" + } + }, + { + "identity" : "languageserverprotocol", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", + "state" : { + "revision" : "f7879c782c0845af9c576de7b8baedd946237286", + "version" : "0.14.0" + } + }, + { + "identity" : "logstream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Wouter01/LogStream", + "state" : { + "revision" : "6f83694b2675dcf3b1cea0a52546ff4469c18282", + "version" : "1.3.0" + } + }, + { + "identity" : "processenv", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ProcessEnv", + "state" : { + "revision" : "83f1ebc9dd6fb1db0bd89a3fcae00488a0f3fdd9", + "version" : "1.0.0" + } + }, + { + "identity" : "queue", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattmassicotte/Queue", + "state" : { + "revision" : "8d6f936097888f97011610ced40313655dc5948d", + "version" : "0.1.4" + } + }, + { + "identity" : "rearrange", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Rearrange", + "state" : { + "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3", + "version" : "2.0.0" + } + }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore", + "state" : { + "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", + "version" : "0.1.0" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle.git", + "state" : { + "revision" : "2a98381dfe72e24bf593c5c06d2c4fc1763c3f19", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", + "version" : "1.0.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" + } + }, + { + "identity" : "swift-glob", + "kind" : "remoteSourceControl", + "location" : "https://github.com/davbeck/swift-glob", + "state" : { + "revision" : "07ba6f47d903a0b1b59f12ca70d6de9949b975d6", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", + "state" : { + "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", + "version" : "1.14.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "3780efccceaa87f17ec39638a9d263d0e742b71c", + "version" : "0.59.1" + } + }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thecoolwinter/SwiftTerm", + "state" : { + "branch" : "codeedit", + "revision" : "2f36f54742d3882e69ff009d084e8675b80934bd" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } + }, + { + "identity" : "textformation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextFormation", + "state" : { + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" + } + }, + { + "identity" : "textstory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextStory", + "state" : { + "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", + "version" : "0.9.1" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "f2f197b6b27ce75c280c20f131d4f71e906b86f7", + "version" : "0.25.8" + } + }, + { + "identity" : "welcomewindow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/WelcomeWindow", + "state" : { + "revision" : "cbd5c0d6f432449e2a8618e2b24e4691acbfcc98", + "version" : "1.1.0" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } + } + ], + "version" : 3 +} diff --git a/CodeEditUI/src/Preferences/ViewOffsetPreferenceKey.swift b/CodeEditUI/src/Preferences/ViewOffsetPreferenceKey.swift deleted file mode 100644 index 831ccc8b9b..0000000000 --- a/CodeEditUI/src/Preferences/ViewOffsetPreferenceKey.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -/// Tracks scroll offset in scrollable views -public struct ViewOffsetPreferenceKey: PreferenceKey { - public typealias Value = CGFloat - public static var defaultValue = CGFloat.zero - public static func reduce(value: inout Value, nextValue: () -> Value) { - value += nextValue() - } -} diff --git a/Packages/CodeEditDomain/Package.swift b/Packages/CodeEditDomain/Package.swift index 18800ce118..8af806f7fa 100644 --- a/Packages/CodeEditDomain/Package.swift +++ b/Packages/CodeEditDomain/Package.swift @@ -9,7 +9,6 @@ let package = Package( .library(name: "CodeEditDomain", targets: ["CodeEditDomain"]) ], targets: [ - .target(name: "CodeEditDomain"), - .testTarget(name: "CodeEditDomainTests", dependencies: ["CodeEditDomain"]) + .target(name: "CodeEditDomain") ] ) From e3f8b164d12c3b09527737e01fa7879fca26fd53 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 16:02:07 +0200 Subject: [PATCH 018/335] =?UTF-8?q?Refactor:=20Rename=20CodeEditDomain=20?= =?UTF-8?q?=E2=86=92=20CodeEditCore=20and=20introduce=20typed=20EventBus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges the domain layer into a single CodeEditCore package to avoid an artificial split with no near-term benefit. Introduces a Combine-based EventBus (per-type PassthroughSubject, NSLock-guarded lazy creation) registered as a Factory singleton. Migrates openWelcomeWindow from stringly-typed NotificationCenter to the typed bus as the end-to-end proof of concept: WorkspaceWindowManager publishes WelcomeWindowRequestedEvent, AppDelegate subscribes and opens the window via SwiftUI's openWindow — observer lifecycle now managed by Set. --- CodeEdit.xcodeproj/project.pbxproj | 10 ++-- CodeEdit.xcworkspace/contents.xcworkspacedata | 2 +- CodeEdit/AppDelegate.swift | 25 +++++---- CodeEdit/CodeEditContainer.swift | 5 ++ .../TaskNotificationHandler.swift | 2 +- .../Notifications/TaskNotificationView.swift | 2 +- .../CEWorkspace/Models/CEWorkspaceFile.swift | 2 +- .../Views/ToolbarBranchPicker.swift | 2 +- .../ViewModels/QuickActionsViewModel.swift | 2 +- .../Commands/Views/QuickActionsView.swift | 2 +- .../Tab/Models/EditorTabRepresentable.swift | 2 +- .../HistoryInspectorItemView.swift | 2 +- .../HistoryInspectorModel.swift | 2 +- .../HistoryInspectorView.swift | 2 +- .../HistoryInspector/HistoryPopoverView.swift | 2 +- .../Features/Keybindings/CommandManager.swift | 2 +- .../Protocols/CommandManaging.swift | 2 +- .../InstallationMethod+PackageManager.swift | 2 +- .../Model/RegistryItem+AppExtensions.swift | 2 +- .../LSP/Registry/PackageManagerProtocol.swift | 2 +- .../PackageManagerInstallOperation.swift | 2 +- .../Sources/CargoPackageManager.swift | 2 +- .../Sources/GithubPackageManager.swift | 2 +- .../Sources/GolangPackageManager.swift | 2 +- .../Sources/NPMPackageManager.swift | 2 +- .../Sources/PipPackageManager.swift | 2 +- .../PackageSourceParser+Cargo.swift | 2 +- .../PackageSourceParser+Gem.swift | 2 +- .../PackageSourceParser+Golang.swift | 2 +- .../PackageSourceParser+NPM.swift | 2 +- .../PackageSourceParser+PYPI.swift | 2 +- .../PackageSourceParser.swift | 2 +- .../Registry/Protocols/RegistryManaging.swift | 2 +- .../Registry/RegistryItemTemplateParser.swift | 2 +- .../RegistryManager+HandleRegistryFile.swift | 2 +- .../LSP/Registry/RegistryManager.swift | 2 +- .../FindNavigator/FindModePicker.swift | 2 +- .../FindNavigator/FindNavigatorForm.swift | 2 +- ...ViewController+NSOutlineViewDelegate.swift | 2 +- .../ProjectNavigatorViewController.swift | 2 +- .../SourceControlNavigatorChangesList.swift | 2 +- .../Views/CommitDetailsHeaderView.swift | 2 +- .../History/Views/CommitDetailsView.swift | 2 +- .../History/Views/CommitListItemView.swift | 2 +- .../SourceControlNavigatorHistoryView.swift | 2 +- .../Models/RepoOutlineGroupItem.swift | 2 +- ...lNavigatorRepositoryView+contextMenu.swift | 2 +- ...SourceControlNavigatorRepositoryView.swift | 2 +- .../ChangedFile/GitChangedFileLabel.swift | 2 +- .../ChangedFile/GitChangedFileListView.swift | 2 +- .../FuzzySearch/Collection+FuzzySearch.swift | 2 +- .../Search/FuzzySearch/FuzzySearchable.swift | 2 +- .../String+LengthOfMatchingPrefix.swift | 2 +- .../Search/FuzzySearch/String+Normalise.swift | 2 +- CodeEdit/Features/Search/SearchState.swift | 2 +- .../Extensions/LanguageServerRowView.swift | 2 +- .../Extensions/LanguageServersView.swift | 2 +- .../Client/GitClient+Branches.swift | 2 +- .../Client/GitClient+Commit.swift | 2 +- .../Client/GitClient+CommitHistory.swift | 2 +- .../Client/GitClient+Remote.swift | 2 +- .../Client/GitClient+Stash.swift | 2 +- .../Client/GitClient+Status.swift | 2 +- .../Client/GitClientProtocol.swift | 2 +- .../Clone/GitCheckoutBranchView.swift | 2 +- .../GitCheckoutBranchViewModel.swift | 2 +- ...ourceControlManager+BranchOperations.swift | 2 +- .../SourceControlManager+FileOperations.swift | 2 +- ...ourceControlManager+RemoteOperations.swift | 2 +- ...SourceControlManager+StashOperations.swift | 2 +- .../SourceControl/SourceControlManager.swift | 2 +- .../Views/RemoteBranchPicker.swift | 2 +- .../Views/SourceControlNewBranchView.swift | 2 +- .../Views/SourceControlRenameBranchView.swift | 2 +- .../Views/SourceControlSwitchView.swift | 2 +- .../Features/Workspace/Models/Workspace.swift | 4 +- .../Services/WorkspaceWindowManager.swift | 12 ++--- .../UseCases/ShutdownApplicationUseCase.swift | 2 +- CodeEdit/WorkspaceSheets.swift | 2 +- .../Package.swift | 6 +-- .../Domain}/Commands/Command.swift | 0 .../Domain}/Editor/EditorItemID.swift | 0 .../CodeEditCore/Domain}/Git/GitBranch.swift | 0 .../Domain}/Git/GitBranchesGroup.swift | 0 .../Domain}/Git/GitChangedFile.swift | 0 .../CodeEditCore/Domain}/Git/GitCommit.swift | 0 .../CodeEditCore/Domain}/Git/GitRemote.swift | 0 .../Domain}/Git/GitStashEntry.swift | 0 .../CodeEditCore/Domain}/Git/GitStatus.swift | 0 .../Domain}/Registry/InstallationMethod.swift | 0 .../Domain}/Registry/PackageManagerType.swift | 0 .../Domain}/Registry/PackageSource.swift | 0 .../Registry/RegistryItem+Source.swift | 0 .../Domain}/Registry/RegistryItem.swift | 0 .../Registry/RegistryManagerError.swift | 0 .../Domain}/Search/FuzzySearchModels.swift | 0 .../Domain}/Search/SearchModeModel.swift | 0 .../Domain}/Tasks/TaskNotificationModel.swift | 0 .../CodeEditCore/Infrastructure/Event.swift | 12 +++++ .../Infrastructure/EventBus.swift | 54 +++++++++++++++++++ .../Events/WelcomeWindowRequestedEvent.swift | 14 +++++ .../Tests/CodeEditCoreTests/.gitkeep | 0 102 files changed, 187 insertions(+), 103 deletions(-) rename Packages/{CodeEditDomain => CodeEditCore}/Package.swift (53%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Commands/Command.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Editor/EditorItemID.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitBranch.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitBranchesGroup.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitChangedFile.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitCommit.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitRemote.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitStashEntry.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Git/GitStatus.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Registry/InstallationMethod.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Registry/PackageManagerType.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Registry/PackageSource.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Registry/RegistryItem+Source.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Registry/RegistryItem.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Registry/RegistryManagerError.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Search/FuzzySearchModels.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Search/SearchModeModel.swift (100%) rename Packages/{CodeEditDomain/Sources/CodeEditDomain => CodeEditCore/Sources/CodeEditCore/Domain}/Tasks/TaskNotificationModel.swift (100%) create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift create mode 100644 Packages/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 64dd535590..a4316a47ca 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; - 58CFC49B2F8BE799009F4AA7 /* CodeEditDomain in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */; }; + 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5E4485602DF600D9008BBE69 /* AboutWindow */; }; @@ -186,7 +186,7 @@ 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, - 58CFC49B2F8BE799009F4AA7 /* CodeEditDomain in Frameworks */, + 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */, 6CCF6DD32E26D48F00B94F75 /* SwiftTerm in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */, @@ -344,7 +344,7 @@ 6CCF6DD22E26D48F00B94F75 /* SwiftTerm */, 6CCF73CF2E26DE3200B94F75 /* SwiftTerm */, 58CF9F392F86D64F009F4AA7 /* Factory */, - 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */, + 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1902,9 +1902,9 @@ package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; productName = FactoryTesting; }; - 58CFC49A2F8BE799009F4AA7 /* CodeEditDomain */ = { + 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */ = { isa = XCSwiftPackageProductDependency; - productName = CodeEditDomain; + productName = CodeEditCore; }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { isa = XCSwiftPackageProductDependency; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index a2d361704c..7ceeb7c98c 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -5,6 +5,6 @@ location = "container:CodeEdit.xcodeproj"> + location = "group:Packages/CodeEditCore"> diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index a60f710bed..66837bab6a 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -5,9 +5,10 @@ // Created by Pavel Kasila on 12.03.22. // +import Combine import SwiftUI import Factory -import CodeEditDomain +import CodeEditCore import CodeEditSymbols import CodeEditSourceEditor import OSLog @@ -26,23 +27,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @LazyInjected(\.workspaceWindowManager) var windowManager + @LazyInjected(\.eventBus) + var eventBus + private let shutdownUseCase = ShutdownApplicationUseCase() - private var welcomeWindowObserver: NSObjectProtocol? + private var cancellables = Set() func applicationDidFinishLaunching(_ notification: Notification) { enableWindowSizeSaveOnQuit() Settings.shared.preferences.general.appAppearance.applyAppearance() checkForFilesToOpen() - // Listen for requests to open the welcome window from non-SwiftUI contexts - welcomeWindowObserver = NotificationCenter.default.addObserver( - forName: .openWelcomeWindow, - object: nil, - queue: .main - ) { [weak self] _ in - self?.openWindow(sceneID: .welcome) - } + // Subscribe to the welcome window event published by WorkspaceWindowManager + eventBus.subscribe(WelcomeWindowRequestedEvent.self) + .receive(on: RunLoop.main) + .sink { [weak self] _ in self?.openWindow(sceneID: .welcome) } + .store(in: &cancellables) NSApp.closeWindow(.welcome, .about) @@ -78,9 +79,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } func applicationWillTerminate(_ aNotification: Notification) { - if let welcomeWindowObserver { - NotificationCenter.default.removeObserver(welcomeWindowObserver) - } + cancellables.removeAll() } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 088b11a396..e915a1fbeb 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 08.04.26. // +import CodeEditCore import Factory extension Container { @@ -35,4 +36,8 @@ extension Container { var registryManager: Factory { self { @MainActor in RegistryManager() }.singleton } + + var eventBus: Factory { + self { EventBus() }.singleton + } } diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift index 1673d8ebc7..5f64a998cf 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift @@ -7,7 +7,7 @@ import Foundation import Combine -import CodeEditDomain +import CodeEditCore /// Manages task-related notifications. /// diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift index 0b36891540..be10d084e0 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct TaskNotificationView: View { @Environment(\.controlActiveState) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift index ff134953df..bb10ea5cf6 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI import UniformTypeIdentifiers import Combine -import CodeEditDomain +import CodeEditCore /// An object containing all necessary information and actions for a specific file in the workspace /// diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index 7992315e4a..e53b5cf9ad 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore import CodeEditSymbols import Combine diff --git a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift index 0f1dd5f272..4a90d888c5 100644 --- a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift +++ b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift @@ -7,7 +7,7 @@ import SwiftUI import Factory -import CodeEditDomain +import CodeEditCore /// Simple state class for command palette view. Contains currently selected command, /// query text and list of filtered commands diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/Views/QuickActionsView.swift index 6d3eb05be9..9445b7427e 100644 --- a/CodeEdit/Features/Commands/Views/QuickActionsView.swift +++ b/CodeEdit/Features/Commands/Views/QuickActionsView.swift @@ -7,7 +7,7 @@ import SwiftUI import Factory -import CodeEditDomain +import CodeEditCore /// Quick actions view struct QuickActionsView: View { diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift index 4fb522ca9f..ed74da4211 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore /// Protocol for data passed to EditorTabView to conform to protocol EditorTabRepresentable { diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift index 88b500ca42..918157d3b9 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct HistoryInspectorItemView: View { var commit: GitCommit diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index 7fe19786a5..77c3de99e1 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore final class HistoryInspectorModel: ObservableObject { private(set) var sourceControlManager: SourceControlManager? diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 97e750e7f0..d9a3589328 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -5,7 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI -import CodeEditDomain +import CodeEditCore struct HistoryInspectorView: View { @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift index af61a79354..3c88d43385 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct HistoryPopoverView: View { diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/Features/Keybindings/CommandManager.swift index c0351f20e9..84d081ece8 100644 --- a/CodeEdit/Features/Keybindings/CommandManager.swift +++ b/CodeEdit/Features/Keybindings/CommandManager.swift @@ -5,7 +5,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /** The object of this class intended to be a hearth of command palette. This object only exists as singleton. diff --git a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift index 1a49dd18e4..70ace72b8d 100644 --- a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift +++ b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Protocol for managing application commands (command palette). protocol CommandManaging: AnyObject, ObservableObject { diff --git a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift b/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift index 09205a257e..e52c28ba41 100644 --- a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension InstallationMethod { func packageManager(installPath: URL) -> PackageManagerProtocol? { diff --git a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift b/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift index 7de2f2171f..ceedf03446 100644 --- a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift +++ b/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension RegistryItem: FuzzySearchable { var searchableString: String { name } diff --git a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift b/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift index d2e5b69747..d987138b63 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// The protocol each package manager conforms to for creating ``PackageManagerInstallOperation``s. protocol PackageManagerProtocol { diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift index 488b779390..3ac7d2652a 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift @@ -8,7 +8,7 @@ import Foundation import Factory import Combine -import CodeEditDomain +import CodeEditCore /// An executable install operation for installing a ``RegistryItem``. /// diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift index 55ac352abb..beba3b1331 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift @@ -7,7 +7,7 @@ import Factory import Foundation -import CodeEditDomain +import CodeEditCore final class CargoPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift index c5be18db0b..91bc71131e 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift @@ -7,7 +7,7 @@ import Factory import Foundation -import CodeEditDomain +import CodeEditCore final class GithubPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift index 0d062f8326..cf07c56a9e 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift @@ -7,7 +7,7 @@ import Factory import Foundation -import CodeEditDomain +import CodeEditCore final class GolangPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift index e406618930..4287518247 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift @@ -7,7 +7,7 @@ import Factory import Foundation -import CodeEditDomain +import CodeEditCore final class NPMPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift index d6ac971be5..a1acaeba98 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift @@ -7,7 +7,7 @@ import Factory import Foundation -import CodeEditDomain +import CodeEditCore final class PipPackageManager: PackageManagerProtocol { private let installationDirectory: URL diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift index 242464bf29..aa2b72c8b3 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift @@ -5,7 +5,7 @@ // Created by Abe Malla on 3/12/25. // -import CodeEditDomain +import CodeEditCore extension PackageSourceParser { static func parseCargoPackage(_ entry: RegistryItem) -> InstallationMethod { diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift index 810c199629..82877e9003 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift @@ -5,7 +5,7 @@ // Created by Abe Malla on 3/12/25. // -import CodeEditDomain +import CodeEditCore extension PackageSourceParser { static func parseRubyGem(_ entry: RegistryItem) -> InstallationMethod { diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift index 871cb04228..99a8beb249 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift @@ -5,7 +5,7 @@ // Created by Abe Malla on 3/12/25. // -import CodeEditDomain +import CodeEditCore extension PackageSourceParser { static func parseGolangPackage(_ entry: RegistryItem) -> InstallationMethod { diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift index b098a7e64e..398b87c449 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift @@ -5,7 +5,7 @@ // Created by Abe Malla on 3/12/25. // -import CodeEditDomain +import CodeEditCore extension PackageSourceParser { static func parseNpmPackage(_ entry: RegistryItem) -> InstallationMethod { diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift index af31fcdb9f..d7ec7ec0c7 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift @@ -5,7 +5,7 @@ // Created by Abe Malla on 3/12/25. // -import CodeEditDomain +import CodeEditCore extension PackageSourceParser { static func parsePythonPackage(_ entry: RegistryItem) -> InstallationMethod { diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift index 2b74015963..5c9910fa5d 100644 --- a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift +++ b/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Parser for package source IDs enum PackageSourceParser { diff --git a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift index c8c63f0f27..5b30d10d89 100644 --- a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift +++ b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Protocol for managing the language server registry. /// diff --git a/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift b/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift index 90a1de9c31..e2c0699b13 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// This parser is used to parse expressions that may be included in a field of a registry item. /// diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift b/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift index c303a47b51..08b1fc1920 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension RegistryManager { /// Downloads the latest registry diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index 7028a79a1e..bcf06662a7 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -10,7 +10,7 @@ import Foundation import ZIPFoundation import Combine import Factory -import CodeEditDomain +import CodeEditCore @MainActor final class RegistryManager: ObservableObject, RegistryManaging { diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift index 0a11cdc0e6..963cd98579 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift @@ -7,7 +7,7 @@ import SwiftUI import Combine -import CodeEditDomain +import CodeEditCore struct FindModePicker: View { var modes: [SearchModeModel] diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift index b409080969..58c6d1283d 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct FindNavigatorForm: View { @ObservedObject private var state: SearchState diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index b78eb09fa3..4f57abe36e 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -6,7 +6,7 @@ // import AppKit -import CodeEditDomain +import CodeEditCore extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView( diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index d960fa10ff..bebe9d5e6c 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -8,7 +8,7 @@ import AppKit import SwiftUI import OSLog -import CodeEditDomain +import CodeEditCore /// A `NSViewController` that handles the **ProjectNavigatorView** in the **NavigatorArea**. /// diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 8793d28932..2879cb8724 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -7,7 +7,7 @@ import AppKit import SwiftUI -import CodeEditDomain +import CodeEditCore struct SourceControlNavigatorChangesList: View { @EnvironmentObject var workspace: Workspace diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift index 455174d2db..3f574c253d 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct CommitDetailsHeaderView: View { var commit: GitCommit diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift index 96c71e0ca1..5a1f84d039 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct CommitDetailsView: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift index f119b290a1..332334a24f 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct CommitListItemView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index 2f57c71205..97392eb4a7 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore import CodeEditSymbols struct SourceControlNavigatorHistoryView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift index 23fb1364ff..7529c94b69 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct RepoOutlineGroupItem: Hashable, Identifiable { enum ImageType: Hashable { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift index 132f42097e..f370c4890b 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore extension SourceControlNavigatorRepositoryView { func handleDelete(_ item: RepoOutlineGroupItem) { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift index f9ca8b8474..6308a31e91 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore import CodeEditSymbols struct SourceControlNavigatorRepositoryView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index e95ec67bd5..42e944251f 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -7,7 +7,7 @@ import SwiftUI import Factory -import CodeEditDomain +import CodeEditCore struct GitChangedFileLabel: View { @EnvironmentObject private var workspace: Workspace diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index 2748b0bcee..934e765a3b 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore /// A view to display a changed file's information in a list view. Optionally displays the staged status. struct GitChangedFileListView: View { diff --git a/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift b/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift index 7ce8a68969..594a5ff932 100644 --- a/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift +++ b/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift @@ -7,7 +7,7 @@ import Foundation import CollectionConcurrencyKit -import CodeEditDomain +import CodeEditCore extension Collection where Iterator.Element: FuzzySearchable { /// Asynchronously performs a fuzzy search on a collection of elements conforming to FuzzySearchable. diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift index 0151b5efc7..afae806871 100644 --- a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift +++ b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// A protocol defining the requirements for an object that can be searched using fuzzy matching. protocol FuzzySearchable { diff --git a/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift b/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift index df7fd78cf7..d710b97e4e 100644 --- a/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift +++ b/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension String { /// Returns the length of the matching prefix content or normalised content at the specified index. diff --git a/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift b/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift index dc2cb5bc9f..e4486e2b5d 100644 --- a/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift +++ b/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension String { /// Normalises the characters of the string by converting them to ASCII representation. diff --git a/CodeEdit/Features/Search/SearchState.swift b/CodeEdit/Features/Search/SearchState.swift index 2ac9700634..00881cce44 100644 --- a/CodeEdit/Features/Search/SearchState.swift +++ b/CodeEdit/Features/Search/SearchState.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Manages the search/find state for a workspace, including indexing, search results, /// and find-and-replace operations. Extracted from Workspace to be independently diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift index 4fb1d09105..d7857b79c7 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore private let iconSize: CGFloat = 26 diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 853ea09968..6fa1d5f787 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -7,7 +7,7 @@ import SwiftUI import Factory -import CodeEditDomain +import CodeEditCore /// Displays a searchable list of packages from the ``RegistryManager``. struct LanguageServersView: View { diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift index 1891bfed66..f9247f023d 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension GitClient { /// Get branches diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift index e51921b609..752dee2891 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift @@ -7,7 +7,7 @@ import Foundation import RegexBuilder -import CodeEditDomain +import CodeEditCore extension GitClient { /// Commit files diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift b/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift index c49398ba28..7a3cfc0917 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension GitClient { /// Gets the commit history log for the specified branch or file diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift index 55bf456c7d..960c5793bb 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension GitClient { /// Gets all remotes diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift index 417cdcbded..c17ee607c0 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore extension GitClient { /// Add uncommited changes to stash diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift index 01da617b7a..59a9002850 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Methods for parsing git's porcelain v2 format and returning the info in a ``GitClient/Status`` struct. /// diff --git a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift b/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift index 70e4c078bb..a66fa7c1ab 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Abstraction over git operations used by ``SourceControlManager``. /// diff --git a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift b/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift index 8b7dbae036..a98bfec591 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift +++ b/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift @@ -7,7 +7,7 @@ import Foundation import SwiftUI -import CodeEditDomain +import CodeEditCore struct GitCheckoutBranchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift index 771037e291..e1ea205e74 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift @@ -7,7 +7,7 @@ import Foundation import Factory -import CodeEditDomain +import CodeEditCore class GitCheckoutBranchViewModel: ObservableObject { @Published var selectedBranch: GitBranch? diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift index 897af31436..7f3560de9a 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Branch-related git operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift index 8e611c0a56..e16c9c1b48 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// File status, staging, committing, and discard operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift index d9df459320..68bf77554d 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Remote, fetch, pull, and push operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift index a6b153deaa..19bed3e4d1 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift @@ -6,7 +6,7 @@ // import Foundation -import CodeEditDomain +import CodeEditCore /// Stash-related git operations. extension SourceControlManager { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index 3f8c036177..4b59a25b08 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -7,7 +7,7 @@ import Foundation import OSLog -import CodeEditDomain +import CodeEditCore /// Stores git state for the workspace and delegates operations to ``GitClient``. /// diff --git a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift b/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift index 65064c2a94..ec7c2bf36d 100644 --- a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift +++ b/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct RemoteBranchPicker: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift index ca90a052b1..1396f49002 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct SourceControlNewBranchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift index 239fee1721..1b7be03bbd 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct SourceControlRenameBranchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift index af57c67526..d8d2401795 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct SourceControlSwitchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index b88492ce78..2a91f12a87 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -17,8 +17,8 @@ final class Workspace: ObservableObject, WorkspaceManaging { @Published var navigatorFilter: String = "" @Published var sourceControlFilter = false - internal(set) var fileURL: URL? - internal(set) var displayName: String = "" + var fileURL: URL? + var displayName: String = "" var workspaceFileManager: CEWorkspaceFileManager? var editorManager: EditorManager? = EditorManager() diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 992ad8cce9..0f72f48294 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -6,17 +6,17 @@ // import AppKit +import CodeEditCore +import Factory import SwiftUI import WelcomeWindow -extension Notification.Name { - static let openWelcomeWindow = Notification.Name("CodeEdit.openWelcomeWindow") -} - /// Manages the lifecycle of workspace windows, replacing NSDocumentController for workspace management. @MainActor final class WorkspaceWindowManager: WorkspaceWindowManaging { + @LazyInjected(\.eventBus) private var eventBus + private let openWorkspaceUseCase = OpenWorkspaceUseCase() private let closeWorkspaceUseCase = CloseWorkspaceUseCase() private lazy var openDocumentUseCase = OpenDocumentUseCase(windowManager: self) @@ -179,8 +179,8 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { if let welcomeWindow = NSApp.findWindow(.welcome) { welcomeWindow.makeKeyAndOrderFront(nil) } else { - // Post notification for AppDelegate to open the welcome window via SwiftUI's openWindow - NotificationCenter.default.post(name: .openWelcomeWindow, object: nil) + // Publish event for AppDelegate to open the welcome window via SwiftUI's openWindow + eventBus.publish(WelcomeWindowRequestedEvent()) } case .quit: NSApplication.shared.terminate(nil) diff --git a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift index ff31fbf1eb..c87fae3b9f 100644 --- a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift @@ -7,7 +7,7 @@ import Foundation import Factory -import CodeEditDomain +import CodeEditCore /// Orchestrates application shutdown: saves workspace paths, checks for unsaved changes, /// prompts the user to save, and terminates running tasks. diff --git a/CodeEdit/WorkspaceSheets.swift b/CodeEdit/WorkspaceSheets.swift index ab99f470e8..d8e6db8674 100644 --- a/CodeEdit/WorkspaceSheets.swift +++ b/CodeEdit/WorkspaceSheets.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CodeEditDomain +import CodeEditCore struct WorkspaceSheets: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/Packages/CodeEditDomain/Package.swift b/Packages/CodeEditCore/Package.swift similarity index 53% rename from Packages/CodeEditDomain/Package.swift rename to Packages/CodeEditCore/Package.swift index 8af806f7fa..ffb6979a2f 100644 --- a/Packages/CodeEditDomain/Package.swift +++ b/Packages/CodeEditCore/Package.swift @@ -3,12 +3,12 @@ import PackageDescription let package = Package( - name: "CodeEditDomain", + name: "CodeEditCore", platforms: [.macOS(.v14)], products: [ - .library(name: "CodeEditDomain", targets: ["CodeEditDomain"]) + .library(name: "CodeEditCore", targets: ["CodeEditCore"]) ], targets: [ - .target(name: "CodeEditDomain") + .target(name: "CodeEditCore") ] ) diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Commands/Command.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Commands/Command.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Editor/EditorItemID.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Editor/EditorItemID.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranch.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranch.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranchesGroup.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitBranchesGroup.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitChangedFile.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitChangedFile.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitCommit.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitCommit.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitRemote.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitRemote.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStashEntry.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStashEntry.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStatus.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Git/GitStatus.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/InstallationMethod.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/InstallationMethod.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageManagerType.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageManagerType.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageSource.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/PackageSource.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem+Source.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem+Source.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryItem.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryManagerError.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Registry/RegistryManagerError.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/FuzzySearchModels.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Search/FuzzySearchModels.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Search/SearchModeModel.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Search/SearchModeModel.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift diff --git a/Packages/CodeEditDomain/Sources/CodeEditDomain/Tasks/TaskNotificationModel.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift similarity index 100% rename from Packages/CodeEditDomain/Sources/CodeEditDomain/Tasks/TaskNotificationModel.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift new file mode 100644 index 0000000000..18c34714d6 --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift @@ -0,0 +1,12 @@ +// +// Event.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 01.07.26. +// + +/// Marker protocol for all typed EventBus events. +/// +/// Conforming types are published via `EventBus` and received by subscribers +/// via `AnyCancellable`. All events must be `Sendable`. +public protocol Event: Sendable {} diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift new file mode 100644 index 0000000000..f7bcab0fc3 --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift @@ -0,0 +1,54 @@ +// +// EventBus.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 01.07.26. +// + +import Combine +import Foundation + +/// Typed, Combine-backed publish/subscribe event bus. +/// +/// Each event type has its own `PassthroughSubject`. Publishing is synchronous +/// on the calling thread. Register as a Factory singleton via `Container.eventBus`. +/// +/// **Publishing:** +/// ```swift +/// eventBus.publish(WelcomeWindowRequestedEvent()) +/// ``` +/// +/// **Subscribing:** +/// ```swift +/// eventBus.subscribe(WelcomeWindowRequestedEvent.self) +/// .receive(on: RunLoop.main) +/// .sink { event in ... } +/// .store(in: &cancellables) +/// ``` +public final class EventBus: @unchecked Sendable { + private var subjects: [ObjectIdentifier: Any] = [:] + private let lock = NSLock() + + public init() {} + + /// Publish an event to all current subscribers. + public func publish(_ event: E) { + subject(for: E.self).send(event) + } + + /// Returns a publisher that emits events of the given type. + public func subscribe(_ type: E.Type) -> AnyPublisher { + subject(for: type).eraseToAnyPublisher() + } + + private func subject(for type: E.Type) -> PassthroughSubject { + lock.lock(); defer { lock.unlock() } + let key = ObjectIdentifier(type) + if let existing = subjects[key] as? PassthroughSubject { + return existing + } + let created = PassthroughSubject() + subjects[key] = created + return created + } +} diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift new file mode 100644 index 0000000000..19e34fe8f7 --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift @@ -0,0 +1,14 @@ +// +// WelcomeWindowRequestedEvent.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 01.07.26. +// + +/// Published by `WorkspaceWindowManager` when the last workspace closes +/// and the user preference is set to show the welcome window. +/// +/// Consumed by `AppDelegate`, which holds the SwiftUI `openWindow` environment action. +public struct WelcomeWindowRequestedEvent: Event { + public init() {} +} diff --git a/Packages/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep b/Packages/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From 8c0be01e515585965432d12df9e87c45bd52b59e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 17:25:37 +0200 Subject: [PATCH 019/335] Refactor: Migrate task notifications to typed EventBus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stringly-typed .taskNotification NotificationCenter posts and their untyped [String: Any] userInfo dicts with a TaskNotificationEvent whose Action enum carries associated values — invalid combinations are now unrepresentable at compile time. This establishes the payload convention for future event migrations. TaskNotificationHandler subscribes to the bus with a typed switch (also fixing a retain cycle in its old sink), and all five publishers — CEActiveTask, SearchState indexing, RegistryManager, ShutdownApplicationUseCase, and AppDelegate — publish directly via @LazyInjected(\.eventBus). The static postTask helper and Notification.Name.taskNotification are deleted. Also repairs the test target, which had not compiled since the NSDocument removal: the SearchState suites construct SearchState directly, GitClientTests uses ShellClient(), and the LSP integration test is ported to the Factory/ WorkspaceWindowManager architecture. Adds a workspace-filtering test for the handler. 142/143 unit tests pass; the one failure (testFindAndReplace) is a pre-existing contradictory assertion from upstream PR #1537. --- CodeEdit/AppDelegate.swift | 4 +- .../TaskNotificationHandler.swift | 261 ++++-------------- .../LSP/Registry/RegistryManager.swift | 41 +-- .../Features/Search/SearchState+Index.swift | 46 ++- CodeEdit/Features/Search/SearchState.swift | 5 + .../Features/Tasks/Models/CEActiveTask.swift | 56 ++-- .../UseCases/ShutdownApplicationUseCase.swift | 7 +- .../TaskNotificationHandlerTests.swift | 98 +++---- ...ment+SearchState+FindAndReplaceTests.swift | 7 +- ...kspaceDocument+SearchState+FindTests.swift | 7 +- ...spaceDocument+SearchState+IndexTests.swift | 7 +- .../LSP/LanguageServer+CodeFileDocument.swift | 23 +- CodeEditTests/Features/LSP/Registry.swift | 1 + .../SourceControl/GitClientTests.swift | 2 +- .../Domain/Tasks/TaskNotificationModel.swift | 2 +- .../Events/TaskNotificationEvent.swift | 44 +++ 16 files changed, 233 insertions(+), 378 deletions(-) create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 66837bab6a..7053265276 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -258,7 +258,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { ) if !lspService.languageClients.isEmpty { - TaskNotificationHandler.postTask(action: .create, model: task) + eventBus.publish(TaskNotificationEvent(.create(task))) } try? await withTimeout( @@ -272,7 +272,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } ) - TaskNotificationHandler.postTask(action: .delete, model: task) + eventBus.publish(TaskNotificationEvent(.delete(id: task.id))) NSApplication.shared.reply(toApplicationShouldTerminate: true) } } diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift index 5f64a998cf..59b9b005f0 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift @@ -8,246 +8,93 @@ import Foundation import Combine import CodeEditCore +import Factory -/// Manages task-related notifications. +/// Maintains the list of task notifications shown in the activity viewer. /// -/// This class listens for notifications named `.taskNotification` and performs actions -/// such as creating, updating, or deleting tasks based on the notification's content. +/// Listens for ``TaskNotificationEvent`` on the ``EventBus`` and creates, updates, +/// or deletes ``TaskNotificationModel`` entries accordingly. The activity viewer +/// displays only the first item in the array; use +/// ``TaskNotificationEvent/Action/createWithPriority(_:)`` to show a notification +/// immediately. /// -/// When a task is created, it is added to the end of the array. The activity viewer displays -/// only the first item in the array. To immediately display a notification, use the -/// `"action": "createWithPriority"` option to insert the task at the beginning of the array. -/// *Note: This option should be reserved for important notifications only.* +/// It is recommended to use `UUID().uuidString` as the task identifier, or any +/// other unique identifier such as a token sent from a language server. Remember +/// to delete notifications when done, either manually or via +/// ``TaskNotificationEvent/Action/deleteWithDelay(id:delay:)``. /// -/// It is recommended to use `UUID().uuidString` to generate a unique identifier for each task. -/// This identifier can then be used to update or delete the task. Alternatively, you can use any -/// unique identifier, such as a token sent from a language server. +/// Events can be restricted to a single workspace by passing a `workspace` URL +/// when publishing; events without one are received by all workspaces. /// -/// Remember to manage your task notifications appropriately. You should either delete task -/// notifications manually or schedule their deletion in advance using the `deleteWithDelay` method. -/// -/// Some tasks should be restricted to a specific workspace. To do this, specify the `workspace` attribute in the -/// notification's `userInfo` dictionary as a `URL`, or use the `toWorkspace` parameter on -/// ``TaskNotificationHandler/postTask(toWorkspace:action:model:)``. -/// -/// ## Available Methods -/// - `create`: -/// Creates a new Task Notification. -/// Required fields: `id` (String), `action` (String), `title` (String). -/// Optional fields: `message` (String), `percentage` (Double), `isLoading` (Bool), `workspace` (URL). -/// - `createWithPriority`: -/// Creates a new Task Notification and inserts it at the start of the array. -/// This ensures it appears in the activity viewer even if there are other task notifications before it. -/// **Note:** This should only be used for important notifications! -/// Required fields: `id` (String), `action` (String), `title` (String). -/// Optional fields: `message` (String), `percentage` (Double), `isLoading` (Bool), `workspace` (URL). -/// - `update`: -/// Updates an existing task notification. It's important to pass the same `id` to update the correct task. -/// Required fields: `id` (String), `action` (String). -/// Optional fields: `title` (String), `message` (String), `percentage` (Double), `isLoading` (Bool), -/// `workspace` (URL). -/// - `delete`: -/// Deletes an existing task notification. -/// Required fields: `id` (String), `action` (String). -/// Optional field: `workspace` (URL). -/// - `deleteWithDelay`: -/// Deletes an existing task notification after a certain `TimeInterval`. -/// Required fields: `id` (String), `action` (String), `delay` (Double). -/// Optional field: `workspace` (URL). -/// **Important:** When specifying the delay, ensure it's a double. -/// For example, '2' would be invalid because it would count as an integer, use '2.0' instead. -/// -/// ## Example Usage: +/// ## Example /// ```swift -/// let uuidString = UUID().uuidString -/// -/// func createTask() { -/// let userInfo: [String: Any] = [ -/// "id": "uniqueTaskID", -/// "action": "create", -/// "title": "Task Title" -/// ] -/// NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) -/// } +/// @LazyInjected(\.eventBus) private var eventBus /// -/// func createTaskWithPriority() { -/// let userInfo: [String: Any] = [ -/// "id": "uniqueTaskID", -/// "action": "createWithPriority", -/// "title": "Priority Task Title" -/// ] -/// NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) -/// } -/// -/// func updateTask() { -/// var userInfo: [String: Any] = [ -/// "id": "uniqueTaskID", -/// "action": "update", -/// "title": "Updated Task Title", -/// "message": "Updated Task Message", -/// "percentage": 0.5, -/// "isLoading": true -/// ] -/// NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) -/// } -/// -/// func deleteTask() { -/// let userInfo: [String: Any] = [ -/// "id": "uniqueTaskID", -/// "action": "delete" -/// ] -/// NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) -/// } -/// -/// func deleteTaskWithDelay() { -/// let userInfo: [String: Any] = [ -/// "id": "uniqueTaskID", -/// "action": "deleteWithDelay", -/// "delay": 4.0 // 4 would be invalid, because it would count as an int -/// ] -/// NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) -/// } +/// eventBus.publish(TaskNotificationEvent( +/// .create(TaskNotificationModel(id: UUID().uuidString, title: "Indexing")) +/// )) /// ``` -/// -/// You can also use the static helper method instead of creating dictionaries manually: -/// ```swift -/// TaskNotificationHandler.postTask(action: .create, model: .init(id: "task_id", "title": "New Task")) -/// ``` -/// -/// - Important: Please refer to ``CodeEdit/TaskNotificationModel`` and ensure you pass the correct values. final class TaskNotificationHandler: ObservableObject { @Published private(set) var notifications: [TaskNotificationModel] = [] var workspaceURL: URL? var cancellables: Set = [] - enum Action: String { - case create - case createWithPriority - case update - case delete - case deleteWithDelay - } + @LazyInjected(\.eventBus) + private var eventBus - /// Post a new task. - /// - Parameters: - /// - toWorkspace: The workspace to restrict the task to. Defaults to `nil`, which is received by all workspaces. - /// - action: The action being taken on the task. - /// - model: The task contents. - @MainActor - static func postTask(toWorkspace: URL? = nil, action: Action, model: TaskNotificationModel) { - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: [ - "id": model.id, - "title": model.title, - "message": model.message as Any, - "percentage": model.percentage as Any, - "isLoading": model.isLoading, - "action": action.rawValue, - "workspace": toWorkspace as Any - ]) - } - - /// Initialises a new `TaskNotificationHandler` and starts observing for task notifications. + /// Initialises a new `TaskNotificationHandler` and starts observing for task notification events. init(workspaceURL: URL? = nil) { self.workspaceURL = workspaceURL - NotificationCenter.default - .publisher(for: .taskNotification) + eventBus.subscribe(TaskNotificationEvent.self) .receive(on: DispatchQueue.main) - .sink { notification in - self.handleNotification(notification) + .sink { [weak self] event in + self?.handle(event) } .store(in: &cancellables) } - deinit { - NotificationCenter.default.removeObserver(self, name: .taskNotification, object: nil) - } - - /// Handles notifications about task events. - /// - /// - Parameter notification: The notification containing task information. - private func handleNotification(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let taskID = userInfo["id"] as? String, - let actionRaw = userInfo["action"] as? String, - let action = Action(rawValue: actionRaw) else { return } - - // If a workspace is specified and doesn't match, don't do anything with this task. - if let workspaceURL = userInfo["workspace"] as? URL, workspaceURL != self.workspaceURL { + /// Applies a task notification event to the notifications array. + private func handle(_ event: TaskNotificationEvent) { + // If a workspace is specified and doesn't match, don't do anything with this event. + if let workspaceURL = event.workspaceURL, workspaceURL != self.workspaceURL { return } - switch action { - case .create, .createWithPriority: - createTask(task: userInfo) - case .update: - updateTask(task: userInfo) - case .delete: - deleteTask(taskID: taskID) - case .deleteWithDelay: - if let delay = userInfo["delay"] as? Double { - deleteTaskAfterDelay(taskID: taskID, delay: delay) - } + switch event.action { + case .create(let model): + notifications.append(model) + case .createWithPriority(let model): + notifications.insert(model, at: 0) + case let .update(id, title, message, percentage, isLoading): + updateTask(id: id, title: title, message: message, percentage: percentage, isLoading: isLoading) + case .delete(let id): + notifications.removeAll { $0.id == id } + case let .deleteWithDelay(id, delay): + deleteTaskAfterDelay(taskID: id, delay: delay) } } - /// Creates a new task or inserts it at the beginning of the tasks array based on the action. - /// - /// - Parameter task: A dictionary containing task information. - private func createTask(task: [AnyHashable: Any]) { - guard let title = task["title"] as? String, - let id = task["id"] as? String, - let action = task["action"] as? String else { - return + /// Updates an existing task, applying only the non-`nil` fields. + private func updateTask(id: String, title: String?, message: String?, percentage: Double?, isLoading: Bool?) { + guard let index = notifications.firstIndex(where: { $0.id == id }) else { return } + if let title { + notifications[index].title = title } - - let task = TaskNotificationModel( - id: id, - title: title, - message: task["message"] as? String, - percentage: task["percentage"] as? Double, - isLoading: task["isLoading"] as? Bool ?? false - ) - - if action == "create" { - notifications.append(task) - } else { - notifications.insert(task, at: 0) + if let message { + notifications[index].message = message + } + if let percentage { + notifications[index].percentage = percentage + } + if let isLoading { + notifications[index].isLoading = isLoading } - } - - /// Updates an existing task with new information. - /// - /// - Parameter task: A dictionary containing task information. - private func updateTask(task: [AnyHashable: Any]) { - guard let taskID = task["id"] as? String else { return } - if let index = self.notifications.firstIndex(where: { $0.id == taskID }) { - if let title = task["title"] as? String { - self.notifications[index].title = title - } - if let message = task["message"] as? String { - self.notifications[index].message = message - } - if let percentage = task["percentage"] as? Double { - self.notifications[index].percentage = percentage - } - if let isLoading = task["isLoading"] as? Bool { - self.notifications[index].isLoading = isLoading - } - } - } - - private func deleteTask(taskID: String) { - self.notifications.removeAll { $0.id == taskID } } private func deleteTaskAfterDelay(taskID: String, delay: Double) { - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - self.notifications.removeAll { $0.id == taskID } + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + self?.notifications.removeAll { $0.id == taskID } } } } - -extension Notification.Name { - static let taskNotification = Notification.Name("taskNotification") -} diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index bcf06662a7..e885a926e1 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -49,6 +49,9 @@ final class RegistryManager: ObservableObject, RegistryManaging { @AppSettings(\.languageServers.installedLanguageServers) var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] + @LazyInjected(\.eventBus) + private var eventBus + init() { // Load the registry items from disk again after cache expires if let items = loadItemsFromDisk() { @@ -83,15 +86,9 @@ final class RegistryManager: ObservableObject, RegistryManaging { } // Add to activity viewer - NotificationCenter.default.post( - name: .taskNotification, - object: nil, - userInfo: [ - "id": packageName, - "action": "create", - "title": "Removing \(packageName)" - ] - ) + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: packageName, title: "Removing \(packageName)")) + )) do { try await Task.detached(priority: .userInitiated) { @@ -141,10 +138,9 @@ final class RegistryManager: ObservableObject, RegistryManaging { // Add to activity viewer let activityTitle = "\(operation.package.name)\("@" + (method.version ?? "latest"))" - TaskNotificationHandler.postTask( - action: .create, - model: TaskNotificationModel(id: operation.package.name, title: "Installing \(activityTitle)") - ) + self?.eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: operation.package.name, title: "Installing \(activityTitle)")) + )) guard !Task.isCancelled else { return } @@ -190,19 +186,12 @@ final class RegistryManager: ObservableObject, RegistryManaging { action: {}, ) } else { - TaskNotificationHandler.postTask( - action: .update, - model: TaskNotificationModel(id: id, title: "Successfully installed \(activityName)", isLoading: false) - ) - NotificationCenter.default.post( - name: .taskNotification, - object: nil, - userInfo: [ - "id": id, - "action": "deleteWithDelay", - "delay": 5.0, - ] - ) + eventBus.publish(TaskNotificationEvent( + .update(id: id, title: "Successfully installed \(activityName)", isLoading: false) + )) + eventBus.publish(TaskNotificationEvent( + .deleteWithDelay(id: id, delay: 5.0) + )) } } diff --git a/CodeEdit/Features/Search/SearchState+Index.swift b/CodeEdit/Features/Search/SearchState+Index.swift index c29e527fb3..e82d16a013 100644 --- a/CodeEdit/Features/Search/SearchState+Index.swift +++ b/CodeEdit/Features/Search/SearchState+Index.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore extension SearchState { /// Adds the contents of the current workspace URL to the search index. @@ -16,14 +17,15 @@ extension SearchState { indexStatus = .indexing(progress: 0.0) let uuidString = UUID().uuidString - let createInfo: [String: Any] = [ - "id": uuidString, - "action": "create", - "title": "Indexing | Processing files", - "message": "Creating an index to enable fast and accurate searches within your codebase.", - "isLoading": true - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: createInfo) + let eventBus = eventBus + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel( + id: uuidString, + title: "Indexing | Processing files", + message: "Creating an index to enable fast and accurate searches within your codebase.", + isLoading: true + )) + )) Task.detached { let filePaths = self.getFileURLs(at: url) @@ -41,12 +43,9 @@ extension SearchState { await MainActor.run { self.indexStatus = .indexing(progress: progress) } - let updateInfo: [String: Any] = [ - "id": uuidString, - "action": "update", - "percentage": progress - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: updateInfo) + eventBus.publish(TaskNotificationEvent( + .update(id: uuidString, percentage: progress) + )) } } asyncController.index.flush() @@ -54,20 +53,13 @@ extension SearchState { await MainActor.run { self.indexStatus = .done } - let updateInfo: [String: Any] = [ - "id": uuidString, - "action": "update", - "title": "Finished indexing", - "isLoading": false - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: updateInfo) + eventBus.publish(TaskNotificationEvent( + .update(id: uuidString, title: "Finished indexing", isLoading: false) + )) - let deleteInfo = [ - "id": uuidString, - "action": "deleteWithDelay", - "delay": 4.0 - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: deleteInfo) + eventBus.publish(TaskNotificationEvent( + .deleteWithDelay(id: uuidString, delay: 4.0) + )) } } diff --git a/CodeEdit/Features/Search/SearchState.swift b/CodeEdit/Features/Search/SearchState.swift index 00881cce44..df0a410cce 100644 --- a/CodeEdit/Features/Search/SearchState.swift +++ b/CodeEdit/Features/Search/SearchState.swift @@ -7,6 +7,7 @@ import Foundation import CodeEditCore +import Factory /// Manages the search/find state for a workspace, including indexing, search results, /// and find-and-replace operations. Extracted from Workspace to be independently @@ -41,6 +42,10 @@ final class SearchState: ObservableObject { @Published var shouldFocusSearchField: Bool = false let workspaceURL: URL + + @LazyInjected(\.eventBus) + var eventBus + var tempSearchResults = [SearchResultModel]() var caseSensitive: Bool = false var indexer: SearchIndexer? diff --git a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift b/CodeEdit/Features/Tasks/Models/CEActiveTask.swift index 40cf6eb1ae..7a3d9de410 100644 --- a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift +++ b/CodeEdit/Features/Tasks/Models/CEActiveTask.swift @@ -8,6 +8,8 @@ import SwiftUI import Combine import SwiftTerm +import CodeEditCore +import Factory /// Stores the state of a task once it's executed class CEActiveTask: ObservableObject, Identifiable, Hashable { @@ -33,6 +35,9 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { var workspaceURL: URL? + @LazyInjected(\.eventBus) + private var eventBus + private var cancellables = Set() init(task: CETask) { @@ -147,46 +152,29 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { } private func createStatusTaskNotification() { - let userInfo: [String: Any] = [ - "id": taskId, - "action": "createWithPriority", - "title": "Running \(self.task.name)", - "message": "Running your task: \(self.task.name).", - "isLoading": true, - "workspace": workspaceURL as Any - ] - - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) + eventBus.publish(TaskNotificationEvent( + .createWithPriority(TaskNotificationModel( + id: taskId, + title: "Running \(self.task.name)", + message: "Running your task: \(self.task.name).", + isLoading: true + )), + workspace: workspaceURL + )) } private func deleteStatusTaskNotification() { - let deleteInfo: [String: Any] = [ - "id": taskId, - "action": "deleteWithDelay", - "delay": 3.0, - "workspace": workspaceURL as Any - ] - - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: deleteInfo) + eventBus.publish(TaskNotificationEvent( + .deleteWithDelay(id: taskId, delay: 3.0), + workspace: workspaceURL + )) } private func updateTaskNotification(title: String? = nil, message: String? = nil, isLoading: Bool? = nil) { - var userInfo: [String: Any] = [ - "id": taskId, - "action": "update", - "workspace": workspaceURL as Any - ] - if let title { - userInfo["title"] = title - } - if let message { - userInfo["message"] = message - } - if let isLoading { - userInfo["isLoading"] = isLoading - } - - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) + eventBus.publish(TaskNotificationEvent( + .update(id: taskId, title: title, message: message, isLoading: isLoading), + workspace: workspaceURL + )) } @MainActor diff --git a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift index c87fae3b9f..3bcbc27912 100644 --- a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift @@ -21,6 +21,9 @@ final class ShutdownApplicationUseCase { @LazyInjected(\.workspaceWindowManager) private var windowManager + @LazyInjected(\.eventBus) + private var eventBus + /// - Returns: `true` if the app should proceed with termination, `false` if the user cancelled. func execute() -> Bool { let workspaces = windowManager.openWorkspaces @@ -55,11 +58,11 @@ final class ShutdownApplicationUseCase { message: "Interrupting all running tasks before quitting...", isLoading: true ) - TaskNotificationHandler.postTask(action: .create, model: task) + eventBus.publish(TaskNotificationEvent(.create(task))) taskManagers.forEach { $0.stopAllTasks() } - TaskNotificationHandler.postTask(action: .delete, model: task) + eventBus.publish(TaskNotificationEvent(.delete(id: task.id))) } } } diff --git a/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift b/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift index 0d02c69f9d..7c2f1309a9 100644 --- a/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift +++ b/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift @@ -6,29 +6,31 @@ // import XCTest +import CodeEditCore +import Factory @testable import CodeEdit final class TaskNotificationHandlerTests: XCTestCase { var taskNotificationHandler: TaskNotificationHandler! + var eventBus: EventBus! override func setUp() { super.setUp() + eventBus = Container.shared.eventBus() taskNotificationHandler = TaskNotificationHandler() } override func tearDown() { taskNotificationHandler = nil + eventBus = nil super.tearDown() } func testCreateTask() { let uuid = UUID().uuidString - let userInfo: [String: Any] = [ - "id": uuid, - "action": "create", - "title": "Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: userInfo) + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: uuid, title: "Task Title")) + )) let testExpectation = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { @@ -39,19 +41,12 @@ final class TaskNotificationHandlerTests: XCTestCase { } func testCreateTaskWithPriority() { - let task1: [String: Any] = [ - "id": UUID().uuidString, - "action": "create", - "title": "Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: task1) - - let task2: [String: Any] = [ - "id": UUID().uuidString, - "action": "createWithPriority", - "title": "Priority Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: task2) + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: UUID().uuidString, title: "Task Title")) + )) + eventBus.publish(TaskNotificationEvent( + .createWithPriority(TaskNotificationModel(id: UUID().uuidString, title: "Priority Task Title")) + )) let testExpectation = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { @@ -63,19 +58,12 @@ final class TaskNotificationHandlerTests: XCTestCase { func testUpdateTask() { let uuid = UUID().uuidString - let taskInfo: [String: Any] = [ - "id": uuid, - "action": "create", - "title": "Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: taskInfo) - - let taskUpdateInfo: [String: Any] = [ - "id": uuid, - "action": "update", - "title": "Updated Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: taskUpdateInfo) + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: uuid, title: "Task Title")) + )) + eventBus.publish(TaskNotificationEvent( + .update(id: uuid, title: "Updated Task Title") + )) let testExpectation = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -87,17 +75,10 @@ final class TaskNotificationHandlerTests: XCTestCase { func testDeleteTask() { let uuid = UUID().uuidString - let createUserInfo: [String: Any] = [ - "id": uuid, - "action": "create", - "title": "Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: createUserInfo) - let deleteUserInfo: [String: Any] = [ - "id": uuid, - "action": "delete" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: deleteUserInfo) + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: uuid, title: "Task Title")) + )) + eventBus.publish(TaskNotificationEvent(.delete(id: uuid))) let testExpectation = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -109,18 +90,10 @@ final class TaskNotificationHandlerTests: XCTestCase { func testDeleteTaskWithDelay() { let uuid = UUID().uuidString - let createUserInfo: [String: Any] = [ - "id": uuid, - "action": "create", - "title": "Task Title" - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: createUserInfo) - let deleteUserInfo: [String: Any] = [ - "id": uuid, - "action": "deleteWithDelay", - "delay": 0.2 - ] - NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: deleteUserInfo) + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: uuid, title: "Task Title")) + )) + eventBus.publish(TaskNotificationEvent(.deleteWithDelay(id: uuid, delay: 0.2))) let testExpectation = XCTestExpectation() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -132,4 +105,19 @@ final class TaskNotificationHandlerTests: XCTestCase { } wait(for: [testExpectation], timeout: 1) } + + func testEventForOtherWorkspaceIsIgnored() { + let uuid = UUID().uuidString + eventBus.publish(TaskNotificationEvent( + .create(TaskNotificationModel(id: uuid, title: "Task Title")), + workspace: URL(fileURLWithPath: "/some/other/workspace") + )) + + let testExpectation = XCTestExpectation() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + XCTAssertTrue(self.taskNotificationHandler.notifications.isEmpty) + testExpectation.fulfill() + } + wait(for: [testExpectation], timeout: 1) + } } diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index dd49f6ae08..6cb021d330 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -12,7 +12,6 @@ import XCTest final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_body_length private var directory: URL! private var files: [CEWorkspaceFile] = [] - private var mockWorkspace: Workspace! private var searchState: SearchState! private var folder1File: CEWorkspaceFile? @@ -34,9 +33,6 @@ final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_bod try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - mockWorkspace = try Workspace(for: directory, withContentsOf: directory, ofType: "") - searchState = mockWorkspace.searchState - // Add a few files let folder1 = directory.appending(path: "Folder 2") folder1File = CEWorkspaceFile(url: folder1) @@ -64,7 +60,8 @@ final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_bod files[1].parent = folder1File files[2].parent = folder2File - mockWorkspace.searchState?.addProjectToIndex() + // SearchState indexes the workspace as part of its initializer. + searchState = SearchState(workspaceURL: directory) // NOTE: This is a temporary solution. In the future, a file watcher should track file updates // and trigger an index update. diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index b878bacd90..43a1517b5a 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -11,7 +11,6 @@ import XCTest final class FindTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] - private var mockWorkspace: Workspace! private var searchState: SearchState! // MARK: - Setup @@ -30,9 +29,6 @@ final class FindTests: XCTestCase { try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - mockWorkspace = try await Workspace(for: directory, withContentsOf: directory, ofType: "") - searchState = await mockWorkspace.searchState - // Add a few files let folder1 = directory.appending(path: "Folder 2") let folder2 = directory.appending(path: "Longer Folder With Some 💯 Special Chars ⁉️") @@ -60,7 +56,8 @@ final class FindTests: XCTestCase { files[1].parent = parent1 files[2].parent = parent2 - await mockWorkspace.searchState?.addProjectToIndex() + // SearchState indexes the workspace as part of its initializer. + searchState = SearchState(workspaceURL: directory) // The following code also tests whether the workspace is indexed correctly // Wait until the index is up to date and flushed diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index 570af4b60a..d002f627a3 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -11,7 +11,6 @@ import XCTest final class WorkspaceIndexTests: XCTestCase { private var directory: URL! private var files: [CEWorkspaceFile] = [] - private var mockWorkspace: Workspace! private var searchState: SearchState! private var folder1File: CEWorkspaceFile? @@ -33,9 +32,6 @@ final class WorkspaceIndexTests: XCTestCase { try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - mockWorkspace = try await Workspace(for: directory, withContentsOf: directory, ofType: "") - searchState = await mockWorkspace.searchState - // Add a few files let folder1 = directory.appending(path: "Folder 2") folder1File = CEWorkspaceFile(url: folder1) @@ -63,7 +59,8 @@ final class WorkspaceIndexTests: XCTestCase { files[1].parent = folder1File files[2].parent = folder2File - await mockWorkspace.searchState?.addProjectToIndex() + // SearchState indexes the workspace as part of its initializer. + searchState = SearchState(workspaceURL: directory) // The following code also tests whether the workspace is indexed correctly // Wait until the index is up to date and flushed diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 532e103cad..2538f90e92 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -10,6 +10,7 @@ import CodeEditTextView import CodeEditSourceEditor import LanguageClient import LanguageServerProtocol +import Factory @testable import CodeEdit @@ -78,9 +79,16 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { return (connection: bufferingConnection, server: server) } + @MainActor func makeTestWorkspace() throws -> (Workspace, CEWorkspaceFileManager) { - let workspace = Workspace() - try workspace.read(from: tempTestDir, ofType: "") + let windowManager = Container.shared.workspaceWindowManager() + try windowManager.openWorkspace(at: tempTestDir) + guard let workspace = windowManager.openWorkspaces.first(where: { + $0.fileURL?.standardizedFileURL.path() == tempTestDir.standardizedFileURL.path() + }) else { + XCTFail("Workspace was not registered with the window manager") + fatalError("Workspace was not registered with the window manager") // never runs + } guard let fileManager = workspace.workspaceFileManager else { XCTFail("No File Manager") fatalError("No File Manager") // never runs @@ -146,12 +154,11 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { let (connection, server) = try await makeTestServer() // This service should receive the didOpen/didClose notifications - let lspService = ServiceContainer.resolve(.singleton, LSPService.self) - await MainActor.run { lspService?.languageClients[.init(.swift, tempTestDir.path() + "/")] = server } + let lspService = Container.shared.lspService() + lspService.languageClients[.init(.swift, tempTestDir.path() + "/")] = server - // Set up workspace - let (workspace, fileManager) = try makeTestWorkspace() - WorkspaceWindowManager.shared.addDocument(workspace) + // Set up workspace. Registers it with the workspace window manager. + let (_, fileManager) = try makeTestWorkspace() // Add a CEWorkspaceFile _ = try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") @@ -167,7 +174,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { ofType: "public.swift-source" ) file.fileDocument = codeFile - WorkspaceWindowManager.shared.addDocument(codeFile) + NSDocumentController.shared.addDocument(codeFile) await waitForClientState( ( diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index fcbc4df851..6b22843144 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import CodeEditCore @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/SourceControl/GitClientTests.swift b/CodeEditTests/Features/SourceControl/GitClientTests.swift index 5f288102e6..dc43ffb072 100644 --- a/CodeEditTests/Features/SourceControl/GitClientTests.swift +++ b/CodeEditTests/Features/SourceControl/GitClientTests.swift @@ -15,7 +15,7 @@ struct GitClientTests { try withTempDir { dirURL in // swiftlint:disable:next line_length let string = "1 .M N... 100644 100644 100644 eaef31cfa2a22418c00d7477da0b7151d122681e eaef31cfa2a22418c00d7477da0b7151d122681e CodeEdit/Features/SourceControl/Client/GitClient+Status.swift\01 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 e0f5ce250b32cf6610a284b7a33ac114079f5159 CodeEditTests/Features/SourceControl/GitClientTests.swift\0" - let client = GitClient(directoryURL: dirURL, shellClient: .live()) + let client = GitClient(directoryURL: dirURL, shellClient: ShellClient()) let status = try client.parseStatusString(string) #expect(status.changedFiles.count == 2) diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift index ca85f0f946..d9f327474b 100644 --- a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a notifications or tasks, that are displayed in the activity viewer -public struct TaskNotificationModel: Equatable { +public struct TaskNotificationModel: Equatable, Sendable { public var id: String public var title: String public var message: String? diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift new file mode 100644 index 0000000000..0333f1dc0e --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift @@ -0,0 +1,44 @@ +// +// TaskNotificationEvent.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 02.07.26. +// + +import Foundation + +/// Describes a mutation of the task-notification list shown in the activity viewer. +/// +/// Published by any feature reporting long-running work (tasks, indexing, +/// package installs); consumed by `TaskNotificationHandler`. +public struct TaskNotificationEvent: Event { + public enum Action: Sendable { + /// Appends a new notification to the end of the list. + case create(TaskNotificationModel) + /// Inserts a new notification at the front of the list so it shows + /// immediately in the activity viewer. Reserve for important notifications. + case createWithPriority(TaskNotificationModel) + /// Updates an existing notification by id. Only non-`nil` fields are applied. + case update( + id: String, + title: String? = nil, + message: String? = nil, + percentage: Double? = nil, + isLoading: Bool? = nil + ) + /// Removes the notification with the given id. + case delete(id: String) + /// Removes the notification with the given id after `delay` seconds. + case deleteWithDelay(id: String, delay: TimeInterval) + } + + public let action: Action + + /// Restricts delivery to the workspace at this URL; `nil` reaches all workspaces. + public let workspaceURL: URL? + + public init(_ action: Action, workspace: URL? = nil) { + self.action = action + self.workspaceURL = workspace + } +} From cc5742df45dd0ca440877c68dec04b12e82f8c1e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 20:22:15 +0200 Subject: [PATCH 020/335] Refactor: Remove dead EditorManager dependency from SourceControlManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SourceControlManager stored an EditorManager reference that was never read — the behavioral coupling was already eliminated during the earlier SourceControlManager decomposition. Removing the stored property and init parameter severs the last direct SourceControl → Editor dependency, so no replacement event or navigation interface is needed. --- .../Views/ChangedFile/GitChangedFileLabel.swift | 4 ++-- CodeEdit/Features/SourceControl/SourceControlManager.swift | 3 --- CodeEdit/Features/Workspace/WorkspaceFactory.swift | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index 42e944251f..d638aca601 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -40,7 +40,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: nil )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init(), shellClient: Container.shared.shellClient())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient())) .environmentObject(Workspace()) GitChangedFileLabel(file: GitChangedFile( @@ -49,7 +49,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: "app2.jsx" )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), editorManager: .init(), shellClient: Container.shared.shellClient())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient())) .environmentObject(Workspace()) }.padding() } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index 4b59a25b08..cde4062c54 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -26,7 +26,6 @@ final class SourceControlManager: ObservableObject { /// The base URL of the workspace let workspaceURL: URL - let editorManager: EditorManager weak var fileManager: CEWorkspaceFileManager? // MARK: - Git State @@ -130,11 +129,9 @@ final class SourceControlManager: ObservableObject { init( workspaceURL: URL, - editorManager: EditorManager, shellClient: ShellClientProtocol ) { self.workspaceURL = workspaceURL - self.editorManager = editorManager gitClient = GitClient(directoryURL: workspaceURL, shellClient: shellClient) } } diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 5cef65cf68..1fefd22ef5 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -45,7 +45,6 @@ enum WorkspaceFactory { let shellClient = Container.shared.shellClient() let sourceControlManager = SourceControlManager( workspaceURL: url, - editorManager: editorManager, shellClient: shellClient ) From cbef09ae1b5685b2001b131d8ba530f71f3dbd5d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 21:43:42 +0200 Subject: [PATCH 021/335] Refactor: Migrate in-app notification events to typed EventBus Replaces the stringly-typed NewNotificationAdded/NotificationDismissed NotificationCenter events with CENotificationEvent (.added/.dismissed) in CodeEditCore. The event carries only the notification id: CENotification is UI-facing (SwiftUI types and an action closure) and cannot be Sendable, so subscribers resolve the model via NotificationManager. Selector-based observers in NotificationPanelViewModel are replaced with a single Combine subscription. Adds NotificationPanelViewModelTests covering both actions. --- .../Notifications/NotificationManager.swift | 15 ++-- ...nPanelViewModel+NotificationHandling.swift | 23 +++--- .../NotificationPanelViewModel.swift | 37 ++++----- .../NotificationPanelViewModelTests.swift | 75 +++++++++++++++++++ .../Events/CENotificationEvent.swift | 30 ++++++++ 5 files changed, 141 insertions(+), 39 deletions(-) create mode 100644 CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift diff --git a/CodeEdit/Features/Notifications/NotificationManager.swift b/CodeEdit/Features/Notifications/NotificationManager.swift index a2cfd1d0d4..4eb8dc4b21 100644 --- a/CodeEdit/Features/Notifications/NotificationManager.swift +++ b/CodeEdit/Features/Notifications/NotificationManager.swift @@ -8,6 +8,8 @@ import SwiftUI import Combine import UserNotifications +import Factory +import CodeEditCore /// Manages the application's notification system, handling both in-app notifications and system notifications. /// This class is responsible for: @@ -19,6 +21,9 @@ final class NotificationManager: NSObject, NotificationManaging { /// Collection of all notifications, both read and unread @Published private(set) var notifications: [CENotification] = [] + @LazyInjected(\.eventBus) + private var eventBus + private var isAppActive: Bool = true /// Number of unread notifications @@ -128,10 +133,7 @@ final class NotificationManager: NSObject, NotificationManaging { // Remove system notification if it exists removeSystemNotification(notification) - NotificationCenter.default.post( - name: .init("NotificationDismissed"), - object: notification - ) + eventBus.publish(CENotificationEvent(.dismissed(id: notification.id))) } /// Marks a notification as read @@ -180,10 +182,7 @@ final class NotificationManager: NSObject, NotificationManaging { self?.notifications.append(notification) // Always notify workspaces of new notification - NotificationCenter.default.post( - name: .init("NewNotificationAdded"), - object: notification - ) + self?.eventBus.publish(CENotificationEvent(.added(id: notification.id))) // Additionally show system notification when app is in background if self?.isAppActive != true { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift index b8582a9ad9..0c88ac6f74 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import CodeEditCore /// Notification insertion, dismissal, and event handling. extension NotificationPanelViewModel { @@ -87,18 +88,22 @@ extension NotificationPanelViewModel { } } - @objc - func handleNewNotificationAdded(_ notification: Notification) { - guard let ceNotification = notification.object as? CENotification else { return } - handleNewNotification(ceNotification) + /// Routes notification list mutations published by `NotificationManager` on the EventBus. + func handle(_ event: CENotificationEvent) { + switch event.action { + case .added(let id): + guard let notification = notificationManager.notifications.first(where: { $0.id == id }) else { + return + } + handleNewNotification(notification) + case .dismissed(let id): + handleNotificationRemoved(id: id) + } } - @objc - func handleNotificationRemoved(_ notification: Notification) { - guard let ceNotification = notification.object as? CENotification else { return } - + private func handleNotificationRemoved(id: UUID) { let operation: () -> Void = { - self.activeNotifications.removeAll(where: { $0.id == ceNotification.id }) + self.activeNotifications.removeAll(where: { $0.id == id }) // If this was the last notification and they were manually shown, hide the panel if self.activeNotifications.isEmpty && self.isPresented { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift index 4d55f8713f..07a72ea687 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -6,7 +6,9 @@ // import SwiftUI +import Combine import Factory +import CodeEditCore /// Coordinates notification display, auto-hide timers, panel visibility, and toolbar integration. /// @@ -17,13 +19,13 @@ import Factory /// - `+Toolbar`: dynamic toolbar item management final class NotificationPanelViewModel: ObservableObject { /// Currently displayed notifications in the panel - @Published internal(set) var activeNotifications: [CENotification] = [] + @Published var activeNotifications: [CENotification] = [] /// Whether notifications panel was manually shown via toolbar - @Published internal(set) var isPresented: Bool = false + @Published var isPresented: Bool = false /// Set of hidden notification IDs - @Published internal(set) var hiddenNotificationIds: Set = [] + @Published var hiddenNotificationIds: Set = [] @Published var scrolledToTop: Bool = true @@ -38,6 +40,11 @@ final class NotificationPanelViewModel: ObservableObject { var notificationManager = Container.shared.notificationManager() + @LazyInjected(\.eventBus) + var eventBus + + private var cancellables = Set() + /// A filtered list of active notifications. var visibleNotifications: [CENotification] { activeNotifications.filter { !hiddenNotificationIds.contains($0.id) } @@ -46,29 +53,15 @@ final class NotificationPanelViewModel: ObservableObject { weak var windowController: NSWindowController? init() { - // Observe new notifications - NotificationCenter.default.addObserver( - self, - selector: #selector(handleNewNotificationAdded(_:)), - name: .init("NewNotificationAdded"), - object: nil - ) - - // Observe notification dismissals - NotificationCenter.default.addObserver( - self, - selector: #selector(handleNotificationRemoved(_:)), - name: .init("NotificationDismissed"), - object: nil - ) + // Observe notification additions and dismissals + eventBus.subscribe(CENotificationEvent.self) + .receive(on: RunLoop.main) + .sink { [weak self] event in self?.handle(event) } + .store(in: &cancellables) // Load initial notifications from NotificationManager notificationManager.notifications.forEach { notification in handleNewNotification(notification) } } - - deinit { - NotificationCenter.default.removeObserver(self) - } } diff --git a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift new file mode 100644 index 0000000000..9cb92c5b4c --- /dev/null +++ b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift @@ -0,0 +1,75 @@ +// +// NotificationPanelViewModelTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 02/07/2026. +// + +import XCTest +import CodeEditCore +import Factory +@testable import CodeEdit + +final class NotificationPanelViewModelTests: XCTestCase { + var notificationManager: NotificationManager! + var viewModel: NotificationPanelViewModel! + + override func setUp() { + super.setUp() + // Fresh manager so the view model doesn't preload notifications from earlier tests. + Container.shared.notificationManager.reset() + notificationManager = Container.shared.notificationManager() + viewModel = NotificationPanelViewModel() + } + + override func tearDown() { + viewModel = nil + notificationManager = nil + Container.shared.notificationManager.reset() + super.tearDown() + } + + func testNotificationAddedAppearsInPanel() { + notificationManager.post( + iconSymbol: "bell", + title: "Test Notification", + description: "A notification for testing", + actionButtonTitle: "OK", + action: {} + ) + + let testExpectation = XCTestExpectation() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertEqual(self.viewModel.activeNotifications.first?.title, "Test Notification") + testExpectation.fulfill() + } + wait(for: [testExpectation], timeout: 1) + } + + func testNotificationDismissedRemovedFromPanel() { + notificationManager.post( + iconSymbol: "bell", + title: "Test Notification", + description: "A notification for testing", + actionButtonTitle: "OK", + action: {} + ) + + let testExpectation = XCTestExpectation() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + guard let notification = self.notificationManager.notifications.first else { + XCTFail("Notification was never added to the manager") + testExpectation.fulfill() + return + } + self.notificationManager.dismissNotification(notification) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertTrue(self.viewModel.activeNotifications.isEmpty) + XCTAssertTrue(self.notificationManager.notifications.isEmpty) + testExpectation.fulfill() + } + } + wait(for: [testExpectation], timeout: 2) + } +} diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift new file mode 100644 index 0000000000..25e149a736 --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift @@ -0,0 +1,30 @@ +// +// CENotificationEvent.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 02/07/2026. +// + +import Foundation + +/// Describes a mutation of the in-app notification list. +/// +/// Published by `NotificationManager`; consumed by `NotificationPanelViewModel`. +/// +/// Carries only the notification id: the full `CENotification` model is UI-facing +/// (SwiftUI types and an action closure) and stays in the app target. Subscribers +/// resolve the model via `NotificationManager` when they need more than the id. +public struct CENotificationEvent: Event { + public enum Action: Sendable { + /// A notification was added to the list. + case added(id: UUID) + /// A notification was dismissed and removed from the list. + case dismissed(id: UUID) + } + + public let action: Action + + public init(_ action: Action) { + self.action = action + } +} From 68dd36aa7f50a853d9ffff294a1ab8f7e3cf4f0e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 21:44:05 +0200 Subject: [PATCH 022/335] Refactor: Replace CodeFileDocument lifecycle notifications with LSP protocol command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the app's last two custom Notification.Names (CodeFileDocument.didOpen/didClose). Document open/close is a request with exactly one rightful handler, not a broadcast fact, so CodeFileDocument now calls openDocument/closeDocument on the injected LSP service directly; both methods are added to LSPServiceProtocol. This completes the NotificationCenter migration: cross-feature signaling is now fully typed — EventBus for facts, protocol commands for single-handler requests. --- .../CodeFileDocument/CodeFileDocument.swift | 19 +++++++++++----- .../Features/LSP/Service/LSPService.swift | 22 ------------------- .../LSP/Service/LSPServiceProtocol.swift | 2 ++ 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index e799e3c571..aa154edbcb 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -15,6 +15,7 @@ import CodeEditLanguages import Combine import OSLog import TextStory +import Factory enum CodeFileError: Error { case failedToDecode @@ -30,10 +31,10 @@ final class CodeFileDocument: NSDocument, ObservableObject { static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") - /// Sent when the document is opened. The document will be sent in the notification's object. - static let didOpenNotification = Notification.Name(rawValue: "CodeFileDocument.didOpen") - /// Sent when the document is closed. The document's `fileURL` will be sent in the notification's object. - static let didCloseNotification = Notification.Name(rawValue: "CodeFileDocument.didClose") + /// Notified when this document is opened (contents available) or closed, + /// so language servers can track the document's lifecycle. + @LazyInjected(\.lspService) + private var lspService /// The text content of the document, stored as a text storage /// @@ -173,7 +174,9 @@ final class CodeFileDocument: NSDocument, ObservableObject { } else { self.content = NSTextStorage(string: nsString as String) } - NotificationCenter.default.post(name: Self.didOpenNotification, object: self) + MainActor.assumeIsolated { + lspService.openDocument(self) + } } /// If this file is already open and being tracked by an undo manager, we register an undo mutation @@ -287,7 +290,11 @@ final class CodeFileDocument: NSDocument, ObservableObject { override func close() { super.close() - NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL) + if let fileURL { + MainActor.assumeIsolated { + lspService.closeDocument(fileURL) + } + } } override func save(_ sender: Any?) { diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index c15720e61f..7c73020ee6 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -139,28 +139,6 @@ final class LSPService: ObservableObject, LSPServiceProtocol { ) } } - - NotificationCenter.default.addObserver( - forName: CodeFileDocument.didOpenNotification, - object: nil, - queue: .main - ) { notification in - MainActor.assumeIsolated { - guard let document = notification.object as? CodeFileDocument else { return } - self.openDocument(document) - } - } - - NotificationCenter.default.addObserver( - forName: CodeFileDocument.didCloseNotification, - object: nil, - queue: .main - ) { notification in - MainActor.assumeIsolated { - guard let url = notification.object as? URL else { return } - self.closeDocument(url) - } - } } /// Gets the language server for the specified language and workspace. diff --git a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift b/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift index 3b718a47e6..8f1297a0af 100644 --- a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift +++ b/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift @@ -14,6 +14,8 @@ import Foundation /// Use `LSPService` directly in those cases. @MainActor protocol LSPServiceProtocol: AnyObject { + func openDocument(_ document: CodeFileDocument) + func closeDocument(_ url: URL) func closeWorkspace(_ workspacePath: String) func stopAllServers() async func killAllServers() From 1985839fb4a2490ab8f4ab1f54ac65551df5b580 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 22:43:35 +0200 Subject: [PATCH 023/335] Fix: Hop to main thread for LSP document lifecycle notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainActor.assumeIsolated traps when NSDocument reads or closes happen off the main thread (AppKit concurrent reads, Swift Testing) — this crashed the test host and took 20 unit tests down with it. Restore the delivery semantics of the NotificationCenter observer this replaced (queue: .main): synchronous when already on main, async hop otherwise. --- .../CodeFileDocument/CodeFileDocument.swift | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index aa154edbcb..f37f168717 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -174,8 +174,25 @@ final class CodeFileDocument: NSDocument, ObservableObject { } else { self.content = NSTextStorage(string: nsString as String) } - MainActor.assumeIsolated { - lspService.openDocument(self) + notifyLSPDidOpen() + } + + /// `LSPService` is main-actor isolated, but document reads and closes can happen off the main + /// thread (AppKit concurrent reads, Swift Testing). Mirrors the NotificationCenter `queue: .main` + /// delivery this replaced: synchronous on main, async hop otherwise. + private func notifyLSPDidOpen() { + if Thread.isMainThread { + MainActor.assumeIsolated { lspService.openDocument(self) } + } else { + DispatchQueue.main.async { self.lspService.openDocument(self) } + } + } + + private func notifyLSPDidClose(_ url: URL) { + if Thread.isMainThread { + MainActor.assumeIsolated { lspService.closeDocument(url) } + } else { + DispatchQueue.main.async { self.lspService.closeDocument(url) } } } @@ -291,9 +308,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { override func close() { super.close() if let fileURL { - MainActor.assumeIsolated { - lspService.closeDocument(fileURL) - } + notifyLSPDidClose(fileURL) } } From 8f25a9b81ef560b4c12d770bb616812bde76e876 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 22:44:08 +0200 Subject: [PATCH 024/335] Refactor: Register CommandManager and KeybindingManager behind protocols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches both Factory registrations to their existing protocols (CommandManaging, KeybindingManaging), following the shellClient pattern. CommandManaging drops its ObservableObject requirement — the only @ObservedObject consumer (QuickActionsView) never read the manager in its body, so nothing observes it. QuickActionsViewModel now injects the manager via @LazyInjected instead of three Container.shared lookups, and the view seeds its state through the view model's reset(). Adds mock-based QuickActionsViewModelTests to prove the new seam. --- CodeEdit/CodeEditContainer.swift | 8 +- .../ViewModels/QuickActionsViewModel.swift | 9 ++- .../Commands/Views/QuickActionsView.swift | 5 +- .../Features/Keybindings/CommandManager.swift | 20 +---- .../Protocols/CommandManaging.swift | 2 +- .../Commands/QuickActionsViewModelTests.swift | 77 +++++++++++++++++++ 6 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index e915a1fbeb..a79cfe1966 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -21,12 +21,12 @@ extension Container { self { ShellClient() as ShellClientProtocol }.singleton } - var commandManager: Factory { - self { CommandManager() }.singleton + var commandManager: Factory { + self { CommandManager() as CommandManaging }.singleton } - var keybindingManager: Factory { - self { KeybindingManager() }.singleton + var keybindingManager: Factory { + self { KeybindingManager() as KeybindingManaging }.singleton } var notificationManager: Factory { diff --git a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift index 4a90d888c5..f2e12bdf88 100644 --- a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift +++ b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift @@ -13,6 +13,9 @@ import CodeEditCore /// query text and list of filtered commands final class QuickActionsViewModel: ObservableObject { + @LazyInjected(\.commandManager) + private var commandManager + @Published var commandQuery: String = "" @Published var selected: Command? @@ -26,15 +29,15 @@ final class QuickActionsViewModel: ObservableObject { func reset() { commandQuery = "" selected = nil - filteredCommands = Container.shared.commandManager().commands + filteredCommands = commandManager.commands } func fetchMatchingCommands(val: String) { if val == "" { - self.filteredCommands = Container.shared.commandManager().commands + self.filteredCommands = commandManager.commands return } - self.filteredCommands = Container.shared.commandManager().commands.filter { $0.title.localizedCaseInsensitiveContains(val) } + self.filteredCommands = commandManager.commands.filter { $0.title.localizedCaseInsensitiveContains(val) } self.selected = self.filteredCommands.first } diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/Views/QuickActionsView.swift index 9445b7427e..b5c3ea967e 100644 --- a/CodeEdit/Features/Commands/Views/QuickActionsView.swift +++ b/CodeEdit/Features/Commands/Views/QuickActionsView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Factory import CodeEditCore /// Quick actions view @@ -17,8 +16,6 @@ struct QuickActionsView: View { @ObservedObject private var state: QuickActionsViewModel - @ObservedObject private var commandManager: CommandManager = Container.shared.commandManager() - @State private var monitor: Any? @State private var selectedItem: Command? @@ -28,7 +25,7 @@ struct QuickActionsView: View { init(state: QuickActionsViewModel, closePalette: @escaping () -> Void) { self.state = state self.closePalette = closePalette - state.filteredCommands = commandManager.commands + state.reset() } func callHandler(command: Command) { diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/Features/Keybindings/CommandManager.swift index 84d081ece8..2af2762600 100644 --- a/CodeEdit/Features/Keybindings/CommandManager.swift +++ b/CodeEdit/Features/Keybindings/CommandManager.swift @@ -7,24 +7,10 @@ import Foundation import CodeEditCore -/** -The object of this class intended to be a hearth of command palette. This object only exists as singleton. - In Order to access its instance use `CommandManager.shared` - -``` - /* To add or execute command see snipper below */ -let mgr = CommandManager.shared -let wrap = CommandClosureWrapper.init(closure: { - print("testing closure") -}) - -mgr.addCommand(name: "test", command: wrap) -mgr.executeCommand("test") - ``` - */ - +/// Registry backing the command palette. Registered as a singleton in the Factory container +/// (`Container.shared.commandManager`); inject via `@LazyInjected(\.commandManager)`. final class CommandManager: CommandManaging { - @Published private var commandsList: [String: Command] + private var commandsList: [String: Command] init() { commandsList = [:] diff --git a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift index 70ace72b8d..e8539905fc 100644 --- a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift +++ b/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift @@ -9,7 +9,7 @@ import Foundation import CodeEditCore /// Protocol for managing application commands (command palette). -protocol CommandManaging: AnyObject, ObservableObject { +protocol CommandManaging: AnyObject { var commands: [Command] { get } func addCommand(name: String, title: String, id: String, command: @escaping () -> Void) func executeCommand(_ id: String) diff --git a/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift b/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift new file mode 100644 index 0000000000..3b23dfdd8f --- /dev/null +++ b/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift @@ -0,0 +1,77 @@ +// +// QuickActionsViewModelTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 02/07/2026. +// + +import XCTest +import CodeEditCore +import Factory +@testable import CodeEdit + +private final class MockCommandManager: CommandManaging { + var commands: [Command] = [ + Command(id: "open.drawer", title: "Toggle Utility Area", closureWrapper: {}), + Command(id: "quick.open", title: "Quick Open", closureWrapper: {}), + Command(id: "toggle.navigator", title: "Toggle Navigator", closureWrapper: {}) + ] + + func addCommand(name: String, title: String, id: String, command: @escaping () -> Void) { + commands.append(Command(id: id, title: title, closureWrapper: command)) + } + + func executeCommand(_ id: String) { + commands.first { $0.id == id }?.closureWrapper() + } +} + +final class QuickActionsViewModelTests: XCTestCase { + private var viewModel: QuickActionsViewModel! + + override func setUp() { + super.setUp() + Container.shared.commandManager.register { MockCommandManager() } + viewModel = QuickActionsViewModel() + } + + override func tearDown() { + viewModel = nil + Container.shared.commandManager.reset() + super.tearDown() + } + + func testFetchMatchingCommandsFiltersCaseInsensitively() { + viewModel.fetchMatchingCommands(val: "toggle") + + XCTAssertEqual( + viewModel.filteredCommands.map(\.title).sorted(), + ["Toggle Navigator", "Toggle Utility Area"] + ) + XCTAssertEqual(viewModel.selected?.id, viewModel.filteredCommands.first?.id) + } + + func testFetchMatchingCommandsWithEmptyQueryReturnsAllCommands() { + viewModel.fetchMatchingCommands(val: "") + + XCTAssertEqual(viewModel.filteredCommands.count, 3) + } + + func testFetchMatchingCommandsWithoutMatchReturnsNothing() { + viewModel.fetchMatchingCommands(val: "nonexistent") + + XCTAssertTrue(viewModel.filteredCommands.isEmpty) + XCTAssertNil(viewModel.selected) + } + + func testResetClearsQueryAndSelectionAndReseedsCommands() { + viewModel.fetchMatchingCommands(val: "quick") + viewModel.commandQuery = "quick" + + viewModel.reset() + + XCTAssertEqual(viewModel.commandQuery, "") + XCTAssertNil(viewModel.selected) + XCTAssertEqual(viewModel.filteredCommands.count, 3) + } +} From 899041783f2ad1ae7cf1ff7d83428aedddc92b9c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 22:44:29 +0200 Subject: [PATCH 025/335] Refactor: Register NotificationManager behind NotificationManaging protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesigns the protocol around a single core post(_ CENotification) requirement plus a notificationsPublisher for observation; the three convenience post builders move to a protocol extension with their default arguments intact (protocol requirements can't express defaults), keeping all call sites source-compatible. unreadCount becomes a protocol-extension computed property and the ObservableObject requirement is dropped. Only NotificationToolbarItem genuinely observed the manager — it now reads a republished unreadCount on NotificationPanelViewModel, which subscribes to notificationsPublisher. The @ObservedObject managers in NotificationBannerView and NotificationPanelView were dead observation and are removed. The view model injects the manager via @LazyInjected instead of scattered Container.shared lookups. --- CodeEdit/CodeEditContainer.swift | 4 +- .../Notifications/NotificationManager.swift | 106 +----------------- .../Protocols/NotificationManaging.swift | 100 ++++++++++++++--- ...nPanelViewModel+NotificationHandling.swift | 9 +- .../NotificationPanelViewModel+Toolbar.swift | 3 +- .../NotificationPanelViewModel.swift | 12 +- .../Views/NotificationBannerView.swift | 2 - .../Views/NotificationPanelView.swift | 2 - .../Views/NotificationToolbarItem.swift | 6 +- .../NotificationPanelViewModelTests.swift | 37 +++++- 10 files changed, 148 insertions(+), 133 deletions(-) diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index a79cfe1966..2a24e5c396 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -29,8 +29,8 @@ extension Container { self { KeybindingManager() as KeybindingManaging }.singleton } - var notificationManager: Factory { - self { NotificationManager() }.singleton + var notificationManager: Factory { + self { NotificationManager() as NotificationManaging }.singleton } var registryManager: Factory { diff --git a/CodeEdit/Features/Notifications/NotificationManager.swift b/CodeEdit/Features/Notifications/NotificationManager.swift index 4eb8dc4b21..566fe8a50a 100644 --- a/CodeEdit/Features/Notifications/NotificationManager.swift +++ b/CodeEdit/Features/Notifications/NotificationManager.swift @@ -21,110 +21,16 @@ final class NotificationManager: NSObject, NotificationManaging { /// Collection of all notifications, both read and unread @Published private(set) var notifications: [CENotification] = [] + /// Fires on any change to ``notifications``, including `isRead` mutations. + var notificationsPublisher: AnyPublisher<[CENotification], Never> { + $notifications.eraseToAnyPublisher() + } + @LazyInjected(\.eventBus) private var eventBus private var isAppActive: Bool = true - /// Number of unread notifications - var unreadCount: Int { - notifications.filter { !$0.isRead }.count - } - - /// Posts a new notification - /// - Parameters: - /// - iconSymbol: SF Symbol or CodeEditSymbol name for the notification icon - /// - iconColor: Color for the icon - /// - title: Main notification title - /// - description: Detailed notification message - /// - actionButtonTitle: Title for the action button - /// - action: Closure to execute when action button is clicked - /// - isSticky: Whether the notification should persist until manually dismissed - func post( - iconSymbol: String, - iconColor: Color? = Color(.systemBlue), - title: String, - description: String, - actionButtonTitle: String, - action: @escaping () -> Void, - isSticky: Bool = false - ) { - let notification = CENotification( - iconSymbol: iconSymbol, - iconColor: iconColor, - title: title, - description: description, - actionButtonTitle: actionButtonTitle, - action: action, - isSticky: isSticky, - isRead: false - ) - - postNotification(notification) - } - - /// Posts a new notification - /// - Parameters: - /// - iconImage: Image for the notification icon - /// - title: Main notification title - /// - description: Detailed notification message - /// - actionButtonTitle: Title for the action button - /// - action: Closure to execute when action button is clicked - /// - isSticky: Whether the notification should persist until manually dismissed - func post( - iconImage: Image, - title: String, - description: String, - actionButtonTitle: String, - action: @escaping () -> Void, - isSticky: Bool = false - ) { - let notification = CENotification( - iconImage: iconImage, - title: title, - description: description, - actionButtonTitle: actionButtonTitle, - action: action, - isSticky: isSticky - ) - - postNotification(notification) - } - - /// Posts a new notification - /// - Parameters: - /// - iconText: Text or emoji for the notification icon - /// - iconTextColor: Color of the text/emoji (defaults to primary label color) - /// - iconColor: Background color for the icon - /// - title: Main notification title - /// - description: Detailed notification message - /// - actionButtonTitle: Title for the action button - /// - action: Closure to execute when action button is clicked - /// - isSticky: Whether the notification should persist until manually dismissed - func post( - iconText: String, - iconTextColor: Color? = nil, - iconColor: Color? = Color(.systemBlue), - title: String, - description: String, - actionButtonTitle: String, - action: @escaping () -> Void, - isSticky: Bool = false - ) { - let notification = CENotification( - iconText: iconText, - iconTextColor: iconTextColor, - iconColor: iconColor, - title: title, - description: description, - actionButtonTitle: actionButtonTitle, - action: action, - isSticky: isSticky - ) - - postNotification(notification) - } - /// Dismisses a specific notification func dismissNotification(_ notification: CENotification) { notifications.removeAll(where: { $0.id == notification.id }) @@ -177,7 +83,7 @@ final class NotificationManager: NSObject, NotificationManaging { } /// Posts a notification to workspaces and system - private func postNotification(_ notification: CENotification) { + func post(_ notification: CENotification) { DispatchQueue.main.async { [weak self] in self?.notifications.append(notification) diff --git a/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift b/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift index 375c6f67ca..a45a60a740 100644 --- a/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift +++ b/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift @@ -6,42 +6,114 @@ // import SwiftUI +import Combine /// Protocol for managing application notifications. -protocol NotificationManaging: AnyObject, ObservableObject { +protocol NotificationManaging: AnyObject { + /// Collection of all notifications, both read and unread. var notifications: [CENotification] { get } - var unreadCount: Int { get } + /// Fires on any change to ``notifications``, including `isRead` mutations. Replays the current value. + var notificationsPublisher: AnyPublisher<[CENotification], Never> { get } + + /// Posts a new notification. + func post(_ notification: CENotification) + + func dismissNotification(_ notification: CENotification) + func markAsRead(_ notification: CENotification) +} + +extension NotificationManaging { + /// Number of unread notifications. + var unreadCount: Int { + notifications.filter { !$0.isRead }.count + } + + /// Posts a new notification + /// - Parameters: + /// - iconSymbol: SF Symbol or CodeEditSymbol name for the notification icon + /// - iconColor: Color for the icon + /// - title: Main notification title + /// - description: Detailed notification message + /// - actionButtonTitle: Title for the action button + /// - action: Closure to execute when action button is clicked + /// - isSticky: Whether the notification should persist until manually dismissed func post( iconSymbol: String, - iconColor: Color?, + iconColor: Color? = Color(.systemBlue), title: String, description: String, actionButtonTitle: String, action: @escaping () -> Void, - isSticky: Bool - ) + isSticky: Bool = false + ) { + post(CENotification( + iconSymbol: iconSymbol, + iconColor: iconColor, + title: title, + description: description, + actionButtonTitle: actionButtonTitle, + action: action, + isSticky: isSticky, + isRead: false + )) + } + /// Posts a new notification + /// - Parameters: + /// - iconImage: Image for the notification icon + /// - title: Main notification title + /// - description: Detailed notification message + /// - actionButtonTitle: Title for the action button + /// - action: Closure to execute when action button is clicked + /// - isSticky: Whether the notification should persist until manually dismissed func post( iconImage: Image, title: String, description: String, actionButtonTitle: String, action: @escaping () -> Void, - isSticky: Bool - ) + isSticky: Bool = false + ) { + post(CENotification( + iconImage: iconImage, + title: title, + description: description, + actionButtonTitle: actionButtonTitle, + action: action, + isSticky: isSticky + )) + } + /// Posts a new notification + /// - Parameters: + /// - iconText: Text or emoji for the notification icon + /// - iconTextColor: Color of the text/emoji (defaults to primary label color) + /// - iconColor: Background color for the icon + /// - title: Main notification title + /// - description: Detailed notification message + /// - actionButtonTitle: Title for the action button + /// - action: Closure to execute when action button is clicked + /// - isSticky: Whether the notification should persist until manually dismissed func post( iconText: String, - iconTextColor: Color?, - iconColor: Color?, + iconTextColor: Color? = nil, + iconColor: Color? = Color(.systemBlue), title: String, description: String, actionButtonTitle: String, action: @escaping () -> Void, - isSticky: Bool - ) - - func dismissNotification(_ notification: CENotification) - func markAsRead(_ notification: CENotification) + isSticky: Bool = false + ) { + post(CENotification( + iconText: iconText, + iconTextColor: iconTextColor, + iconColor: iconColor, + title: title, + description: description, + actionButtonTitle: actionButtonTitle, + action: action, + isSticky: isSticky + )) + } } diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift index 0c88ac6f74..01111b6b52 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Factory import CodeEditCore /// Notification insertion, dismissal, and event handling. @@ -64,8 +63,8 @@ extension NotificationPanelViewModel { if let index = activeNotifications.firstIndex(where: { $0.id == notification.id }) { if disableAnimation { self.activeNotifications.removeAll(where: { $0.id == notification.id }) - Container.shared.notificationManager().markAsRead(notification) - Container.shared.notificationManager().dismissNotification(notification) + notificationManager.markAsRead(notification) + notificationManager.dismissNotification(notification) return } @@ -82,8 +81,8 @@ extension NotificationPanelViewModel { } } - Container.shared.notificationManager().markAsRead(notification) - Container.shared.notificationManager().dismissNotification(notification) + self.notificationManager.markAsRead(notification) + self.notificationManager.dismissNotification(notification) } } } diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift index 51e1d3f002..aab02145aa 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift @@ -6,7 +6,6 @@ // import AppKit -import Factory /// Dynamic toolbar item management for the notification badge. extension NotificationPanelViewModel { @@ -16,7 +15,7 @@ extension NotificationPanelViewModel { return } - let shouldShow = !self.visibleNotifications.isEmpty || Container.shared.notificationManager().unreadCount > 0 + let shouldShow = !self.visibleNotifications.isEmpty || notificationManager.unreadCount > 0 if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { guard let activityItemIdx = toolbar.items .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift index 07a72ea687..c79cfb8c16 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -29,6 +29,9 @@ final class NotificationPanelViewModel: ObservableObject { @Published var scrolledToTop: Bool = true + /// Number of unread notifications, republished from the notification manager for view observation. + @Published private(set) var unreadCount: Int = 0 + /// Timers for notifications var timers: [UUID: Timer] = [:] @@ -38,7 +41,8 @@ final class NotificationPanelViewModel: ObservableObject { /// Whether notifications are paused var isPaused: Bool = false - var notificationManager = Container.shared.notificationManager() + @LazyInjected(\.notificationManager) + var notificationManager @LazyInjected(\.eventBus) var eventBus @@ -59,6 +63,12 @@ final class NotificationPanelViewModel: ObservableObject { .sink { [weak self] event in self?.handle(event) } .store(in: &cancellables) + // Republish the unread count for views (the manager is behind a protocol and not observable). + notificationManager.notificationsPublisher + .map { notifications in notifications.filter { !$0.isRead }.count } + .receive(on: RunLoop.main) + .assign(to: &$unreadCount) + // Load initial notifications from NotificationManager notificationManager.notifications.forEach { notification in handleNewNotification(notification) diff --git a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift index 5972e6fb98..cdaef53298 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift @@ -6,14 +6,12 @@ // import SwiftUI -import Factory struct NotificationBannerView: View { @Environment(\.colorScheme) private var colorScheme @EnvironmentObject private var notificationPanel: NotificationPanelViewModel - @ObservedObject private var notificationManager = Container.shared.notificationManager() let notification: CENotification let onDismiss: () -> Void diff --git a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift b/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift index 7ea18e6ea4..7047780a24 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift @@ -6,14 +6,12 @@ // import SwiftUI -import Factory struct NotificationPanelView: View { @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @Environment(\.controlActiveState) private var controlActiveState - @ObservedObject private var notificationManager = Container.shared.notificationManager() @FocusState private var isFocused: Bool // ID for the top anchor diff --git a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift b/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift index 24d9b5506d..33e218a3b2 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift @@ -6,18 +6,16 @@ // import SwiftUI -import Factory struct NotificationToolbarItem: View { @EnvironmentObject private var notificationPanel: NotificationPanelViewModel - @ObservedObject private var notificationManager = Container.shared.notificationManager() @Environment(\.controlActiveState) private var controlActiveState var body: some View { let visibleNotifications = notificationPanel.visibleNotifications - if notificationManager.unreadCount > 0 || !visibleNotifications.isEmpty { + if notificationPanel.unreadCount > 0 || !visibleNotifications.isEmpty { Button { notificationPanel.toggleNotificationsVisibility() } label: { @@ -25,7 +23,7 @@ struct NotificationToolbarItem: View { Image(systemName: "bell.badge.fill") .symbolRenderingMode(.palette) .foregroundStyle(controlActiveState == .inactive ? .secondary : Color.accentColor, .primary) - Text("\(notificationManager.unreadCount)") + Text("\(notificationPanel.unreadCount)") .monospacedDigit() } } diff --git a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift index 9cb92c5b4c..4396207878 100644 --- a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift +++ b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift @@ -11,7 +11,7 @@ import Factory @testable import CodeEdit final class NotificationPanelViewModelTests: XCTestCase { - var notificationManager: NotificationManager! + var notificationManager: (any NotificationManaging)! var viewModel: NotificationPanelViewModel! override func setUp() { @@ -72,4 +72,39 @@ final class NotificationPanelViewModelTests: XCTestCase { } wait(for: [testExpectation], timeout: 2) } + + func testUnreadCountRepublishedToViewModel() { + notificationManager.post( + iconSymbol: "bell", + title: "First", + description: "A notification for testing", + actionButtonTitle: "OK", + action: {} + ) + notificationManager.post( + iconSymbol: "bell", + title: "Second", + description: "A notification for testing", + actionButtonTitle: "OK", + action: {} + ) + + let testExpectation = XCTestExpectation() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertEqual(self.viewModel.unreadCount, 2) + + guard let notification = self.notificationManager.notifications.first else { + XCTFail("Notifications were never added to the manager") + testExpectation.fulfill() + return + } + self.notificationManager.markAsRead(notification) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertEqual(self.viewModel.unreadCount, 1) + testExpectation.fulfill() + } + } + wait(for: [testExpectation], timeout: 2) + } } From 89221cf9239932cc6c74b2ead57409da61310f8a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 23:20:18 +0200 Subject: [PATCH 026/335] Refactor: Extract SourceControlViewModel from SourceControlManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI presentation state (sheet/alert booleans, operation fields, switchToBranch) moved to a new SourceControlViewModel — leaving SourceControlManager as a pure git domain-state container. Prerequisite for SourceControl feature modularization. --- .../SourceControlViewModel.swift | 86 +++++++++++++++++++ .../SourceControlViewModelTests.swift | 76 ++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 CodeEdit/Features/SourceControl/SourceControlViewModel.swift create mode 100644 CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift diff --git a/CodeEdit/Features/SourceControl/SourceControlViewModel.swift b/CodeEdit/Features/SourceControl/SourceControlViewModel.swift new file mode 100644 index 0000000000..3c37901ec6 --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlViewModel.swift @@ -0,0 +1,86 @@ +// +// SourceControlViewModel.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 02/07/2026. +// + +import Foundation +import CodeEditCore + +/// Manages UI presentation state for the SourceControl feature. +/// +/// Domain state (branches, changed files, remotes, etc.) lives in ``SourceControlManager``. +/// This view model owns only ephemeral UI state: which sheets are presented, +/// alert flags, and the shared operation fields used by the push and pull sheets. +@MainActor +final class SourceControlViewModel: ObservableObject { + + // MARK: - Sheet State + + /// Is the push sheet presented + @Published var pushSheetIsPresented: Bool = false { + didSet { resetOperationFields() } + } + + /// Is the pull sheet presented + @Published var pullSheetIsPresented: Bool = false { + didSet { resetOperationFields() } + } + + /// Is the fetch sheet presented + @Published var fetchSheetIsPresented: Bool = false + + /// Is the stash sheet presented + @Published var stashSheetIsPresented: Bool = false + + /// Is the remote sheet presented + @Published var addExistingRemoteSheetIsPresented: Bool = false + + /// Branch to switch to + @Published var switchToBranch: GitBranch? + + // MARK: - Operation Fields + + /// Branch selected for source control operations (shared between push and pull) + @Published var operationBranch: GitBranch? + + /// Remote selected for source control operations + @Published var operationRemote: GitRemote? + + /// Rebase boolean set for source control operations + @Published var operationRebase: Bool = false + + /// Force boolean set for source control operations + @Published var operationForce: Bool = false + + /// Include tags boolean set for source control operations + @Published var operationIncludeTags: Bool = false + + // MARK: - Alert State + + /// Is discard all alert presented + @Published var discardAllAlertIsPresented: Bool = false + + /// Is no changes to stage alert presented + @Published var noChangesToStageAlertIsPresented: Bool = false + + /// Is no changes to unstage alert presented + @Published var noChangesToUnstageAlertIsPresented: Bool = false + + /// Is no changes to stash alert presented + @Published var noChangesToStashAlertIsPresented: Bool = false + + /// Is no changes to discard alert presented + @Published var noChangesToDiscardAlertIsPresented: Bool = false + + // MARK: - Private + + private func resetOperationFields() { + operationBranch = nil + operationRemote = nil + operationRebase = false + operationForce = false + operationIncludeTags = false + } +} diff --git a/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift b/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift new file mode 100644 index 0000000000..6ee9d1db32 --- /dev/null +++ b/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift @@ -0,0 +1,76 @@ +// +// SourceControlViewModelTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 02/07/2026. +// + +import XCTest +@testable import CodeEdit + +@MainActor +final class SourceControlViewModelTests: XCTestCase { + var viewModel: SourceControlViewModel! + + override func setUp() { + super.setUp() + viewModel = SourceControlViewModel() + } + + override func tearDown() { + viewModel = nil + super.tearDown() + } + + // MARK: - Operation field reset + + func testPushSheetPresentedResetsOperationFields() { + viewModel.operationRebase = true + viewModel.operationForce = true + viewModel.operationIncludeTags = true + + viewModel.pushSheetIsPresented = true + + XCTAssertNil(viewModel.operationBranch) + XCTAssertNil(viewModel.operationRemote) + XCTAssertFalse(viewModel.operationRebase) + XCTAssertFalse(viewModel.operationForce) + XCTAssertFalse(viewModel.operationIncludeTags) + } + + func testPullSheetPresentedResetsOperationFields() { + viewModel.operationRebase = true + viewModel.operationForce = true + viewModel.operationIncludeTags = true + + viewModel.pullSheetIsPresented = true + + XCTAssertNil(viewModel.operationBranch) + XCTAssertNil(viewModel.operationRemote) + XCTAssertFalse(viewModel.operationRebase) + XCTAssertFalse(viewModel.operationForce) + XCTAssertFalse(viewModel.operationIncludeTags) + } + + // MARK: - Sheet independence + + func testSheetBooleansAreIndependent() { + viewModel.pushSheetIsPresented = true + + XCTAssertFalse(viewModel.pullSheetIsPresented) + XCTAssertFalse(viewModel.fetchSheetIsPresented) + XCTAssertFalse(viewModel.stashSheetIsPresented) + XCTAssertFalse(viewModel.addExistingRemoteSheetIsPresented) + } + + // MARK: - Alert independence + + func testAlertBooleansAreIndependent() { + viewModel.discardAllAlertIsPresented = true + + XCTAssertFalse(viewModel.noChangesToStageAlertIsPresented) + XCTAssertFalse(viewModel.noChangesToUnstageAlertIsPresented) + XCTAssertFalse(viewModel.noChangesToStashAlertIsPresented) + XCTAssertFalse(viewModel.noChangesToDiscardAlertIsPresented) + } +} From 60fbbe4c8d9888c20ea6bfee47236fbac7880a34 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 2 Jul 2026 23:20:25 +0200 Subject: [PATCH 027/335] Refactor: Update all call sites to use SourceControlViewModel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All reads/writes of UI presentation state switch from SourceControlManager to SourceControlViewModel across 19 files — injection sites, navigator views, sheet views, toolbar, and SourceControlCommands. --- .../Protocols/WorkspaceManaging.swift | 1 + .../SourceControlNavigatorNoRemotesView.swift | 4 +- .../SourceControlNavigatorSyncView.swift | 5 +- ...lNavigatorRepositoryView+contextMenu.swift | 4 +- ...SourceControlNavigatorRepositoryView.swift | 1 + .../SourceControlNavigatorToolbarBottom.swift | 9 +-- .../Views/SourceControlNavigatorView.swift | 5 +- .../SourceControl/SourceControlManager.swift | 64 ------------------- .../Views/RemoteBranchPicker.swift | 3 +- .../SourceControlAddExistingRemoteView.swift | 5 +- .../Views/SourceControlPullView.swift | 17 ++--- .../Views/SourceControlPushView.swift | 17 ++--- .../Views/SourceControlStashView.swift | 35 +++++----- .../Views/SourceControlSwitchView.swift | 3 +- .../SourceControlCommands.swift | 24 ++++--- .../Features/Workspace/Models/Workspace.swift | 2 + .../Features/Workspace/WorkspaceFactory.swift | 1 + CodeEdit/WorkspaceSheets.swift | 41 ++++++------ CodeEdit/WorkspaceView.swift | 10 ++- 19 files changed, 107 insertions(+), 144 deletions(-) diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index c007228a74..5650dcfbbb 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -20,6 +20,7 @@ protocol WorkspaceManaging: AnyObject, ObservableObject { var openQuicklyViewModel: OpenQuicklyViewModel? { get } var commandsPaletteState: QuickActionsViewModel? { get } var sourceControlManager: SourceControlManager? { get } + var sourceControlViewModel: SourceControlViewModel? { get } var taskManager: TaskManager? { get } var workspaceSettingsManager: CEWorkspaceSettings? { get } var statePersistence: WorkspaceStatePersistence? { get } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift index e6eb446395..187c8c468f 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SourceControlNavigatorNoRemotesView: View { - @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel var body: some View { VStack(spacing: 0) { @@ -23,7 +23,7 @@ struct SourceControlNavigatorNoRemotesView: View { ) Spacer() Button("Add") { - sourceControlManager.addExistingRemoteSheetIsPresented = true + sourceControlViewModel.addExistingRemoteSheetIsPresented = true } } } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift index a17834a1d2..1c80b2e435 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SourceControlNavigatorSyncView: View { @ObservedObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @State private var isLoading: Bool = false var body: some View { @@ -38,7 +39,7 @@ struct SourceControlNavigatorSyncView: View { Spacer() if sourceControlManager.numberOfUnsyncedCommits.behind > 0 { Button { - sourceControlManager.pullSheetIsPresented = true + sourceControlViewModel.pullSheetIsPresented = true } label: { Text("Pull...") } @@ -46,7 +47,7 @@ struct SourceControlNavigatorSyncView: View { } else if sourceControlManager.numberOfUnsyncedCommits.ahead > 0 || currentBranch.upstream == nil { Button { - sourceControlManager.pushSheetIsPresented = true + sourceControlViewModel.pushSheetIsPresented = true } label: { Text("Push...") } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift index f370c4890b..c36ba1874d 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift @@ -27,7 +27,7 @@ extension SourceControlNavigatorRepositoryView { @ViewBuilder func contextMenu(for item: RepoOutlineGroupItem, branch: GitBranch) -> some View { Button("Switch...") { - sourceControlManager.switchToBranch = branch + sourceControlViewModel.switchToBranch = branch } .disabled(item.branch == nil || sourceControlManager.currentBranch == item.branch) Divider() @@ -51,7 +51,7 @@ extension SourceControlNavigatorRepositoryView { .disabled(item.branch == nil || item.branch?.isRemote == true) Divider() Button("Add Existing Remote...") { - sourceControlManager.addExistingRemoteSheetIsPresented = true + sourceControlViewModel.addExistingRemoteSheetIsPresented = true } .disabled(item.id != "RemotesGroup") Divider() diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift index 6308a31e91..d0c84b7b41 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift @@ -14,6 +14,7 @@ struct SourceControlNavigatorRepositoryView: View { var controlActiveState @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @State var selection = Set() @State var showNewBranch: Bool = false diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift index 11c601c66d..1f394b2dc8 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift @@ -10,6 +10,7 @@ import SwiftUI struct SourceControlNavigatorToolbarBottom: View { @EnvironmentObject private var workspace: Workspace @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @State private var text = "" @@ -49,16 +50,16 @@ struct SourceControlNavigatorToolbarBottom: View { Menu { Button("Discard All Changes...") { if sourceControlManager.changedFiles.isEmpty { - sourceControlManager.noChangesToDiscardAlertIsPresented = true + sourceControlViewModel.noChangesToDiscardAlertIsPresented = true } else { - sourceControlManager.discardAllAlertIsPresented = true + sourceControlViewModel.discardAllAlertIsPresented = true } } Button("Stash Changes...") { if sourceControlManager.changedFiles.isEmpty { - sourceControlManager.noChangesToStashAlertIsPresented = true + sourceControlViewModel.noChangesToStashAlertIsPresented = true } else { - sourceControlManager.stashSheetIsPresented = true + sourceControlViewModel.stashSheetIsPresented = true } } } label: {} diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index f9ae981b36..8895e2d88d 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -14,10 +14,12 @@ struct SourceControlNavigatorView: View { var fetchRefreshServerStatus var body: some View { - if let sourceControlManager = workspace.workspaceFileManager?.sourceControlManager { + if let sourceControlManager = workspace.workspaceFileManager?.sourceControlManager, + let sourceControlViewModel = workspace.sourceControlViewModel { VStack(spacing: 0) { SourceControlNavigatorTabs() .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) .task { do { while true { @@ -34,6 +36,7 @@ struct SourceControlNavigatorView: View { .safeAreaInset(edge: .bottom, spacing: 0) { SourceControlNavigatorToolbarBottom() .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) } } } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index cde4062c54..e623331ce6 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -51,70 +51,6 @@ final class SourceControlManager: ObservableObject { /// Is project a git repository @Published var isGitRepository: Bool = false - // MARK: - UI Presentation State - - /// Is the push sheet presented - @Published var pushSheetIsPresented: Bool = false { - didSet { - self.operationBranch = nil - self.operationRebase = false - self.operationForce = false - self.operationIncludeTags = false - } - } - - /// Is the pull sheet presented - @Published var pullSheetIsPresented: Bool = false { - didSet { - self.operationBranch = nil - self.operationRebase = false - self.operationForce = false - self.operationIncludeTags = false - } - } - - /// Is the fetch sheet presented - @Published var fetchSheetIsPresented: Bool = false - - /// Is the stash sheet presented - @Published var stashSheetIsPresented: Bool = false - - /// Is the remote sheet presented - @Published var addExistingRemoteSheetIsPresented: Bool = false - - /// Branch selected for source control operations - @Published var operationBranch: GitBranch? - - /// Remote selected for source control operations - @Published var operationRemote: GitRemote? - - /// Rebase boolean set for source control operations - @Published var operationRebase: Bool = false - - /// Force boolean set for source control operations - @Published var operationForce: Bool = false - - /// Include tags boolean set for source control operations - @Published var operationIncludeTags: Bool = false - - /// Branch to switch to - @Published var switchToBranch: GitBranch? - - /// Is discard all alert presented - @Published var discardAllAlertIsPresented: Bool = false - - /// Is no changes to stage alert presented - @Published var noChangesToStageAlertIsPresented: Bool = false - - /// Is no changes to unstage alert presented - @Published var noChangesToUnstageAlertIsPresented: Bool = false - - /// Is no changes to stash alert presented - @Published var noChangesToStashAlertIsPresented: Bool = false - - /// Is no changes to discard alert presented - @Published var noChangesToDiscardAlertIsPresented: Bool = false - // MARK: - Computed Properties var orderedLocalBranches: [GitBranch] { diff --git a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift b/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift index ec7c2bf36d..e7c14031c4 100644 --- a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift +++ b/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift @@ -10,6 +10,7 @@ import CodeEditCore struct RemoteBranchPicker: View { @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @Binding var branch: GitBranch? @Binding var remote: GitRemote? @@ -70,7 +71,7 @@ struct RemoteBranchPicker: View { } .onChange(of: remote) { _, newValue in if newValue == nil { - sourceControlManager.addExistingRemoteSheetIsPresented = true + sourceControlViewModel.addExistingRemoteSheetIsPresented = true } else { updateBranch() } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift index c891e381a0..2d3e89358b 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SourceControlAddExistingRemoteView: View { @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @Environment(\.dismiss) private var dismiss @@ -71,8 +72,8 @@ struct SourceControlAddExistingRemoteView: View { Task { do { try await sourceControlManager.addRemote(name: name, location: location) - if sourceControlManager.pullSheetIsPresented || sourceControlManager.pushSheetIsPresented { - sourceControlManager.operationRemote = sourceControlManager.remotes.first( + if sourceControlViewModel.pullSheetIsPresented || sourceControlViewModel.pushSheetIsPresented { + sourceControlViewModel.operationRemote = sourceControlManager.remotes.first( where: { $0.name == name } ) } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift index 87e4b92bc3..95791f54bc 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift @@ -13,6 +13,7 @@ struct SourceControlPullView: View { private var dismiss @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) @@ -25,8 +26,8 @@ struct SourceControlPullView: View { Form { Section { RemoteBranchPicker( - branch: $sourceControlManager.operationBranch, - remote: $sourceControlManager.operationRemote, + branch: $sourceControlViewModel.operationBranch, + remote: $sourceControlViewModel.operationRemote, onSubmit: submit, canCreateBranch: false ) @@ -34,7 +35,7 @@ struct SourceControlPullView: View { Text("Pull remote changes from") } Section { - Toggle("Rebase local changes onto upstream changes", isOn: $sourceControlManager.operationRebase) + Toggle("Rebase local changes onto upstream changes", isOn: $sourceControlViewModel.operationRebase) } } .formStyle(.grouped) @@ -44,7 +45,7 @@ struct SourceControlPullView: View { Task { preferRebaseWhenPulling = try await gitConfig.get(key: "pull.rebase", global: true) ?? false if preferRebaseWhenPulling { - sourceControlManager.operationRebase = true + sourceControlViewModel.operationRebase = true } } } @@ -84,13 +85,13 @@ struct SourceControlPullView: View { Task { do { if !sourceControlManager.changedFiles.isEmpty { - sourceControlManager.stashSheetIsPresented = true + sourceControlViewModel.stashSheetIsPresented = true } else { self.loading = true try await sourceControlManager.pull( - remote: sourceControlManager.operationRemote?.name ?? nil, - branch: sourceControlManager.operationBranch?.name ?? nil, - rebase: sourceControlManager.operationRebase + remote: sourceControlViewModel.operationRemote?.name ?? nil, + branch: sourceControlViewModel.operationBranch?.name ?? nil, + rebase: sourceControlViewModel.operationRebase ) self.loading = false dismiss() diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift index 8b280cb806..4b434f6953 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift @@ -12,6 +12,7 @@ struct SourceControlPushView: View { private var dismiss @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @State var loading: Bool = false @@ -20,8 +21,8 @@ struct SourceControlPushView: View { Form { Section { RemoteBranchPicker( - branch: $sourceControlManager.operationBranch, - remote: $sourceControlManager.operationRemote, + branch: $sourceControlViewModel.operationBranch, + remote: $sourceControlViewModel.operationRemote, onSubmit: submit, canCreateBranch: true ) @@ -29,8 +30,8 @@ struct SourceControlPushView: View { Text("Push local changes to") } Section { - Toggle("Force", isOn: $sourceControlManager.operationForce) - Toggle("Include Tags", isOn: $sourceControlManager.operationIncludeTags) + Toggle("Force", isOn: $sourceControlViewModel.operationForce) + Toggle("Include Tags", isOn: $sourceControlViewModel.operationIncludeTags) } } .formStyle(.grouped) @@ -73,11 +74,11 @@ struct SourceControlPushView: View { do { self.loading = true try await sourceControlManager.push( - remote: sourceControlManager.operationRemote?.name ?? nil, - branch: sourceControlManager.operationBranch?.name ?? nil, + remote: sourceControlViewModel.operationRemote?.name ?? nil, + branch: sourceControlViewModel.operationBranch?.name ?? nil, setUpstream: sourceControlManager.currentBranch?.upstream == nil, - force: sourceControlManager.operationForce, - tags: sourceControlManager.operationIncludeTags + force: sourceControlViewModel.operationForce, + tags: sourceControlViewModel.operationIncludeTags ) self.loading = false dismiss() diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift index 457b09a962..428c6566b2 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SourceControlStashView: View { @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @Environment(\.dismiss) private var dismiss @@ -27,8 +28,8 @@ struct SourceControlStashView: View { } header: { Text("Stash Changes") Group { - if sourceControlManager.pullSheetIsPresented - || sourceControlManager.switchToBranch != nil { + if sourceControlViewModel.pullSheetIsPresented + || sourceControlViewModel.switchToBranch != nil { Text("Your local repository has uncommitted changes that need to be stashed " + "before you can continue. Enter a description for your changes.") } else { @@ -39,8 +40,8 @@ struct SourceControlStashView: View { .multilineTextAlignment(.leading) .lineLimit(nil) } - if sourceControlManager.pullSheetIsPresented - || sourceControlManager.switchToBranch != nil { + if sourceControlViewModel.pullSheetIsPresented + || sourceControlViewModel.switchToBranch != nil { Section { Toggle("Apply stash after operation", isOn: $applyStashAfterOperation) } @@ -63,9 +64,9 @@ struct SourceControlStashView: View { submit() } label: { Text( - sourceControlManager.pullSheetIsPresented + sourceControlViewModel.pullSheetIsPresented ? "Stash and Pull" - : sourceControlManager.switchToBranch != nil + : sourceControlViewModel.switchToBranch != nil ? "Stash and Switch" : "Stash" ) @@ -85,17 +86,17 @@ struct SourceControlStashView: View { try await sourceControlManager.stashChanges(message: message) message = "" - if sourceControlManager.pullSheetIsPresented - || sourceControlManager.switchToBranch != nil { - if sourceControlManager.pullSheetIsPresented { + if sourceControlViewModel.pullSheetIsPresented + || sourceControlViewModel.switchToBranch != nil { + if sourceControlViewModel.pullSheetIsPresented { try await sourceControlManager.pull( - remote: sourceControlManager.operationRemote?.name, - branch: sourceControlManager.operationBranch?.name, - rebase: sourceControlManager.operationRebase + remote: sourceControlViewModel.operationRemote?.name, + branch: sourceControlViewModel.operationBranch?.name, + rebase: sourceControlViewModel.operationRebase ) } - if let branch = sourceControlManager.switchToBranch { + if let branch = sourceControlViewModel.switchToBranch { try await sourceControlManager.checkoutBranch(branch: branch) } @@ -110,10 +111,10 @@ struct SourceControlStashView: View { try await sourceControlManager.applyStashEntry(stashEntry: lastStashEntry) } - sourceControlManager.operationRemote = nil - sourceControlManager.operationBranch = nil - sourceControlManager.pullSheetIsPresented = false - sourceControlManager.switchToBranch = nil + sourceControlViewModel.operationRemote = nil + sourceControlViewModel.operationBranch = nil + sourceControlViewModel.pullSheetIsPresented = false + sourceControlViewModel.switchToBranch = nil } dismiss() diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift index d8d2401795..29ac093e5d 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift @@ -13,6 +13,7 @@ struct SourceControlSwitchView: View { private var dismiss @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel var branch: GitBranch @@ -64,7 +65,7 @@ struct SourceControlSwitchView: View { Task { do { if !sourceControlManager.changedFiles.isEmpty { - sourceControlManager.stashSheetIsPresented = true + sourceControlViewModel.stashSheetIsPresented = true } else { try await sourceControlManager.checkoutBranch(branch: branch) dismiss() diff --git a/CodeEdit/Features/WindowCommands/SourceControlCommands.swift b/CodeEdit/Features/WindowCommands/SourceControlCommands.swift index 6bf494f412..8ea5be2a31 100644 --- a/CodeEdit/Features/WindowCommands/SourceControlCommands.swift +++ b/CodeEdit/Features/WindowCommands/SourceControlCommands.swift @@ -16,6 +16,10 @@ struct SourceControlCommands: Commands { windowController?.workspace?.sourceControlManager } + var sourceControlViewModel: SourceControlViewModel? { + windowController?.workspace?.sourceControlViewModel + } + var body: some Commands { CommandMenu("Source Control") { Group { @@ -25,16 +29,16 @@ struct SourceControlCommands: Commands { .disabled(true) Button("Push...") { - sourceControlManager?.pushSheetIsPresented = true + sourceControlViewModel?.pushSheetIsPresented = true } Button("Pull...") { - sourceControlManager?.pullSheetIsPresented = true + sourceControlViewModel?.pullSheetIsPresented = true } .keyboardShortcut("x", modifiers: [.command, .option]) Button("Fetch Changes") { - sourceControlManager?.fetchSheetIsPresented = true + sourceControlViewModel?.fetchSheetIsPresented = true } Divider() @@ -42,7 +46,7 @@ struct SourceControlCommands: Commands { Button("Stage All Changes") { guard let sourceControlManager else { return } if sourceControlManager.changedFiles.isEmpty { - sourceControlManager.noChangesToStageAlertIsPresented = true + sourceControlViewModel?.noChangesToStageAlertIsPresented = true } else { Task { do { @@ -60,7 +64,7 @@ struct SourceControlCommands: Commands { Button("Unstage All Changes") { guard let sourceControlManager else { return } if sourceControlManager.changedFiles.isEmpty { - sourceControlManager.noChangesToUnstageAlertIsPresented = true + sourceControlViewModel?.noChangesToUnstageAlertIsPresented = true } else { Task { do { @@ -86,9 +90,9 @@ struct SourceControlCommands: Commands { Button("Stash Changes...") { if sourceControlManager?.changedFiles.isEmpty ?? false { - sourceControlManager?.noChangesToStashAlertIsPresented = true + sourceControlViewModel?.noChangesToStashAlertIsPresented = true } else { - sourceControlManager?.stashSheetIsPresented = true + sourceControlViewModel?.stashSheetIsPresented = true } } @@ -96,16 +100,16 @@ struct SourceControlCommands: Commands { Button("Discard All Changes...") { if sourceControlManager?.changedFiles.isEmpty ?? false { - sourceControlManager?.noChangesToDiscardAlertIsPresented = true + sourceControlViewModel?.noChangesToDiscardAlertIsPresented = true } else { - sourceControlManager?.discardAllAlertIsPresented = true + sourceControlViewModel?.discardAllAlertIsPresented = true } } Divider() Button("Add Exisiting Remote...") { - sourceControlManager?.addExistingRemoteSheetIsPresented = true + sourceControlViewModel?.addExistingRemoteSheetIsPresented = true } } .disabled(windowController?.workspace == nil) diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 2a91f12a87..d1dc19b402 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -29,6 +29,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { var commandsPaletteState: QuickActionsViewModel? var listenerModel: WorkspaceNotificationModel = .init() var sourceControlManager: SourceControlManager? + var sourceControlViewModel: SourceControlViewModel? var taskManager: TaskManager? var workspaceSettingsManager: CEWorkspaceSettings? @@ -64,6 +65,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { openQuicklyViewModel = nil commandsPaletteState = nil sourceControlManager = nil + sourceControlViewModel = nil workspaceFileManager?.cleanUp() workspaceFileManager = nil workspaceSettingsManager?.cleanUp() diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 1fefd22ef5..3224f71b08 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -57,6 +57,7 @@ enum WorkspaceFactory { sourceControlManager.fileManager = workspaceFileManager workspace.sourceControlManager = sourceControlManager + workspace.sourceControlViewModel = SourceControlViewModel() workspace.workspaceFileManager = workspaceFileManager // --- Phase 2: Independent managers --- diff --git a/CodeEdit/WorkspaceSheets.swift b/CodeEdit/WorkspaceSheets.swift index d8e6db8674..5f09e7928c 100644 --- a/CodeEdit/WorkspaceSheets.swift +++ b/CodeEdit/WorkspaceSheets.swift @@ -10,49 +10,50 @@ import CodeEditCore struct WorkspaceSheets: View { @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var sourceControlViewModel: SourceControlViewModel var body: some View { EmptyView() .sheet(isPresented: Binding( - get: { sourceControlManager.pushSheetIsPresented && - !sourceControlManager.addExistingRemoteSheetIsPresented }, - set: { sourceControlManager.pushSheetIsPresented = $0 } + get: { sourceControlViewModel.pushSheetIsPresented && + !sourceControlViewModel.addExistingRemoteSheetIsPresented }, + set: { sourceControlViewModel.pushSheetIsPresented = $0 } )) { SourceControlPushView() } .sheet(isPresented: Binding( - get: { sourceControlManager.pullSheetIsPresented && - !sourceControlManager.addExistingRemoteSheetIsPresented && - !sourceControlManager.stashSheetIsPresented }, - set: { sourceControlManager.pullSheetIsPresented = $0 } + get: { sourceControlViewModel.pullSheetIsPresented && + !sourceControlViewModel.addExistingRemoteSheetIsPresented && + !sourceControlViewModel.stashSheetIsPresented }, + set: { sourceControlViewModel.pullSheetIsPresented = $0 } )) { - if sourceControlManager.addExistingRemoteSheetIsPresented == true { + if sourceControlViewModel.addExistingRemoteSheetIsPresented == true { SourceControlAddExistingRemoteView() } else { SourceControlPullView() } } - .sheet(isPresented: $sourceControlManager.fetchSheetIsPresented) { + .sheet(isPresented: $sourceControlViewModel.fetchSheetIsPresented) { SourceControlFetchView() } - .sheet(isPresented: $sourceControlManager.stashSheetIsPresented) { + .sheet(isPresented: $sourceControlViewModel.stashSheetIsPresented) { SourceControlStashView() } - .sheet(isPresented: $sourceControlManager.addExistingRemoteSheetIsPresented) { + .sheet(isPresented: $sourceControlViewModel.addExistingRemoteSheetIsPresented) { SourceControlAddExistingRemoteView() } .sheet(item: Binding( get: { - sourceControlManager.switchToBranch != nil - && sourceControlManager.stashSheetIsPresented + sourceControlViewModel.switchToBranch != nil + && sourceControlViewModel.stashSheetIsPresented ? nil - : sourceControlManager.switchToBranch + : sourceControlViewModel.switchToBranch }, - set: { sourceControlManager.switchToBranch = $0 } + set: { sourceControlViewModel.switchToBranch = $0 } )) { branch in SourceControlSwitchView(branch: branch) } - .alert(isPresented: $sourceControlManager.discardAllAlertIsPresented) { + .alert(isPresented: $sourceControlViewModel.discardAllAlertIsPresented) { Alert( title: Text("Do you want to discard all uncommitted, local changes?"), message: Text("This action cannot be undone."), @@ -62,22 +63,22 @@ struct WorkspaceSheets: View { secondaryButton: .cancel() ) } - .alert("Cannot Stage Changes", isPresented: $sourceControlManager.noChangesToStageAlertIsPresented) { + .alert("Cannot Stage Changes", isPresented: $sourceControlViewModel.noChangesToStageAlertIsPresented) { Button("OK", role: .cancel) {} } message: { Text("There are no uncommitted changes in the local repository for this project.") } - .alert("Cannot Unstage Changes", isPresented: $sourceControlManager.noChangesToUnstageAlertIsPresented) { + .alert("Cannot Unstage Changes", isPresented: $sourceControlViewModel.noChangesToUnstageAlertIsPresented) { Button("OK", role: .cancel) {} } message: { Text("There are no uncommitted changes in the local repository for this project.") } - .alert("Cannot Stash Changes", isPresented: $sourceControlManager.noChangesToStashAlertIsPresented) { + .alert("Cannot Stash Changes", isPresented: $sourceControlViewModel.noChangesToStashAlertIsPresented) { Button("OK", role: .cancel) {} } message: { Text("There are no uncommitted changes in the local repository for this project.") } - .alert("Cannot Discard Changes", isPresented: $sourceControlManager.noChangesToDiscardAlertIsPresented) { + .alert("Cannot Discard Changes", isPresented: $sourceControlViewModel.noChangesToDiscardAlertIsPresented) { Button("OK", role: .cancel) {} } message: { Text("There are no uncommitted changes in the local repository for this project.") diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 59aa4c502d..8f09f65451 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -38,7 +38,9 @@ struct WorkspaceView: View { private let statusbarHeight: CGFloat = 29 var body: some View { - if workspace.workspaceFileManager != nil, let sourceControlManager = workspace.sourceControlManager { + if workspace.workspaceFileManager != nil, + let sourceControlManager = workspace.sourceControlManager, + let sourceControlViewModel = workspace.sourceControlViewModel { VStack { SplitViewReader { proxy in SplitView(axis: .vertical) { @@ -119,7 +121,11 @@ struct WorkspaceView: View { } } .background(EffectView(.contentBackground)) - .background(WorkspaceSheets().environmentObject(sourceControlManager)) + .background( + WorkspaceSheets() + .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) + ) .onDrop(of: [.fileURL], isTargeted: nil) { providers in _ = handleDrop(providers: providers) return true From 518a5784aa12c6122f7f0f0db60413318bf21749 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 11:53:47 +0200 Subject: [PATCH 028/335] Refactor: Add workspace env keys, extract ProjectNavigatorViewModel, migrate ProjectNavigator off Workspace hub --- .../CodeEditSplitViewController.swift | 5 +++ .../Protocols/WorkspaceManaging.swift | 4 +- .../Models/ProjectNavigatorViewModel.swift | 19 +++++++++ .../ProjectNavigatorOutlineView.swift | 29 +++++++------ ...ewController+NSOutlineViewDataSource.swift | 15 ++++--- ...ViewController+NSOutlineViewDelegate.swift | 8 ++-- .../ProjectNavigatorViewController.swift | 5 ++- .../ProjectNavigatorToolbarBottom.swift | 42 +++++++++++-------- .../Models/Environment+Workspace.swift | 37 ++++++++++++++++ .../Features/Workspace/Models/Workspace.swift | 5 +-- .../ProjectNavigatorViewModelTests.swift | 39 +++++++++++++++++ 11 files changed, 162 insertions(+), 46 deletions(-) create mode 100644 CodeEdit/Features/NavigatorArea/ProjectNavigator/Models/ProjectNavigatorViewModel.swift create mode 100644 CodeEdit/Features/Workspace/Models/Environment+Workspace.swift create mode 100644 CodeEditTests/Features/NavigatorArea/ProjectNavigatorViewModelTests.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 0399371798..0b41581d5c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -54,6 +54,7 @@ final class CodeEditSplitViewController: NSSplitViewController { let editorManager = workspace.editorManager, let statusBarViewModel = workspace.statusBarViewModel, let utilityAreaModel = workspace.utilityAreaModel, + let projectNavigatorViewModel = workspace.projectNavigatorViewModel, let taskManager = workspace.taskManager else { // swiftlint:disable:next line_length assertionFailure("Missing a workspace model: workspace=\(workspace == nil), navigator=\(navigatorViewModel == nil), editorManager=\(workspace?.editorManager == nil), statusBarModel=\(workspace?.statusBarViewModel == nil), utilityAreaModel=\(workspace?.utilityAreaModel == nil), taskManager=\(workspace?.taskManager == nil)") @@ -66,6 +67,10 @@ final class CodeEditSplitViewController: NSSplitViewController { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) .environmentObject(editorManager) + .environmentObject(workspace.listenerModel) + .environmentObject(projectNavigatorViewModel) + .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.workspaceFileURL, workspace.fileURL) }) addSplitViewItem(navigator) diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index 5650dcfbbb..754b702106 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -28,7 +28,5 @@ protocol WorkspaceManaging: AnyObject, ObservableObject { var undoRegistration: UndoManagerRegistration { get } var notificationPanel: NotificationPanelViewModel { get } var taskNotificationHandler: TaskNotificationHandler { get } - var navigatorFilter: String { get set } - var sourceControlFilter: Bool { get set } - var sortFoldersOnTop: Bool { get set } + var projectNavigatorViewModel: ProjectNavigatorViewModel? { get } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/Models/ProjectNavigatorViewModel.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/Models/ProjectNavigatorViewModel.swift new file mode 100644 index 0000000000..4b19ad6d2d --- /dev/null +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/Models/ProjectNavigatorViewModel.swift @@ -0,0 +1,19 @@ +// +// ProjectNavigatorViewModel.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 03/07/2026. +// + +import Foundation + +/// UI state for the Project Navigator: its filter text and sort/filter toggles. +/// Extracted from `Workspace` so navigator views don't depend on the whole workspace hub. +@MainActor +final class ProjectNavigatorViewModel: ObservableObject { + @Published var navigatorFilter: String = "" + @Published var sortFoldersOnTop: Bool = true + @Published var sourceControlFilter: Bool = false + + init() {} +} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index ffdbc28695..b3ae44e9b0 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -65,18 +65,23 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { self?.controller?.updateSelection(itemID: editorInstance?.file.id) } .store(in: &cancellables) - workspace.$navigatorFilter - .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) - .sink { [weak self] _ in - self?.controller?.handleFilterChange() - } - .store(in: &cancellables) - Publishers.Merge(workspace.$sourceControlFilter, workspace.$sortFoldersOnTop) - .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) - .sink { [weak self] _ in - self?.controller?.handleFilterChange() - } - .store(in: &cancellables) + if let projectNavigatorViewModel = workspace.projectNavigatorViewModel { + projectNavigatorViewModel.$navigatorFilter + .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) + .sink { [weak self] _ in + self?.controller?.handleFilterChange() + } + .store(in: &cancellables) + Publishers.Merge( + projectNavigatorViewModel.$sourceControlFilter, + projectNavigatorViewModel.$sortFoldersOnTop + ) + .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) + .sink { [weak self] _ in + self?.controller?.handleFilterChange() + } + .store(in: &cancellables) + } } var cancellables: Set = [] diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 8857da9c5a..12891dc430 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -13,17 +13,22 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { if let cachedChildren = filteredContentChildren[item] { return cachedChildren .sorted { lhs, rhs in - workspace?.sortFoldersOnTop == true ? lhs.isFolder && !rhs.isFolder : lhs.name < rhs.name + workspace?.projectNavigatorViewModel?.sortFoldersOnTop == true + ? lhs.isFolder && !rhs.isFolder : lhs.name < rhs.name } } if let workspace, let children = workspace.workspaceFileManager?.childrenOfFile(item) { - if !workspace.navigatorFilter.isEmpty || workspace.sourceControlFilter { + let navigatorFilter = workspace.projectNavigatorViewModel?.navigatorFilter ?? "" + let sourceControlFilter = workspace.projectNavigatorViewModel?.sourceControlFilter ?? false + let sortFoldersOnTop = workspace.projectNavigatorViewModel?.sortFoldersOnTop ?? true + + if !navigatorFilter.isEmpty || sourceControlFilter { let filteredChildren = children.filter { fileSearchMatches( - workspace.navigatorFilter, + navigatorFilter, for: $0, - sourceControlFilter: workspace.sourceControlFilter + sourceControlFilter: sourceControlFilter ) } @@ -33,7 +38,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { return children .sorted { lhs, rhs in - workspace.sortFoldersOnTop ? lhs.isFolder && !rhs.isFolder : lhs.name < rhs.name + sortFoldersOnTop ? lhs.isFolder && !rhs.isFolder : lhs.name < rhs.name } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 4f57abe36e..bc09fca118 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -29,7 +29,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { frame: frameRect, item: item as? CEWorkspaceFile, delegate: self, - navigatorFilter: workspace?.navigatorFilter + navigatorFilter: workspace?.projectNavigatorViewModel?.navigatorFilter ) return cell } @@ -61,7 +61,8 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineViewItemDidExpand(_ notification: Notification) { /// Save expanded items' state to restore when finish filtering. guard let workspace else { return } - if workspace.navigatorFilter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { + if workspace.projectNavigatorViewModel?.navigatorFilter.isEmpty ?? true, + let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { expandedItems.insert(item) } @@ -80,7 +81,8 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineViewItemDidCollapse(_ notification: Notification) { /// Save expanded items' state to restore when finish filtering. guard let workspace else { return } - if workspace.navigatorFilter.isEmpty, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { + if workspace.projectNavigatorViewModel?.navigatorFilter.isEmpty ?? true, + let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { expandedItems.remove(item) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index bebe9d5e6c..267e9375e1 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -68,7 +68,8 @@ final class ProjectNavigatorViewController: NSViewController { var shouldReloadAfterDoneEditing: Bool = false var filterIsEmpty: Bool { - workspace?.navigatorFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true + workspace?.projectNavigatorViewModel?.navigatorFilter + .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true } /// Setup the ``scrollView`` and ``outlineView`` @@ -200,7 +201,7 @@ final class ProjectNavigatorViewController: NSViewController { guard let workspace else { return } /// If the filter is empty, show all items and restore the expanded state. - if workspace.sourceControlFilter || !filterIsEmpty { + if workspace.projectNavigatorViewModel?.sourceControlFilter == true || !filterIsEmpty { outlineView.autosaveExpandedItems = false /// Expand all items for search. outlineView.expandItem(outlineView.item(atRow: 0), expandChildren: true) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 94c0c34f48..b6dd1ea2ef 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -14,8 +14,12 @@ struct ProjectNavigatorToolbarBottom: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject var workspace: Workspace @EnvironmentObject var editorManager: EditorManager + @EnvironmentObject var listenerModel: WorkspaceNotificationModel + @EnvironmentObject var projectNavigatorViewModel: ProjectNavigatorViewModel + + @Environment(\.workspaceFileManager) + private var workspaceFileManager @State var recentsFilter: Bool = false @@ -24,23 +28,23 @@ struct ProjectNavigatorToolbarBottom: View { addNewFileButton PaneTextField( "Filter", - text: $workspace.navigatorFilter, + text: $projectNavigatorViewModel.navigatorFilter, leadingAccessories: { FilterDropDownIconButton(menu: { ForEach([(true, "Folders on top"), (false, "Alphabetically")], id: \.0) { value, title in Toggle(title, isOn: Binding(get: { - workspace.sortFoldersOnTop == value + projectNavigatorViewModel.sortFoldersOnTop == value }, set: { _ in // Avoid calling the handleFilterChange method - if workspace.sortFoldersOnTop != value { - workspace.sortFoldersOnTop = value + if projectNavigatorViewModel.sortFoldersOnTop != value { + projectNavigatorViewModel.sortFoldersOnTop = value } })) } - }, isOn: !workspace.navigatorFilter.isEmpty) + }, isOn: !projectNavigatorViewModel.navigatorFilter.isEmpty) .padding(.leading, 4) .foregroundStyle( - workspace.navigatorFilter.isEmpty + projectNavigatorViewModel.navigatorFilter.isEmpty ? Color(nsColor: .secondaryLabelColor) : Color(nsColor: .controlAccentColor) ) @@ -52,7 +56,7 @@ struct ProjectNavigatorToolbarBottom: View { Image(systemName: "clock") } .help("Show only recent files") - Toggle(isOn: $workspace.sourceControlFilter) { + Toggle(isOn: $projectNavigatorViewModel.sourceControlFilter) { Image(systemName: "plusminus.circle") } .help("Show only files with source-control status") @@ -61,7 +65,9 @@ struct ProjectNavigatorToolbarBottom: View { .padding(.trailing, 2.5) }, clearable: true, - hasValue: !workspace.navigatorFilter.isEmpty || recentsFilter || workspace.sourceControlFilter + hasValue: !projectNavigatorViewModel.navigatorFilter.isEmpty + || recentsFilter + || projectNavigatorViewModel.sourceControlFilter ) } .padding(.horizontal, 5) @@ -93,21 +99,21 @@ struct ProjectNavigatorToolbarBottom: View { } } - return workspace.workspaceFileManager.unsafelyUnwrapped.folderUrl + return workspaceFileManager.unsafelyUnwrapped.folderUrl } private var addNewFileButton: some View { Menu { Button("Add File") { let filePathURL = activeTabURL() - guard let rootFile = workspace.workspaceFileManager?.getFile(filePathURL.path) else { return } + guard let rootFile = workspaceFileManager?.getFile(filePathURL.path) else { return } do { - if let newFile = try workspace.workspaceFileManager?.addFile( + if let newFile = try workspaceFileManager?.addFile( fileName: "untitled", toFile: rootFile ) { - workspace.listenerModel.highlightedFileItem = newFile - workspace.editorManager?.openTab(item: newFile) + listenerModel.highlightedFileItem = newFile + editorManager.openTab(item: newFile) } } catch { let alert = NSAlert(error: error) @@ -118,13 +124,13 @@ struct ProjectNavigatorToolbarBottom: View { Button("Add Folder") { let filePathURL = activeTabURL() - guard let rootFile = workspace.workspaceFileManager?.getFile(filePathURL.path) else { return } + guard let rootFile = workspaceFileManager?.getFile(filePathURL.path) else { return } do { - if let newFolder = try workspace.workspaceFileManager?.addFolder( + if let newFolder = try workspaceFileManager?.addFolder( folderName: "untitled", toFile: rootFile ) { - workspace.listenerModel.highlightedFileItem = newFolder + listenerModel.highlightedFileItem = newFolder } } catch { let alert = NSAlert(error: error) @@ -149,7 +155,7 @@ struct ProjectNavigatorToolbarBottom: View { /// when the user clears the filter. private var clearFilterButton: some View { Button { - workspace.navigatorFilter = "" + projectNavigatorViewModel.navigatorFilter = "" NSApp.keyWindow?.makeFirstResponder(nil) } label: { Image(systemName: "xmark.circle.fill") diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift new file mode 100644 index 0000000000..1302a2bd0a --- /dev/null +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -0,0 +1,37 @@ +// +// Environment+Workspace.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 03/07/2026. +// + +import SwiftUI + +private struct WorkspaceFileManagerKey: EnvironmentKey { + static let defaultValue: CEWorkspaceFileManager? = nil +} + +private struct WorkspaceFileURLKey: EnvironmentKey { + static let defaultValue: URL? = nil +} + +private struct WorkspaceStatePersistenceKey: EnvironmentKey { + static let defaultValue: (any WorkspaceStatePersisting)? = nil +} + +extension EnvironmentValues { + var workspaceFileManager: CEWorkspaceFileManager? { + get { self[WorkspaceFileManagerKey.self] } + set { self[WorkspaceFileManagerKey.self] = newValue } + } + + var workspaceFileURL: URL? { + get { self[WorkspaceFileURLKey.self] } + set { self[WorkspaceFileURLKey.self] = newValue } + } + + var workspaceStatePersistence: (any WorkspaceStatePersisting)? { + get { self[WorkspaceStatePersistenceKey.self] } + set { self[WorkspaceStatePersistenceKey.self] = newValue } + } +} diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index d1dc19b402..66543bd3c1 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -13,9 +13,7 @@ import Foundation /// Replaces `WorkspaceDocument` (NSDocument) with no framework coupling. @MainActor final class Workspace: ObservableObject, WorkspaceManaging { - @Published var sortFoldersOnTop: Bool = true - @Published var navigatorFilter: String = "" - @Published var sourceControlFilter = false + var projectNavigatorViewModel: ProjectNavigatorViewModel? = ProjectNavigatorViewModel() var fileURL: URL? var displayName: String = "" @@ -66,6 +64,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { commandsPaletteState = nil sourceControlManager = nil sourceControlViewModel = nil + projectNavigatorViewModel = nil workspaceFileManager?.cleanUp() workspaceFileManager = nil workspaceSettingsManager?.cleanUp() diff --git a/CodeEditTests/Features/NavigatorArea/ProjectNavigatorViewModelTests.swift b/CodeEditTests/Features/NavigatorArea/ProjectNavigatorViewModelTests.swift new file mode 100644 index 0000000000..7a7caa54b2 --- /dev/null +++ b/CodeEditTests/Features/NavigatorArea/ProjectNavigatorViewModelTests.swift @@ -0,0 +1,39 @@ +// +// ProjectNavigatorViewModelTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 03/07/2026. +// + +import XCTest +@testable import CodeEdit + +@MainActor +final class ProjectNavigatorViewModelTests: XCTestCase { + var viewModel: ProjectNavigatorViewModel! + + override func setUp() { + super.setUp() + viewModel = ProjectNavigatorViewModel() + } + + override func tearDown() { + viewModel = nil + super.tearDown() + } + + func testDefaults() { + XCTAssertEqual(viewModel.navigatorFilter, "") + XCTAssertTrue(viewModel.sortFoldersOnTop) + XCTAssertFalse(viewModel.sourceControlFilter) + } + + func testFilterMutationPublishes() { + let expectation = expectation(description: "objectWillChange fires") + let cancellable = viewModel.objectWillChange.sink { expectation.fulfill() } + viewModel.navigatorFilter = "abc" + wait(for: [expectation], timeout: 1) + cancellable.cancel() + XCTAssertEqual(viewModel.navigatorFilter, "abc") + } +} From dfe48758bd3a59cde291bf83084602c4569ed016 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 12:07:34 +0200 Subject: [PATCH 029/335] Refactor: Migrate SourceControl views off Workspace hub --- .../CodeEditSplitViewController.swift | 11 +++++ .../HistoryInspectorView.swift | 4 +- .../SourceControlNavigatorChangesList.swift | 9 ++-- .../ChangedFile/GitChangedFileLabel.swift | 6 ++- .../ChangedFile/GitChangedFileListView.swift | 7 +++- .../SourceControlNavigatorToolbarBottom.swift | 1 - .../Views/SourceControlNavigatorView.swift | 42 +++++++++---------- .../Views/SourceControlFetchView.swift | 6 ++- .../Documents/DocumentsUnitTests.swift | 6 +++ 9 files changed, 58 insertions(+), 34 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 0b41581d5c..4686077d3b 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -55,6 +55,8 @@ final class CodeEditSplitViewController: NSSplitViewController { let statusBarViewModel = workspace.statusBarViewModel, let utilityAreaModel = workspace.utilityAreaModel, let projectNavigatorViewModel = workspace.projectNavigatorViewModel, + let sourceControlManager = workspace.sourceControlManager, + let sourceControlViewModel = workspace.sourceControlViewModel, let taskManager = workspace.taskManager else { // swiftlint:disable:next line_length assertionFailure("Missing a workspace model: workspace=\(workspace == nil), navigator=\(navigatorViewModel == nil), editorManager=\(workspace?.editorManager == nil), statusBarModel=\(workspace?.statusBarViewModel == nil), utilityAreaModel=\(workspace?.utilityAreaModel == nil), taskManager=\(workspace?.taskManager == nil)") @@ -69,6 +71,8 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(editorManager) .environmentObject(workspace.listenerModel) .environmentObject(projectNavigatorViewModel) + .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) }) @@ -83,8 +87,13 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(statusBarViewModel) .environmentObject(utilityAreaModel) .environmentObject(taskManager) + .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) + .environmentObject(workspace.listenerModel) .environmentObject(workspace.undoRegistration) .environmentObject(workspace.notificationPanel) + .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.workspaceFileURL, workspace.fileURL) } } @@ -98,6 +107,8 @@ final class CodeEditSplitViewController: NSSplitViewController { InspectorAreaView(viewModel: InspectorAreaViewModel()) .environmentObject(workspace) .environmentObject(editorManager) + .environmentObject(sourceControlManager) + .environment(\.workspaceFileManager, workspace.workspaceFileManager) }) addSplitViewItem(inspector) diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index d9a3589328..ec4263a761 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -11,7 +11,7 @@ struct HistoryInspectorView: View { @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) var showMergeCommitsPerFileLog - @EnvironmentObject private var workspace: Workspace + @EnvironmentObject private var sourceControlManager: SourceControlManager @EnvironmentObject private var editorManager: EditorManager @@ -61,7 +61,7 @@ struct HistoryInspectorView: View { } } .task { - await model.setWorkspace(sourceControlManager: workspace.sourceControlManager) + await model.setWorkspace(sourceControlManager: sourceControlManager) await model.setFile(url: editorManager.activeEditor.selectedTab?.file.url.path) } .onChange(of: showMergeCommitsPerFileLog) { _, _ in diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 2879cb8724..6edac9c2bb 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -10,8 +10,11 @@ import SwiftUI import CodeEditCore struct SourceControlNavigatorChangesList: View { - @EnvironmentObject var workspace: Workspace @EnvironmentObject var sourceControlManager: SourceControlManager + @EnvironmentObject var editorManager: EditorManager + + @Environment(\.workspaceFileManager) + private var workspaceFileManager @State var selection = Set() @@ -73,11 +76,11 @@ struct SourceControlNavigatorChangesList: View { } private func openGitFile(_ file: GitChangedFile) { - guard let ceFile = workspace.workspaceFileManager?.getFile(file.ceFileKey, createIfNotFound: true) else { + guard let ceFile = workspaceFileManager?.getFile(file.ceFileKey, createIfNotFound: true) else { return } DispatchQueue.main.async { - workspace.editorManager?.openTab(item: ceFile, asTemporary: true) + editorManager.openTab(item: ceFile, asTemporary: true) } } } diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index d638aca601..fbc50f2ed9 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -10,9 +10,11 @@ import Factory import CodeEditCore struct GitChangedFileLabel: View { - @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var sourceControlManager: SourceControlManager + @Environment(\.workspaceFileManager) + private var workspaceFileManager + let file: GitChangedFile var body: some View { @@ -21,7 +23,7 @@ struct GitChangedFileLabel: View { .lineLimit(1) .truncationMode(.middle) } icon: { - if let ceFile = workspace.workspaceFileManager?.getFile(file.ceFileKey, createIfNotFound: true) { + if let ceFile = workspaceFileManager?.getFile(file.ceFileKey, createIfNotFound: true) { Image(nsImage: ceFile.nsIcon) .renderingMode(.template) } else { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index 934e765a3b..5d80fc684c 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -12,8 +12,11 @@ import CodeEditCore struct GitChangedFileListView: View { @AppSettings(\.general.fileIconStyle) private var fileIconStyle - @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var sourceControlManager: SourceControlManager + + @Environment(\.workspaceFileManager) + private var workspaceFileManager + @Binding private var changedFile: GitChangedFile @State private var staged: Bool @@ -58,7 +61,7 @@ struct GitChangedFileListView: View { } private var listItemTint: Color { - if let ceFile = workspace.workspaceFileManager?.getFile(changedFile.ceFileKey, createIfNotFound: true) { + if let ceFile = workspaceFileManager?.getFile(changedFile.ceFileKey, createIfNotFound: true) { iconForegroundColor(ceFile) } else { iconForegroundColor(nil) diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift index 1f394b2dc8..78cf8a048a 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift @@ -8,7 +8,6 @@ import SwiftUI struct SourceControlNavigatorToolbarBottom: View { - @EnvironmentObject private var workspace: Workspace @EnvironmentObject var sourceControlManager: SourceControlManager @EnvironmentObject var sourceControlViewModel: SourceControlViewModel diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 8895e2d88d..185d5f0789 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -8,36 +8,34 @@ import SwiftUI struct SourceControlNavigatorView: View { - @EnvironmentObject private var workspace: Workspace + @EnvironmentObject private var sourceControlManager: SourceControlManager + @EnvironmentObject private var sourceControlViewModel: SourceControlViewModel @AppSettings(\.sourceControl.general.fetchRefreshServerStatus) var fetchRefreshServerStatus var body: some View { - if let sourceControlManager = workspace.workspaceFileManager?.sourceControlManager, - let sourceControlViewModel = workspace.sourceControlViewModel { - VStack(spacing: 0) { - SourceControlNavigatorTabs() - .environmentObject(sourceControlManager) - .environmentObject(sourceControlViewModel) - .task { - do { - while true { - if fetchRefreshServerStatus { - try await sourceControlManager.fetch() - } - try await Task.sleep(for: .seconds(10)) + VStack(spacing: 0) { + SourceControlNavigatorTabs() + .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) + .task { + do { + while true { + if fetchRefreshServerStatus { + try await sourceControlManager.fetch() } - } catch { - // TODO: if source fetching fails, display message + try await Task.sleep(for: .seconds(10)) } + } catch { + // TODO: if source fetching fails, display message } - } - .safeAreaInset(edge: .bottom, spacing: 0) { - SourceControlNavigatorToolbarBottom() - .environmentObject(sourceControlManager) - .environmentObject(sourceControlViewModel) - } + } + } + .safeAreaInset(edge: .bottom, spacing: 0) { + SourceControlNavigatorToolbarBottom() + .environmentObject(sourceControlManager) + .environmentObject(sourceControlViewModel) } } } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift index 0867d9bd7e..02f2c038ff 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift @@ -12,10 +12,12 @@ struct SourceControlFetchView: View { private var dismiss @EnvironmentObject var sourceControlManager: SourceControlManager - @EnvironmentObject var workspace: Workspace + + @Environment(\.workspaceFileManager) + private var workspaceFileManager var projectName: String { - workspace.workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" + workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" } var body: some View { diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 46c03c0a33..05553fe67d 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -6,6 +6,7 @@ // import XCTest +import Factory @testable import CodeEdit @MainActor @@ -24,6 +25,11 @@ final class DocumentsUnitTests: XCTestCase { hapticFeedbackPerformerMock = NSHapticFeedbackPerformerMock() navigatorViewModel = .init() workspace.taskManager = TaskManager(workspaceSettings: CEWorkspaceSettingsData(), workspaceURL: nil) + workspace.sourceControlManager = SourceControlManager( + workspaceURL: URL(filePath: "/tmp"), + shellClient: Container.shared.shellClient() + ) + workspace.sourceControlViewModel = SourceControlViewModel() window = NSWindow() splitViewController = .init( workspace: workspace, From d65db8a81efa1bd3da5f951099d4364385df4433 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 12:13:23 +0200 Subject: [PATCH 030/335] Refactor: Migrate Editor tab/jump bar views off Workspace hub --- .../JumpBar/Views/EditorJumpBarComponent.swift | 7 ++++--- .../Editor/TabBar/Tabs/Tab/EditorTabView.swift | 8 +++++--- .../Editor/TabBar/Tabs/Views/EditorTabs.swift | 5 +---- .../TabBar/Views/EditorTabBarContextMenu.swift | 13 +++++++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift index 6f3acfd85c..2cc183e30a 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift @@ -20,7 +20,8 @@ struct EditorJumpBarComponent: View { @Environment(\.controlActiveState) private var activeState - @EnvironmentObject var workspace: Workspace + @Environment(\.workspaceFileManager) + private var workspaceFileManager @State var position: NSPoint? @State var selection: CEWorkspaceFile @@ -42,7 +43,7 @@ struct EditorJumpBarComponent: View { } var siblings: [CEWorkspaceFile] { - guard let fileManager = workspace.workspaceFileManager, + guard let fileManager = workspaceFileManager, let parent = fileItem.parent else { return [fileItem] } @@ -55,7 +56,7 @@ struct EditorJumpBarComponent: View { var body: some View { NSPopUpButtonView(selection: $selection) { - guard let fileManager = workspace.workspaceFileManager else { return NSPopUpButton() } + guard let fileManager = workspaceFileManager else { return NSPopUpButton() } button.menu = EditorJumpBarMenu( fileItems: siblings, diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index f93d105524..823f41220f 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -21,9 +21,11 @@ struct EditorTabView: View { @Environment(\.isFullscreen) private var isFullscreen - @EnvironmentObject var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager + @Environment(\.workspaceFileManager) + private var workspaceFileManager + @StateObject private var fileObserver: EditorTabFileObserver @AppSettings(\.general.fileIconStyle) @@ -265,10 +267,10 @@ struct EditorTabView: View { .tabBarContextMenu(item: tabFile, isTemporary: isTemporary) .accessibilityElement(children: .contain) .onAppear { - workspace.workspaceFileManager?.addObserver(fileObserver) + workspaceFileManager?.addObserver(fileObserver) } .onDisappear { - workspace.workspaceFileManager?.removeObserver(fileObserver) + workspaceFileManager?.removeObserver(fileObserver) } } } diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift index 7704121920..62c5d1838d 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift @@ -15,9 +15,6 @@ struct EditorTabs: View { @Environment(\.colorScheme) private var colorScheme - /// The workspace document. - @EnvironmentObject private var workspace: Workspace - @EnvironmentObject var editor: Editor /// The tab id of current dragging tab. @@ -41,7 +38,7 @@ struct EditorTabs: View { /// Current opened tabs. /// - /// This is a copy of `workspace.selectionState.openedTabs`. + /// This is a copy of `editor.tabs`. /// I am making a copy of it because using state will hugely improve the dragging performance. /// Updating ObservedObject too often will generate lags. @State var openedTabs: [TabID] = [] diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index f2c7f7b4de..bef5d1d759 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -23,7 +23,12 @@ struct EditorTabBarContextMenu: ViewModifier { self.isTemporary = isTemporary } - @EnvironmentObject var workspace: Workspace + @EnvironmentObject var editorManager: EditorManager + + @EnvironmentObject var listenerModel: WorkspaceNotificationModel + + @Environment(\.workspaceFileManager) + private var workspaceFileManager @EnvironmentObject var tabs: Editor @@ -101,7 +106,7 @@ struct EditorTabBarContextMenu: ViewModifier { } Button("Reveal in Project Navigator") { - workspace.listenerModel.highlightedFileItem = item + listenerModel.highlightedFileItem = item } Button("Open in New Window") { @@ -140,13 +145,13 @@ struct EditorTabBarContextMenu: ViewModifier { let newEditor = Editor(files: [item], searchState: tabs.searchState) splitEditor(edge, newEditor) tabs.closeTab(file: item) - workspace.editorManager?.activeEditor = newEditor + editorManager.activeEditor = newEditor } /// Copies the relative path from the workspace folder to the given file item to the pasteboard. /// - Parameter item: The `FileItem` to use. private func copyRelativePath(item: CEWorkspaceFile) { - guard let rootPath = workspace.workspaceFileManager?.folderUrl else { + guard let rootPath = workspaceFileManager?.folderUrl else { return } let destinationComponents = item.url.standardizedFileURL.pathComponents From 2c852d7d9614657af50c3d553cb484a0c2fd46aa Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 12:22:30 +0200 Subject: [PATCH 031/335] Refactor: Migrate UtilityArea, Tasks, Search, OpenQuickly off Workspace hub --- .../Controllers/CodeEditSplitViewController.swift | 2 ++ .../CodeEditWindowController+Toolbar.swift | 4 ++-- .../Controllers/CodeEditWindowController.swift | 2 +- .../FindNavigator/FindNavigatorView.swift | 8 +------- .../OpenQuickly/Views/OpenQuicklyView.swift | 5 +++-- .../Tasks/Views/StartTaskToolbarButton.swift | 6 +++--- .../View/UtilityAreaOutputSourcePicker.swift | 5 +++-- .../UtilityAreaTerminalSidebar.swift | 14 ++++++++------ .../TerminalUtility/UtilityAreaTerminalView.swift | 5 +++-- .../Features/Documents/DocumentsUnitTests.swift | 1 + 10 files changed, 27 insertions(+), 25 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 4686077d3b..265f0bde0c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -57,6 +57,7 @@ final class CodeEditSplitViewController: NSSplitViewController { let projectNavigatorViewModel = workspace.projectNavigatorViewModel, let sourceControlManager = workspace.sourceControlManager, let sourceControlViewModel = workspace.sourceControlViewModel, + let searchState = workspace.searchState, let taskManager = workspace.taskManager else { // swiftlint:disable:next line_length assertionFailure("Missing a workspace model: workspace=\(workspace == nil), navigator=\(navigatorViewModel == nil), editorManager=\(workspace?.editorManager == nil), statusBarModel=\(workspace?.statusBarViewModel == nil), utilityAreaModel=\(workspace?.utilityAreaModel == nil), taskManager=\(workspace?.taskManager == nil)") @@ -73,6 +74,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(projectNavigatorViewModel) .environmentObject(sourceControlManager) .environmentObject(sourceControlViewModel) + .environmentObject(searchState) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) }) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index d86ce792ea..cd3fcd22bd 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -214,11 +214,11 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.startTaskSidebarItem) guard let taskManager = workspace?.taskManager else { return nil } - guard let workspace = workspace else { return nil } + guard let utilityAreaModel = workspace?.utilityAreaModel else { return nil } let view = NSHostingView( rootView: StartTaskToolbarButton(taskManager: taskManager) - .environmentObject(workspace) + .environmentObject(utilityAreaModel) ) toolbarItem.view = view diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index d529dd4a85..5ab0091d0f 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -180,7 +180,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs self.panelOpen = false } openFile: { file in workspace.editorManager?.openTab(item: file) - }.environmentObject(workspace) + }.environment(\.workspaceFileManager, workspace.workspaceFileManager) panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) window?.addChildWindow(panel, ordered: .above) diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift index 423bbaaf95..6030fd2911 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift @@ -8,13 +8,7 @@ import SwiftUI struct FindNavigatorView: View { - @EnvironmentObject private var workspace: Workspace - - private var state: SearchState { - // SearchState is always initialized in Workspace.initWorkspaceState - // before any views are created, so this is safe to force unwrap. - workspace.searchState! - } + @EnvironmentObject private var state: SearchState @State private var foundFilesCount: Int = 0 @State private var searchResultCount: Int = 0 diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift index 32b2482313..2b11a4d9bc 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift @@ -8,7 +8,8 @@ import SwiftUI struct OpenQuicklyView: View { - @EnvironmentObject private var workspace: Workspace + @Environment(\.workspaceFileManager) + private var workspaceFileManager private let onClose: () -> Void private let openFile: (CEWorkspaceFile) -> Void @@ -42,7 +43,7 @@ struct OpenQuicklyView: View { } preview: { searchResult in OpenQuicklyPreviewView(item: CEWorkspaceFile(url: searchResult.fileURL)) } onRowClick: { searchResult in - guard let file = workspace.workspaceFileManager?.getFile( + guard let file = workspaceFileManager?.getFile( searchResult.fileURL.relativePath, createIfNotFound: true ) else { diff --git a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift index 68d464f4db..41ab0a2f91 100644 --- a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift +++ b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift @@ -13,10 +13,10 @@ struct StartTaskToolbarButton: View { private var activeState @ObservedObject var taskManager: TaskManager - @EnvironmentObject var workspace: Workspace + @EnvironmentObject var utilityAreaModel: UtilityAreaViewModel var utilityAreaCollapsed: Bool { - workspace.utilityAreaModel?.isCollapsed ?? true + utilityAreaModel.isCollapsed } var body: some View { @@ -25,7 +25,7 @@ struct StartTaskToolbarButton: View { if utilityAreaCollapsed { Container.shared.commandManager().executeCommand("open.drawer") } - workspace.utilityAreaModel?.selectedTab = .debugConsole + utilityAreaModel.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID } label: { Label("Start", systemImage: "play.fill") diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index bdbdeee416..962e46f5ff 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -11,7 +11,8 @@ import Factory struct UtilityAreaOutputSourcePicker: View { typealias Sources = UtilityAreaOutputView.Sources - @EnvironmentObject private var workspace: Workspace + @Environment(\.workspaceFileURL) + private var workspaceFileURL @AppSettings(\.developerSettings.showInternalDevelopmentInspector) var showInternalDevelopmentInspector @@ -77,7 +78,7 @@ struct UtilityAreaOutputSourcePicker: View { func updateLanguageServers(_ clients: [LSPService.ClientKey: LSPService.LanguageServerType]) { languageServerClients = clients .compactMap { (key, value) in - if key.workspacePath == workspace.fileURL?.absolutePath { + if key.workspacePath == workspaceFileURL?.absolutePath { return value } return nil diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index 8f7b45d755..d6981ca6c6 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -10,7 +10,9 @@ import SwiftUI /// The view that displays the list of available terminals in the utility area. /// See ``UtilityAreaTerminalView`` for use. struct UtilityAreaTerminalSidebar: View { - @EnvironmentObject private var workspace: Workspace + @Environment(\.workspaceFileURL) + private var workspaceFileURL + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel var body: some View { @@ -34,29 +36,29 @@ struct UtilityAreaTerminalSidebar: View { .accentColor(.secondary) .contextMenu { Button("New Terminal") { - utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) + utilityAreaViewModel.addTerminal(rootURL: workspaceFileURL) } Menu("New Terminal With Profile") { Button("Default") { - utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) + utilityAreaViewModel.addTerminal(rootURL: workspaceFileURL) } Divider() ForEach(Shell.allCases, id: \.self) { shell in Button(shell.rawValue) { - utilityAreaViewModel.addTerminal(shell: shell, rootURL: workspace.fileURL) + utilityAreaViewModel.addTerminal(shell: shell, rootURL: workspaceFileURL) } } } } .onChange(of: utilityAreaViewModel.terminals) { _, newValue in if newValue.isEmpty { - utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) + utilityAreaViewModel.addTerminal(rootURL: workspaceFileURL) } } .paneToolbar { PaneToolbarSection { Button { - utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) + utilityAreaViewModel.addTerminal(rootURL: workspaceFileURL) } label: { Image(systemName: "plus") } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 8536aad15e..6bd19cb2f7 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -25,7 +25,8 @@ struct UtilityAreaTerminalView: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject private var workspace: Workspace + @Environment(\.workspaceFileURL) + private var workspaceFileURL @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @@ -162,7 +163,7 @@ struct UtilityAreaTerminalView: View { UtilityAreaTerminalSidebar() } .onAppear { - guard let workspaceURL = workspace.fileURL else { + guard let workspaceURL = workspaceFileURL else { assertionFailure("Workspace does not have a file URL.") return } diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 05553fe67d..139654cd35 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -30,6 +30,7 @@ final class DocumentsUnitTests: XCTestCase { shellClient: Container.shared.shellClient() ) workspace.sourceControlViewModel = SourceControlViewModel() + workspace.searchState = SearchState(workspaceURL: URL(filePath: "/tmp")) window = NSWindow() splitViewController = .init( workspace: workspace, From f9390b7549e89a2cb89ae5d5152944f9212b2050 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 12:46:17 +0200 Subject: [PATCH 032/335] Refactor: Migrate Inspector/WorkspaceView and stop injecting Workspace into main/inspector subtrees --- .../Controllers/CodeEditSplitViewController.swift | 3 +-- .../FileInspector/FileInspectorView.swift | 11 ++++++----- .../InspectorArea/Views/InspectorAreaView.swift | 1 - CodeEdit/WorkspaceView.swift | 15 ++++++++++----- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 265f0bde0c..9e98a914eb 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -84,7 +84,6 @@ final class CodeEditSplitViewController: NSSplitViewController { let workspaceView = SettingsInjector { WindowObserver(window: WindowBox(value: windowRef)) { WorkspaceView() - .environmentObject(workspace) .environmentObject(editorManager) .environmentObject(statusBarViewModel) .environmentObject(utilityAreaModel) @@ -96,6 +95,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(workspace.notificationPanel) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) + .environment(\.workspaceStatePersistence, workspace.statePersistence) } } @@ -107,7 +107,6 @@ final class CodeEditSplitViewController: NSSplitViewController { let inspector = makeInspector(view: SettingsInjector { InspectorAreaView(viewModel: InspectorAreaViewModel()) - .environmentObject(workspace) .environmentObject(editorManager) .environmentObject(sourceControlManager) .environment(\.workspaceFileManager, workspace.workspaceFileManager) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index efa9532cd0..cef7e67262 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -8,7 +8,8 @@ import SwiftUI import CodeEditLanguages struct FileInspectorView: View { - @EnvironmentObject private var workspace: Workspace + @Environment(\.workspaceFileManager) + private var workspaceFileManager @EnvironmentObject private var editorManager: EditorManager @@ -92,9 +93,9 @@ struct FileInspectorView: View { let destinationURL = file.url .deletingLastPathComponent() .appending(path: fileName) - DispatchQueue.main.async { [weak workspace] in + DispatchQueue.main.async { [weak workspaceFileManager] in do { - if let newItem = try workspace?.workspaceFileManager?.move( + if let newItem = try workspaceFileManager?.move( file: file, to: destinationURL ), @@ -141,9 +142,9 @@ struct FileInspectorView: View { } // This is ugly but if the tab is opened at the same time as closing the others, it doesn't open // And if the files are re-built at the same time as the tab is opened, it causes a memory error - DispatchQueue.main.async { [weak workspace] in + DispatchQueue.main.async { [weak workspaceFileManager] in do { - guard let newItem = try workspace?.workspaceFileManager?.move(file: file, to: newURL), + guard let newItem = try workspaceFileManager?.move(file: file, to: newURL), !newItem.isFolder else { return } diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index 923ace3fc6..62f98ca616 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -8,7 +8,6 @@ import SwiftUI struct InspectorAreaView: View { - @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager @ObservedObject private var extensionManager = ExtensionManager.shared @ObservedObject public var viewModel: InspectorAreaViewModel diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 8f09f65451..e9d6fb2305 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -23,9 +23,16 @@ struct WorkspaceView: View { @AppSettings(\.sourceControl.general.sourceControlIsEnabled) var sourceControlIsEnabled - @EnvironmentObject private var workspace: Workspace @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel + @EnvironmentObject private var sourceControlManager: SourceControlManager + @EnvironmentObject private var sourceControlViewModel: SourceControlViewModel + + @Environment(\.workspaceFileManager) + private var workspaceFileManager + + @Environment(\.workspaceStatePersistence) + private var statePersistence @StateObject private var themeModel: ThemeModel = .shared @@ -38,9 +45,7 @@ struct WorkspaceView: View { private let statusbarHeight: CGFloat = 29 var body: some View { - if workspace.workspaceFileManager != nil, - let sourceControlManager = workspace.sourceControlManager, - let sourceControlViewModel = workspace.sourceControlViewModel { + if workspaceFileManager != nil { VStack { SplitViewReader { proxy in SplitView(axis: .vertical) { @@ -112,7 +117,7 @@ struct WorkspaceView: View { .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { output in if let window = output.object as? NSWindow, self.window == window { - workspace.statePersistence?.set( + statePersistence?.set( key: .workspaceWindowSize, value: NSStringFromRect(window.frame) ) From 54b5116fa1abfbc3c84cf8362830cee15775048e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 15:40:50 +0200 Subject: [PATCH 033/335] Fix: Route welcome window recents through WorkspaceWindowManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a recent folder in the welcome window crashed with an NSInternalInconsistencyException (readFromFileWrapper:ofType:error: must be overridden by CodeFileDocument). The welcome window's recents list fell back to the package's default NSDocumentController.openDocument(at:), which — after the NSDocument-architecture removal dropped the custom document controller — opened folders as CodeFileDocument and failed on the directory. Pass an openHandler that routes URLs through WorkspaceWindowManager.openDocument(at:) so folders open as workspaces, matching the menu-bar recents and the onDrop handler. --- CodeEdit/CodeEditApp.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 0eadde5056..34e272a235 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -41,6 +41,13 @@ struct CodeEditApp: App { print("Failed to open workspace: \(error)") } } + }, + openHandler: { urls, dismissWindow in + let windowManager = Container.shared.workspaceWindowManager() + for url in urls { + windowManager.openDocument(at: url, onCompletion: {}) + } + dismissWindow() } ) From 74004a0194aad3de5b310833027f7959a1c7910c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 16:14:33 +0200 Subject: [PATCH 034/335] Fix: Hold security-scoped access for workspaces opened from recents Recents are persisted as security-scoped bookmarks; opening one gave an empty project navigator in the sandboxed build because the resolved URL's security scope was never activated, so CEWorkspaceFileManager couldn't enumerate the directory. WorkspaceFactory now starts security-scoped access on the original URL when opening (no-op for open-panel/Powerbox URLs), and Workspace.tearDown releases it. Fixes both the welcome-window and menu-bar recents paths. --- CodeEdit/Features/Workspace/Models/Workspace.swift | 8 ++++++++ CodeEdit/Features/Workspace/WorkspaceFactory.swift | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 66543bd3c1..0b08620b88 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -39,6 +39,11 @@ final class Workspace: ObservableObject, WorkspaceManaging { var notificationPanel = NotificationPanelViewModel() + /// The original (possibly bookmark-derived) security-scoped URL whose access is held for this + /// workspace's lifetime. Set by `WorkspaceFactory` when the URL is security-scoped (e.g. opened + /// from recents in the sandbox); released in ``tearDown()``. + var securityScopedURL: URL? + // MARK: - Initialization init(url: URL) { @@ -71,6 +76,9 @@ final class Workspace: ObservableObject, WorkspaceManaging { workspaceSettingsManager = nil taskManager = nil statePersistence = nil + + securityScopedURL?.stopAccessingSecurityScopedResource() + securityScopedURL = nil } // MARK: - Unsaved Changes diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 3224f71b08..fd3025a53c 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -26,6 +26,14 @@ enum WorkspaceFactory { /// - url: The root URL of the workspace folder. @MainActor static func populate(_ workspace: Workspace, url: URL) { + // Begin security-scoped access on the original (possibly bookmark-derived) URL so a + // sandboxed build can read a workspace opened from recents. `startAccessingSecurityScopedResource` + // returns `false` for non-scoped URLs (e.g. from the open panel / Powerbox), which access + // fine without it. Released in `Workspace.tearDown`. + if url.startAccessingSecurityScopedResource() { + workspace.securityScopedURL = url + } + // Normalize the URL to always end with "/" var url = url if !url.absoluteString.hasSuffix("/") { From a2fff0c95a6ef79a41e0d8b0648e37c0aada84f9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 16:34:51 +0200 Subject: [PATCH 035/335] Fix: Disable App Sandbox for the app so git/LSP/terminal work PR #2147 enabled App Sandbox (added com.apple.security.app-sandbox and ENABLE_APP_SANDBOX = YES, and dropped the cs.allow-jit / cs.disable-library-validation exceptions). The sandbox blocks CodeEdit from spawning subprocesses, breaking git ("xcrun: error: cannot be used within an App Sandbox"), LSP servers, the terminal, and package installs. Restore the pre-#2147 non-sandboxed configuration: remove the app-sandbox entitlement, set ENABLE_APP_SANDBOX = NO on the app target's build configs, and re-add the JIT / library-validation hardened-runtime exceptions. The OpenWithCodeEdit extension is untouched (extensions must remain sandboxed). --- CodeEdit.xcodeproj/project.pbxproj | 10 +++++----- CodeEdit/CodeEdit.entitlements | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index a4316a47ca..3b46b4e646 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -684,7 +684,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -885,7 +885,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -1158,7 +1158,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -1431,7 +1431,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -1475,7 +1475,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; diff --git a/CodeEdit/CodeEdit.entitlements b/CodeEdit/CodeEdit.entitlements index 83a78156ea..f8765c8a2d 100644 --- a/CodeEdit/CodeEdit.entitlements +++ b/CodeEdit/CodeEdit.entitlements @@ -2,10 +2,12 @@ - com.apple.security.app-sandbox - com.apple.security.application-groups + com.apple.security.cs.allow-jit + + com.apple.security.cs.disable-library-validation + com.apple.security.files.bookmarks.app-scope com.apple.security.files.user-selected.read-write From d34c8423c8f865c17db3c5a2afb4d547a309b49d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 21:22:26 +0200 Subject: [PATCH 036/335] Refactor: Scaffold CodeEditUI package and wire it into the workspace and app --- CodeEdit.xcodeproj/project.pbxproj | 7 ++++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 +++ Packages/CodeEditUI/Package.resolved | 15 +++++++++++++ Packages/CodeEditUI/Package.swift | 22 +++++++++++++++++++ .../Sources/CodeEditUI/_Placeholder.swift | 9 ++++++++ .../Tests/CodeEditUITests/_Placeholder.swift | 5 +++++ 6 files changed, 61 insertions(+) create mode 100644 Packages/CodeEditUI/Package.resolved create mode 100644 Packages/CodeEditUI/Package.swift create mode 100644 Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift create mode 100644 Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 3b46b4e646..05f74adf65 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 30818CB42D4E563900967860 /* ZIPFoundation */; }; 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; + 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; @@ -177,6 +178,7 @@ 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */, 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, + 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, @@ -345,6 +347,7 @@ 6CCF73CF2E26DE3200B94F75 /* SwiftTerm */, 58CF9F392F86D64F009F4AA7 /* Factory */, 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, + 5800E2F72FF843390085ECF1 /* CodeEditUI */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1887,6 +1890,10 @@ package = 30CB64922C16CA9100CC8A9E /* XCRemoteSwiftPackageReference "LanguageClient" */; productName = LanguageClient; }; + 5800E2F72FF843390085ECF1 /* CodeEditUI */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditUI; + }; 583E529B29361BAB001AB554 /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 7ceeb7c98c..9574b3ad98 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -7,4 +7,7 @@ + + diff --git a/Packages/CodeEditUI/Package.resolved b/Packages/CodeEditUI/Package.resolved new file mode 100644 index 0000000000..7abbbdc792 --- /dev/null +++ b/Packages/CodeEditUI/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "3c000b73fbec03fd047682022ccf98d1b2e50c527b016e1d0d54ae0a5a45decb", + "pins" : [ + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + } + ], + "version" : 3 +} diff --git a/Packages/CodeEditUI/Package.swift b/Packages/CodeEditUI/Package.swift new file mode 100644 index 0000000000..837186cd4f --- /dev/null +++ b/Packages/CodeEditUI/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CodeEditUI", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CodeEditUI", targets: ["CodeEditUI"]) + ], + dependencies: [ + // Pin matches the app's Package.resolved to avoid a second resolved copy. + .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3") + ], + targets: [ + .target( + name: "CodeEditUI", + dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")] + ), + .testTarget(name: "CodeEditUITests", dependencies: ["CodeEditUI"]) + ] +) diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift b/Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift new file mode 100644 index 0000000000..597d67b39c --- /dev/null +++ b/Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift @@ -0,0 +1,9 @@ +// +// _Placeholder.swift +// CodeEditUI +// +// Created by Matthijs Eikelenboom on 03/07/2026. +// + +// Temporary: keeps the target non-empty until components are moved in (Task 3). Removed then. +enum CodeEditUIPlaceholder {} diff --git a/Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift b/Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift new file mode 100644 index 0000000000..63a1ca3e10 --- /dev/null +++ b/Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift @@ -0,0 +1,5 @@ +import XCTest + +final class CodeEditUIPlaceholderTests: XCTestCase { + func testPackageLoads() { XCTAssertTrue(true) } +} From 1e9c249c3326b5a9c1473a0a80a7dc7d6b8638f4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 21:23:59 +0200 Subject: [PATCH 037/335] Refactor: Remove unused CodeEditUI views (ScrollOffsetPreferenceKey, SettingsTextEditor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Divided was NOT removed — it is used by SourceControlNavigatorChangesView, so it will move into the CodeEditUI package instead. --- .../Views/ScrollOffsetPreferenceKey.swift | 10 --- .../CodeEditUI/Views/SettingsTextEditor.swift | 80 ------------------- 2 files changed, 90 deletions(-) delete mode 100644 CodeEdit/Features/CodeEditUI/Views/ScrollOffsetPreferenceKey.swift delete mode 100644 CodeEdit/Features/CodeEditUI/Views/SettingsTextEditor.swift diff --git a/CodeEdit/Features/CodeEditUI/Views/ScrollOffsetPreferenceKey.swift b/CodeEdit/Features/CodeEditUI/Views/ScrollOffsetPreferenceKey.swift deleted file mode 100644 index 13ca285116..0000000000 --- a/CodeEdit/Features/CodeEditUI/Views/ScrollOffsetPreferenceKey.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -/// Tracks scroll offset in scrollable views -struct ScrollOffsetPreferenceKey: PreferenceKey { - typealias Value = CGFloat - static var defaultValue = CGFloat.zero - static func reduce(value: inout Value, nextValue: () -> Value) { - value += nextValue() - } -} diff --git a/CodeEdit/Features/CodeEditUI/Views/SettingsTextEditor.swift b/CodeEdit/Features/CodeEditUI/Views/SettingsTextEditor.swift deleted file mode 100644 index 54328c2c82..0000000000 --- a/CodeEdit/Features/CodeEditUI/Views/SettingsTextEditor.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// SettingsTextEditor.swift -// -// -// Created by Andrey Plotnikov on 07.05.2022. -// - -import Foundation -import SwiftUI - -struct SettingsTextEditor: View { - @State private var isFocus: Bool = false - - @Binding var text: String - - init(text: Binding) { - self._text = text - } - - var body: some View { - Representable(text: $text, isFocused: $isFocus) - .overlay(focusOverlay) - } - - private var focusOverlay: some View { - Rectangle().stroke(Color.accentColor.opacity(isFocus ? 0.4 : 0), lineWidth: 2) - } -} - -private extension SettingsTextEditor { - struct Representable: NSViewRepresentable { - - @Binding var text: String - @Binding var isFocused: Bool - - func makeNSView(context: Context) -> NSScrollView { - let scrollView = NSTextView.scrollableTextView() - scrollView.verticalScroller?.alphaValue = 0 - let textView = scrollView.documentView as? NSTextView - textView?.backgroundColor = .windowBackgroundColor - textView?.isEditable = true - textView?.delegate = context.coordinator - textView?.string = text - return scrollView - } - - func updateNSView(_ nsView: NSScrollView, context: Context) { - - } - - func makeCoordinator() -> Coordinator { - Coordinator(parent: self) - } - - class Coordinator: NSObject, NSTextViewDelegate { - var parent: Representable - - init(parent: Representable) { - self.parent = parent - } - - func textDidBeginEditing(_ notification: Notification) { - parent.isFocused = true - } - - func textDidEndEditing(_ notification: Notification) { - parent.isFocused = false - } - - func textDidChange(_ notification: Notification) { - guard let textView = notification.object as? NSTextView else { - return - } - // Update text - self.parent.text = textView.string - } - } - } - -} From 54b03f036389dc5c38d7b85d6b28c706a7a76c45 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 21:28:59 +0200 Subject: [PATCH 038/335] Refactor: Move shared button/toggle styles into CodeEditUI package --- CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift | 1 + .../Editor/TabBar/Views/EditorHistoryMenus.swift | 1 + .../Editor/TabBar/Views/EditorTabBarAccessory.swift | 1 + .../TabBar/Views/EditorTabBarTrailingAccessories.swift | 1 + .../ProjectNavigatorToolbarBottom.swift | 1 + .../Notifications/Views/NotificationBannerView.swift | 1 + .../Pages/GeneralSettings/View+actionBar.swift | 1 + .../Pages/ThemeSettings/ThemeSettingThemeRow.swift | 1 + .../Pages/ThemeSettings/ThemeSettingsThemeToken.swift | 1 + .../Pages/ThemeSettings/ThemeSettingsView.swift | 1 + CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift | 1 + .../StatusBarToggleUtilityAreaButton.swift | 1 + .../DebugUtility/TaskOutputActionsView.swift | 1 + CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift | 1 + .../UtilityArea/Views/UtilityAreaTabView.swift | 1 + .../Sources}/CodeEditUI/Styles/IconButtonStyle.swift | 6 +++--- .../Sources}/CodeEditUI/Styles/IconToggleStyle.swift | 6 +++--- .../CodeEditUI/Styles/MenuWithButtonStyle.swift | 10 ++++++++-- .../CodeEditUI/Styles/OverlayButtonStyle.swift | 6 +++--- .../CodeEditUI/Sources/CodeEditUI/_Placeholder.swift | 9 --------- 20 files changed, 32 insertions(+), 20 deletions(-) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Styles/IconButtonStyle.swift (95%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Styles/IconToggleStyle.swift (88%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Styles/MenuWithButtonStyle.swift (79%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Styles/OverlayButtonStyle.swift (84%) delete mode 100644 Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift diff --git a/CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift b/CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift index 3997a9dd55..e9b40ef41b 100644 --- a/CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift +++ b/CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import Combine struct PaneTextField: View { diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift index f223f69c25..44b41d0a1c 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct EditorHistoryMenus: View { @EnvironmentObject private var editorManager: EditorManager diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarAccessory.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarAccessory.swift index a8fb5f0c2b..6f98f2f0f3 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarAccessory.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarAccessory.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI /// Accessory icon's view for tab bar. struct EditorTabBarAccessoryIcon: View { diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 295fde8d77..f5a1809fd5 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct EditorTabBarTrailingAccessories: View { @AppSettings(\.textEditing.wrapLinesToEditorWidth) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index b6dd1ea2ef..30acc94550 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct ProjectNavigatorToolbarBottom: View { @Environment(\.controlActiveState) diff --git a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift index cdaef53298..1f1343fb0d 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift +++ b/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct NotificationBannerView: View { @Environment(\.colorScheme) diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift b/CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift index 2da0f779af..0cb266b14b 100644 --- a/CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift +++ b/CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI extension View { func actionBar(@ViewBuilder content: () -> Content) -> some View { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index 58f2403de5..864802745b 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct ThemeSettingsThemeRow: View { @Binding var theme: Theme diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift index d02f2b912b..7c84ba3188 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct ThemeSettingsThemeToken: View { var label: String diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 04c94db1b6..f56fdced00 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI /// A view that implements the `Theme` preference section struct ThemeSettingsView: View { diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift b/CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift index 0df163ba9e..c34415189d 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI /// Accessory icon view for status bar. struct StatusBarIcon: View { diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift index 442762c0f6..08252569bb 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import Factory internal struct StatusBarToggleUtilityAreaButton: View { diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift b/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift index 34f28a18bf..ae8b9652a2 100644 --- a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift +++ b/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct TaskOutputActionsView: View { @ObservedObject var activeTask: CEActiveTask diff --git a/CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift b/CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift index 658cc46fca..b3ecc0f954 100644 --- a/CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift +++ b/CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct PaneToolbar: View { @ViewBuilder var content: Content diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTabView.swift b/CodeEdit/Features/UtilityArea/Views/UtilityAreaTabView.swift index bb36bcc5ab..ac695b7938 100644 --- a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTabView.swift +++ b/CodeEdit/Features/UtilityArea/Views/UtilityAreaTabView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct UtilityAreaTabView: View { @ObservedObject var model: UtilityAreaTabViewModel diff --git a/CodeEdit/Features/CodeEditUI/Styles/IconButtonStyle.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift similarity index 95% rename from CodeEdit/Features/CodeEditUI/Styles/IconButtonStyle.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift index dfc6ff7852..bcc1c2ee4c 100644 --- a/CodeEdit/Features/CodeEditUI/Styles/IconButtonStyle.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift @@ -7,7 +7,7 @@ import SwiftUI -struct IconButtonStyle: ButtonStyle { +public struct IconButtonStyle: ButtonStyle { var isActive: Bool? var font: Font? var size: CGSize? @@ -30,7 +30,7 @@ struct IconButtonStyle: ButtonStyle { self.size = nil } - func makeBody(configuration: ButtonStyle.Configuration) -> some View { + public func makeBody(configuration: ButtonStyle.Configuration) -> some View { IconButton( configuration: configuration, isActive: isActive, @@ -95,7 +95,7 @@ struct IconButtonStyle: ButtonStyle { } } -extension ButtonStyle where Self == IconButtonStyle { +public extension ButtonStyle where Self == IconButtonStyle { static func icon( isActive: Bool? = false, font: Font? = Font.system(size: 14.5, weight: .regular, design: .default), diff --git a/CodeEdit/Features/CodeEditUI/Styles/IconToggleStyle.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift similarity index 88% rename from CodeEdit/Features/CodeEditUI/Styles/IconToggleStyle.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift index 2382dfc346..e734e8baf2 100644 --- a/CodeEdit/Features/CodeEditUI/Styles/IconToggleStyle.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift @@ -7,7 +7,7 @@ import SwiftUI -struct IconToggleStyle: ToggleStyle { +public struct IconToggleStyle: ToggleStyle { var font: Font? var size: CGSize? @@ -28,7 +28,7 @@ struct IconToggleStyle: ToggleStyle { self.size = nil } - func makeBody(configuration: ToggleStyle.Configuration) -> some View { + public func makeBody(configuration: ToggleStyle.Configuration) -> some View { Button( action: { configuration.isOn.toggle() }, label: { configuration.label } @@ -37,7 +37,7 @@ struct IconToggleStyle: ToggleStyle { } } -extension ToggleStyle where Self == IconToggleStyle { +public extension ToggleStyle where Self == IconToggleStyle { static func icon( font: Font? = Font.system(size: 14.5, weight: .regular, design: .default), size: CGFloat? = 24 diff --git a/CodeEdit/Features/CodeEditUI/Styles/MenuWithButtonStyle.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift similarity index 79% rename from CodeEdit/Features/CodeEditUI/Styles/MenuWithButtonStyle.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift index 2e432354f4..23195dbc6a 100644 --- a/CodeEdit/Features/CodeEditUI/Styles/MenuWithButtonStyle.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift @@ -8,10 +8,16 @@ import SwiftUI /// A menu styled to resemble a bordered button. -struct MenuWithButtonStyle: View { +public struct MenuWithButtonStyle: View { var systemImage: String var menu: () -> MenuView - var body: some View { + + public init(systemImage: String, menu: @escaping () -> MenuView) { + self.systemImage = systemImage + self.menu = menu + } + + public var body: some View { Menu { menu() } label: {} .background { Button {} label: { diff --git a/CodeEdit/Features/CodeEditUI/Styles/OverlayButtonStyle.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift similarity index 84% rename from CodeEdit/Features/CodeEditUI/Styles/OverlayButtonStyle.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift index 7e9b962c68..4d03301bd0 100644 --- a/CodeEdit/Features/CodeEditUI/Styles/OverlayButtonStyle.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift @@ -1,11 +1,11 @@ import SwiftUI /// A button style for overlay buttons (like close, action buttons in notifications) -struct OverlayButtonStyle: ButtonStyle { +public struct OverlayButtonStyle: ButtonStyle { @Environment(\.colorScheme) private var colorScheme - func makeBody(configuration: Configuration) -> some View { + public func makeBody(configuration: Configuration) -> some View { configuration.label .font(.system(size: 10)) .foregroundColor(.secondary) @@ -26,7 +26,7 @@ struct OverlayButtonStyle: ButtonStyle { } } -extension ButtonStyle where Self == OverlayButtonStyle { +public extension ButtonStyle where Self == OverlayButtonStyle { /// A button style for overlay buttons static var overlay: OverlayButtonStyle { OverlayButtonStyle() diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift b/Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift deleted file mode 100644 index 597d67b39c..0000000000 --- a/Packages/CodeEditUI/Sources/CodeEditUI/_Placeholder.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// _Placeholder.swift -// CodeEditUI -// -// Created by Matthijs Eikelenboom on 03/07/2026. -// - -// Temporary: keeps the target non-empty until components are moved in (Task 3). Removed then. -enum CodeEditUIPlaceholder {} From f0f213c3e9c4d75a49efc5d269416ff69b08fd38 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 21:49:27 +0200 Subject: [PATCH 039/335] Refactor: Move shared UI views into CodeEditUI package Move 17 clean presentation views into CodeEditUI (public API) and add import CodeEditUI across ~52 app call sites. PopoverContainer's .if(.tahoe) was inlined to avoid dragging the app-wide View+if util into the package. KeyValueTable was kept in the app (depends on Settings' .actionBar overlay). Minor Swift 6 concurrency fixes required by the package's strict-concurrency build (TrackableScrollView PreferenceKey static let; InstantPopoverModifier Coordinator @MainActor with main-queue-delivered assumeIsolated). --- .../Tasks/DropdownMenuItemStyleModifier.swift | 1 + .../Tasks/SchemeDropDownView.swift | 1 + .../Tasks/TaskDropDownView.swift | 1 + .../CodeEditUI/Views/KeyValueTable.swift | 24 ++++++++----- .../CodeEditUI/Views/PopoverContainer.swift | 31 ---------------- .../CodeEditUI/Views/SearchPanelView.swift | 1 + .../CodeEditUI/Views/WorkspacePanelView.swift | 1 + .../CodeEditWindowController.swift | 1 + .../TabBar/Tabs/Tab/EditorTabBackground.swift | 1 + .../Editor/TabBar/Tabs/Views/EditorTabs.swift | 1 + .../Features/Editor/Views/CodeFileView.swift | 1 + .../Editor/Views/EditorAreaView.swift | 1 + CodeEdit/Features/Feedback/FeedbackView.swift | 1 + .../HistoryInspectorItemView.swift | 1 + .../HistoryInspectorView.swift | 1 + .../HistoryInspector/HistoryPopoverView.swift | 1 + .../Views/NoSelectionInspectorView.swift | 1 + .../FindNavigator/FindNavigatorForm.swift | 1 + .../FindNavigatorToolbarBottom.swift | 1 + .../FindNavigator/FindNavigatorView.swift | 1 + ...rceControlNavigatorChangesCommitView.swift | 1 + .../SourceControlNavigatorChangesView.swift | 1 + .../History/Views/CommitDetailsView.swift | 1 + .../SourceControlNavigatorHistoryView.swift | 1 + ...SourceControlNavigatorRepositoryView.swift | 1 + .../SourceControlNavigatorToolbarBottom.swift | 1 + .../Views/SourceControlNavigatorView.swift | 1 + .../AccountsSettingsAccountLink.swift | 1 + .../AccountsSettingsProviderRow.swift | 1 + .../AccountsSettingsSigninView.swift | 1 + .../DeveloperSettingsView.swift | 1 + .../LanguageServerInstallView.swift | 1 + .../SourceControlSettingsView.swift | 1 + .../Views/InvisibleCharacterWarningList.swift | 1 + .../Settings/Views/SettingsForm.swift | 1 + .../Settings/Views/SettingsPageView.swift | 1 + .../DebugUtility/UtilityAreaDebugView.swift | 1 + .../UtilityAreaTerminalView.swift | 1 + CodeEdit/WorkspaceView.swift | 1 + .../Features/CodeEditUI/CodeEditUITests.swift | 1 + .../Views/CEContentUnavailableView.swift | 6 ++-- .../CodeEditUI/Views/CEOutlineGroup.swift | 4 +-- .../Sources}/CodeEditUI/Views/Divided.swift | 6 ++-- .../CodeEditUI/Views/EffectView.swift | 10 +++--- .../Views/ErrorDescriptionLabel.swift | 8 +++-- .../CodeEditUI/Views/FeatureIcon.swift | 10 +++--- .../CodeEditUI/Views/GlassEffectView.swift | 8 ++--- .../CodeEditUI/Views/HelpButton.swift | 6 ++-- .../Views/InstantPopoverModifier.swift | 22 ++++++------ .../CodeEditUI/Views/PaneTextField.swift | 7 ++-- .../CodeEditUI/Views/PanelDivider.swift | 6 ++-- .../CodeEditUI/Views/PopoverContainer.swift | 36 +++++++++++++++++++ .../Views/PressActionsModifier.swift | 8 ++--- .../CodeEditUI/Views/SearchField.swift | 14 ++++---- .../CodeEditUI/Views/SearchPanel.swift | 8 ++--- .../CodeEditUI/Views/SegmentedControl.swift | 10 +++--- .../Views/TrackableScrollView.swift | 10 +++--- 57 files changed, 163 insertions(+), 109 deletions(-) delete mode 100644 CodeEdit/Features/CodeEditUI/Views/PopoverContainer.swift rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/CEContentUnavailableView.swift (93%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/CEOutlineGroup.swift (93%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/Divided.swift (82%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/EffectView.swift (88%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/ErrorDescriptionLabel.swift (84%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/FeatureIcon.swift (95%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/GlassEffectView.swift (72%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/HelpButton.swift (90%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/InstantPopoverModifier.swift (88%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/PaneTextField.swift (96%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/PanelDivider.swift (78%) create mode 100644 Packages/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/PressActionsModifier.swift (82%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/SearchField.swift (65%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/SearchPanel.swift (74%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/SegmentedControl.swift (95%) rename {CodeEdit/Features => Packages/CodeEditUI/Sources}/CodeEditUI/Views/TrackableScrollView.swift (95%) diff --git a/CodeEdit/Features/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift b/CodeEdit/Features/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift index 89226ae038..1d12f8e1d9 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI extension View { @ViewBuilder diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift index 5067871f69..337c1b3249 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SchemeDropDownView: View { @Environment(\.colorScheme) diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift index 6ce8699311..156c53021b 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct TaskDropDownView: View { @Environment(\.colorScheme) diff --git a/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift b/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift index 4fc7ab95d8..eeccb1eccb 100644 --- a/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift +++ b/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift @@ -6,11 +6,17 @@ // import SwiftUI +import CodeEditUI -struct KeyValueItem: Identifiable, Equatable { - let id = UUID() - let key: String - let value: String +public struct KeyValueItem: Identifiable, Equatable { + public let id = UUID() + public let key: String + public let value: String + + public init(key: String, value: String) { + self.key = key + self.value = value + } } private struct NewListTableItemView: View { @@ -27,7 +33,7 @@ private struct NewListTableItemView: View { let headerView: HeaderView? var completion: (String, String) -> Void - init( + public init( key: String? = nil, value: String? = nil, _ keyColumnName: String, @@ -47,7 +53,7 @@ private struct NewListTableItemView: View { self.completion = completion } - var body: some View { + public var body: some View { VStack(spacing: 0) { Form { Section { @@ -102,7 +108,7 @@ private struct NewListTableItemView: View { } } -struct KeyValueTable: View { +public struct KeyValueTable: View { @Binding var items: [String: String] let validKeys: [String] @@ -116,7 +122,7 @@ struct KeyValueTable: View { @State private var selection: Set = [] @State private var tableItems: [KeyValueItem] = [] - init( + public init( items: Binding<[String: String]>, validKeys: [String] = [], keyColumnName: String, @@ -134,7 +140,7 @@ struct KeyValueTable: View { self.actionBarTrailing = actionBarTrailing } - var body: some View { + public var body: some View { Table(tableItems, selection: $selection) { TableColumn(keyColumnName) { item in Text(item.key) diff --git a/CodeEdit/Features/CodeEditUI/Views/PopoverContainer.swift b/CodeEdit/Features/CodeEditUI/Views/PopoverContainer.swift deleted file mode 100644 index 7d62b94f56..0000000000 --- a/CodeEdit/Features/CodeEditUI/Views/PopoverContainer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PopoverContainer.swift -// CodeEdit -// -// Created by Khan Winter on 8/29/25. -// - -import SwiftUI - -/// Container for SwiftUI views presented in a popover. -/// On tahoe and above, adds the correct container shape. -struct PopoverContainer: View { - let content: () -> ContentView - - init(@ViewBuilder content: @escaping () -> ContentView) { - self.content = content - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - content() - } - .font(.subheadline) - .if(.tahoe) { - $0.padding(13).containerShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) - } else: { - $0.padding(5) - } - .frame(minWidth: 215) - } -} diff --git a/CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift b/CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift index 973a849acd..49b5178f44 100644 --- a/CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift +++ b/CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import CodeEditUI struct SearchPanelView: View { @ViewBuilder let rowViewBuilder: ((Option) -> RowView) diff --git a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift b/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift index 4637d2785e..71ff220821 100644 --- a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift +++ b/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct WorkspacePanelView: View { @ObservedObject var viewModel: ViewModel diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 5ab0091d0f..eab10c6e15 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -7,6 +7,7 @@ import Cocoa import SwiftUI +import CodeEditUI import Factory import Combine diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift index 91c9ccd514..e1fc801298 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct EditorTabBackground: View { var isActive: Bool diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift index 62c5d1838d..35467bb46f 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI // - TODO: EditorTabView drop-outside event handler. diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index f22f6cce3d..a0b16da6af 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import CodeEditUI import CodeEditSourceEditor import CodeEditTextView import CodeEditLanguages diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index f2659969c6..790950477f 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditTextView import UniformTypeIdentifiers diff --git a/CodeEdit/Features/Feedback/FeedbackView.swift b/CodeEdit/Features/Feedback/FeedbackView.swift index 03586cc845..7794423851 100644 --- a/CodeEdit/Features/Feedback/FeedbackView.swift +++ b/CodeEdit/Features/Feedback/FeedbackView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct FeedbackView: View { @ObservedObject private var feedbackModel: FeedbackModel = .shared diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift index 918157d3b9..307e6b143e 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore struct HistoryInspectorItemView: View { diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index ec4263a761..2d4e022026 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI +import CodeEditUI import CodeEditCore struct HistoryInspectorView: View { diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift index 3c88d43385..98ed0bb861 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore struct HistoryPopoverView: View { diff --git a/CodeEdit/Features/InspectorArea/Views/NoSelectionInspectorView.swift b/CodeEdit/Features/InspectorArea/Views/NoSelectionInspectorView.swift index 6e5d4f7ad3..0115ac7349 100644 --- a/CodeEdit/Features/InspectorArea/Views/NoSelectionInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/Views/NoSelectionInspectorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct NoSelectionInspectorView: View { var body: some View { diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift index 58c6d1283d..f6ce860729 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore struct FindNavigatorForm: View { diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorToolbarBottom.swift index 073296f719..dab5aba217 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorToolbarBottom.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct FindNavigatorToolbarBottom: View { @State private var text = "" diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift index 6030fd2911..609b830f80 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct FindNavigatorView: View { @EnvironmentObject private var state: SearchState diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift index bdc273160f..4c37c944f1 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SourceControlNavigatorChangesCommitView: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift index 3d84d310b4..ab4b5fc48d 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SourceControlNavigatorChangesView: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift index 5a1f84d039..d35c322d32 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore struct CommitDetailsView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index 97392eb4a7..6ee98a7730 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore import CodeEditSymbols diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift index d0c84b7b41..9dd8f90fb3 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore import CodeEditSymbols diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift index 78cf8a048a..69c776c273 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SourceControlNavigatorToolbarBottom: View { @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 185d5f0789..90df569130 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SourceControlNavigatorView: View { @EnvironmentObject private var sourceControlManager: SourceControlManager diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift index d655a8d16e..709b7f01e0 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct AccountsSettingsAccountLink: View { @Binding var account: SourceControlAccount diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift index cc9c687dcf..eb6b13aa81 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct AccountsSettingsProviderRow: View { var name: String diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift index 1b648057c0..4de2a2f996 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct AccountsSettingsSigninView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift index 0b1bcf0ba4..bf9e3eaf58 100644 --- a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import LanguageServerProtocol /// A view that implements the Developer settings section diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift index 7562668c65..cc629a92d1 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI /// A view for initiating a package install and monitoring progress. struct LanguageServerInstallView: View { diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift index 14ee02523f..5fc1ae18e8 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SourceControlSettingsView: View { @AppSettings(\.sourceControl.general) diff --git a/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift b/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift index cf7bd58f20..ef0fae3f6e 100644 --- a/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift +++ b/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct InvisibleCharacterWarningList: View { @Binding var items: [UInt16: String] diff --git a/CodeEdit/Features/Settings/Views/SettingsForm.swift b/CodeEdit/Features/Settings/Views/SettingsForm.swift index ee093864a9..085564174b 100644 --- a/CodeEdit/Features/Settings/Views/SettingsForm.swift +++ b/CodeEdit/Features/Settings/Views/SettingsForm.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import SwiftUIIntrospect struct SettingsForm: View { diff --git a/CodeEdit/Features/Settings/Views/SettingsPageView.swift b/CodeEdit/Features/Settings/Views/SettingsPageView.swift index caf1c46e40..1713799470 100644 --- a/CodeEdit/Features/Settings/Views/SettingsPageView.swift +++ b/CodeEdit/Features/Settings/Views/SettingsPageView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct SettingsPageView: View { var page: SettingsPage diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift b/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift index 996a66cbd3..211a080d40 100644 --- a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift +++ b/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct UtilityAreaDebugView: View { @AppSettings(\.theme.matchAppearance) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 6bd19cb2f7..611944a657 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import Cocoa struct UtilityAreaTerminalView: View { diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index e9d6fb2305..cdf8460c61 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import UniformTypeIdentifiers struct WorkspaceView: View { diff --git a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift index 335eef00d4..2774230642 100644 --- a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift +++ b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift @@ -6,6 +6,7 @@ // @testable import CodeEdit +import CodeEditUI import Foundation import SnapshotTesting import SwiftUI diff --git a/CodeEdit/Features/CodeEditUI/Views/CEContentUnavailableView.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift similarity index 93% rename from CodeEdit/Features/CodeEditUI/Views/CEContentUnavailableView.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift index 818185b808..577e826dbf 100644 --- a/CodeEdit/Features/CodeEditUI/Views/CEContentUnavailableView.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift @@ -7,13 +7,13 @@ import SwiftUI -struct CEContentUnavailableView: View { +public struct CEContentUnavailableView: View { var label: String var description: String? var systemImage: String? var actions: Actions? - init( + public init( _ label: String, description: String? = nil, systemImage: String? = nil, @@ -52,7 +52,7 @@ struct CEContentUnavailableView: View { .controlSize(.small) } - var body: some View { + public var body: some View { if #available(macOS 14, *) { contentUnavailableView .buttonStyle(.accessoryBarAction) diff --git a/CodeEdit/Features/CodeEditUI/Views/CEOutlineGroup.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift similarity index 93% rename from CodeEdit/Features/CodeEditUI/Views/CEOutlineGroup.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift index 9715ffb4a5..13e4673052 100644 --- a/CodeEdit/Features/CodeEditUI/Views/CEOutlineGroup.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift @@ -9,7 +9,7 @@ import SwiftUI // This view replaces OutlineGroup, which lacks support for controlling the expanded state. -struct CEOutlineGroup: View where DataElement: Identifiable, ID: Hashable, Leaf: View { +public struct CEOutlineGroup: View where DataElement: Identifiable, ID: Hashable, Leaf: View { let root: DataElement var expandedIds: Binding<[ID: Bool]>? @State var expanded: Bool = false @@ -44,7 +44,7 @@ struct CEOutlineGroup: View where DataElement: Identifiab .tag(root[keyPath: idKeyPath]) } - var body: some View { + public var body: some View { switch root[keyPath: childrenKeyPath] { case .none: itemView diff --git a/CodeEdit/Features/CodeEditUI/Views/Divided.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift similarity index 82% rename from CodeEdit/Features/CodeEditUI/Views/Divided.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift index 7a64f5a33c..b42e405fac 100644 --- a/CodeEdit/Features/CodeEditUI/Views/Divided.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift @@ -7,14 +7,14 @@ import SwiftUI -struct Divided: View { +public struct Divided: View { var content: Content - init(@ViewBuilder content: () -> Content) { + public init(@ViewBuilder content: () -> Content) { self.content = content() } - var body: some View { + public var body: some View { _VariadicView.Tree(DividedLayout()) { content } diff --git a/CodeEdit/Features/CodeEditUI/Views/EffectView.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift similarity index 88% rename from CodeEdit/Features/CodeEditUI/Views/EffectView.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift index f9a1e6eb04..71f8e80cfb 100644 --- a/CodeEdit/Features/CodeEditUI/Views/EffectView.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift @@ -13,7 +13,7 @@ import SwiftUI /// ```swift /// EffectView(material: .headerView, blendingMode: .withinWindow) /// ``` -struct EffectView: NSViewRepresentable { +public struct EffectView: NSViewRepresentable { private let material: NSVisualEffectView.Material private let blendingMode: NSVisualEffectView.BlendingMode private let emphasized: Bool @@ -32,7 +32,7 @@ struct EffectView: NSViewRepresentable { /// - material: The material to use. Defaults to `.headerView`. /// - blendingMode: The blending mode to use. Defaults to `.withinWindow`. /// - emphasized:A Boolean value indicating whether to emphasize the look of the material. Defaults to `false`. - init( + public init( _ material: NSVisualEffectView.Material = .headerView, blendingMode: NSVisualEffectView.BlendingMode = .withinWindow, emphasized: Bool = false @@ -42,7 +42,7 @@ struct EffectView: NSViewRepresentable { self.emphasized = emphasized } - func makeNSView(context: Context) -> NSVisualEffectView { + public func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.material = material view.blendingMode = blendingMode @@ -51,7 +51,7 @@ struct EffectView: NSViewRepresentable { return view } - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { + public func updateNSView(_ nsView: NSVisualEffectView, context: Context) { nsView.material = material nsView.blendingMode = blendingMode } @@ -62,7 +62,7 @@ struct EffectView: NSViewRepresentable { /// - Parameter condition: The condition of when to apply the background. Defaults to `true`. /// - Returns: A View @ViewBuilder - static func selectionBackground(_ condition: Bool = true) -> some View { + public static func selectionBackground(_ condition: Bool = true) -> some View { if condition { EffectView(.selection, blendingMode: .withinWindow, emphasized: true) } else { diff --git a/CodeEdit/Features/CodeEditUI/Views/ErrorDescriptionLabel.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift similarity index 84% rename from CodeEdit/Features/CodeEditUI/Views/ErrorDescriptionLabel.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift index 9a64190da8..ee9ab2f6f5 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ErrorDescriptionLabel.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift @@ -7,10 +7,14 @@ import SwiftUI -struct ErrorDescriptionLabel: View { +public struct ErrorDescriptionLabel: View { let error: Error - var body: some View { + public init(error: Error) { + self.error = error + } + + public var body: some View { VStack(alignment: .leading) { if let error = error as? LocalizedError { if let description = error.errorDescription { diff --git a/CodeEdit/Features/CodeEditUI/Views/FeatureIcon.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift similarity index 95% rename from CodeEdit/Features/CodeEditUI/Views/FeatureIcon.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift index 33d0ae09cf..b6c41f9781 100644 --- a/CodeEdit/Features/CodeEditUI/Views/FeatureIcon.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift @@ -8,12 +8,12 @@ import SwiftUI import CodeEditSymbols -struct FeatureIcon: View { +public struct FeatureIcon: View { private let content: IconContent private let color: Color? private let size: CGFloat - init( + public init( symbol: String, color: Color? = nil, size: CGFloat? = nil @@ -23,7 +23,7 @@ struct FeatureIcon: View { self.size = size ?? 20 } - init( + public init( text: String, textColor: Color? = nil, color: Color? = nil, @@ -34,7 +34,7 @@ struct FeatureIcon: View { self.size = size ?? 20 } - init( + public init( image: Image, size: CGFloat? = nil ) { @@ -51,7 +51,7 @@ struct FeatureIcon: View { } } - var body: some View { + public var body: some View { RoundedRectangle(cornerRadius: size / 4, style: .continuous) .fill(background) .overlay { diff --git a/CodeEdit/Features/CodeEditUI/Views/GlassEffectView.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift similarity index 72% rename from CodeEdit/Features/CodeEditUI/Views/GlassEffectView.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift index 8532ef7fa7..d24c740c88 100644 --- a/CodeEdit/Features/CodeEditUI/Views/GlassEffectView.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift @@ -8,14 +8,14 @@ import SwiftUI import AppKit -struct GlassEffectView: NSViewRepresentable { +public struct GlassEffectView: NSViewRepresentable { var tintColor: NSColor? - init(tintColor: NSColor? = nil) { + public init(tintColor: NSColor? = nil) { self.tintColor = tintColor } - func makeNSView(context: Context) -> NSView { + public func makeNSView(context: Context) -> NSView { #if compiler(>=6.2) if #available(macOS 26, *) { let view = NSGlassEffectView() @@ -27,7 +27,7 @@ struct GlassEffectView: NSViewRepresentable { return NSView() } - func updateNSView(_ nsView: NSView, context: Context) { + public func updateNSView(_ nsView: NSView, context: Context) { #if compiler(>=6.2) if #available(macOS 26, *), let view = nsView as? NSGlassEffectView { view.tintColor = tintColor diff --git a/CodeEdit/Features/CodeEditUI/Views/HelpButton.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift similarity index 90% rename from CodeEdit/Features/CodeEditUI/Views/HelpButton.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift index f46dea8fed..a2015ad12f 100644 --- a/CodeEdit/Features/CodeEditUI/Views/HelpButton.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift @@ -8,17 +8,17 @@ import SwiftUI /// A Button representing a system Help button displaying a question mark symbol. -struct HelpButton: View { +public struct HelpButton: View { private var action: () -> Void /// Initializes the ``HelpButton`` with an action closure /// - Parameter action: A closure that gets called once the button is pressed. - init(action: @escaping () -> Void) { + public init(action: @escaping () -> Void) { self.action = action } - var body: some View { + public var body: some View { Button(action: action, label: { ZStack { Circle() diff --git a/CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift similarity index 88% rename from CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift index 037f7a701d..12dabdcdbc 100644 --- a/CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift @@ -15,7 +15,7 @@ struct InstantPopoverModifier: ViewModifier { let arrowEdge: Edge let popoverContent: PopoverContent - func body(content: Content) -> some View { + public func body(content: Content) -> some View { content .background( PopoverPresenter( @@ -35,9 +35,9 @@ struct PopoverPresenter: NSViewRepresentable { let arrowEdge: Edge let contentView: ContentView - func makeNSView(context: Context) -> NSView { NSView() } + public func makeNSView(context: Context) -> NSView { NSView() } - func updateNSView(_ nsView: NSView, context: Context) { + public func updateNSView(_ nsView: NSView, context: Context) { if isPresented && context.coordinator.popover == nil { let popover = NSPopover() popover.animates = false @@ -64,10 +64,11 @@ struct PopoverPresenter: NSViewRepresentable { } } - func makeCoordinator() -> Coordinator { + public func makeCoordinator() -> Coordinator { Coordinator(isPresented: $isPresented) } + @MainActor class Coordinator: NSObject, NSPopoverDelegate { @Binding var isPresented: Bool var popover: NSPopover? @@ -84,9 +85,10 @@ struct PopoverPresenter: NSViewRepresentable { object: window, queue: .main ) { [weak self] _ in - guard let self = self else { return } - /// The parent window is no longer focused, close the popover - DispatchQueue.main.async { + /// Delivered on the main queue, so it is safe to assume main-actor isolation. + MainActor.assumeIsolated { + guard let self = self else { return } + /// The parent window is no longer focused, close the popover self.isPresented = false self.popover?.close() } @@ -94,9 +96,7 @@ struct PopoverPresenter: NSViewRepresentable { } func popoverWillClose(_ notification: Notification) { - DispatchQueue.main.async { - self.isPresented = false - } + isPresented = false } func popoverDidClose(_ notification: Notification) { @@ -114,7 +114,7 @@ struct PopoverPresenter: NSViewRepresentable { } } -extension View { +public extension View { /// A custom view modifier that presents a popover attached to the view with no animation. /// - Warning: Views presented using this sheet must be dismissed by negating the `isPresented` binding. Using /// SwiftUI's `dismiss` will likely cause a crash. See [FB16221871](rdar://FB16221871) diff --git a/CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift similarity index 96% rename from CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift index e9b40ef41b..b1d1e96f89 100644 --- a/CodeEdit/Features/CodeEditUI/Views/PaneTextField.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift @@ -6,10 +6,9 @@ // import SwiftUI -import CodeEditUI import Combine -struct PaneTextField: View { +public struct PaneTextField: View { @Environment(\.colorScheme) var colorScheme @@ -34,7 +33,7 @@ struct PaneTextField: View var hasValue: Bool - init( + public init( _ label: String, text: Binding, axis: Axis? = .horizontal, @@ -77,7 +76,7 @@ struct PaneTextField: View } } - var body: some View { + public var body: some View { HStack(alignment: .top, spacing: 0) { if let leading = leadingAccessories { leading diff --git a/CodeEdit/Features/CodeEditUI/Views/PanelDivider.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift similarity index 78% rename from CodeEdit/Features/CodeEditUI/Views/PanelDivider.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift index abca2627c5..85cb9283ee 100644 --- a/CodeEdit/Features/CodeEditUI/Views/PanelDivider.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift @@ -7,11 +7,13 @@ import SwiftUI -struct PanelDivider: View { +public struct PanelDivider: View { @Environment(\.colorScheme) private var colorScheme - var body: some View { + public init() {} + + public var body: some View { Divider() .opacity(0) .overlay( diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift new file mode 100644 index 0000000000..ef46e7d8ca --- /dev/null +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift @@ -0,0 +1,36 @@ +// +// PopoverContainer.swift +// CodeEdit +// +// Created by Khan Winter on 8/29/25. +// + +import SwiftUI + +/// Container for SwiftUI views presented in a popover. +/// On tahoe and above, adds the correct container shape. +public struct PopoverContainer: View { + let content: () -> ContentView + + public init(@ViewBuilder content: @escaping () -> ContentView) { + self.content = content + } + + public var body: some View { + let base = VStack(alignment: .leading, spacing: 0) { + content() + } + .font(.subheadline) + + return Group { + if #available(macOS 26, *) { + base + .padding(13) + .containerShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + } else { + base.padding(5) + } + } + .frame(minWidth: 215) + } +} diff --git a/CodeEdit/Features/CodeEditUI/Views/PressActionsModifier.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift similarity index 82% rename from CodeEdit/Features/CodeEditUI/Views/PressActionsModifier.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift index 9fb32ae3c5..bc498b99a6 100644 --- a/CodeEdit/Features/CodeEditUI/Views/PressActionsModifier.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift @@ -7,16 +7,16 @@ import SwiftUI -struct PressActions: ViewModifier { +public struct PressActions: ViewModifier { var onPress: () -> Void var onRelease: (() -> Void)? - init(onPress: @escaping () -> Void, onRelease: (() -> Void)? = nil) { + public init(onPress: @escaping () -> Void, onRelease: (() -> Void)? = nil) { self.onPress = onPress self.onRelease = onRelease } - func body(content: Content) -> some View { + public func body(content: Content) -> some View { content .simultaneousGesture( DragGesture(minimumDistance: 0) @@ -26,7 +26,7 @@ struct PressActions: ViewModifier { } } -extension View { +public extension View { /// A custom view modifier for press actions with callbacks for `onPress` and `onRelease`. /// - Parameters: diff --git a/CodeEdit/Features/CodeEditUI/Views/SearchField.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift similarity index 65% rename from CodeEdit/Features/CodeEditUI/Views/SearchField.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift index ed6c8e2c7d..376c6212b7 100644 --- a/CodeEdit/Features/CodeEditUI/Views/SearchField.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift @@ -7,38 +7,38 @@ import SwiftUI -struct SearchField: NSViewRepresentable { +public struct SearchField: NSViewRepresentable { @Binding var text: String var placeholder: String - init(_ placeholder: String, text: Binding) { + public init(_ placeholder: String, text: Binding) { self.placeholder = placeholder self._text = text } - func makeNSView(context: Context) -> NSSearchField { + public func makeNSView(context: Context) -> NSSearchField { let searchField = NSSearchField() searchField.delegate = context.coordinator searchField.placeholderString = placeholder return searchField } - func updateNSView(_ nsView: NSSearchField, context: Context) { + public func updateNSView(_ nsView: NSSearchField, context: Context) { nsView.stringValue = text } - func makeCoordinator() -> Coordinator { + public func makeCoordinator() -> Coordinator { Coordinator(self) } - class Coordinator: NSObject, NSSearchFieldDelegate { + public class Coordinator: NSObject, NSSearchFieldDelegate { var parent: SearchField init(_ parent: SearchField) { self.parent = parent } - func controlTextDidChange(_ obj: Notification) { + public func controlTextDidChange(_ obj: Notification) { if let searchField = obj.object as? NSSearchField { parent.text = searchField.stringValue } diff --git a/CodeEdit/Features/CodeEditUI/Views/SearchPanel.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift similarity index 74% rename from CodeEdit/Features/CodeEditUI/Views/SearchPanel.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift index b0b6269d95..29a800fedb 100644 --- a/CodeEdit/Features/CodeEditUI/Views/SearchPanel.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift @@ -7,8 +7,8 @@ import Cocoa -final class SearchPanel: NSPanel, NSWindowDelegate { - init() { +public final class SearchPanel: NSPanel, NSWindowDelegate { + public init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 500, height: 48), styleMask: [.fullSizeContentView, .titled, .resizable], @@ -20,13 +20,13 @@ final class SearchPanel: NSPanel, NSWindowDelegate { self.isMovableByWindowBackground = true } - override func standardWindowButton(_ button: NSWindow.ButtonType) -> NSButton? { + override public func standardWindowButton(_ button: NSWindow.ButtonType) -> NSButton? { let button = super.standardWindowButton(button) button?.isHidden = true return button } - func windowDidResignKey(_ notification: Notification) { + public func windowDidResignKey(_ notification: Notification) { if let panel = notification.object as? SearchPanel { panel.close() } diff --git a/CodeEdit/Features/CodeEditUI/Views/SegmentedControl.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift similarity index 95% rename from CodeEdit/Features/CodeEditUI/Views/SegmentedControl.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift index e91ce0abdc..b40cdcbc94 100644 --- a/CodeEdit/Features/CodeEditUI/Views/SegmentedControl.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift @@ -8,7 +8,7 @@ import SwiftUI /// A view that creates a segmented control from an array of text labels. -struct SegmentedControl: View { +public struct SegmentedControl: View { private var options: [String] private var prominent: Bool @@ -20,7 +20,7 @@ struct SegmentedControl: View { /// - options: the options to display as an array of strings. /// - prominent: A Bool indicating whether to use a prominent appearance instead /// of the muted selection color. Defaults to `false`. - init( + public init( _ selection: Binding, options: [String], prominent: Bool = false @@ -30,7 +30,7 @@ struct SegmentedControl: View { self.prominent = prominent } - var body: some View { + public var body: some View { HStack(spacing: 4) { ForEach(options.indices, id: \.self) { index in SegmentedControlItem( @@ -48,7 +48,7 @@ struct SegmentedControl: View { } } -struct SegmentedControlItem: View { +public struct SegmentedControlItem: View { private let color: Color = Color(nsColor: .selectedControlColor) let label: String let active: Bool @@ -65,7 +65,7 @@ struct SegmentedControlItem: View { @State var isPressing: Bool = false - var body: some View { + public var body: some View { Text(label) .font(.subheadline) .foregroundColor(textColor) diff --git a/CodeEdit/Features/CodeEditUI/Views/TrackableScrollView.swift b/Packages/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift similarity index 95% rename from CodeEdit/Features/CodeEditUI/Views/TrackableScrollView.swift rename to Packages/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift index 32a1a7acbb..fd49c8c62c 100644 --- a/CodeEdit/Features/CodeEditUI/Views/TrackableScrollView.swift +++ b/Packages/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift @@ -13,21 +13,21 @@ import SwiftUI private struct ScrollViewOffsetPreferenceKey: PreferenceKey { typealias Value = [CGFloat] - static var defaultValue: [CGFloat] = [0] + static let defaultValue: [CGFloat] = [0] static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) { value.append(contentsOf: nextValue()) } } -struct TrackableScrollView: View where Content: View { +public struct TrackableScrollView: View where Content: View { let axes: Axis.Set let showIndicators: Bool @Binding var contentOffset: CGFloat @Binding var contentTrailingOffset: CGFloat? let content: Content - init( + public init( _ axes: Axis.Set = .vertical, showIndicators: Bool = true, contentOffset: Binding, @@ -40,7 +40,7 @@ struct TrackableScrollView: View where Content: View { self.content = content() } - init( + public init( _ axes: Axis.Set = .vertical, showIndicators: Bool = true, contentOffset: Binding, @@ -54,7 +54,7 @@ struct TrackableScrollView: View where Content: View { self.content = content() } - var body: some View { + public var body: some View { GeometryReader { outsideProxy in ScrollView(self.axes, showsIndicators: self.showIndicators) { ZStack(alignment: self.axes == .vertical ? .top : .leading) { From de4c7269f90073e035358ea3f0a10ab7e69d0ee5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 3 Jul 2026 21:52:44 +0200 Subject: [PATCH 040/335] Refactor: Drop CodeEditUI package test target; UI tests stay in app target CodeEditUIUnitTests (snapshot tests) stay in the app test target: they mix package types (EffectView/SegmentedControl/HelpButton) with the app-resident ToolbarBranchPicker straggler and use SnapshotTesting, and package test targets are not run by the app scheme (matching CodeEditCore, which declares no test target). They now exercise the package via import CodeEditUI. --- Packages/CodeEditUI/Package.swift | 3 +-- Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift diff --git a/Packages/CodeEditUI/Package.swift b/Packages/CodeEditUI/Package.swift index 837186cd4f..3726d54003 100644 --- a/Packages/CodeEditUI/Package.swift +++ b/Packages/CodeEditUI/Package.swift @@ -16,7 +16,6 @@ let package = Package( .target( name: "CodeEditUI", dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")] - ), - .testTarget(name: "CodeEditUITests", dependencies: ["CodeEditUI"]) + ) ] ) diff --git a/Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift b/Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift deleted file mode 100644 index 63a1ca3e10..0000000000 --- a/Packages/CodeEditUI/Tests/CodeEditUITests/_Placeholder.swift +++ /dev/null @@ -1,5 +0,0 @@ -import XCTest - -final class CodeEditUIPlaceholderTests: XCTestCase { - func testPackageLoads() { XCTAssertTrue(true) } -} From 490cda912c4670e47fdba6905aeb9a7f91c26386 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 11:20:10 +0200 Subject: [PATCH 041/335] Refactor: Add SearchResultFile and WorkspaceFileOpener to Core; move DI keys into CodeEditCore CodeEditCore gains a Factory dependency (2.5.3) and now owns the \.eventBus key (relocated from CodeEditContainer) plus a new \.workspaceFileOpener key defaulting to a no-op. The app registers AppWorkspaceFileOpener at startup, delegating to WorkspaceWindowManager.openFileInWorkspace(url:). --- CodeEdit/CodeEditApp.swift | 2 ++ CodeEdit/CodeEditContainer.swift | 4 ---- .../Services/AppWorkspaceFileOpener.swift | 23 +++++++++++++++++++ Packages/CodeEditCore/Package.swift | 9 +++++++- .../Domain/Search/SearchResultFile.swift | 22 ++++++++++++++++++ .../Infrastructure/CoreContainer.swift | 21 +++++++++++++++++ .../Infrastructure/WorkspaceFileOpener.swift | 20 ++++++++++++++++ 7 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift create mode 100644 Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 34e272a235..a5d08de68e 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import Factory import WelcomeWindow import AboutWindow @@ -20,6 +21,7 @@ struct CodeEditApp: App { init() { NSMenuItem.swizzle() NSSplitViewItem.swizzle() + Container.shared.workspaceFileOpener.register { AppWorkspaceFileOpener() } } var body: some Scene { diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 2a24e5c396..bed4653f68 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -36,8 +36,4 @@ extension Container { var registryManager: Factory { self { @MainActor in RegistryManager() }.singleton } - - var eventBus: Factory { - self { EventBus() }.singleton - } } diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift new file mode 100644 index 0000000000..f958095206 --- /dev/null +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift @@ -0,0 +1,23 @@ +// +// AppWorkspaceFileOpener.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Foundation +import CodeEditCore +import Factory + +/// App-shell binding of the `WorkspaceFileOpener` command interface. +/// Delegates to `WorkspaceWindowManager.openFileInWorkspace(url:)`, which maps the +/// URL to the workspace that owns it, opens the tab, and focuses that workspace. +final class AppWorkspaceFileOpener: WorkspaceFileOpener { + @LazyInjected(\.workspaceWindowManager) + private var windowManager + + @MainActor + func openFile(at url: URL) { + _ = windowManager.openFileInWorkspace(url: url) + } +} diff --git a/Packages/CodeEditCore/Package.swift b/Packages/CodeEditCore/Package.swift index ffb6979a2f..7d2ded171d 100644 --- a/Packages/CodeEditCore/Package.swift +++ b/Packages/CodeEditCore/Package.swift @@ -8,7 +8,14 @@ let package = Package( products: [ .library(name: "CodeEditCore", targets: ["CodeEditCore"]) ], + dependencies: [ + // Pin matches the app's Package.resolved. + .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") + ], targets: [ - .target(name: "CodeEditCore") + .target( + name: "CodeEditCore", + dependencies: [.product(name: "Factory", package: "Factory")] + ) ] ) diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift new file mode 100644 index 0000000000..dd4a501a59 --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift @@ -0,0 +1,22 @@ +// +// SearchResultFile.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Foundation + +/// A lightweight, framework-free representation of a file appearing in search results. +/// Search never needs the full app file model — only identity, location, and a display name. +public struct SearchResultFile: Identifiable, Hashable, Sendable { + public let id: String + public let url: URL + public let name: String + + public init(url: URL, name: String? = nil) { + self.id = url.absoluteString + self.url = url + self.name = name ?? url.lastPathComponent + } +} diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift new file mode 100644 index 0000000000..2f15354a32 --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift @@ -0,0 +1,21 @@ +// +// CoreContainer.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Factory + +/// Factory keys for Core-owned cross-cutting dependencies. +/// Keys live beside the types they vend; the app shell registers real +/// implementations where a default is not sufficient. +extension Container { + public var eventBus: Factory { + self { EventBus() }.singleton + } + + public var workspaceFileOpener: Factory { + self { NoOpWorkspaceFileOpener() }.singleton + } +} diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift new file mode 100644 index 0000000000..9ac072fe7c --- /dev/null +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift @@ -0,0 +1,20 @@ +// +// WorkspaceFileOpener.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Foundation + +/// Command interface for opening a file in the workspace that owns it. +/// One rightful handler (the app shell binds it); features request, never resolve. +public protocol WorkspaceFileOpener: AnyObject { + @MainActor func openFile(at url: URL) +} + +/// Default no-op used until the app registers a real implementation. +public final class NoOpWorkspaceFileOpener: WorkspaceFileOpener { + public init() {} + @MainActor public func openFile(at url: URL) {} +} From 4db56319079fdc10855d510c1c1b679780957630 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 11:32:52 +0200 Subject: [PATCH 042/335] Refactor: Scaffold Search package and wire it into the workspace and app --- CodeEdit.xcodeproj/project.pbxproj | 7 +++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 +++ Packages/Search/Package.swift | 26 +++++++++++++++++++ .../Search/Sources/Search/_Placeholder.swift | 9 +++++++ 4 files changed, 45 insertions(+) create mode 100644 Packages/Search/Package.swift create mode 100644 Packages/Search/Sources/Search/_Placeholder.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 05f74adf65..3dd1a987b4 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; + 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; @@ -202,6 +203,7 @@ 5EACE6222DF4BF08005E08B8 /* WelcomeWindow in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, + 588950C52FFA5C05004BE116 /* Search in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, 6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */, @@ -348,6 +350,7 @@ 58CF9F392F86D64F009F4AA7 /* Factory */, 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, 5800E2F72FF843390085ECF1 /* CodeEditUI */, + 588950C42FFA5C05004BE116 /* Search */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1899,6 +1902,10 @@ package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; productName = SnapshotTesting; }; + 588950C42FFA5C05004BE116 /* Search */ = { + isa = XCSwiftPackageProductDependency; + productName = Search; + }; 58CF9F392F86D64F009F4AA7 /* Factory */ = { isa = XCSwiftPackageProductDependency; package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 9574b3ad98..7379e9229d 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -10,4 +10,7 @@ + + diff --git a/Packages/Search/Package.swift b/Packages/Search/Package.swift new file mode 100644 index 0000000000..4d8a0e14f8 --- /dev/null +++ b/Packages/Search/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Search", + platforms: [.macOS(.v14)], + products: [ + .library(name: "Search", targets: ["Search"]) + ], + dependencies: [ + .package(path: "../CodeEditCore"), + .package(path: "../CodeEditUI"), + .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") + ], + targets: [ + .target( + name: "Search", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditUI", package: "CodeEditUI"), + .product(name: "Factory", package: "Factory") + ] + ) + ] +) diff --git a/Packages/Search/Sources/Search/_Placeholder.swift b/Packages/Search/Sources/Search/_Placeholder.swift new file mode 100644 index 0000000000..4b008c82c4 --- /dev/null +++ b/Packages/Search/Sources/Search/_Placeholder.swift @@ -0,0 +1,9 @@ +// +// _Placeholder.swift +// Search +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +// Temporary: keeps the target non-empty until sources move in (Task 3). Removed then. +enum SearchPackagePlaceholder {} From 08a23d0e70ca46ddee001bcbcdee4152d06ae72d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 11:39:03 +0200 Subject: [PATCH 043/335] Refactor: Move SearchIndexer and AsyncFileIterator into the Search package Swift 6 strict-concurrency fixes required by the package build: SearchResult, ProgressiveSearch.Results, and AsyncManager.TextFile become final Sendable (immutable classes); SearchIndexer is @unchecked Sendable (SearchKit is thread-safe; mutations serialized on modifyIndexQueue); task-group closures capture the index explicitly. --- .../Features/Search/SearchState+Find.swift | 1 + .../Search/SearchState+FindAndReplace.swift | 1 + .../Features/Search/SearchState+Index.swift | 1 + CodeEdit/Features/Search/SearchState.swift | 1 + .../Indexer/AsyncIndexingTests.swift | 1 + .../Indexer/MemoryIndexingTests.swift | 1 + .../Documents/Indexer/MemorySearchTests.swift | 1 + ...kspaceDocument+SearchState+FindTests.swift | 1 + .../Search}/Indexer/AsyncFileIterator.swift | 14 ++++++---- .../Sources/Search}/Indexer/FileHelper.swift | 0 .../Search}/Indexer/SearchIndexer+Add.swift | 0 .../SearchIndexer+AsyncController.swift | 28 +++++++++---------- .../Search}/Indexer/SearchIndexer+File.swift | 0 .../SearchIndexer+InternalMethods.swift | 0 .../Indexer/SearchIndexer+Memory.swift | 0 .../SearchIndexer+ProgressiveSearch.swift | 8 +++--- .../Indexer/SearchIndexer+Search.swift | 0 .../Search}/Indexer/SearchIndexer+Terms.swift | 0 .../Search}/Indexer/SearchIndexer.swift | 5 +++- .../Search/Sources/Search/_Placeholder.swift | 9 ------ 20 files changed, 39 insertions(+), 33 deletions(-) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/AsyncFileIterator.swift (74%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/FileHelper.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+Add.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+AsyncController.swift (91%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+File.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+InternalMethods.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+Memory.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+ProgressiveSearch.swift (96%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+Search.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer+Terms.swift (100%) rename {CodeEdit/Features/Documents => Packages/Search/Sources/Search}/Indexer/SearchIndexer.swift (96%) delete mode 100644 Packages/Search/Sources/Search/_Placeholder.swift diff --git a/CodeEdit/Features/Search/SearchState+Find.swift b/CodeEdit/Features/Search/SearchState+Find.swift index f25e2d5c32..647caa49b5 100644 --- a/CodeEdit/Features/Search/SearchState+Find.swift +++ b/CodeEdit/Features/Search/SearchState+Find.swift @@ -6,6 +6,7 @@ // import Foundation +import Search extension SearchState: @unchecked Sendable {} diff --git a/CodeEdit/Features/Search/SearchState+FindAndReplace.swift b/CodeEdit/Features/Search/SearchState+FindAndReplace.swift index 177f6b9688..6bd0372f1e 100644 --- a/CodeEdit/Features/Search/SearchState+FindAndReplace.swift +++ b/CodeEdit/Features/Search/SearchState+FindAndReplace.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import AppKit extension SearchState { diff --git a/CodeEdit/Features/Search/SearchState+Index.swift b/CodeEdit/Features/Search/SearchState+Index.swift index e82d16a013..8830e748e7 100644 --- a/CodeEdit/Features/Search/SearchState+Index.swift +++ b/CodeEdit/Features/Search/SearchState+Index.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import CodeEditCore extension SearchState { diff --git a/CodeEdit/Features/Search/SearchState.swift b/CodeEdit/Features/Search/SearchState.swift index df0a410cce..32ab3288c2 100644 --- a/CodeEdit/Features/Search/SearchState.swift +++ b/CodeEdit/Features/Search/SearchState.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import CodeEditCore import Factory diff --git a/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift b/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift index ffaa0030c8..cee9260821 100644 --- a/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift +++ b/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift @@ -6,6 +6,7 @@ // import XCTest +import Search @testable import CodeEdit final class AsyncIndexingTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift b/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift index a823c4ed95..97c7dc18e7 100644 --- a/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift +++ b/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift @@ -6,6 +6,7 @@ // import XCTest +import Search @testable import CodeEdit final class MemoryIndexingTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift b/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift index 0afedcaf18..ff019a2e92 100644 --- a/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift +++ b/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift @@ -6,6 +6,7 @@ // import XCTest +import Search @testable import CodeEdit final class MemoryIndexSearchTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 43a1517b5a..61eebe3618 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -6,6 +6,7 @@ // import XCTest +import Search @testable import CodeEdit final class FindTests: XCTestCase { diff --git a/CodeEdit/Features/Documents/Indexer/AsyncFileIterator.swift b/Packages/Search/Sources/Search/Indexer/AsyncFileIterator.swift similarity index 74% rename from CodeEdit/Features/Documents/Indexer/AsyncFileIterator.swift rename to Packages/Search/Sources/Search/Indexer/AsyncFileIterator.swift index f45134d1ec..97c9878956 100644 --- a/CodeEdit/Features/Documents/Indexer/AsyncFileIterator.swift +++ b/Packages/Search/Sources/Search/Indexer/AsyncFileIterator.swift @@ -9,14 +9,18 @@ import Foundation /// Given a list of file URLs, asynchronously fetches their contents and returns them iteratively. /// Returns files as a ``SearchIndexer/AsyncManager/TextFile`` struct, used to index workspaces. -struct AsyncFileIterator: AsyncSequence, AsyncIteratorProtocol { - typealias TextFile = SearchIndexer.AsyncManager.TextFile - typealias Element = (TextFile, Int) +public struct AsyncFileIterator: AsyncSequence, AsyncIteratorProtocol { + public typealias TextFile = SearchIndexer.AsyncManager.TextFile + public typealias Element = (TextFile, Int) let fileURLs: [URL] var currentIdx = 0 - mutating func next() async -> Element? { + public init(fileURLs: [URL]) { + self.fileURLs = fileURLs + } + + public mutating func next() async -> Element? { guard !Task.isCancelled else { return nil } @@ -42,7 +46,7 @@ struct AsyncFileIterator: AsyncSequence, AsyncIteratorProtocol { return (foundContent!, currentIdx) } - func makeAsyncIterator() -> AsyncFileIterator { + public func makeAsyncIterator() -> AsyncFileIterator { self } } diff --git a/CodeEdit/Features/Documents/Indexer/FileHelper.swift b/Packages/Search/Sources/Search/Indexer/FileHelper.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/FileHelper.swift rename to Packages/Search/Sources/Search/Indexer/FileHelper.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+Add.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+Add.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+Add.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+Add.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+AsyncController.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift similarity index 91% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+AsyncController.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift index 885573763e..3e5ff04b53 100644 --- a/CodeEdit/Features/Documents/Indexer/SearchIndexer+AsyncController.swift +++ b/Packages/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift @@ -9,26 +9,26 @@ import Foundation extension SearchIndexer { /// Manager for SearchIndexer object that supports async calls to the index - class AsyncManager { + public class AsyncManager { /// An instance of the SearchIndexer - let index: SearchIndexer + public let index: SearchIndexer private let addQueue = DispatchQueue(label: "app.codeedit.CodeEdit.AddFilesToIndex", attributes: .concurrent) private let searchQueue = DispatchQueue(label: "app.codeedit.CodeEdit.SearchIndex", attributes: .concurrent) - init(index: SearchIndexer) { + public init(index: SearchIndexer) { self.index = index } - class TextFile { - let url: URL - let text: String + public final class TextFile: Sendable { + public let url: URL + public let text: String /// Create a text async task /// /// - Parameters: /// - url: the identifying document URL /// - text: The text to add to the index - init(url: URL, text: String) { + public init(url: URL, text: String) { self.url = url self.text = text } @@ -61,7 +61,7 @@ extension SearchIndexer { /// print(result) /// } /// ``` - func search( + public func search( query: String, _ maxResults: Int, timeout: TimeInterval = 1.0 @@ -89,7 +89,7 @@ extension SearchIndexer { /// the index when the operation is complete. Default is `false`. /// /// - Returns: An array of booleans indicating the success of adding each file to the index. - func addText( + public func addText( files: [TextFile], flushWhenComplete: Bool = false ) async -> [Bool] { @@ -99,9 +99,9 @@ extension SearchIndexer { // Asynchronously iterate through the provided files using a task group await withTaskGroup(of: Bool.self) { taskGroup in for file in files { - taskGroup.addTask { + taskGroup.addTask { [index] in // Add the file to the index and return the success status - return self.index.addFileWithText(file.url, text: file.text, canReplace: true) + return index.addFileWithText(file.url, text: file.text, canReplace: true) } } @@ -127,7 +127,7 @@ extension SearchIndexer { /// - Returns: An array of booleans indicating the success of adding each file to the index. /// - Warning: Prefer using `addText` when possible as SearchKit does not have the ability /// to read every file type. For example, it is often not possible to read Swift files. - func addFiles( + public func addFiles( urls: [URL], flushWhenComplete: Bool = false ) async -> [Bool] { @@ -135,8 +135,8 @@ extension SearchIndexer { await withTaskGroup(of: Bool.self) { taskGroup in for url in urls { - taskGroup.addTask { - return self.index.addFile(fileURL: url, canReplace: true) + taskGroup.addTask { [index] in + return index.addFile(fileURL: url, canReplace: true) } } diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+File.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+File.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+File.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+File.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+InternalMethods.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+InternalMethods.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+Memory.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+Memory.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+ProgressiveSearch.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift similarity index 96% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+ProgressiveSearch.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift index db06a8d0b6..887b445922 100644 --- a/CodeEdit/Features/Documents/Indexer/SearchIndexer+ProgressiveSearch.swift +++ b/Packages/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift @@ -9,12 +9,12 @@ import Foundation extension SearchIndexer { /// Object representing the search results - public class SearchResult { + public final class SearchResult: Sendable { /// The identifying url for the document - let url: URL + public let url: URL /// The search score for the document, higher means more relevant - let score: Float + public let score: Float init(url: URL, score: Float) { self.url = url @@ -34,7 +34,7 @@ extension SearchIndexer { /// A search starts on creation and can be cancelled at any time. public class ProgressiveSearch { /// A class representing the results of a search request. - public class Results { + public final class Results: Sendable { /// Create a search result /// /// - Parameters: diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+Search.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+Search.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+Search.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+Search.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer+Terms.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift similarity index 100% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer+Terms.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift diff --git a/CodeEdit/Features/Documents/Indexer/SearchIndexer.swift b/Packages/Search/Sources/Search/Indexer/SearchIndexer.swift similarity index 96% rename from CodeEdit/Features/Documents/Indexer/SearchIndexer.swift rename to Packages/Search/Sources/Search/Indexer/SearchIndexer.swift index 5b2150c6fa..221034d981 100644 --- a/CodeEdit/Features/Documents/Indexer/SearchIndexer.swift +++ b/Packages/Search/Sources/Search/Indexer/SearchIndexer.swift @@ -8,7 +8,10 @@ import Foundation /// Indexer using SKIndex -public class SearchIndexer { +/// +/// `@unchecked Sendable`: SearchKit is documented thread-safe, and all index +/// mutations are serialized on `modifyIndexQueue`. +public class SearchIndexer: @unchecked Sendable { let modifyIndexQueue = DispatchQueue(label: "app.codeedit.CodeEdit.ModifySearchIndex") var index: SKIndex? diff --git a/Packages/Search/Sources/Search/_Placeholder.swift b/Packages/Search/Sources/Search/_Placeholder.swift deleted file mode 100644 index 4b008c82c4..0000000000 --- a/Packages/Search/Sources/Search/_Placeholder.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// _Placeholder.swift -// Search -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -// Temporary: keeps the target non-empty until sources move in (Task 3). Removed then. -enum SearchPackagePlaceholder {} From f7fb1388acbac8436505e280e1a534a5097b974a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 11:52:53 +0200 Subject: [PATCH 044/335] Refactor: Move SearchState, search models, and FindNavigator into the Search package Search models now carry Core's SearchResultFile instead of CEWorkspaceFile. FindNavigatorListViewController no longer holds a Workspace (resolving the last #24 Option-A exception for Search): opening results goes through the injected WorkspaceFileOpener command, layout prefs arrive via FindNavigatorConfiguration from the app-side FindNavigatorTab wrapper, and file rows render with the package-owned SearchResultFileCell. Tasks 4+5 land together: the intermediate state (models swapped, UI not yet decoupled) cannot compile, so one boundary is honest. Swift 6 fixes: Sendable enums; Coordinator sink uses @MainActor + assumeIsolated (all searchResult mutations are main-actor-confined); Array+Index moved into the package (only Search used it). --- .../Protocols/WorkspaceManaging.swift | 1 + .../Editor/Models/Editor/Editor.swift | 1 + .../Editor/Models/EditorInstance.swift | 1 + .../EditorLayout+StateRestoration.swift | 1 + .../UseCases/RestoreEditorStateUseCase.swift | 1 + .../FindNavigator/FindNavigatorTab.swift | 28 +++++++++++++ .../NavigatorArea/Models/NavigatorTab.swift | 2 +- .../Features/Workspace/Models/Workspace.swift | 1 + .../Features/Workspace/WorkspaceFactory.swift | 1 + .../Documents/DocumentsUnitTests.swift | 1 + ...ment+SearchState+FindAndReplaceTests.swift | 1 + ...kspaceDocument+SearchState+FindTests.swift | 2 +- ...spaceDocument+SearchState+IndexTests.swift | 1 + .../Search/Extensions}/Array+Index.swift | 2 +- .../FindNavigator/FindModePicker.swift | 0 .../FindNavigatorConfiguration.swift | 22 ++++++++++ .../FindNavigator/FindNavigatorForm.swift | 0 .../FindNavigator/FindNavigatorIndexBar.swift | 0 .../FindNavigatorListViewController.swift | 32 ++++++++------- .../FindNavigatorMatchListCell.swift | 0 .../FindNavigatorResultList.swift | 36 ++++++++-------- .../SearchResultFileCell.swift | 41 +++++++++++++++++++ .../FindNavigatorToolbarBottom.swift | 0 .../FindNavigator/FindNavigatorView.swift | 12 ++++-- .../Search/Model/SearchResultMatchModel.swift | 23 ++++++----- .../Search/Model/SearchResultModel.swift | 17 ++++---- .../SearchState}/SearchState+Find.swift | 5 +-- .../SearchState+FindAndReplace.swift | 3 +- .../SearchState}/SearchState+Index.swift | 1 - .../SearchState+MatchExtraction.swift | 7 ++-- .../SearchState+QueryProcessing.swift | 0 .../Search/SearchState}/SearchState.swift | 33 ++++++++------- 32 files changed, 192 insertions(+), 84 deletions(-) create mode 100644 CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift rename {CodeEdit/Utils/Extensions/Array => Packages/Search/Sources/Search/Extensions}/Array+Index.swift (90%) rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindModePicker.swift (100%) create mode 100644 Packages/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorForm.swift (100%) rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorIndexBar.swift (100%) rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift (92%) rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift (100%) rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift (50%) create mode 100644 Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorToolbarBottom.swift (100%) rename {CodeEdit/Features/NavigatorArea => Packages/Search/Sources/Search}/FindNavigator/FindNavigatorView.swift (90%) rename {CodeEdit/Features => Packages/Search/Sources}/Search/Model/SearchResultMatchModel.swift (83%) rename {CodeEdit/Features => Packages/Search/Sources}/Search/Model/SearchResultModel.swift (67%) rename {CodeEdit/Features/Search => Packages/Search/Sources/Search/SearchState}/SearchState+Find.swift (98%) rename {CodeEdit/Features/Search => Packages/Search/Sources/Search/SearchState}/SearchState+FindAndReplace.swift (98%) rename {CodeEdit/Features/Search => Packages/Search/Sources/Search/SearchState}/SearchState+Index.swift (99%) rename {CodeEdit/Features/Search => Packages/Search/Sources/Search/SearchState}/SearchState+MatchExtraction.swift (98%) rename {CodeEdit/Features/Search => Packages/Search/Sources/Search/SearchState}/SearchState+QueryProcessing.swift (100%) rename {CodeEdit/Features/Search => Packages/Search/Sources/Search/SearchState}/SearchState.swift (68%) diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index 754b702106..39b692fe6a 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import Search /// Protocol defining the interface that workspace consumers depend on. /// Enables testability via mock implementations and decouples views from the concrete Workspace type. diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index d9db395eaf..8de4e12d84 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import OrderedCollections import DequeModule import AppKit diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index 4cb9cd6440..e056f6f241 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import AppKit import Combine import CodeEditTextView diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 460e56a4b4..d518e7b74d 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import SwiftUI import OrderedCollections diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift index a2c9e3420a..3d9ab8bd63 100644 --- a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift +++ b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import OSLog import OrderedCollections diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift new file mode 100644 index 0000000000..798d5332d4 --- /dev/null +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift @@ -0,0 +1,28 @@ +// +// FindNavigatorTab.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import SwiftUI +import Search + +/// App-side wrapper for the Search package's find navigator: reads Settings +/// (which the package cannot import) and passes them down as configuration. +struct FindNavigatorTab: View { + @AppSettings(\.general.projectNavigatorSize) + var projectNavigatorSize + + @AppSettings(\.general.findNavigatorDetail) + var findNavigatorDetail + + var body: some View { + FindNavigatorView( + configuration: FindNavigatorConfiguration( + rowHeight: projectNavigatorSize.rowHeight, + matchDetailLineLimit: findNavigatorDetail.rawValue + ) + ) + } +} diff --git a/CodeEdit/Features/NavigatorArea/Models/NavigatorTab.swift b/CodeEdit/Features/NavigatorArea/Models/NavigatorTab.swift index f8d240e798..de585a51f8 100644 --- a/CodeEdit/Features/NavigatorArea/Models/NavigatorTab.swift +++ b/CodeEdit/Features/NavigatorArea/Models/NavigatorTab.swift @@ -55,7 +55,7 @@ enum NavigatorTab: WorkspacePanelTab { case .sourceControl: SourceControlNavigatorView() case .search: - FindNavigatorView() + FindNavigatorTab() case let .uiExtension(endpoint, data): ExtensionSceneView(with: endpoint, sceneID: data.sceneID) } diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 0b08620b88..34225a22c4 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -6,6 +6,7 @@ // import AppKit +import Search import SwiftUI import Foundation diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index fd3025a53c..18de29ae46 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -6,6 +6,7 @@ // import Foundation +import Search import Factory /// Constructs and wires the manager/service object graph for a ``Workspace``. diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 139654cd35..3ca137289a 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -7,6 +7,7 @@ import XCTest import Factory +import Search @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 6cb021d330..45a775a6a0 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -6,6 +6,7 @@ // import XCTest +@testable import Search @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 61eebe3618..24a09843e6 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -6,7 +6,7 @@ // import XCTest -import Search +@testable import Search @testable import CodeEdit final class FindTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index d002f627a3..affb2b1536 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -6,6 +6,7 @@ // import XCTest +@testable import Search @testable import CodeEdit final class WorkspaceIndexTests: XCTestCase { diff --git a/CodeEdit/Utils/Extensions/Array/Array+Index.swift b/Packages/Search/Sources/Search/Extensions/Array+Index.swift similarity index 90% rename from CodeEdit/Utils/Extensions/Array/Array+Index.swift rename to Packages/Search/Sources/Search/Extensions/Array+Index.swift index 6cafca5ef2..69b73c4c12 100644 --- a/CodeEdit/Utils/Extensions/Array/Array+Index.swift +++ b/Packages/Search/Sources/Search/Extensions/Array+Index.swift @@ -5,7 +5,7 @@ // Created by Abe Malla on 7/24/25. // -extension Array { +public extension Array { var second: Element? { self.count > 1 ? self[1] : nil } diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift b/Packages/Search/Sources/Search/FindNavigator/FindModePicker.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindModePicker.swift rename to Packages/Search/Sources/Search/FindNavigator/FindModePicker.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift new file mode 100644 index 0000000000..8ae899df26 --- /dev/null +++ b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift @@ -0,0 +1,22 @@ +// +// FindNavigatorConfiguration.swift +// Search +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Foundation + +/// Layout preferences supplied by the app (derived from Settings, which the +/// Search package cannot import). +public struct FindNavigatorConfiguration: Equatable, Sendable { + /// Row height for file rows and minimum height for match rows. + public var rowHeight: Double + /// Maximum number of preview lines for a match row. + public var matchDetailLineLimit: Int + + public init(rowHeight: Double, matchDetailLineLimit: Int) { + self.rowHeight = rowHeight + self.matchDetailLineLimit = matchDetailLineLimit + } +} diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorForm.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorIndexBar.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorIndexBar.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift similarity index 92% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index 78648183e8..7a7e18e11d 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -6,16 +6,21 @@ // import SwiftUI +import CodeEditCore +import Factory final class FindNavigatorListViewController: NSViewController { - public var workspace: Workspace + @LazyInjected(\.workspaceFileOpener) + private var fileOpener + + var configuration: FindNavigatorConfiguration + public var selectedItem: Any? private var searchItems: [SearchResultModel] = [] private var scrollView: NSScrollView! private var outlineView: NSOutlineView! - private let prefs = Settings.shared.preferences private var collapsedRows: Set = [] var rowHeight: Double = 22 { @@ -44,8 +49,8 @@ final class FindNavigatorListViewController: NSViewController { self.scrollView.contentView.contentInsets = .init(top: 0, left: 0, bottom: 0, right: 0) } - init(workspace: Workspace) { - self.workspace = workspace + init(configuration: FindNavigatorConfiguration) { + self.configuration = configuration super.init(nibName: nil, bundle: nil) } @@ -168,17 +173,14 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: outlineView.rowHeight) return FindNavigatorListMatchCell(frame: frameRect, matchItem: item) } else { + guard let file = (item as? SearchResultModel)?.file else { return nil } let frameRect = NSRect( x: 0, y: 0, width: tableColumn.width, - height: prefs.general.projectNavigatorSize.rowHeight - ) - let view = ProjectNavigatorTableViewCell( - frame: frameRect, - item: (item as? SearchResultModel)?.file, - isEditable: false + height: configuration.rowHeight ) + let view = SearchResultFileCell(frame: frameRect, file: file, rowHeight: configuration.rowHeight) // We're using a medium label for file names b/c it makes it easier to // distinguish quickly which results are from which files. view.textField?.font = .systemFont(ofSize: 13, weight: .medium) @@ -197,13 +199,13 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { let selectedMatch = self.selectedItem as? SearchResultMatchModel if selectedItem == nil || selectedMatch != item { self.selectedItem = item - workspace.editorManager?.openTab(item: item.file) + fileOpener.openFile(at: item.file.url) } } else if let item = outlineView.item(atRow: selectedIndex) as? SearchResultModel { let selectedFile = self.selectedItem as? SearchResultModel if selectedItem == nil || selectedFile != item { self.selectedItem = item - workspace.editorManager?.openTab(item: item.file) + fileOpener.openFile(at: item.file.url) } } } @@ -222,7 +224,7 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { guard availableWidth > 0 else { // Not enough space to display anything, return minimum height - return max(rowHeight, Settings.shared.preferences.general.projectNavigatorSize.rowHeight) + return max(rowHeight, configuration.rowHeight) } let attributedString = matchItem.attributedLabel() @@ -239,7 +241,7 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { tempView.cell?.wraps = true tempView.cell?.usesSingleLineMode = false tempView.lineBreakMode = .byWordWrapping - tempView.maximumNumberOfLines = Settings.shared.preferences.general.findNavigatorDetail.rawValue + tempView.maximumNumberOfLines = configuration.matchDetailLineLimit tempView.preferredMaxLayoutWidth = availableWidth var calculatedHeight = tempView.sizeThatFits( @@ -252,7 +254,7 @@ extension FindNavigatorListViewController: NSOutlineViewDelegate { return max(calculatedHeight, self.rowHeight) } // For parent items - return prefs.general.projectNavigatorSize.rowHeight + return configuration.rowHeight } func outlineViewColumnDidResize(_ notification: Notification) { diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift similarity index 50% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift index f13defbc17..f2d4502693 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift +++ b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift @@ -10,38 +10,37 @@ import Combine struct FindNavigatorResultList: NSViewControllerRepresentable { - @EnvironmentObject var workspace: Workspace + @EnvironmentObject var state: SearchState - @AppSettings(\.general.projectNavigatorSize) - var projectNavigatorSize + let configuration: FindNavigatorConfiguration typealias NSViewControllerType = FindNavigatorListViewController func makeNSViewController(context: Context) -> FindNavigatorListViewController { - let controller = FindNavigatorListViewController(workspace: workspace) - controller.setSearchResults(workspace.searchState?.searchResult ?? []) - controller.rowHeight = projectNavigatorSize.rowHeight + let controller = FindNavigatorListViewController(configuration: configuration) + controller.setSearchResults(state.searchResult) + controller.rowHeight = configuration.rowHeight context.coordinator.controller = controller return controller } func updateNSViewController(_ nsViewController: FindNavigatorListViewController, context: Context) { - nsViewController.updateNewSearchResults( - workspace.searchState?.searchResult ?? [] - ) - if nsViewController.rowHeight != projectNavigatorSize.rowHeight { - nsViewController.rowHeight = projectNavigatorSize.rowHeight + nsViewController.updateNewSearchResults(state.searchResult) + if nsViewController.configuration != configuration { + nsViewController.configuration = configuration + nsViewController.rowHeight = configuration.rowHeight } return } func makeCoordinator() -> Coordinator { Coordinator( - state: workspace.searchState, + state: state, controller: nil ) } + @MainActor class Coordinator: NSObject { init(state: SearchState?, controller: FindNavigatorListViewController?) { self.controller = controller @@ -49,17 +48,18 @@ struct FindNavigatorResultList: NSViewControllerRepresentable { self.listener = state? .$searchResult .sink(receiveValue: { [weak self] searchResults in - self?.controller?.updateNewSearchResults(searchResults) + // `searchResult` is only mutated on the main actor (`setSearchResults` + // is @MainActor; `clearResults` hops to main), so delivery is main-thread. + MainActor.assumeIsolated { + self?.controller?.updateNewSearchResults(searchResults) + } }) } var listener: AnyCancellable? var controller: FindNavigatorListViewController? - deinit { - controller = nil - listener?.cancel() - listener = nil - } + // No explicit deinit: `AnyCancellable` cancels its subscription automatically + // on deallocation, and a nonisolated deinit may not touch main-actor state. } } diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift new file mode 100644 index 0000000000..2a842fb0ba --- /dev/null +++ b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift @@ -0,0 +1,41 @@ +// +// SearchResultFileCell.swift +// Search +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import AppKit +import CodeEditCore + +/// File-row cell for the find navigator's result list. +/// Owned by the Search package so it renders from `SearchResultFile` +/// (name + system file icon) without the project navigator's cell. +final class SearchResultFileCell: NSTableCellView { + init(frame frameRect: NSRect, file: SearchResultFile, rowHeight: Double) { + super.init(frame: frameRect) + + let icon = NSImageView(frame: NSRect(x: 2, y: 0, width: rowHeight, height: frameRect.height)) + icon.image = NSWorkspace.shared.icon(forFile: file.url.path) + icon.symbolConfiguration = .init(pointSize: rowHeight * 0.64, weight: .regular) + addSubview(icon) + imageView = icon + + let label = NSTextField(labelWithString: file.name) + label.frame = NSRect( + x: icon.frame.maxX + 4, + y: (frameRect.height - 17) / 2, + width: frameRect.width - icon.frame.maxX - 8, + height: 17 + ) + label.font = .systemFont(ofSize: NSFont.systemFontSize(for: .regular)) + label.lineBreakMode = .byTruncatingTail + addSubview(label) + textField = label + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorToolbarBottom.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorToolbarBottom.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorView.swift similarity index 90% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift rename to Packages/Search/Sources/Search/FindNavigator/FindNavigatorView.swift index 609b830f80..3825f9a1bd 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorView.swift +++ b/Packages/Search/Sources/Search/FindNavigator/FindNavigatorView.swift @@ -8,15 +8,21 @@ import SwiftUI import CodeEditUI -struct FindNavigatorView: View { +public struct FindNavigatorView: View { @EnvironmentObject private var state: SearchState + private let configuration: FindNavigatorConfiguration + @State private var foundFilesCount: Int = 0 @State private var searchResultCount: Int = 0 @State private var findNavigatorStatus: SearchState.FindNavigatorStatus = .none @State private var findResultMessage: String? - var body: some View { + public init(configuration: FindNavigatorConfiguration) { + self.configuration = configuration + } + + public var body: some View { VStack { VStack { FindNavigatorForm(state: state) @@ -67,7 +73,7 @@ struct FindNavigatorView: View { systemImage: "exclamationmark.magnifyingglass" ) } else { - FindNavigatorResultList() + FindNavigatorResultList(configuration: configuration) } case .replaced(let updatedFiles): CEContentUnavailableView( diff --git a/CodeEdit/Features/Search/Model/SearchResultMatchModel.swift b/Packages/Search/Sources/Search/Model/SearchResultMatchModel.swift similarity index 83% rename from CodeEdit/Features/Search/Model/SearchResultMatchModel.swift rename to Packages/Search/Sources/Search/Model/SearchResultMatchModel.swift index 1ad0b68102..5ee0b2dcb4 100644 --- a/CodeEdit/Features/Search/Model/SearchResultMatchModel.swift +++ b/Packages/Search/Sources/Search/Model/SearchResultMatchModel.swift @@ -7,12 +7,13 @@ import Foundation import Cocoa +import CodeEditCore /// A struct for holding information about a search match. -class SearchResultMatchModel: Hashable, Identifiable { - init( +public class SearchResultMatchModel: Hashable, Identifiable { + public init( rangeWithinFile: Range, - file: CEWorkspaceFile, + file: SearchResultFile, lineContent: String, keywordRange: Range ) { @@ -23,13 +24,13 @@ class SearchResultMatchModel: Hashable, Identifiable { self.keywordRange = keywordRange } - var id: UUID - var file: CEWorkspaceFile - var rangeWithinFile: Range - var lineContent: String - var keywordRange: Range + public var id: UUID + public var file: SearchResultFile + public var rangeWithinFile: Range + public var lineContent: String + public var keywordRange: Range - static func == (lhs: SearchResultMatchModel, rhs: SearchResultMatchModel) -> Bool { + public static func == (lhs: SearchResultMatchModel, rhs: SearchResultMatchModel) -> Bool { return lhs.id == rhs.id && lhs.file == rhs.file && lhs.rangeWithinFile == rhs.rangeWithinFile @@ -37,7 +38,7 @@ class SearchResultMatchModel: Hashable, Identifiable { && lhs.keywordRange == rhs.keywordRange } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(file) hasher.combine(rangeWithinFile) @@ -48,7 +49,7 @@ class SearchResultMatchModel: Hashable, Identifiable { /// Returns a formatted `NSAttributedString` with the search result bolded. /// Will only return 60 characters before and after the matched result. /// - Returns: The formatted `NSAttributedString` - func attributedLabel() -> NSAttributedString { + public func attributedLabel() -> NSAttributedString { // By default `NSTextView` will ignore any paragraph wrapping set to the label when it's // using an `NSAttributedString` so we need to set the wrap mode here. let paragraphStyle = NSMutableParagraphStyle() diff --git a/CodeEdit/Features/Search/Model/SearchResultModel.swift b/Packages/Search/Sources/Search/Model/SearchResultModel.swift similarity index 67% rename from CodeEdit/Features/Search/Model/SearchResultModel.swift rename to Packages/Search/Sources/Search/Model/SearchResultModel.swift index 92de452e5c..8a26d8010c 100644 --- a/CodeEdit/Features/Search/Model/SearchResultModel.swift +++ b/Packages/Search/Sources/Search/Model/SearchResultModel.swift @@ -6,19 +6,20 @@ // import Foundation +import CodeEditCore /// A struct for holding information about a file and any matches it may have for a search query. -class SearchResultModel: Hashable { +public class SearchResultModel: Hashable { - var file: CEWorkspaceFile + public var file: SearchResultFile // The score represents how well the file matches the search query. // The higher the score is, the better the file matches the search query. // The score is assign by Search Kit. - var score: Float - var lineMatches: [SearchResultMatchModel] + public var score: Float + public var lineMatches: [SearchResultMatchModel] - init( - file: CEWorkspaceFile, + public init( + file: SearchResultFile, score: Float, lineMatches: [SearchResultMatchModel] = [] ) { @@ -27,12 +28,12 @@ class SearchResultModel: Hashable { self.lineMatches = lineMatches } - static func == (lhs: SearchResultModel, rhs: SearchResultModel) -> Bool { + public static func == (lhs: SearchResultModel, rhs: SearchResultModel) -> Bool { return lhs.file == rhs.file && lhs.lineMatches == rhs.lineMatches } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(file) hasher.combine(lineMatches) } diff --git a/CodeEdit/Features/Search/SearchState+Find.swift b/Packages/Search/Sources/Search/SearchState/SearchState+Find.swift similarity index 98% rename from CodeEdit/Features/Search/SearchState+Find.swift rename to Packages/Search/Sources/Search/SearchState/SearchState+Find.swift index 647caa49b5..e680ec3da0 100644 --- a/CodeEdit/Features/Search/SearchState+Find.swift +++ b/Packages/Search/Sources/Search/SearchState/SearchState+Find.swift @@ -6,7 +6,6 @@ // import Foundation -import Search extension SearchState: @unchecked Sendable {} @@ -24,7 +23,7 @@ extension SearchState { /// for more information on search results and matches. /// /// - Parameter query: The search query to search for. - func search(_ query: String) async { + public func search(_ query: String) async { clearResults() await MainActor.run { @@ -105,7 +104,7 @@ extension SearchState { } /// Resets the search results along with counts for overall results and file-specific results. - func clearResults() { + public func clearResults() { DispatchQueue.main.async { self.searchResult.removeAll() self.searchResultsCount = 0 diff --git a/CodeEdit/Features/Search/SearchState+FindAndReplace.swift b/Packages/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift similarity index 98% rename from CodeEdit/Features/Search/SearchState+FindAndReplace.swift rename to Packages/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift index 6bd0372f1e..411f694384 100644 --- a/CodeEdit/Features/Search/SearchState+FindAndReplace.swift +++ b/Packages/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift @@ -6,7 +6,6 @@ // import Foundation -import Search import AppKit extension SearchState { @@ -19,7 +18,7 @@ extension SearchState { /// - Important: This function relies on an indexer and assumes that it has been previously set. /// If the indexer is not available, the function will return early. /// Also make sure to flush any pending changes to the index before calling this function. - func findAndReplace(query: String, replacingTerm: String) async throws { + public func findAndReplace(query: String, replacingTerm: String) async throws { await setStatus(.replacing) let searchQuery = getSearchTerm(query) guard let indexer = indexer else { return } diff --git a/CodeEdit/Features/Search/SearchState+Index.swift b/Packages/Search/Sources/Search/SearchState/SearchState+Index.swift similarity index 99% rename from CodeEdit/Features/Search/SearchState+Index.swift rename to Packages/Search/Sources/Search/SearchState/SearchState+Index.swift index 8830e748e7..e82d16a013 100644 --- a/CodeEdit/Features/Search/SearchState+Index.swift +++ b/Packages/Search/Sources/Search/SearchState/SearchState+Index.swift @@ -6,7 +6,6 @@ // import Foundation -import Search import CodeEditCore extension SearchState { diff --git a/CodeEdit/Features/Search/SearchState+MatchExtraction.swift b/Packages/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift similarity index 98% rename from CodeEdit/Features/Search/SearchState+MatchExtraction.swift rename to Packages/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift index fccfa229c0..2a8ba641fd 100644 --- a/CodeEdit/Features/Search/SearchState+MatchExtraction.swift +++ b/Packages/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore extension SearchState { /// Evaluates a matched file to determine if it contains any search matches. @@ -24,7 +25,7 @@ extension SearchState { regexPattern: String ) async -> SearchResultModel? { var newResult = SearchResultModel( - file: CEWorkspaceFile(url: fileURL), + file: SearchResultFile(url: fileURL), score: fileScore ) @@ -98,7 +99,7 @@ extension SearchState { /// - Parameters: /// - matchRange: The range of the matched substring within the entire file content. /// - fileContent: The content of the file where the match was found. - /// - file: The `CEWorkspaceFile` object representing the file containing the match. + /// - file: The `SearchResultFile` representing the file containing the match. /// - matchWordLength: The length of the matched substring. /// /// - Returns: A `SearchResultMatchModel` instance representing the matching occurrence. @@ -112,7 +113,7 @@ extension SearchState { private func createMatchModel( from matchRange: Range, fileContent: String, - file: CEWorkspaceFile, + file: SearchResultFile, matchWordLength: Int ) -> SearchResultMatchModel { let preLine = extractPreLine(from: matchRange, fileContent: fileContent) diff --git a/CodeEdit/Features/Search/SearchState+QueryProcessing.swift b/Packages/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift similarity index 100% rename from CodeEdit/Features/Search/SearchState+QueryProcessing.swift rename to Packages/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift diff --git a/CodeEdit/Features/Search/SearchState.swift b/Packages/Search/Sources/Search/SearchState/SearchState.swift similarity index 68% rename from CodeEdit/Features/Search/SearchState.swift rename to Packages/Search/Sources/Search/SearchState/SearchState.swift index 32ab3288c2..5cea43cef8 100644 --- a/CodeEdit/Features/Search/SearchState.swift +++ b/Packages/Search/Sources/Search/SearchState/SearchState.swift @@ -6,21 +6,20 @@ // import Foundation -import Search import CodeEditCore import Factory /// Manages the search/find state for a workspace, including indexing, search results, /// and find-and-replace operations. Extracted from Workspace to be independently /// injectable and testable. -final class SearchState: ObservableObject { - enum IndexStatus: Equatable { +public final class SearchState: ObservableObject { + public enum IndexStatus: Equatable, Sendable { case none case indexing(progress: Double) case done } - enum FindNavigatorStatus: Equatable { + public enum FindNavigatorStatus: Equatable, Sendable { case none case searching case replacing @@ -29,34 +28,34 @@ final class SearchState: ObservableObject { case failed(errorMessage: String) } - @Published var searchResult: [SearchResultModel] = [] - @Published var searchResultsFileCount: Int = 0 - @Published var searchResultsCount: Int = 0 + @Published public var searchResult: [SearchResultModel] = [] + @Published public var searchResultsFileCount: Int = 0 + @Published public var searchResultsCount: Int = 0 /// Stores the user's input, shown when no files are found, and persists across navigation items. - @Published var searchQuery: String = "" - @Published var replaceText: String = "" + @Published public var searchQuery: String = "" + @Published public var replaceText: String = "" - @Published var indexStatus: IndexStatus = .none + @Published public var indexStatus: IndexStatus = .none - @Published var findNavigatorStatus: FindNavigatorStatus = .none + @Published public var findNavigatorStatus: FindNavigatorStatus = .none - @Published var shouldFocusSearchField: Bool = false + @Published public var shouldFocusSearchField: Bool = false - let workspaceURL: URL + public let workspaceURL: URL @LazyInjected(\.eventBus) var eventBus var tempSearchResults = [SearchResultModel]() - var caseSensitive: Bool = false - var indexer: SearchIndexer? - var selectedMode: [SearchModeModel] = [ + public var caseSensitive: Bool = false + public var indexer: SearchIndexer? + public var selectedMode: [SearchModeModel] = [ .Find, .Text, .Containing ] - init(workspaceURL: URL) { + public init(workspaceURL: URL) { self.workspaceURL = workspaceURL self.indexer = SearchIndexer.Memory.create() addProjectToIndex() From bc129ff3674ab5718277829d63985ad565f3b459 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 12:12:44 +0200 Subject: [PATCH 045/335] Refactor: Move ShellClientProtocol into CodeEditCore --- .../PackageManagers/Install/PackageManagerProgressModel.swift | 1 + CodeEdit/Features/SourceControl/Client/GitClient.swift | 1 + CodeEdit/Features/SourceControl/Client/GitConfigClient.swift | 1 + CodeEdit/Services/ShellClient/ShellClient.swift | 1 + .../CodeEditCore/Infrastructure}/ShellClientProtocol.swift | 4 ++-- 5 files changed, 6 insertions(+), 2 deletions(-) rename {CodeEdit/Services/ShellClient => Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure}/ShellClientProtocol.swift (94%) diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift index b0d80714c7..e0e74bd908 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift @@ -8,6 +8,7 @@ import Combine import Factory import Foundation +import CodeEditCore /// This model is injected into each ``PackageManagerInstallStep`` when executing a ``PackageManagerInstallOperation``. /// A single model is used for each step. Output is collected by the ``PackageManagerInstallOperation``. diff --git a/CodeEdit/Features/SourceControl/Client/GitClient.swift b/CodeEdit/Features/SourceControl/Client/GitClient.swift index 57b7417c04..f820cc9cf7 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import CodeEditCore import OSLog class GitClient: GitClientProtocol { diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift b/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift index b660445162..fb9b207d47 100644 --- a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift +++ b/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore /// A client for managing Git configuration settings. /// Provides methods to read and write Git configuration values at both diff --git a/CodeEdit/Services/ShellClient/ShellClient.swift b/CodeEdit/Services/ShellClient/ShellClient.swift index 16c91fdd3d..63007e62a0 100644 --- a/CodeEdit/Services/ShellClient/ShellClient.swift +++ b/CodeEdit/Services/ShellClient/ShellClient.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import CodeEditCore /// Errors that can occur during shell operations enum ShellClientError: Error { diff --git a/CodeEdit/Services/ShellClient/ShellClientProtocol.swift b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift similarity index 94% rename from CodeEdit/Services/ShellClient/ShellClientProtocol.swift rename to Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift index f983944fc4..23b2b52492 100644 --- a/CodeEdit/Services/ShellClient/ShellClientProtocol.swift +++ b/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift @@ -9,7 +9,7 @@ import Combine import Foundation /// Protocol for executing shell commands. -protocol ShellClientProtocol: Sendable { +public protocol ShellClientProtocol: Sendable { /// Run a command synchronously. /// - Parameter args: Arguments passed to the shell. /// - Returns: The command output. @@ -28,7 +28,7 @@ protocol ShellClientProtocol: Sendable { func runAsync(_ args: [String]) -> AsyncThrowingStream } -extension ShellClientProtocol { +public extension ShellClientProtocol { /// Convenience variadic overload for `run`. @discardableResult func run(_ args: String...) throws -> String { From bb10b3bcb2ca4509589d91d66fe2dd022f1fafe6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 12:24:15 +0200 Subject: [PATCH 046/335] Refactor: Scaffold CodeEditServices package with ShellClient target Also gitignore per-package Package.resolved (workspace's shared resolved is authoritative) and untrack Packages/CodeEditUI/Package.resolved, committed by accident in Phase 1. --- .gitignore | 5 ++++ CodeEdit.xcodeproj/project.pbxproj | 7 ++++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 +++ Packages/CodeEditServices/Package.swift | 25 +++++++++++++++++++ .../Sources/ShellClient/_Placeholder.swift | 9 +++++++ Packages/CodeEditUI/Package.resolved | 15 ----------- 6 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 Packages/CodeEditServices/Package.swift create mode 100644 Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift delete mode 100644 Packages/CodeEditUI/Package.resolved diff --git a/.gitignore b/.gitignore index cc4bf648f6..0db340eae5 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,11 @@ playground.xcworkspace .build/ +# Per-package resolved files for local workspace packages — the workspace's +# shared Package.resolved is authoritative; these are Xcode-generated noise. +Packages/*/Package.resolved +Packages/*/.swiftpm/ + # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 3dd1a987b4..a15f792c78 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; + 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; @@ -191,6 +192,7 @@ 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */, 6CCF6DD32E26D48F00B94F75 /* SwiftTerm in Frameworks */, + 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */, 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, @@ -351,6 +353,7 @@ 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, 5800E2F72FF843390085ECF1 /* CodeEditUI */, 588950C42FFA5C05004BE116 /* Search */, + 588957122FFA679E004BE116 /* CodeEditServices */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1906,6 +1909,10 @@ isa = XCSwiftPackageProductDependency; productName = Search; }; + 588957122FFA679E004BE116 /* CodeEditServices */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditServices; + }; 58CF9F392F86D64F009F4AA7 /* Factory */ = { isa = XCSwiftPackageProductDependency; package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 7379e9229d..f3aab6a1b4 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -13,4 +13,7 @@ + + diff --git a/Packages/CodeEditServices/Package.swift b/Packages/CodeEditServices/Package.swift new file mode 100644 index 0000000000..f63b0395b8 --- /dev/null +++ b/Packages/CodeEditServices/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CodeEditServices", + platforms: [.macOS(.v14)], + products: [ + // Umbrella product: the app links this once; each service target is + // its own module (`import ShellClient`). Adding a service later is a + // manifest-only change. + .library(name: "CodeEditServices", targets: ["ShellClient"]) + ], + dependencies: [ + .package(path: "../CodeEditCore") + ], + targets: [ + // Tier rule: service targets depend on CodeEditCore ONLY — + // never on sibling targets, CodeEditUI, features, or Factory. + .target( + name: "ShellClient", + dependencies: [.product(name: "CodeEditCore", package: "CodeEditCore")] + ) + ] +) diff --git a/Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift b/Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift new file mode 100644 index 0000000000..75626c38d6 --- /dev/null +++ b/Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift @@ -0,0 +1,9 @@ +// +// _Placeholder.swift +// CodeEditServices +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +// Temporary: keeps the target non-empty until the implementation moves in (Task 3). +enum ShellClientPlaceholder {} diff --git a/Packages/CodeEditUI/Package.resolved b/Packages/CodeEditUI/Package.resolved deleted file mode 100644 index 7abbbdc792..0000000000 --- a/Packages/CodeEditUI/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "3c000b73fbec03fd047682022ccf98d1b2e50c527b016e1d0d54ae0a5a45decb", - "pins" : [ - { - "identity" : "codeeditsymbols", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", - "state" : { - "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", - "version" : "0.2.3" - } - } - ], - "version" : 3 -} From 46ced390094bef388e1f48c41ae168d189881b3e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 12:27:20 +0200 Subject: [PATCH 047/335] Refactor: Move ShellClient implementation into CodeEditServices Lock-confines the cancellables dictionary (fixing a latent race between the runLive caller and the notification sink, and a retain cycle via [weak self]) so ShellClient satisfies its Sendable protocol requirement under the package's strict-concurrency mode. --- CodeEdit/CodeEditContainer.swift | 1 + .../SourceControl/GitClientTests.swift | 1 + .../Sources}/ShellClient/ShellClient.swift | 31 +++++++++++++------ .../Sources/ShellClient/_Placeholder.swift | 9 ------ 4 files changed, 24 insertions(+), 18 deletions(-) rename {CodeEdit/Services => Packages/CodeEditServices/Sources}/ShellClient/ShellClient.swift (81%) delete mode 100644 Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index bed4653f68..724fd01578 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -6,6 +6,7 @@ // import CodeEditCore +import ShellClient import Factory extension Container { diff --git a/CodeEditTests/Features/SourceControl/GitClientTests.swift b/CodeEditTests/Features/SourceControl/GitClientTests.swift index dc43ffb072..584d471d3a 100644 --- a/CodeEditTests/Features/SourceControl/GitClientTests.swift +++ b/CodeEditTests/Features/SourceControl/GitClientTests.swift @@ -6,6 +6,7 @@ // import Testing +import ShellClient @testable import CodeEdit @Suite diff --git a/CodeEdit/Services/ShellClient/ShellClient.swift b/Packages/CodeEditServices/Sources/ShellClient/ShellClient.swift similarity index 81% rename from CodeEdit/Services/ShellClient/ShellClient.swift rename to Packages/CodeEditServices/Sources/ShellClient/ShellClient.swift index 63007e62a0..caab8dc32e 100644 --- a/CodeEdit/Services/ShellClient/ShellClient.swift +++ b/Packages/CodeEditServices/Sources/ShellClient/ShellClient.swift @@ -10,14 +10,19 @@ import Foundation import CodeEditCore /// Errors that can occur during shell operations -enum ShellClientError: Error { +public enum ShellClientError: Error { case failedToDecodeOutput case taskTerminated(code: Int) } /// Shell Client /// Run commands in shell -final class ShellClient: ShellClientProtocol { +/// +/// `@unchecked Sendable`: the only mutable state, `cancellables`, is confined +/// by `cancellablesLock`; everything else is immutable per call. +public final class ShellClient: ShellClientProtocol, @unchecked Sendable { + public init() {} + /// Generate a process and pipe to run commands /// - Parameter args: commands to run /// - Returns: command output @@ -35,14 +40,15 @@ final class ShellClient: ShellClientProtocol { return (task, pipe) } - /// Cancellable tasks + private let cancellablesLock = NSLock() + /// Cancellable tasks. Access only while holding `cancellablesLock`. private var cancellables: [UUID: AnyCancellable] = [:] /// Run a command /// - Parameter args: command to run /// - Returns: command output @discardableResult - func run(_ args: [String]) throws -> String { + public func run(_ args: [String]) throws -> String { let (task, pipe) = generateProcessAndPipe(args) try task.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() @@ -56,7 +62,7 @@ final class ShellClient: ShellClientProtocol { /// - Parameter args: command to run /// - Returns: command output @discardableResult - func runLive(_ args: [String]) -> AnyPublisher { + public func runLive(_ args: [String]) -> AnyPublisher { let subject = PassthroughSubject() let (task, pipe) = generateProcessAndPipe(args) let outputHandler = pipe.fileHandleForReading @@ -64,16 +70,20 @@ final class ShellClient: ShellClientProtocol { // the Notification with Name: `NSFileHandleDataAvailable` outputHandler.waitForDataInBackgroundAndNotify() let id = UUID() - self.cancellables[id] = NotificationCenter + let cancellable = NotificationCenter .default .publisher(for: .NSFileHandleDataAvailable, object: outputHandler) - .sink { _ in + .sink { [weak self] _ in let data = outputHandler.availableData guard !data.isEmpty else { // if no data is available anymore // we should cancel this cancellable // and mark the subject as finished - self.cancellables.removeValue(forKey: id) + if let self { + self.cancellablesLock.withLock { + _ = self.cancellables.removeValue(forKey: id) + } + } subject.send(completion: .finished) return } @@ -85,6 +95,9 @@ final class ShellClient: ShellClientProtocol { .forEach({ subject.send(String($0)) }) outputHandler.waitForDataInBackgroundAndNotify() } + cancellablesLock.withLock { + cancellables[id] = cancellable + } task.launch() return subject.eraseToAnyPublisher() } @@ -92,7 +105,7 @@ final class ShellClient: ShellClientProtocol { /// Run a command with AsyncStream /// - Parameter args: command to run /// - Returns: async stream of command output - func runAsync(_ args: [String]) -> AsyncThrowingStream { + public func runAsync(_ args: [String]) -> AsyncThrowingStream { let (task, pipe) = generateProcessAndPipe(args) return AsyncThrowingStream { continuation in diff --git a/Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift b/Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift deleted file mode 100644 index 75626c38d6..0000000000 --- a/Packages/CodeEditServices/Sources/ShellClient/_Placeholder.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// _Placeholder.swift -// CodeEditServices -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -// Temporary: keeps the target non-empty until the implementation moves in (Task 3). -enum ShellClientPlaceholder {} From 09e8cc4662696346d568ef096ceb714474ee91f7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 15:25:40 +0200 Subject: [PATCH 048/335] Refactor: Group local packages under Packages/{Foundation,Services,Features} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the four packages into tier subfolders (Foundation: CodeEditCore, CodeEditUI; Services: CodeEditServices; Features: Search), so the filesystem mirrors the workspace navigator groups. Updates the 3 cross-tier .package(path:) references and the workspace group locations (now backed by the real tier folders — removes the phantom root group-folders). Fix .swiftlint.yml's stale CodeEditModules/.build exclusion → **/.build so SwiftPM dependency checkouts under any package are never linted. --- .swiftlint.yml | 3 +- CodeEdit.xcworkspace/contents.xcworkspacedata | 36 ++++++++++++------- Packages/Features/Search/Package.resolved | 24 +++++++++++++ Packages/{ => Features}/Search/Package.swift | 4 +-- .../Search/Extensions/Array+Index.swift | 0 .../Search/FindNavigator/FindModePicker.swift | 0 .../FindNavigatorConfiguration.swift | 0 .../FindNavigator/FindNavigatorForm.swift | 0 .../FindNavigator/FindNavigatorIndexBar.swift | 0 .../FindNavigatorListViewController.swift | 0 .../FindNavigatorMatchListCell.swift | 0 .../FindNavigatorResultList.swift | 0 .../SearchResultFileCell.swift | 0 .../FindNavigatorToolbarBottom.swift | 0 .../FindNavigator/FindNavigatorView.swift | 0 .../Search/Indexer/AsyncFileIterator.swift | 0 .../Sources/Search/Indexer/FileHelper.swift | 0 .../Search/Indexer/SearchIndexer+Add.swift | 0 .../SearchIndexer+AsyncController.swift | 0 .../Search/Indexer/SearchIndexer+File.swift | 0 .../SearchIndexer+InternalMethods.swift | 0 .../Search/Indexer/SearchIndexer+Memory.swift | 0 .../SearchIndexer+ProgressiveSearch.swift | 0 .../Search/Indexer/SearchIndexer+Search.swift | 0 .../Search/Indexer/SearchIndexer+Terms.swift | 0 .../Search/Indexer/SearchIndexer.swift | 0 .../Search/Model/SearchResultMatchModel.swift | 0 .../Search/Model/SearchResultModel.swift | 0 .../Search/SearchState/SearchState+Find.swift | 0 .../SearchState+FindAndReplace.swift | 0 .../SearchState/SearchState+Index.swift | 0 .../SearchState+MatchExtraction.swift | 0 .../SearchState+QueryProcessing.swift | 0 .../Search/SearchState/SearchState.swift | 0 .../CodeEditCore/Package.swift | 0 .../Domain/Commands/Command.swift | 0 .../Domain/Editor/EditorItemID.swift | 0 .../CodeEditCore/Domain/Git/GitBranch.swift | 0 .../Domain/Git/GitBranchesGroup.swift | 0 .../Domain/Git/GitChangedFile.swift | 0 .../CodeEditCore/Domain/Git/GitCommit.swift | 0 .../CodeEditCore/Domain/Git/GitRemote.swift | 0 .../Domain/Git/GitStashEntry.swift | 0 .../CodeEditCore/Domain/Git/GitStatus.swift | 0 .../Domain/Registry/InstallationMethod.swift | 0 .../Domain/Registry/PackageManagerType.swift | 0 .../Domain/Registry/PackageSource.swift | 0 .../Domain/Registry/RegistryItem+Source.swift | 0 .../Domain/Registry/RegistryItem.swift | 0 .../Registry/RegistryManagerError.swift | 0 .../Domain/Search/FuzzySearchModels.swift | 0 .../Domain/Search/SearchModeModel.swift | 0 .../Domain/Search/SearchResultFile.swift | 0 .../Domain/Tasks/TaskNotificationModel.swift | 0 .../Infrastructure/CoreContainer.swift | 0 .../CodeEditCore/Infrastructure/Event.swift | 0 .../Infrastructure/EventBus.swift | 0 .../Events/CENotificationEvent.swift | 0 .../Events/TaskNotificationEvent.swift | 0 .../Events/WelcomeWindowRequestedEvent.swift | 0 .../Infrastructure/ShellClientProtocol.swift | 0 .../Infrastructure/WorkspaceFileOpener.swift | 0 .../Tests/CodeEditCoreTests/.gitkeep | 0 .../{ => Foundation}/CodeEditUI/Package.swift | 0 .../CodeEditUI/Styles/IconButtonStyle.swift | 0 .../CodeEditUI/Styles/IconToggleStyle.swift | 0 .../Styles/MenuWithButtonStyle.swift | 0 .../Styles/OverlayButtonStyle.swift | 0 .../Views/CEContentUnavailableView.swift | 0 .../CodeEditUI/Views/CEOutlineGroup.swift | 0 .../Sources/CodeEditUI/Views/Divided.swift | 0 .../Sources/CodeEditUI/Views/EffectView.swift | 0 .../Views/ErrorDescriptionLabel.swift | 0 .../CodeEditUI/Views/FeatureIcon.swift | 0 .../CodeEditUI/Views/GlassEffectView.swift | 0 .../Sources/CodeEditUI/Views/HelpButton.swift | 0 .../Views/InstantPopoverModifier.swift | 0 .../CodeEditUI/Views/PaneTextField.swift | 0 .../CodeEditUI/Views/PanelDivider.swift | 0 .../CodeEditUI/Views/PopoverContainer.swift | 0 .../Views/PressActionsModifier.swift | 0 .../CodeEditUI/Views/SearchField.swift | 0 .../CodeEditUI/Views/SearchPanel.swift | 0 .../CodeEditUI/Views/SegmentedControl.swift | 0 .../Views/TrackableScrollView.swift | 0 .../CodeEditServices/Package.resolved | 15 ++++++++ .../CodeEditServices/Package.swift | 2 +- .../Sources/ShellClient/ShellClient.swift | 0 88 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 Packages/Features/Search/Package.resolved rename Packages/{ => Features}/Search/Package.swift (85%) rename Packages/{ => Features}/Search/Sources/Search/Extensions/Array+Index.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindModePicker.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/FindNavigator/FindNavigatorView.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/AsyncFileIterator.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/FileHelper.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+Add.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+File.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+Search.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Indexer/SearchIndexer.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Model/SearchResultMatchModel.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/Model/SearchResultModel.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/SearchState/SearchState+Find.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/SearchState/SearchState+Index.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift (100%) rename Packages/{ => Features}/Search/Sources/Search/SearchState/SearchState.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Package.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift (100%) rename Packages/{ => Foundation}/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep (100%) rename Packages/{ => Foundation}/CodeEditUI/Package.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift (100%) rename Packages/{ => Foundation}/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift (100%) create mode 100644 Packages/Services/CodeEditServices/Package.resolved rename Packages/{ => Services}/CodeEditServices/Package.swift (93%) rename Packages/{ => Services}/CodeEditServices/Sources/ShellClient/ShellClient.swift (100%) diff --git a/.swiftlint.yml b/.swiftlint.yml index 1aa8fd0d3a..6db51196ae 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -15,7 +15,8 @@ identifier_name: # paths to ignore during linting. excluded: - - CodeEditModules/.build # Where Swift Package Manager checks out dependency sources + - "**/.build" # SwiftPM dependency checkouts (inside any local package under Packages/) + - "**/.swiftpm" - DerivedData opt_in_rules: diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index f3aab6a1b4..f80928ab57 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -4,16 +4,28 @@ - - - - - - - - + + + + + + + + + + + + + + diff --git a/Packages/Features/Search/Package.resolved b/Packages/Features/Search/Package.resolved new file mode 100644 index 0000000000..2498af37f1 --- /dev/null +++ b/Packages/Features/Search/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "861c101de336c2f73d77f6b01de83702d01ca8143fc22eeec15fff8cbf5e3fa4", + "pins" : [ + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory", + "state" : { + "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", + "version" : "2.5.3" + } + } + ], + "version" : 3 +} diff --git a/Packages/Search/Package.swift b/Packages/Features/Search/Package.swift similarity index 85% rename from Packages/Search/Package.swift rename to Packages/Features/Search/Package.swift index 4d8a0e14f8..92f5022caa 100644 --- a/Packages/Search/Package.swift +++ b/Packages/Features/Search/Package.swift @@ -9,8 +9,8 @@ let package = Package( .library(name: "Search", targets: ["Search"]) ], dependencies: [ - .package(path: "../CodeEditCore"), - .package(path: "../CodeEditUI"), + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditUI"), .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") ], targets: [ diff --git a/Packages/Search/Sources/Search/Extensions/Array+Index.swift b/Packages/Features/Search/Sources/Search/Extensions/Array+Index.swift similarity index 100% rename from Packages/Search/Sources/Search/Extensions/Array+Index.swift rename to Packages/Features/Search/Sources/Search/Extensions/Array+Index.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindModePicker.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindModePicker.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindModePicker.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindModePicker.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift diff --git a/Packages/Search/Sources/Search/FindNavigator/FindNavigatorView.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorView.swift similarity index 100% rename from Packages/Search/Sources/Search/FindNavigator/FindNavigatorView.swift rename to Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorView.swift diff --git a/Packages/Search/Sources/Search/Indexer/AsyncFileIterator.swift b/Packages/Features/Search/Sources/Search/Indexer/AsyncFileIterator.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/AsyncFileIterator.swift rename to Packages/Features/Search/Sources/Search/Indexer/AsyncFileIterator.swift diff --git a/Packages/Search/Sources/Search/Indexer/FileHelper.swift b/Packages/Features/Search/Sources/Search/Indexer/FileHelper.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/FileHelper.swift rename to Packages/Features/Search/Sources/Search/Indexer/FileHelper.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+Add.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Add.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+Add.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Add.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+File.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+File.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+File.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+File.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+Search.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Search.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+Search.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Search.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift diff --git a/Packages/Search/Sources/Search/Indexer/SearchIndexer.swift b/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer.swift similarity index 100% rename from Packages/Search/Sources/Search/Indexer/SearchIndexer.swift rename to Packages/Features/Search/Sources/Search/Indexer/SearchIndexer.swift diff --git a/Packages/Search/Sources/Search/Model/SearchResultMatchModel.swift b/Packages/Features/Search/Sources/Search/Model/SearchResultMatchModel.swift similarity index 100% rename from Packages/Search/Sources/Search/Model/SearchResultMatchModel.swift rename to Packages/Features/Search/Sources/Search/Model/SearchResultMatchModel.swift diff --git a/Packages/Search/Sources/Search/Model/SearchResultModel.swift b/Packages/Features/Search/Sources/Search/Model/SearchResultModel.swift similarity index 100% rename from Packages/Search/Sources/Search/Model/SearchResultModel.swift rename to Packages/Features/Search/Sources/Search/Model/SearchResultModel.swift diff --git a/Packages/Search/Sources/Search/SearchState/SearchState+Find.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState+Find.swift similarity index 100% rename from Packages/Search/Sources/Search/SearchState/SearchState+Find.swift rename to Packages/Features/Search/Sources/Search/SearchState/SearchState+Find.swift diff --git a/Packages/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift similarity index 100% rename from Packages/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift rename to Packages/Features/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift diff --git a/Packages/Search/Sources/Search/SearchState/SearchState+Index.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState+Index.swift similarity index 100% rename from Packages/Search/Sources/Search/SearchState/SearchState+Index.swift rename to Packages/Features/Search/Sources/Search/SearchState/SearchState+Index.swift diff --git a/Packages/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift similarity index 100% rename from Packages/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift rename to Packages/Features/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift diff --git a/Packages/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift similarity index 100% rename from Packages/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift rename to Packages/Features/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift diff --git a/Packages/Search/Sources/Search/SearchState/SearchState.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift similarity index 100% rename from Packages/Search/Sources/Search/SearchState/SearchState.swift rename to Packages/Features/Search/Sources/Search/SearchState/SearchState.swift diff --git a/Packages/CodeEditCore/Package.swift b/Packages/Foundation/CodeEditCore/Package.swift similarity index 100% rename from Packages/CodeEditCore/Package.swift rename to Packages/Foundation/CodeEditCore/Package.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift diff --git a/Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift similarity index 100% rename from Packages/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift diff --git a/Packages/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep similarity index 100% rename from Packages/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep rename to Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep diff --git a/Packages/CodeEditUI/Package.swift b/Packages/Foundation/CodeEditUI/Package.swift similarity index 100% rename from Packages/CodeEditUI/Package.swift rename to Packages/Foundation/CodeEditUI/Package.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift diff --git a/Packages/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift similarity index 100% rename from Packages/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift diff --git a/Packages/Services/CodeEditServices/Package.resolved b/Packages/Services/CodeEditServices/Package.resolved new file mode 100644 index 0000000000..23cb55baf6 --- /dev/null +++ b/Packages/Services/CodeEditServices/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "7162d49eba1cbb48351105ade4ed394fdf37929e9d5153f2f0711d2b1102d4c0", + "pins" : [ + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory", + "state" : { + "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", + "version" : "2.5.3" + } + } + ], + "version" : 3 +} diff --git a/Packages/CodeEditServices/Package.swift b/Packages/Services/CodeEditServices/Package.swift similarity index 93% rename from Packages/CodeEditServices/Package.swift rename to Packages/Services/CodeEditServices/Package.swift index f63b0395b8..fb5c932e33 100644 --- a/Packages/CodeEditServices/Package.swift +++ b/Packages/Services/CodeEditServices/Package.swift @@ -12,7 +12,7 @@ let package = Package( .library(name: "CodeEditServices", targets: ["ShellClient"]) ], dependencies: [ - .package(path: "../CodeEditCore") + .package(path: "../../Foundation/CodeEditCore") ], targets: [ // Tier rule: service targets depend on CodeEditCore ONLY — diff --git a/Packages/CodeEditServices/Sources/ShellClient/ShellClient.swift b/Packages/Services/CodeEditServices/Sources/ShellClient/ShellClient.swift similarity index 100% rename from Packages/CodeEditServices/Sources/ShellClient/ShellClient.swift rename to Packages/Services/CodeEditServices/Sources/ShellClient/ShellClient.swift From ed95d93ba42623523861503f32977fe78a3acb7f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 15:59:27 +0200 Subject: [PATCH 049/335] Refactor: Scaffold Notifications package and wire it into the workspace and app --- CodeEdit.xcodeproj/project.pbxproj | 7 +++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 +++ Packages/Features/Notifications/Package.swift | 26 +++++++++++++++++++ .../Sources/Notifications/_Placeholder.swift | 9 +++++++ 4 files changed, 45 insertions(+) create mode 100644 Packages/Features/Notifications/Package.swift create mode 100644 Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index a15f792c78..ff00f20deb 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; + 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* Notifications */; }; 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; @@ -185,6 +186,7 @@ 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, 6CCF73D02E26DE3200B94F75 /* SwiftTerm in Frameworks */, + 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */, 6C315FC82E05E33D0011BFC5 /* CodeEditSourceEditor in Frameworks */, 6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */, 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, @@ -354,6 +356,7 @@ 5800E2F72FF843390085ECF1 /* CodeEditUI */, 588950C42FFA5C05004BE116 /* Search */, 588957122FFA679E004BE116 /* CodeEditServices */, + 5889639D2FFA9A87004BE116 /* Notifications */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1913,6 +1916,10 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditServices; }; + 5889639D2FFA9A87004BE116 /* Notifications */ = { + isa = XCSwiftPackageProductDependency; + productName = Notifications; + }; 58CF9F392F86D64F009F4AA7 /* Factory */ = { isa = XCSwiftPackageProductDependency; package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index f80928ab57..1202877b10 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -27,5 +27,8 @@ + + diff --git a/Packages/Features/Notifications/Package.swift b/Packages/Features/Notifications/Package.swift new file mode 100644 index 0000000000..1e8210d6b9 --- /dev/null +++ b/Packages/Features/Notifications/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Notifications", + platforms: [.macOS(.v14)], + products: [ + .library(name: "Notifications", targets: ["Notifications"]) + ], + dependencies: [ + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditUI"), + .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") + ], + targets: [ + .target( + name: "Notifications", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditUI", package: "CodeEditUI"), + .product(name: "Factory", package: "Factory") + ] + ) + ] +) diff --git a/Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift b/Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift new file mode 100644 index 0000000000..1e087ec2ef --- /dev/null +++ b/Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift @@ -0,0 +1,9 @@ +// +// _Placeholder.swift +// Notifications +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +// Temporary: keeps the target non-empty until the feature moves in (Task 2). Removed then. +enum NotificationsPlaceholder {} From 13e8b54d78f8c7f6e23f74f0210781d62fc7bfcf Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 16:42:35 +0200 Subject: [PATCH 050/335] Refactor: Move Notifications feature into Notifications package Relocate the notification subsystem out of the app target and into the Packages/Features/Notifications package, completing the extraction scaffolded in the prior task. - Move CENotification, NotificationManager (+extensions), NotificationManaging, NotificationPanelViewModel (+extensions) and the panel/banner/toolbar views into the package; make the app-facing surface public (CENotification create API, NotificationManaging, NotificationPanelViewModel, NotificationPanelView, NotificationToolbarItem). - Make the subsystem @MainActor end-to-end (protocol, manager, view model), turning post() synchronous and resolving Swift 6 strict-concurrency across the timer/visibility closures and the UNUserNotificationCenter delegate. - Relocate the \.notificationManager Factory key into NotificationsContainer in the package so the packaged view model can inject it. - Introduce an onToolbarUpdateRequested seam: the package exposes the hook, the app keeps the toolbar-identifier-dependent updateToolbarItem() in an app-side extension, wired up in WorkspaceWindowManager. - Promote the shared BlurButtonStyle and ViewOffsetKey atoms into CodeEditUI (previously siloed in the About and Settings features). - Add import Notifications to app consumers; convert the view-model tests to async @MainActor with @testable import Notifications. - Widen the Package.resolved / .swiftpm gitignore to Packages/** after the tier reorg moved packages one level deeper. --- .gitignore | 4 +- CodeEdit/CodeEditContainer.swift | 4 - .../CodeEditSplitViewController.swift | 1 + .../CodeEditWindowController+Toolbar.swift | 1 + .../NotificationPanelViewModel+Toolbar.swift | 12 ++- .../Protocols/WorkspaceManaging.swift | 1 + ...InternalDevelopmentNotificationsView.swift | 1 + .../LSP/Registry/RegistryManager.swift | 1 + .../Features/LSP/Service/LSPService.swift | 1 + .../Settings/Views/SettingsForm.swift | 8 -- .../Features/Workspace/Models/Workspace.swift | 1 + .../Services/WorkspaceWindowManager.swift | 8 +- CodeEdit/WorkspaceView.swift | 1 + .../NotificationPanelViewModelTests.swift | 76 +++++++++---------- .../Notifications/Models/CENotification.swift | 14 ++-- .../NotificationManager+Delegate.swift | 29 ++++--- .../NotificationManager+System.swift | 0 .../Notifications/NotificationManager.swift | 31 ++++---- .../NotificationsContainer.swift | 17 +++++ .../Protocols/NotificationManaging.swift | 9 ++- ...nPanelViewModel+NotificationHandling.swift | 4 +- ...cationPanelViewModel+TimerManagement.swift | 14 ++-- ...otificationPanelViewModel+Visibility.swift | 23 +++--- .../NotificationPanelViewModel.swift | 14 +++- .../Views/NotificationBannerView.swift | 0 .../Views/NotificationPanelView.swift | 7 +- .../Views/NotificationToolbarItem.swift | 6 +- .../Sources/Notifications/_Placeholder.swift | 9 --- .../CodeEditUI/Styles}/BlurButtonStyle.swift | 14 ++-- .../CodeEditUI/Views/ViewOffsetKey.swift | 17 +++++ 30 files changed, 196 insertions(+), 132 deletions(-) rename CodeEdit/Features/{Notifications/ViewModels => Documents/Controllers}/NotificationPanelViewModel+Toolbar.swift (65%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/Models/CENotification.swift (92%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/NotificationManager+Delegate.swift (60%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/NotificationManager+System.swift (100%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/NotificationManager.swift (69%) create mode 100644 Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/Protocols/NotificationManaging.swift (92%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift (97%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift (75%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift (74%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/ViewModels/NotificationPanelViewModel.swift (81%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/Views/NotificationBannerView.swift (100%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/Views/NotificationPanelView.swift (98%) rename {CodeEdit/Features => Packages/Features/Notifications/Sources}/Notifications/Views/NotificationToolbarItem.swift (90%) delete mode 100644 Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift rename {CodeEdit/Features/About => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles}/BlurButtonStyle.swift (86%) create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ViewOffsetKey.swift diff --git a/.gitignore b/.gitignore index 0db340eae5..83cf70a4eb 100644 --- a/.gitignore +++ b/.gitignore @@ -51,8 +51,8 @@ playground.xcworkspace # Per-package resolved files for local workspace packages — the workspace's # shared Package.resolved is authoritative; these are Xcode-generated noise. -Packages/*/Package.resolved -Packages/*/.swiftpm/ +Packages/**/Package.resolved +Packages/**/.swiftpm/ # CocoaPods # diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 724fd01578..82364a9f01 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -30,10 +30,6 @@ extension Container { self { KeybindingManager() as KeybindingManaging }.singleton } - var notificationManager: Factory { - self { NotificationManager() as NotificationManaging }.singleton - } - var registryManager: Factory { self { @MainActor in RegistryManager() }.singleton } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 9e98a914eb..c31f52036e 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -7,6 +7,7 @@ import Cocoa import SwiftUI +import Notifications final class CodeEditSplitViewController: NSSplitViewController { static let minSidebarWidth: CGFloat = 242 diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index cd3fcd22bd..fa762682e6 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -8,6 +8,7 @@ import AppKit import SwiftUI import Combine +import Notifications extension CodeEditWindowController { internal func setupToolbar() { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift similarity index 65% rename from CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift rename to CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift index aab02145aa..91945c11b4 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift @@ -6,8 +6,15 @@ // import AppKit +import Factory +import Notifications -/// Dynamic toolbar item management for the notification badge. +/// App-shell integration for the notification toolbar badge. +/// +/// Window-toolbar mutation is app-shell responsibility (it uses the app-defined +/// `.notificationItem` / `.activityViewer` identifiers), so this lives in the app +/// target rather than the `Notifications` package. The package signals a refresh via +/// ``NotificationPanelViewModel/onToolbarUpdateRequested``, wired up in `WorkspaceWindowManager`. extension NotificationPanelViewModel { func updateToolbarItem() { if #available(macOS 15.0, *) { @@ -15,7 +22,8 @@ extension NotificationPanelViewModel { return } - let shouldShow = !self.visibleNotifications.isEmpty || notificationManager.unreadCount > 0 + let shouldShow = !visibleNotifications.isEmpty + || Container.shared.notificationManager().unreadCount > 0 if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { guard let activityItemIdx = toolbar.items .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index 39b692fe6a..a1254cc2af 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import Notifications import Search /// Protocol defining the interface that workspace consumers depend on. diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift index 4a962a5a11..8524cb4a3b 100644 --- a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift @@ -7,6 +7,7 @@ import SwiftUI import Factory +import Notifications struct InternalDevelopmentNotificationsView: View { enum IconType: String, CaseIterable { diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index e885a926e1..2d4c013d35 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -11,6 +11,7 @@ import ZIPFoundation import Combine import Factory import CodeEditCore +import Notifications @MainActor final class RegistryManager: ObservableObject, RegistryManaging { diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 7c73020ee6..57f7feb299 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -13,6 +13,7 @@ import Factory import LanguageClient import LanguageServerProtocol import CodeEditLanguages +import Notifications /// `LSPService` is a service class responsible for managing the lifecycle and event handling /// of Language Server Protocol (LSP) clients within the CodeEdit application. It handles the initialization, diff --git a/CodeEdit/Features/Settings/Views/SettingsForm.swift b/CodeEdit/Features/Settings/Views/SettingsForm.swift index 085564174b..6650e3e2df 100644 --- a/CodeEdit/Features/Settings/Views/SettingsForm.swift +++ b/CodeEdit/Features/Settings/Views/SettingsForm.swift @@ -77,11 +77,3 @@ struct SettingsForm: View { } } } - -struct ViewOffsetKey: PreferenceKey { - typealias Value = CGFloat - static var defaultValue = CGFloat.zero - static func reduce(value: inout Value, nextValue: () -> Value) { - value += nextValue() - } -} diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 34225a22c4..47f5d14bd5 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -6,6 +6,7 @@ // import AppKit +import Notifications import Search import SwiftUI import Foundation diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 0f72f48294..6d59eacb00 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -8,6 +8,7 @@ import AppKit import CodeEditCore import Factory +import Notifications import SwiftUI import WelcomeWindow @@ -40,7 +41,12 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { openWorkspaces.append(result.workspace) windowControllers[ObjectIdentifier(result.workspace)] = result.windowController - result.workspace.notificationPanel.windowController = result.windowController + let notificationPanel = result.workspace.notificationPanel + notificationPanel.windowController = result.windowController + // App shell owns window-toolbar mutation; the package signals a refresh via this hook. + notificationPanel.onToolbarUpdateRequested = { [weak notificationPanel] in + notificationPanel?.updateToolbarItem() + } result.window.makeKeyAndOrderFront(nil) diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index cdf8460c61..debd4f4e7e 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -7,6 +7,7 @@ import SwiftUI import CodeEditUI +import Notifications import UniformTypeIdentifiers struct WorkspaceView: View { diff --git a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift index 4396207878..670364e160 100644 --- a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift +++ b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift @@ -8,8 +8,10 @@ import XCTest import CodeEditCore import Factory +@testable import Notifications @testable import CodeEdit +@MainActor final class NotificationPanelViewModelTests: XCTestCase { var notificationManager: (any NotificationManaging)! var viewModel: NotificationPanelViewModel! @@ -29,7 +31,7 @@ final class NotificationPanelViewModelTests: XCTestCase { super.tearDown() } - func testNotificationAddedAppearsInPanel() { + func testNotificationAddedAppearsInPanel() async throws { notificationManager.post( iconSymbol: "bell", title: "Test Notification", @@ -38,15 +40,13 @@ final class NotificationPanelViewModelTests: XCTestCase { action: {} ) - let testExpectation = XCTestExpectation() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - XCTAssertEqual(self.viewModel.activeNotifications.first?.title, "Test Notification") - testExpectation.fulfill() - } - wait(for: [testExpectation], timeout: 1) + // Allow the Combine republish from manager to view model to propagate. + try await Task.sleep(for: .milliseconds(200)) + + XCTAssertEqual(viewModel.activeNotifications.first?.title, "Test Notification") } - func testNotificationDismissedRemovedFromPanel() { + func testNotificationDismissedRemovedFromPanel() async throws { notificationManager.post( iconSymbol: "bell", title: "Test Notification", @@ -55,25 +55,21 @@ final class NotificationPanelViewModelTests: XCTestCase { action: {} ) - let testExpectation = XCTestExpectation() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - guard let notification = self.notificationManager.notifications.first else { - XCTFail("Notification was never added to the manager") - testExpectation.fulfill() - return - } - self.notificationManager.dismissNotification(notification) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - XCTAssertTrue(self.viewModel.activeNotifications.isEmpty) - XCTAssertTrue(self.notificationManager.notifications.isEmpty) - testExpectation.fulfill() - } - } - wait(for: [testExpectation], timeout: 2) + try await Task.sleep(for: .milliseconds(200)) + + let notification = try XCTUnwrap( + notificationManager.notifications.first, + "Notification was never added to the manager" + ) + notificationManager.dismissNotification(notification) + + try await Task.sleep(for: .milliseconds(200)) + + XCTAssertTrue(viewModel.activeNotifications.isEmpty) + XCTAssertTrue(notificationManager.notifications.isEmpty) } - func testUnreadCountRepublishedToViewModel() { + func testUnreadCountRepublishedToViewModel() async throws { notificationManager.post( iconSymbol: "bell", title: "First", @@ -89,22 +85,18 @@ final class NotificationPanelViewModelTests: XCTestCase { action: {} ) - let testExpectation = XCTestExpectation() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - XCTAssertEqual(self.viewModel.unreadCount, 2) - - guard let notification = self.notificationManager.notifications.first else { - XCTFail("Notifications were never added to the manager") - testExpectation.fulfill() - return - } - self.notificationManager.markAsRead(notification) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - XCTAssertEqual(self.viewModel.unreadCount, 1) - testExpectation.fulfill() - } - } - wait(for: [testExpectation], timeout: 2) + try await Task.sleep(for: .milliseconds(200)) + + XCTAssertEqual(viewModel.unreadCount, 2) + + let notification = try XCTUnwrap( + notificationManager.notifications.first, + "Notifications were never added to the manager" + ) + notificationManager.markAsRead(notification) + + try await Task.sleep(for: .milliseconds(200)) + + XCTAssertEqual(viewModel.unreadCount, 1) } } diff --git a/CodeEdit/Features/Notifications/Models/CENotification.swift b/Packages/Features/Notifications/Sources/Notifications/Models/CENotification.swift similarity index 92% rename from CodeEdit/Features/Notifications/Models/CENotification.swift rename to Packages/Features/Notifications/Sources/Notifications/Models/CENotification.swift index f71d39a49e..c045d1f9b1 100644 --- a/CodeEdit/Features/Notifications/Models/CENotification.swift +++ b/Packages/Features/Notifications/Sources/Notifications/Models/CENotification.swift @@ -8,8 +8,8 @@ import Foundation import SwiftUI -struct CENotification: Identifiable, Equatable { - let id: UUID +public struct CENotification: Identifiable, Equatable { + public let id: UUID let icon: IconType let title: String let description: String @@ -20,13 +20,13 @@ struct CENotification: Identifiable, Equatable { let timestamp: Date var isBeingDismissed: Bool = false - enum IconType { + public enum IconType { case symbol(name: String, color: Color?) case image(Image) case text(String, backgroundColor: Color?, textColor: Color?) } - init( + public init( id: UUID = UUID(), iconSymbol: String, iconColor: Color? = nil, @@ -49,7 +49,7 @@ struct CENotification: Identifiable, Equatable { ) } - init( + public init( id: UUID = UUID(), iconText: String, iconTextColor: Color? = nil, @@ -73,7 +73,7 @@ struct CENotification: Identifiable, Equatable { ) } - init( + public init( id: UUID = UUID(), iconImage: Image, title: String, @@ -116,7 +116,7 @@ struct CENotification: Identifiable, Equatable { self.timestamp = Date() } - static func == (lhs: CENotification, rhs: CENotification) -> Bool { + public static func == (lhs: CENotification, rhs: CENotification) -> Bool { lhs.id == rhs.id } } diff --git a/CodeEdit/Features/Notifications/NotificationManager+Delegate.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift similarity index 60% rename from CodeEdit/Features/Notifications/NotificationManager+Delegate.swift rename to Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift index 967023db1f..5e7dbc6a0f 100644 --- a/CodeEdit/Features/Notifications/NotificationManager+Delegate.swift +++ b/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift @@ -9,33 +9,40 @@ import AppKit import UserNotifications extension NotificationManager: UNUserNotificationCenterDelegate { - func userNotificationCenter( + // System-invoked (not guaranteed main); `nonisolated` + hop to the main actor for state. + nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { - if let notification = notifications.first(where: { - $0.id.uuidString == response.notification.request.identifier - }) { + // Extract Sendable values before crossing to the main actor (UNNotificationResponse isn't Sendable). + // Extract Sendable values; the completion handler isn't Sendable so call it here + // (it only signals the delegate finished), and run the main-actor action work async. + let identifier = response.notification.request.identifier + let actionIdentifier = response.actionIdentifier + Task { @MainActor in + guard let notification = self.notifications.first(where: { $0.id.uuidString == identifier }) else { + return + } // Focus CodeEdit and run action if action button was clicked - if response.actionIdentifier == "ACTION_BUTTON" || - response.actionIdentifier == UNNotificationDefaultActionIdentifier { + if actionIdentifier == "ACTION_BUTTON" || + actionIdentifier == UNNotificationDefaultActionIdentifier { NSApp.activate(ignoringOtherApps: true) notification.action() } // Remove the notification for both action and dismiss - if response.actionIdentifier == "ACTION_BUTTON" || - response.actionIdentifier == UNNotificationDefaultActionIdentifier || - response.actionIdentifier == UNNotificationDismissActionIdentifier { - dismissNotification(notification) + if actionIdentifier == "ACTION_BUTTON" || + actionIdentifier == UNNotificationDefaultActionIdentifier || + actionIdentifier == UNNotificationDismissActionIdentifier { + self.dismissNotification(notification) } } completionHandler() } - func userNotificationCenter( + nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void diff --git a/CodeEdit/Features/Notifications/NotificationManager+System.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationManager+System.swift similarity index 100% rename from CodeEdit/Features/Notifications/NotificationManager+System.swift rename to Packages/Features/Notifications/Sources/Notifications/NotificationManager+System.swift diff --git a/CodeEdit/Features/Notifications/NotificationManager.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift similarity index 69% rename from CodeEdit/Features/Notifications/NotificationManager.swift rename to Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift index 566fe8a50a..139a35d9d4 100644 --- a/CodeEdit/Features/Notifications/NotificationManager.swift +++ b/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift @@ -16,13 +16,14 @@ import CodeEditCore /// - Managing notification persistence /// - Tracking notification read status /// - Broadcasting notifications to workspaces +@MainActor final class NotificationManager: NSObject, NotificationManaging { /// Collection of all notifications, both read and unread - @Published private(set) var notifications: [CENotification] = [] + @Published public private(set) var notifications: [CENotification] = [] /// Fires on any change to ``notifications``, including `isRead` mutations. - var notificationsPublisher: AnyPublisher<[CENotification], Never> { + public var notificationsPublisher: AnyPublisher<[CENotification], Never> { $notifications.eraseToAnyPublisher() } @@ -32,7 +33,7 @@ final class NotificationManager: NSObject, NotificationManaging { private var isAppActive: Bool = true /// Dismisses a specific notification - func dismissNotification(_ notification: CENotification) { + public func dismissNotification(_ notification: CENotification) { notifications.removeAll(where: { $0.id == notification.id }) markAsRead(notification) @@ -44,7 +45,7 @@ final class NotificationManager: NSObject, NotificationManaging { /// Marks a notification as read /// - Parameter notification: The notification to mark as read - func markAsRead(_ notification: CENotification) { + public func markAsRead(_ notification: CENotification) { if let index = notifications.firstIndex(where: { $0.id == notification.id }) { notifications[index].isRead = true } @@ -82,18 +83,20 @@ final class NotificationManager: NSObject, NotificationManaging { isAppActive = false } - /// Posts a notification to workspaces and system - func post(_ notification: CENotification) { - DispatchQueue.main.async { [weak self] in - self?.notifications.append(notification) + /// Posts a notification to workspaces and system. + /// + /// Runs synchronously on the main actor (the class is `@MainActor` and all callers are too); + /// the previous `DispatchQueue.main.async` hop only ensured main-thread execution, which the + /// actor now guarantees — call order (and thus notification order) is preserved. + public func post(_ notification: CENotification) { + notifications.append(notification) - // Always notify workspaces of new notification - self?.eventBus.publish(CENotificationEvent(.added(id: notification.id))) + // Always notify workspaces of new notification + eventBus.publish(CENotificationEvent(.added(id: notification.id))) - // Additionally show system notification when app is in background - if self?.isAppActive != true { - self?.showSystemNotification(notification) - } + // Additionally show system notification when app is in background + if !isAppActive { + showSystemNotification(notification) } } } diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift new file mode 100644 index 0000000000..325fd4c335 --- /dev/null +++ b/Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift @@ -0,0 +1,17 @@ +// +// NotificationsContainer.swift +// Notifications +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Factory + +public extension Container { + /// The app-wide notification manager. Owned by the Notifications package so the + /// package's own view models can `@LazyInjected` it; the default is the real + /// `NotificationManager` singleton. + var notificationManager: Factory { + self { @MainActor in NotificationManager() as NotificationManaging }.singleton + } +} diff --git a/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift b/Packages/Features/Notifications/Sources/Notifications/Protocols/NotificationManaging.swift similarity index 92% rename from CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift rename to Packages/Features/Notifications/Sources/Notifications/Protocols/NotificationManaging.swift index a45a60a740..83888cb63e 100644 --- a/CodeEdit/Features/Notifications/Protocols/NotificationManaging.swift +++ b/Packages/Features/Notifications/Sources/Notifications/Protocols/NotificationManaging.swift @@ -9,7 +9,12 @@ import SwiftUI import Combine /// Protocol for managing application notifications. -protocol NotificationManaging: AnyObject { +/// +/// `@MainActor`: notifications drive UI (the panel, banner, and toolbar badge), so the +/// whole subsystem is main-actor-isolated. All consumers (LSPService, RegistryManager, +/// the panel view-model, views) are already `@MainActor`. +@MainActor +public protocol NotificationManaging: AnyObject { /// Collection of all notifications, both read and unread. var notifications: [CENotification] { get } @@ -23,7 +28,7 @@ protocol NotificationManaging: AnyObject { func markAsRead(_ notification: CENotification) } -extension NotificationManaging { +public extension NotificationManaging { /// Number of unread notifications. var unreadCount: Int { notifications.filter { !$0.isRead }.count diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift similarity index 97% rename from CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift rename to Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift index 01111b6b52..b34a9739a0 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift +++ b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift @@ -45,7 +45,7 @@ extension NotificationPanelViewModel { if #available(macOS 26, *) { withAnimation(.easeInOut(duration: 0.3), operation) { - self.updateToolbarItem() + self.onToolbarUpdateRequested?() } } else { withAnimation(.easeInOut(duration: 0.3), operation) @@ -113,7 +113,7 @@ extension NotificationPanelViewModel { // Just remove from active notifications without triggering global state changes if #available(macOS 26, *) { withAnimation(.easeOut(duration: 0.2), operation) { - self.updateToolbarItem() + self.onToolbarUpdateRequested?() } } else { withAnimation(.easeOut(duration: 0.2), operation) diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift similarity index 75% rename from CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift rename to Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift index 2be98ac4cb..70abadd535 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift +++ b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift @@ -18,22 +18,24 @@ extension NotificationPanelViewModel { guard !isPaused else { return } - timers[notification.id] = Timer.scheduledTimer( + let notificationId = notification.id + timers[notificationId] = Timer.scheduledTimer( withTimeInterval: displayDuration, repeats: false ) { [weak self] _ in - guard let self = self else { return } - self.timers[notification.id] = nil + // The timer is scheduled from the main actor, so it fires on the main run loop. + // Capture only the Sendable `id` (CENotification isn't Sendable — it carries a closure). + MainActor.assumeIsolated { + guard let self else { return } + self.timers[notificationId] = nil - // Ensure we're on the main thread and animate the change - DispatchQueue.main.async { NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 context.allowsImplicitAnimation = true withAnimation(.easeInOut(duration: 0.3)) { var newHiddenIds = self.hiddenNotificationIds - newHiddenIds.insert(notification.id) + newHiddenIds.insert(notificationId) self.hiddenNotificationIds = newHiddenIds } } diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift similarity index 74% rename from CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift rename to Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift index ababa01abe..ca382fdeeb 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift +++ b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift @@ -43,16 +43,21 @@ extension NotificationPanelViewModel { } // After the slide-out animation, hide notifications - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - // Hide non-sticky notifications - self.activeNotifications - .filter { !$0.isSticky } - .forEach { self.hiddenNotificationIds.insert($0.id) } - self.objectWillChange.send() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + MainActor.assumeIsolated { + guard let self else { return } + // Hide non-sticky notifications + self.activeNotifications + .filter { !$0.isSticky } + .forEach { self.hiddenNotificationIds.insert($0.id) } + self.objectWillChange.send() - // After notifications are hidden, reset scroll position - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.scrolledToTop = true + // After notifications are hidden, reset scroll position + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + MainActor.assumeIsolated { + self?.scrolledToTop = true + } + } } } } else { diff --git a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift similarity index 81% rename from CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift rename to Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift index c79cfb8c16..7fd719dafb 100644 --- a/CodeEdit/Features/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -17,7 +17,8 @@ import CodeEditCore /// - `+Visibility`: panel show/hide, focus handling /// - `+NotificationHandling`: insertion, dismissal, event handling /// - `+Toolbar`: dynamic toolbar item management -final class NotificationPanelViewModel: ObservableObject { +@MainActor +public final class NotificationPanelViewModel: ObservableObject { /// Currently displayed notifications in the panel @Published var activeNotifications: [CENotification] = [] @@ -50,13 +51,18 @@ final class NotificationPanelViewModel: ObservableObject { private var cancellables = Set() /// A filtered list of active notifications. - var visibleNotifications: [CENotification] { + public var visibleNotifications: [CENotification] { activeNotifications.filter { !hiddenNotificationIds.contains($0.id) } } - weak var windowController: NSWindowController? + public weak var windowController: NSWindowController? - init() { + /// Hook set by the app shell to refresh the window toolbar's notification item when + /// notification state changes. Toolbar mutation is app-shell responsibility (it uses + /// app-defined `NSToolbarItem.Identifier`s), so the package only signals; the app acts. + public var onToolbarUpdateRequested: (() -> Void)? + + public init() { // Observe notification additions and dismissals eventBus.subscribe(CENotificationEvent.self) .receive(on: RunLoop.main) diff --git a/CodeEdit/Features/Notifications/Views/NotificationBannerView.swift b/Packages/Features/Notifications/Sources/Notifications/Views/NotificationBannerView.swift similarity index 100% rename from CodeEdit/Features/Notifications/Views/NotificationBannerView.swift rename to Packages/Features/Notifications/Sources/Notifications/Views/NotificationBannerView.swift diff --git a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift b/Packages/Features/Notifications/Sources/Notifications/Views/NotificationPanelView.swift similarity index 98% rename from CodeEdit/Features/Notifications/Views/NotificationPanelView.swift rename to Packages/Features/Notifications/Sources/Notifications/Views/NotificationPanelView.swift index 7047780a24..b3b58733fb 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationPanelView.swift +++ b/Packages/Features/Notifications/Sources/Notifications/Views/NotificationPanelView.swift @@ -6,8 +6,9 @@ // import SwiftUI +import CodeEditUI -struct NotificationPanelView: View { +public struct NotificationPanelView: View { @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @Environment(\.controlActiveState) private var controlActiveState @@ -124,7 +125,9 @@ struct NotificationPanelView: View { } } - var body: some View { + public init() {} + + public var body: some View { Group { if #available(macOS 14.0, *) { notificationsWithScrollView diff --git a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift b/Packages/Features/Notifications/Sources/Notifications/Views/NotificationToolbarItem.swift similarity index 90% rename from CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift rename to Packages/Features/Notifications/Sources/Notifications/Views/NotificationToolbarItem.swift index 33e218a3b2..f2110fd7db 100644 --- a/CodeEdit/Features/Notifications/Views/NotificationToolbarItem.swift +++ b/Packages/Features/Notifications/Sources/Notifications/Views/NotificationToolbarItem.swift @@ -7,12 +7,14 @@ import SwiftUI -struct NotificationToolbarItem: View { +public struct NotificationToolbarItem: View { @EnvironmentObject private var notificationPanel: NotificationPanelViewModel @Environment(\.controlActiveState) private var controlActiveState - var body: some View { + public init() {} + + public var body: some View { let visibleNotifications = notificationPanel.visibleNotifications if notificationPanel.unreadCount > 0 || !visibleNotifications.isEmpty { diff --git a/Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift b/Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift deleted file mode 100644 index 1e087ec2ef..0000000000 --- a/Packages/Features/Notifications/Sources/Notifications/_Placeholder.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// _Placeholder.swift -// Notifications -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -// Temporary: keeps the target non-empty until the feature moves in (Task 2). Removed then. -enum NotificationsPlaceholder {} diff --git a/CodeEdit/Features/About/BlurButtonStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift similarity index 86% rename from CodeEdit/Features/About/BlurButtonStyle.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift index a86f21bcfe..4f363499cd 100644 --- a/CodeEdit/Features/About/BlurButtonStyle.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift @@ -1,19 +1,19 @@ // // BlurButtonStyle.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 21/01/2023. // import SwiftUI -extension ButtonStyle where Self == BlurButtonStyle { +public extension ButtonStyle where Self == BlurButtonStyle { static var blur: BlurButtonStyle { BlurButtonStyle() } static var secondaryBlur: BlurButtonStyle { BlurButtonStyle(isSecondary: true) } } -struct BlurButtonStyle: ButtonStyle { - var isSecondary: Bool = false +public struct BlurButtonStyle: ButtonStyle { + var isSecondary: Bool @Environment(\.controlSize) var controlSize @@ -21,6 +21,10 @@ struct BlurButtonStyle: ButtonStyle { @Environment(\.colorScheme) var colorScheme + public init(isSecondary: Bool = false) { + self.isSecondary = isSecondary + } + var height: CGFloat { switch controlSize { case .large: @@ -30,7 +34,7 @@ struct BlurButtonStyle: ButtonStyle { } } - func makeBody(configuration: Configuration) -> some View { + public func makeBody(configuration: Configuration) -> some View { configuration.label .padding(.horizontal, 8) .frame(height: height) diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ViewOffsetKey.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ViewOffsetKey.swift new file mode 100644 index 0000000000..1bf60e63cd --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ViewOffsetKey.swift @@ -0,0 +1,17 @@ +// +// ViewOffsetKey.swift +// CodeEditUI +// +// Created by Austin Condiff on 4/8/23. +// + +import SwiftUI + +/// A `PreferenceKey` that accumulates a scalar view offset, used to track scroll position. +public struct ViewOffsetKey: PreferenceKey { + public typealias Value = CGFloat + public static let defaultValue = CGFloat.zero + public static func reduce(value: inout Value, nextValue: () -> Value) { + value += nextValue() + } +} From 7c4fc63c5e1bb60fb834373ca0f0781223c9770b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 19:16:11 +0200 Subject: [PATCH 051/335] Refactor: Move String+Escaped helpers into CodeEditCore Relocate the string-escaping extension out of the app Utils folder into CodeEditCore as a public extension. Required so CETask.fullCommand can compile in Core; also consumed by SourceControl and LSP. --- .../Features/SourceControl/Client/GitClient+Clone.swift | 1 + CodeEditTests/Utils/UnitTests_Extensions.swift | 1 + .../Sources/CodeEditCore/Extensions}/String+Escaped.swift | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) rename {CodeEdit/Utils/Extensions/String => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions}/String+Escaped.swift (94%) diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift b/CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift index 9d8a9d93aa..ab1b00595b 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift +++ b/CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift @@ -7,6 +7,7 @@ import Foundation import Combine +import CodeEditCore extension GitClient { struct CloneProgress { diff --git a/CodeEditTests/Utils/UnitTests_Extensions.swift b/CodeEditTests/Utils/UnitTests_Extensions.swift index 9118f1a1f6..cadcb7c40a 100644 --- a/CodeEditTests/Utils/UnitTests_Extensions.swift +++ b/CodeEditTests/Utils/UnitTests_Extensions.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import XCTest +import CodeEditCore @testable import CodeEdit final class CodeEditUtilsExtensionsUnitTests: XCTestCase { diff --git a/CodeEdit/Utils/Extensions/String/String+Escaped.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift similarity index 94% rename from CodeEdit/Utils/Extensions/String/String+Escaped.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift index f6ff3109f2..2929b20557 100644 --- a/CodeEdit/Utils/Extensions/String/String+Escaped.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift @@ -1,13 +1,13 @@ // -// String+escapedWhiteSpaces.swift -// CodeEdit +// String+Escaped.swift +// CodeEditCore // // Created by Paul Ebose on 2024/07/05. // import Foundation -extension String { +public extension String { /// Escapes the string so it's an always-valid directory func escapedDirectory() -> String { "\"\(self.escapedQuotes())\"" From bd59f8d5371eb2b30e3a926a406c15555edf62b3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 19:22:35 +0200 Subject: [PATCH 052/335] Refactor: Move workspace-settings value types into CodeEditCore Convert CETask, CEWorkspaceSettingsData, and ProjectSettings from ObservableObject reference classes into public Codable/Sendable value structs in CodeEditCore. The CEWorkspaceSettings loader stays app-side and remains the single observable owner; views bind through it and TaskManager observes the service. Value semantics also fix a latent edit-does-not-persist bug. Unblocks the future Tasks feature package. --- .../ActivityViewer/Tasks/TaskView.swift | 3 +- .../Tasks/TasksPopoverMenuItem.swift | 1 + .../Models/CEWorkspaceSettings.swift | 1 + .../Views/AddCETaskView.swift | 8 +-- .../Views/CETaskFormView.swift | 3 +- .../CEWorkspaceSettingsTaskListView.swift | 12 ++-- .../Views/CEWorkspaceSettingsView.swift | 4 +- .../Views/EditCETaskView.swift | 5 +- .../Views/EnvironmentVariableListItem.swift | 1 + .../Features/Tasks/Models/CEActiveTask.swift | 8 +-- CodeEdit/Features/Tasks/TaskManager.swift | 15 ++-- .../Features/Workspace/WorkspaceFactory.swift | 2 +- .../Documents/DocumentsUnitTests.swift | 5 +- .../Features/Tasks/CEActiveTaskTests.swift | 5 +- .../Features/Tasks/TaskManagerTests.swift | 19 ++--- .../Domain/WorkspaceSettings}/CETask.swift | 70 ++++++++----------- ...orkspaceSettingsData+ProjectSettings.swift | 16 ++--- .../CEWorkspaceSettingsData.swift | 23 +++--- 18 files changed, 98 insertions(+), 103 deletions(-) rename {CodeEdit/Features/CEWorkspaceSettings/Models => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings}/CETask.swift (69%) rename {CodeEdit/Features/CEWorkspaceSettings/Models => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings}/CEWorkspaceSettingsData+ProjectSettings.swift (54%) rename {CodeEdit/Features/CEWorkspaceSettings/Models => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings}/CEWorkspaceSettingsData.swift (63%) diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift index 4b0d4268b1..7082056dfc 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift @@ -6,12 +6,13 @@ // import SwiftUI +import CodeEditCore /// `TaskView` represents a single active task and observes its state. /// - Parameter task: The task to be displayed and observed. /// - Parameter status: The status of the task to be displayed. struct TaskView: View { - @ObservedObject var task: CETask + let task: CETask var status: CETaskStatus var body: some View { diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift index 528e0c96b5..433ff88f8e 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore /// - Note: This view **cannot** use the `dismiss` environment value to dismiss the sheet. It has to negate the boolean /// value that presented it initially. diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings.swift b/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings.swift index 21d5f661a9..d4b7d21005 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings.swift @@ -7,6 +7,7 @@ import SwiftUI import Combine +import CodeEditCore /// The CodeEdit workspace settings model. final class CEWorkspaceSettings: ObservableObject { diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift index 2863e90e46..e00d4a4ab9 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift @@ -6,20 +6,18 @@ // import SwiftUI +import CodeEditCore struct AddCETaskView: View { @Environment(\.dismiss) var dismiss @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings - @StateObject var newTask: CETask + @State private var newTask = CETask(target: "My Mac") - init() { - self._newTask = StateObject(wrappedValue: CETask(target: "My Mac")) - } var body: some View { VStack(spacing: 0) { - CETaskFormView(task: newTask) + CETaskFormView(task: $newTask) Divider() HStack { Button { diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift index 6e7f84057c..4261fb4116 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift @@ -6,10 +6,11 @@ // import SwiftUI +import CodeEditCore struct CETaskFormView: View { @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings - @ObservedObject var task: CETask + @Binding var task: CETask @State private var selectedEnvID: UUID? var body: some View { diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift index 331bc2345b..e6f67d8568 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift @@ -6,23 +6,22 @@ // import SwiftUI +import CodeEditCore struct CEWorkspaceSettingsTaskListView: View { @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings @EnvironmentObject var taskManager: TaskManager - @ObservedObject var settings: CEWorkspaceSettingsData - @Binding var selectedTaskID: UUID? @Binding var showAddTaskSheet: Bool var body: some View { - if settings.tasks.isEmpty { + if workspaceSettingsManager.settings.tasks.isEmpty { Text("No tasks") .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .center) } else { - ForEach(settings.tasks) { task in + ForEach(workspaceSettingsManager.settings.tasks) { task in TaskTile(task: task) .contentShape(Rectangle()) .onTapGesture { @@ -37,7 +36,7 @@ struct CEWorkspaceSettingsTaskListView: View { Text("Edit") } Button { - settings.tasks.removeAll { $0.id == task.id } + workspaceSettingsManager.settings.tasks.removeAll { $0.id == task.id } try? workspaceSettingsManager.savePreferences() taskManager.deleteTask(taskID: task.id) } label: { @@ -48,9 +47,8 @@ struct CEWorkspaceSettingsTaskListView: View { } } - // Every task as to be observed individually private struct TaskTile: View { - @ObservedObject var task: CETask + let task: CETask var body: some View { HStack { Text(task.name) diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift index ae30cbbaea..8c134f5143 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct CEWorkspaceSettingsView: View { var dismiss: () -> Void @@ -31,7 +32,6 @@ struct CEWorkspaceSettingsView: View { Section { CEWorkspaceSettingsTaskListView( - settings: workspaceSettingsManager.settings, selectedTaskID: $selectedTaskID, showAddTaskSheet: $showAddTaskSheet ) @@ -70,7 +70,7 @@ struct CEWorkspaceSettingsView: View { $0.id == selectedTaskID }) { EditCETaskView( - task: workspaceSettingsManager.settings.tasks[selectedTaskIndex], + task: $workspaceSettingsManager.settings.tasks[selectedTaskIndex], selectedTaskIndex: selectedTaskIndex ) } else { diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift index 8d12b39f5f..e588ce27df 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct EditCETaskView: View { @Environment(\.dismiss) @@ -13,13 +14,13 @@ struct EditCETaskView: View { @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings @EnvironmentObject var taskManager: TaskManager - @ObservedObject var task: CETask + @Binding var task: CETask let selectedTaskIndex: Int var body: some View { VStack(spacing: 0) { - CETaskFormView(task: task) + CETaskFormView(task: $task) Divider() HStack { Button(role: .destructive) { diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/EnvironmentVariableListItem.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/EnvironmentVariableListItem.swift index e16bd5c763..0be2fab580 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/EnvironmentVariableListItem.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/EnvironmentVariableListItem.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct EnvironmentVariableListItem: View { @FocusState private var isKeyFocused: Bool diff --git a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift b/CodeEdit/Features/Tasks/Models/CEActiveTask.swift index 7a3d9de410..825176cacc 100644 --- a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift +++ b/CodeEdit/Features/Tasks/Models/CEActiveTask.swift @@ -22,7 +22,7 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { @Published private(set) var status: CETaskStatus = .notRunning /// The name of the associated task. - @ObservedObject var task: CETask + let task: CETask /// Prevents tasks overwriting each other. /// Say a user cancels one task, then runs it immediately, the cancel message should show and then the @@ -38,14 +38,8 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { @LazyInjected(\.eventBus) private var eventBus - private var cancellables = Set() - init(task: CETask) { self.task = task - - self.task.objectWillChange.sink { _ in - self.objectWillChange.send() - }.store(in: &cancellables) } @MainActor diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/CodeEdit/Features/Tasks/TaskManager.swift index f185320cab..3369703560 100644 --- a/CodeEdit/Features/Tasks/TaskManager.swift +++ b/CodeEdit/Features/Tasks/TaskManager.swift @@ -7,6 +7,7 @@ import SwiftUI import Combine +import CodeEditCore /// This class handles the execution of tasks @MainActor @@ -15,16 +16,18 @@ class TaskManager: ObservableObject { @Published var selectedTaskID: UUID? @Published var taskShowingOutput: UUID? - @ObservedObject var workspaceSettings: CEWorkspaceSettingsData + private let settingsStore: CEWorkspaceSettings private var workspaceURL: URL? private var settingsListener: AnyCancellable? - init(workspaceSettings: CEWorkspaceSettingsData, workspaceURL: URL?) { + init(settingsStore: CEWorkspaceSettings, workspaceURL: URL?) { self.workspaceURL = workspaceURL - self.workspaceSettings = workspaceSettings + self.settingsStore = settingsStore - settingsListener = workspaceSettings.$tasks + settingsListener = settingsStore.$settings + .map(\.tasks) + .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.updateSelectedTaskID() @@ -48,7 +51,7 @@ class TaskManager: ObservableObject { } var availableTasks: [CETask] { - return workspaceSettings.tasks + return settingsStore.settings.tasks } func taskStatus(taskID: UUID) -> CETaskStatus { @@ -61,7 +64,7 @@ class TaskManager: ObservableObject { } func executeActiveTask() { - guard let task = workspaceSettings.tasks.first(where: { $0.id == selectedTaskID }) else { return } + guard let task = settingsStore.settings.tasks.first(where: { $0.id == selectedTaskID }) else { return } Task { await runTask(task: task) } diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 18de29ae46..18cfdedcd5 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -76,7 +76,7 @@ enum WorkspaceFactory { workspace.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) if let workspaceSettingsManager = workspace.workspaceSettingsManager { workspace.taskManager = TaskManager( - workspaceSettings: workspaceSettingsManager.settings, + settingsStore: workspaceSettingsManager, workspaceURL: url ) } diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 3ca137289a..45b93d5446 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -25,7 +25,10 @@ final class DocumentsUnitTests: XCTestCase { super.setUp() hapticFeedbackPerformerMock = NSHapticFeedbackPerformerMock() navigatorViewModel = .init() - workspace.taskManager = TaskManager(workspaceSettings: CEWorkspaceSettingsData(), workspaceURL: nil) + workspace.taskManager = TaskManager( + settingsStore: CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())), + workspaceURL: nil + ) workspace.sourceControlManager = SourceControlManager( workspaceURL: URL(filePath: "/tmp"), shellClient: Container.shared.shellClient() diff --git a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift index dda948257c..40b430e591 100644 --- a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift +++ b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift @@ -6,6 +6,7 @@ // import Testing +import CodeEditCore @testable import CodeEdit @MainActor @@ -50,7 +51,9 @@ class CEActiveTaskTests { @Test(arguments: [Shell.zsh, Shell.bash]) func testHandleProcessFinished(_ shell: Shell) async throws { - task.command = "aNon-existentCommand" + // CETask is a value type, so build a fresh active task around the failing command + // rather than mutating `task` after `activeTask` already copied it. + let activeTask = CEActiveTask(task: CETask(name: "Test Task", command: "aNon-existentCommand")) activeTask.run(workspaceURL: nil, shell: shell) activeTask.waitForExit() diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index be384edd30..ab1a1b4b6e 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -7,23 +7,24 @@ import Foundation import Testing +import CodeEditCore @testable import CodeEdit @MainActor @Suite(.serialized) class TaskManagerTests { var taskManager: TaskManager! - var mockWorkspaceSettings: CEWorkspaceSettingsData! + var settingsStore: CEWorkspaceSettings! init() throws { - let workspaceSettings = try JSONDecoder().decode(CEWorkspaceSettingsData.self, from: Data("{}".utf8)) - mockWorkspaceSettings = workspaceSettings - taskManager = TaskManager(workspaceSettings: mockWorkspaceSettings, workspaceURL: nil) + settingsStore = CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())) + settingsStore.settings = try JSONDecoder().decode(CEWorkspaceSettingsData.self, from: Data("{}".utf8)) + taskManager = TaskManager(settingsStore: settingsStore, workspaceURL: nil) } func testInitialization() { #expect(taskManager != nil) - #expect(taskManager.availableTasks == mockWorkspaceSettings.tasks) + #expect(taskManager.availableTasks == settingsStore.settings.tasks) } @Test @@ -31,7 +32,7 @@ class TaskManagerTests { Settings.shared.preferences.terminal.shell = .zsh let task = CETask(name: "Test Task", command: "echo 'Hello World'") - mockWorkspaceSettings.tasks.append(task) + settingsStore.settings.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() @@ -50,7 +51,7 @@ class TaskManagerTests { Settings.shared.preferences.terminal.shell = .bash let task = CETask(name: "Test Task", command: "echo 'Hello World'") - mockWorkspaceSettings.tasks.append(task) + settingsStore.settings.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() @@ -67,7 +68,7 @@ class TaskManagerTests { @Test(.disabled("Not sure why but tasks run in shells seem to never receive signals.")) func terminateSelectedTask() async throws { let task = CETask(name: "Test Task", command: "sleep 10") - mockWorkspaceSettings.tasks.append(task) + settingsStore.settings.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() @@ -93,7 +94,7 @@ class TaskManagerTests { @Test(.disabled("Not sure why but tasks run in shells seem to never receive signals.")) func suspendAndResumeTask() async throws { let task = CETask(name: "Test Task", command: "sleep 5") - mockWorkspaceSettings.tasks.append(task) + settingsStore.settings.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CETask.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift similarity index 69% rename from CodeEdit/Features/CEWorkspaceSettings/Models/CETask.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift index 9f416e9a3a..f813a1bc92 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Models/CETask.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift @@ -1,6 +1,6 @@ // // CETask.swift -// CodeEdit +// CodeEditCore // // Created by Axel Martinez on 2/4/24. // @@ -8,15 +8,15 @@ import Foundation /// CodeEdit task that will be executed by the task manager. -class CETask: ObservableObject, Identifiable, Hashable, Codable { - @Published var id = UUID() - @Published var name: String = "" - @Published var target: String = "" - @Published var workingDirectory: String = "" - @Published var command: String = "" - @Published var environmentVariables: [EnvironmentVariable] = [] - - init( +public struct CETask: Identifiable, Hashable, Codable, Sendable { + public var id = UUID() + public var name: String + public var target: String + public var workingDirectory: String + public var command: String + public var environmentVariables: [EnvironmentVariable] + + public init( name: String = "", target: String = "", workingDirectory: String = "", @@ -30,17 +30,21 @@ class CETask: ObservableObject, Identifiable, Hashable, Codable { self.environmentVariables = environmentVariables } - init(target: String) { + public init(target: String) { + self.name = "" self.target = target + self.workingDirectory = "" + self.command = "" + self.environmentVariables = [] } - var isInvalid: Bool { + public var isInvalid: Bool { name.isEmpty || command.isEmpty } /// Ensures that the shell navigates to the correct folder, and then executes the specified command. - var fullCommand: String { + public var fullCommand: String { // Move into the specified folder if needed let changeDirectoryCommand = workingDirectory.isEmpty ? "" : "cd \(workingDirectory.escapedDirectory()) && " @@ -51,7 +55,7 @@ class CETask: ObservableObject, Identifiable, Hashable, Codable { /// Converts an array of `EnvironmentVariable` to a dictionary. /// /// - Returns: A dictionary with the environment variable keys and values. - var environmentVariablesDictionary: [String: String] { + public var environmentVariablesDictionary: [String: String] { return environmentVariables.reduce(into: [String: String]()) { result, environmentVariable in result[environmentVariable.key] = environmentVariable.value } @@ -65,20 +69,20 @@ class CETask: ObservableObject, Identifiable, Hashable, Codable { case environmentVariables } - struct EnvironmentVariable: Identifiable, Hashable { - var id = UUID() - var key: String = "" - var value: String = "" + public struct EnvironmentVariable: Identifiable, Hashable, Sendable { + public var id = UUID() + public var key: String = "" + public var value: String = "" - init() {} + public init() {} - init(key: String, value: String) { + public init(key: String, value: String) { self.key = key self.value = value } } - required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) target = try container.decodeIfPresent(String.self, forKey: .target) ?? "" @@ -88,10 +92,12 @@ class CETask: ObservableObject, Identifiable, Hashable, Codable { // Decode environment variables from a dictionary-like structure if let envDict = try container.decodeIfPresent([String: String].self, forKey: .environmentVariables) { environmentVariables = envDict.map { EnvironmentVariable(key: $0.key, value: $0.value) } + } else { + environmentVariables = [] } } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) if !name.isEmpty { try container.encode(name, forKey: .name) @@ -118,23 +124,3 @@ class CETask: ObservableObject, Identifiable, Hashable, Codable { } } } - -extension CETask { - static func == (lhs: CETask, rhs: CETask) -> Bool { - return lhs.id == rhs.id && - lhs.name == rhs.name && - lhs.target == rhs.target && - lhs.workingDirectory == rhs.workingDirectory && - lhs.command == rhs.command && - lhs.environmentVariables == rhs.environmentVariables - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(name) - hasher.combine(target) - hasher.combine(workingDirectory) - hasher.combine(command) - hasher.combine(environmentVariables) - } -} diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettingsData+ProjectSettings.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift similarity index 54% rename from CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettingsData+ProjectSettings.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift index 110fcfcdf2..12225c8c07 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettingsData+ProjectSettings.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift @@ -1,24 +1,24 @@ // -// ProjectCEWorkspaceSettings.swift -// CodeEdit +// CEWorkspaceSettingsData+ProjectSettings.swift +// CodeEditCore // // Created by Axel Martinez on 27/3/24. // -import SwiftUI +import Foundation -class ProjectSettings: ObservableObject, Codable { - var projectName: String = "" +public struct ProjectSettings: Codable, Sendable, Equatable { + public var projectName: String = "" - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.projectName = try container.decodeIfPresent(String.self, forKey: .projectName) ?? "" } - func isEmpty() -> Bool { + public func isEmpty() -> Bool { projectName == "" } } diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettingsData.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift similarity index 63% rename from CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettingsData.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift index 9fcbb2a1a3..b263e10150 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettingsData.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift @@ -1,6 +1,6 @@ // // CEWorkspaceSettingsData.swift -// CodeEdit +// CodeEditCore // // Created by Tommy Ludwig on 01.07.24. // @@ -8,27 +8,30 @@ import Foundation /// The model of the workspace settings for `CodeEdit` that control the behavior of some functionality at the workspace -/// level like the workspace name or defining tasks. A `JSON` representation is persisted in the workspace's -/// `./codeedit/settings.json`. file -class CEWorkspaceSettingsData: ObservableObject, Codable { - @Published var project: ProjectSettings = .init() - @Published var tasks: [CETask] = [] +/// level like the workspace name or defining tasks. A `JSON` representation is persisted in the workspace's +/// `.codeedit/settings.json` file. +public struct CEWorkspaceSettingsData: Codable, Sendable, Equatable { + public var project: ProjectSettings + public var tasks: [CETask] - init() { } + public init(project: ProjectSettings = .init(), tasks: [CETask] = []) { + self.project = project + self.tasks = tasks + } enum CodingKeys: CodingKey { case project, tasks } /// Explicit decoder init for setting default values when key is not present in `JSON` - required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.project = try container.decodeIfPresent(ProjectSettings.self, forKey: .project) ?? .init() self.tasks = try container.decodeIfPresent([CETask].self, forKey: .tasks) ?? [] } /// Encode the instance into the encoder - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) if !project.isEmpty() { try container.encode(project, forKey: .project) @@ -38,7 +41,7 @@ class CEWorkspaceSettingsData: ObservableObject, Codable { } } - func isEmpty() -> Bool { + public func isEmpty() -> Bool { project.isEmpty() && tasks.isEmpty } } From a44e789f1c32028bdd4962287034a52c304b4af6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 19:23:52 +0200 Subject: [PATCH 053/335] Test: Add CodeEditCore workspace-settings value-type unit tests Codable round-trip (on-disk JSON contract), isEmpty, isInvalid, fullCommand, and environment-variable dictionary coverage for the relocated CETask/CEWorkspaceSettingsData/ProjectSettings value types. --- .../WorkspaceSettingsValueTypeTests.swift | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CodeEditTests/Features/WorkspaceSettings/WorkspaceSettingsValueTypeTests.swift diff --git a/CodeEditTests/Features/WorkspaceSettings/WorkspaceSettingsValueTypeTests.swift b/CodeEditTests/Features/WorkspaceSettings/WorkspaceSettingsValueTypeTests.swift new file mode 100644 index 0000000000..e4c72abf06 --- /dev/null +++ b/CodeEditTests/Features/WorkspaceSettings/WorkspaceSettingsValueTypeTests.swift @@ -0,0 +1,92 @@ +// +// WorkspaceSettingsValueTypeTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import XCTest +import CodeEditCore + +final class WorkspaceSettingsValueTypeTests: XCTestCase { + + // MARK: - CETask computed properties + + func testFullCommandWithoutWorkingDirectory() { + let task = CETask(name: "Build", command: "swift build") + XCTAssertEqual(task.fullCommand, "swift build") + } + + func testFullCommandWithWorkingDirectory() { + let task = CETask(workingDirectory: "/tmp/my project", command: "swift build") + XCTAssertEqual(task.fullCommand, #"cd "/tmp/my project" && swift build"#) + } + + func testEnvironmentVariablesDictionary() { + let task = CETask( + command: "run", + environmentVariables: [ + .init(key: "A", value: "1"), + .init(key: "B", value: "2") + ] + ) + XCTAssertEqual(task.environmentVariablesDictionary, ["A": "1", "B": "2"]) + } + + func testIsInvalid() { + XCTAssertTrue(CETask(name: "", command: "x").isInvalid) + XCTAssertTrue(CETask(name: "x", command: "").isInvalid) + XCTAssertFalse(CETask(name: "x", command: "y").isInvalid) + } + + // MARK: - Codable round-trips (on-disk contract) + + func testCETaskRoundTripPreservesFields() throws { + let task = CETask( + name: "Build", + target: "SSH", + workingDirectory: "/tmp", + command: "swift build", + environmentVariables: [.init(key: "K", value: "V")] + ) + let data = try JSONEncoder().encode(task) + let decoded = try JSONDecoder().decode(CETask.self, from: data) + + XCTAssertEqual(decoded.name, "Build") + XCTAssertEqual(decoded.target, "SSH") + XCTAssertEqual(decoded.workingDirectory, "/tmp") + XCTAssertEqual(decoded.command, "swift build") + XCTAssertEqual(decoded.environmentVariablesDictionary, ["K": "V"]) + } + + func testCETaskOmitsDefaultTargetAndEmptyFields() throws { + let task = CETask(name: "Build", target: "My Mac", command: "swift build") + let json = String(data: try JSONEncoder().encode(task), encoding: .utf8) ?? "" + // "My Mac" is the implicit default and must not be written; empty workingDirectory omitted. + XCTAssertFalse(json.contains("My Mac")) + XCTAssertFalse(json.contains("workingDirectory")) + XCTAssertTrue(json.contains("swift build")) + } + + func testSettingsDataDefaultsMissingKeys() throws { + let decoded = try JSONDecoder().decode(CEWorkspaceSettingsData.self, from: Data("{}".utf8)) + XCTAssertTrue(decoded.isEmpty()) + XCTAssertEqual(decoded.tasks.count, 0) + XCTAssertEqual(decoded.project.projectName, "") + } + + func testSettingsDataIsEmpty() { + XCTAssertTrue(CEWorkspaceSettingsData().isEmpty()) + var withName = CEWorkspaceSettingsData() + withName.project.projectName = "MyProject" + XCTAssertFalse(withName.isEmpty()) + var withTask = CEWorkspaceSettingsData() + withTask.tasks.append(CETask(name: "t", command: "c")) + XCTAssertFalse(withTask.isEmpty()) + } + + func testEmptySettingsEncodeToEmptyObject() throws { + let json = String(data: try JSONEncoder().encode(CEWorkspaceSettingsData()), encoding: .utf8) ?? "" + XCTAssertEqual(json, "{}") + } +} From 753b504f5b95aee48c95c04041bc4718f34d57b3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 19:58:14 +0200 Subject: [PATCH 054/335] Fix: Prevent index-out-of-bounds crash when deleting a task EditCETaskView previously received its task as a by-index Binding into settings.tasks[selectedTaskIndex]. Deleting the task shrank the array, so re-reading the binding subscripted an out-of-bounds index and crashed. Give EditCETaskView a self-owned @State draft (mirroring AddCETaskView): edit a local copy, commit it back by id on Done, and delete by id. No code subscripts the array by a stale index, eliminating the crash. --- .../Views/CEWorkspaceSettingsView.swift | 3 +-- .../Views/EditCETaskView.swift | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift index 8c134f5143..1c3f5843f6 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift @@ -70,8 +70,7 @@ struct CEWorkspaceSettingsView: View { $0.id == selectedTaskID }) { EditCETaskView( - task: $workspaceSettingsManager.settings.tasks[selectedTaskIndex], - selectedTaskIndex: selectedTaskIndex + task: workspaceSettingsManager.settings.tasks[selectedTaskIndex] ) } else { AddCETaskView() diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift index e588ce27df..337cb7f00b 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift @@ -14,9 +14,17 @@ struct EditCETaskView: View { @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings @EnvironmentObject var taskManager: TaskManager - @Binding var task: CETask - let selectedTaskIndex: Int + /// A self-owned draft of the task being edited. Committed back into the settings on "Done". + /// Editing a draft (rather than binding into `settings.tasks` by index) avoids an + /// out-of-bounds crash when the underlying array changes — e.g. on delete. + @State private var task: CETask + private let taskID: UUID + + init(task: CETask) { + self._task = State(initialValue: task) + self.taskID = task.id + } var body: some View { VStack(spacing: 0) { @@ -26,10 +34,10 @@ struct EditCETaskView: View { Button(role: .destructive) { do { workspaceSettingsManager.settings.tasks.removeAll(where: { - $0.id == task.id + $0.id == taskID }) try workspaceSettingsManager.savePreferences() - taskManager.deleteTask(taskID: task.id) + taskManager.deleteTask(taskID: taskID) self.dismiss() } catch { NSAlert(error: error).runModal() @@ -44,6 +52,11 @@ struct EditCETaskView: View { Button { do { + if let index = workspaceSettingsManager.settings.tasks.firstIndex(where: { + $0.id == taskID + }) { + workspaceSettingsManager.settings.tasks[index] = task + } try workspaceSettingsManager.savePreferences() self.dismiss() } catch { From a1942539ac249927f298358f88868d2a58d09f4b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 22:49:10 +0200 Subject: [PATCH 055/335] Refactor: Move CEWorkspaceFile and FileType into CodeEditCore Relocate the file-tree model to CodeEditCore as a UI-free reference class, with a type-erased weak fileDocumentObject. Presentation (icon/color/Finder intents/label), Settings-driven naming, and the editor document/tab coupling move to app-target extensions on the Core type; an Editor extension bridges fileDocumentObject back to a typed fileDocument: CodeFileDocument?. Extract the FileType enum out of the app-side FileIcon into Core. Unblocks the CEWorkspaceFileManager service (C) and future feature packages. --- .../Tasks/WorkspaceMenuItemView.swift | 1 + .../Models/CEWorkspaceFile+Editor.swift | 32 ++ .../Models/CEWorkspaceFile+Presentation.swift | 92 +++++ .../Models/CEWorkspaceFile+Recursion.swift | 1 + .../CEWorkspace/Models/CEWorkspaceFile.swift | 315 ------------------ .../Models/CEWorkspaceFileIcon.swift | 82 +---- ...WorkspaceFileManager+DirectoryEvents.swift | 1 + ...EWorkspaceFileManager+FileManagement.swift | 1 + .../Models/CEWorkspaceFileManager.swift | 1 + .../UseCases/AcceptDroppedFilesUseCase.swift | 1 + .../UseCases/MoveFileUseCase.swift | 1 + .../Views/EditorJumpBarComponent.swift | 1 + .../JumpBar/Views/EditorJumpBarMenu.swift | 1 + .../JumpBar/Views/EditorJumpBarView.swift | 1 + .../Editor/Models/Editor/Editor.swift | 1 + .../Editor/Models/EditorInstance.swift | 1 + .../EditorLayout+StateRestoration.swift | 1 + .../Models/EditorLayout/EditorLayout.swift | 1 + .../Editor/Models/EditorManager.swift | 1 + .../Restoration/UndoManagerRegistration.swift | 1 + .../Tabs/Tab/EditorFileTabCloseButton.swift | 1 + .../TabBar/Tabs/Tab/EditorTabView.swift | 1 + .../Tab/Models/EditorTabFileObserver.swift | 1 + .../Tabs/Views/EditorTabOnDropDelegate.swift | 1 + .../Editor/TabBar/Tabs/Views/EditorTabs.swift | 1 + .../Views/EditorTabBarContextMenu.swift | 1 + .../UseCases/RestoreEditorStateUseCase.swift | 1 + .../Editor/Views/EditorAreaView.swift | 1 + .../Editor/Views/WindowCodeFileView.swift | 1 + .../FileInspector/FileInspectorView.swift | 1 + .../OutlineView/FileSystemTableViewCell.swift | 1 + .../OutlineView/ProjectNavigatorMenu.swift | 1 + .../ProjectNavigatorMenuActions.swift | 1 + .../ProjectNavigatorNSOutlineView.swift | 1 + .../ProjectNavigatorOutlineView.swift | 1 + .../ProjectNavigatorTableViewCell.swift | 1 + ...vigatorViewController+NSMenuDelegate.swift | 1 + ...ewController+NSOutlineViewDataSource.swift | 1 + ...troller+OutlineTableViewCellDelegate.swift | 1 + .../Views/OpenQuicklyPreviewView.swift | 1 + .../OpenQuickly/Views/OpenQuicklyView.swift | 1 + .../Models/WorkspaceNotificationModel.swift | 1 + CodeEdit/WorkspaceView.swift | 1 + ...ment+SearchState+FindAndReplaceTests.swift | 1 + ...kspaceDocument+SearchState+FindTests.swift | 1 + ...spaceDocument+SearchState+IndexTests.swift | 1 + .../Editor/UndoManagerRegistrationTests.swift | 1 + .../LSP/LanguageServer+CodeFileDocument.swift | 1 + .../CEWorkspaceFileManagerTests.swift | 1 + .../Domain/Workspace/CEWorkspaceFile.swift | 177 ++++++++++ .../Domain/Workspace/FileType.swift | 21 ++ 51 files changed, 368 insertions(+), 396 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift create mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift delete mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift b/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift index 9c12b49342..62a3aa8ee3 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct WorkspaceMenuItemView: View { var workspaceFileManager: CEWorkspaceFileManager? diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift new file mode 100644 index 0000000000..34909f2fa7 --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift @@ -0,0 +1,32 @@ +// +// CEWorkspaceFile+Editor.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import SwiftUI +import Combine +import CodeEditCore + +extension CEWorkspaceFile: EditorTabRepresentable { + /// The `id` in `EditorTabID` form. + var tabID: EditorTabID { .codeEditor(id) } + + /// The file's open document, if any. Bridges the Core type-erased ``fileDocumentObject``. + var fileDocument: CodeFileDocument? { + get { fileDocumentObject as? CodeFileDocument } + set { fileDocumentObject = newValue } + } + + /// Publisher for ``fileDocument``. + var fileDocumentPublisher: AnyPublisher { + fileDocumentObjectPublisher.map { $0 as? CodeFileDocument }.eraseToAnyPublisher() + } + + /// Loads ``fileDocument`` with a new `CodeFileDocument`. + func loadCodeFile() throws { + let codeFile = try CodeFileDocument(contentsOf: resolvedURL, ofType: contentType?.identifier ?? "") + self.fileDocument = codeFile + } +} diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift new file mode 100644 index 0000000000..f5177158a8 --- /dev/null +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift @@ -0,0 +1,92 @@ +// +// CEWorkspaceFile+Presentation.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import SwiftUI +import CodeEditCore + +extension CEWorkspaceFile { + /// The file's icon as a SwiftUI `Image`. + var icon: Image { + if let customImage = NSImage.symbol(named: systemImage) { + return Image(nsImage: customImage) + } else { + return Image(systemName: systemImage) + } + } + + /// The file's icon as an `NSImage`. + var nsIcon: NSImage { + if let customImage = NSImage.symbol(named: systemImage) { + return customImage + } else { + return NSImage(systemSymbolName: systemImage, accessibilityDescription: systemImage) + ?? NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")! + } + } + + /// SF Symbol name for the file/folder. + var systemImage: String { + if isFolder { + return folderIcon() + } else { + return FileIcon.fileIcon(fileType: type) + } + } + + /// Icon tint color for the file type. + var iconColor: Color { + FileIcon.iconColor(fileType: type) + } + + /// SF Symbol name for folders (root / `.codeedit` / populated / empty). + private func folderIcon() -> String { + if self.parent == nil { return "folder.fill.badge.gearshape" } + if self.name == ".codeedit" { return "folder.fill.badge.gearshape" } + return isEmptyFolder ? "folder" : "folder.fill" + } + + // MARK: Intents + + /// Reveal the file/folder in Finder. + func showInFinder() { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + + /// Open the file/folder with the system default application. + func openWithExternalEditor() { + NSWorkspace.shared.open(url) + } + + // MARK: Display name (user preference driven) + + /// A display name honoring the user's file-extension-visibility preference. + func labelFileName() -> String { + let prefs = Settings.shared.preferences.general + switch prefs.fileExtensionsVisibility { + case .hideAll: + return self.fileName(typeHidden: true) + case .showAll: + return self.fileName(typeHidden: false) + case .showOnly: + return self.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(self.type.rawValue)) + case .hideOnly: + return self.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(self.type.rawValue)) + } + } + + func validateFileName(for newName: String) -> Bool { + guard newName != labelFileName() && + !newName.isEmpty && + newName.isValidFilename && + !FileManager.default.fileExists( + atPath: self.url.deletingLastPathComponent().appending(path: newName).path + ) else { + return false + } + return true + } +} diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift index 1647514a4e..9cb3304bf4 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore extension CEWorkspaceFile { /// Flattens the children of `self` recursively with depth. diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift deleted file mode 100644 index bb10ea5cf6..0000000000 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift +++ /dev/null @@ -1,315 +0,0 @@ -// -// FileItem.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 07/02/2023. -// - -import Foundation -import SwiftUI -import UniformTypeIdentifiers -import Combine -import CodeEditCore - -/// An object containing all necessary information and actions for a specific file in the workspace -/// -/// The ``CEWorkspaceFile`` represents every type of file that can exist on the file system. Directories, files, -/// symlinks, etc. This class does not assume anything about what it is representing, but it can be interrogated to find -/// out what it represents. All information about the file is derived from the `URL` passed to the initializer of the -/// object. -/// -/// This object works to provide a consistent API for any component that needs to work with files, and is as small as -/// possible. -/// -/// These objects should be fetched from the ``CEWorkspaceFileManager`` whenever possible. Objects fetched from there -/// will be connected in CodeEdit's file tree, and structural properties like ``CEWorkspaceFile/parent`` will exist. -/// They can, however, be created standalone when necessary. Creating a standalone ``CEWorkspaceFile`` is useful if -/// loading all intermediate subdirectories (from the nearest cached parent to the file) has not been done yet and doing -/// so would be unnecessary. -/// -/// An example of this is in the ``OpenQuicklyView``. This view finds a file URL via a search bar, and needs to display -/// a quick preview of the file. There's a good chance the file is deep in some subdirectory of the workspace, so -/// fetching it from the ``CEWorkspaceFileManager`` may require loading and caching multiple directories. Instead, it -/// just makes a disconnected object and uses it for the preview. Then, when opening the file in the workspace it -/// forces the file to be loaded and cached. -final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, EditorTabRepresentable { - - /// The id of the ``CEWorkspaceFile``. - var id: String - - /// Returns the file name (e.g.: `Package.swift`) - var name: String { url.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) } - - /// Returns the extension of the file or an empty string if no extension is present. - var type: FileIcon.FileType { - let filename = url.fileName - - /// First, check if there is a valid file extension. - if let type = FileIcon.FileType(rawValue: filename) { - return type - } else { - /// If there's not, verifies every extension for a valid type. - let extensions = filename.dropFirst().components(separatedBy: ".").reversed() - - return extensions - .compactMap { FileIcon.FileType(rawValue: $0) } - .first - /// Returns .txt for invalid type. - ?? .txt - } - } - - /// Returns the URL of the ``CEWorkspaceFile`` - let url: URL - - /// Returns the resolved symlink url of this object. - lazy var resolvedURL: URL = { - url.isSymbolicLink ? url.resolvingSymlinksInPath() : url - }() - - /// Return the icon of the file as `Image` - var icon: Image { - if let customImage = NSImage.symbol(named: systemImage) { - return Image(nsImage: customImage) - } else { - return Image(systemName: systemImage) - } - } - - /// Return the icon of the file as `NSImage` - var nsIcon: NSImage { - if let customImage = NSImage.symbol(named: systemImage) { - return customImage - } else { - return NSImage(systemSymbolName: systemImage, accessibilityDescription: systemImage) - ?? NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")! - } - } - - /// Returns a parent ``CEWorkspaceFile``. - /// - /// If the item already is the top-level ``CEWorkspaceFile`` this returns `nil`. - weak var parent: CEWorkspaceFile? - - private let fileDocumentSubject = PassthroughSubject() - - weak var fileDocument: CodeFileDocument? { - didSet { - fileDocumentSubject.send(fileDocument) - } - } - - /// Publisher for fileDocument property - var fileDocumentPublisher: AnyPublisher { - fileDocumentSubject.eraseToAnyPublisher() - } - - var fileIdentifier = UUID().uuidString - - /// Returns the Git status of a file as ``GitStatus`` - var gitStatus: GitStatus? - - /// Returns a boolean that is true if the file is staged for commit - var staged: Bool? - - /// Returns the `id` in ``EditorTabID`` enum form - var tabID: EditorTabID { .codeEditor(id) } - - /// Returns a boolean that is true if the resource represented by this object is a directory. - lazy var isFolder: Bool = { - resolvedURL.isFolder - }() - - /// Returns a boolean that is true if the contents of the directory at this path are - /// - /// Does not indicate if this is a folder, see ``isFolder`` to first check if this object is also a directory. - var isEmptyFolder: Bool { - (try? CEWorkspaceFile.fileManager.contentsOfDirectory( - at: resolvedURL, - includingPropertiesForKeys: nil, - options: .skipsSubdirectoryDescendants - ).isEmpty) ?? true - } - - /// Returns a boolean that is true if the file item is the root folder of the workspace. - var isRoot: Bool { parent == nil } - - /// Returns a boolean that is true if the file item actually exists in the file system - var doesExist: Bool { CEWorkspaceFile.fileManager.fileExists(atPath: self.url.path) } - - /// Returns a string describing a SFSymbol for the current ``CEWorkspaceFile`` - /// - /// Use it like this - /// ```swift - /// Image(systemName: item.systemImage) - /// ``` - var systemImage: String { - if isFolder { - // item is a folder - return folderIcon() - } else { - // item is a file - return FileIcon.fileIcon(fileType: type) - } - } - - /// Return the file's UTType - var contentType: UTType? { - url.contentType - } - - /// Returns a `Color` for a specific `fileType` - /// - /// If not specified otherwise this will return `Color.accentColor` - var iconColor: Color { - FileIcon.iconColor(fileType: type) - } - - init( - id: String, - url: URL, - changeType: GitStatus? = nil, - staged: Bool? = false - ) { - self.id = id - self.url = url - self.gitStatus = changeType - self.staged = staged - } - - convenience init( - url: URL, - changeType: GitStatus? = nil, - staged: Bool? = false - ) { - self.init( - id: url.relativePath, - url: url, - changeType: changeType, - staged: staged - ) - } - - enum CodingKeys: String, CodingKey { - case id - case name - case url - case changeType - case staged - } - - required init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - id = try values.decode(String.self, forKey: .id) - url = try values.decode(URL.self, forKey: .url) - gitStatus = try values.decode(GitStatus.self, forKey: .changeType) - staged = try values.decode(Bool.self, forKey: .staged) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(name, forKey: .name) - try container.encode(url, forKey: .url) - try container.encode(gitStatus, forKey: .changeType) - try container.encode(staged, forKey: .staged) - } - - /// Returns a string describing a SFSymbol for folders - /// - /// If it is the top-level folder this will return `"square.dashed.inset.filled"`. - /// If it is a `.codeedit` folder this will return `"folder.fill.badge.gearshape"`. - /// If it has children this will return `"folder.fill"` otherwise `"folder"`. - private func folderIcon() -> String { - if self.parent == nil { - return "folder.fill.badge.gearshape" - } - if self.name == ".codeedit" { - return "folder.fill.badge.gearshape" - } - return isEmptyFolder ? "folder" : "folder.fill" - } - - /// Returns the file name with optional extension (e.g.: `Package.swift`) - func fileName(typeHidden: Bool = false) -> String { - typeHidden ? url.deletingPathExtension() - .lastPathComponent - .trimmingCharacters(in: .whitespacesAndNewlines) : name - } - - /// Generates a string based on user's file name preferences. - /// - Returns: A `String` suitable for display. - func labelFileName() -> String { - let prefs = Settings.shared.preferences.general - switch prefs.fileExtensionsVisibility { - case .hideAll: - return self.fileName(typeHidden: true) - case .showAll: - return self.fileName(typeHidden: false) - case .showOnly: - return self.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(self.type.rawValue)) - case .hideOnly: - return self.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(self.type.rawValue)) - } - } - - func validateFileName(for newName: String) -> Bool { - // Name must be: new, nonempty, valid characters, and not exist in the filesystem. - guard newName != labelFileName() && - !newName.isEmpty && - newName.isValidFilename && - !FileManager.default.fileExists( - atPath: self.url.deletingLastPathComponent().appending(path: newName).path - ) else { - return false - } - - return true - } - - /// Loads the ``fileDocument`` property with a new ``CodeFileDocument``. - func loadCodeFile() throws { - let codeFile = try CodeFileDocument(contentsOf: resolvedURL, ofType: contentType?.identifier ?? "") - self.fileDocument = codeFile - } - - // MARK: Statics - /// The default `FileManager` instance - static let fileManager = FileManager.default - - // MARK: Intents - /// Allows the user to view the file or folder in the finder application - func showInFinder() { - NSWorkspace.shared.activateFileViewerSelecting([url]) - } - - /// Allows the user to launch the file or folder as it would be in finder - func openWithExternalEditor() { - NSWorkspace.shared.open(url) - } - - /// Nearest folder refers to the parent directory if this is a non-folder item, or itself if the item is a folder. - var nearestFolder: URL { - (self.isFolder ? - self.url : - self.url.deletingLastPathComponent()) - } - - // MARK: Comparable - - static func == (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { - lhs.id == rhs.id - } - - static func < (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { - lhs.url.lastPathComponent < rhs.url.lastPathComponent - } - - // MARK: Hashable - - func hash(into hasher: inout Hasher) { - hasher.combine(url) - hasher.combine(id) - } - -} diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift index e26f15af78..1611a20f37 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift @@ -6,91 +6,11 @@ // import SwiftUI +import CodeEditCore // TODO: DOCS (Nanashi Li) enum FileIcon { - // swiftlint:disable identifier_name - enum FileType: String { - case adb - case aif - case avi - case bash - case c - case cetheme - case clj - case cls - case cs - case css - case d - case dart - case elm - case entitlements - case env - case ex - case example - case f95 - case fs - case gitignore - case go - case gs - case h - case hs - case html - case ico - case java - case jl - case jpeg - case jpg - case js - case json - case jsx - case kt - case l - case LICENSE - case lock - case lsp - case lua - case m - case Makefile - case md - case mid - case mjs - case mk - case mod - case mov - case mp3 - case mp4 - case pas - case pdf - case pl - case plist - case png - case py - case resolved - case rb - case rs - case rtf - case scm - case scpt - case sh - case ss - case strings - case sum - case svg - case swift - case ts - case tsx - case txt = "text" - case vue - case wav - case xcconfig - case yml - case zsh - } - - // swiftlint:enable identifier_name - /// Returns a string describing a SFSymbol for files /// If not specified otherwise this will return `"doc"` static func fileIcon(fileType: FileType?) -> String { // swiftlint:disable:this cyclomatic_complexity function_body_length line_length diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift index a40b6b4c59..e9cd052d8e 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore /// This extension handles the file system events triggered by changes in the root folder. extension CEWorkspaceFileManager { diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift index 82989fbffc..910f30bb9d 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import AppKit extension CEWorkspaceFileManager { diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index 2e0f824d11..138018dc85 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditCore import Foundation import AppKit import OSLog diff --git a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift index a796df58c9..21c0dbcc34 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore /// Resolves dropped file URLs into copy/move operations, handling source resolution and replace conflicts. @MainActor diff --git a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift index e7cbad551d..108d646d14 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore /// Moves a file within a workspace, closing any open tabs for it and reopening the new location. /// diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift index 2cc183e30a..c74fc8033e 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import Combine import CodeEditSymbols diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift index 84c97766bc..41aefe9cd7 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditCore final class EditorJumpBarMenu: NSMenu, NSMenuDelegate { private let fileItems: [CEWorkspaceFile] diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift index 9dda335439..b6a7188d8b 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct EditorJumpBarView: View { private let file: CEWorkspaceFile? diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index 8de4e12d84..8956239306 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import Search import OrderedCollections import DequeModule diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index e056f6f241..e7f422ec9d 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import Search import AppKit import Combine diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index d518e7b74d..c56a32acb1 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import Search import SwiftUI import OrderedCollections diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift index ee803a76ab..9af83b6fbd 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore enum EditorLayout: Equatable { case one(Editor) diff --git a/CodeEdit/Features/Editor/Models/EditorManager.swift b/CodeEdit/Features/Editor/Models/EditorManager.swift index 4e8eae3493..94c3f9d476 100644 --- a/CodeEdit/Features/Editor/Models/EditorManager.swift +++ b/CodeEdit/Features/Editor/Models/EditorManager.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditCore import Foundation import DequeModule import os diff --git a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift index a665c8991b..1dd0e934e2 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift +++ b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditTextView /// Very simple class for registering undo manager for files for a project session. This does not do any saving, it diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift index a53d4d1e6a..0e8a7c47f4 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import SwiftUI import Combine diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 823f41220f..6a1d921e45 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct EditorTabView: View { diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift index c9e78d7886..aeff65889e 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import SwiftUI /// Observer ViewModel for tracking file deletion diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift index 6035b548f6..e0a11eb0c2 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct EditorTabOnDropDelegate: DropDelegate { typealias TabID = CEWorkspaceFile.ID diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift index 35467bb46f..7f775dbcf4 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditUI // - TODO: EditorTabView drop-outside event handler. diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index bef5d1d759..87056f051c 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import Foundation extension View { diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift index 3d9ab8bd63..2b044bb765 100644 --- a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift +++ b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import Search import OSLog import OrderedCollections diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index 790950477f..b324496e7d 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditUI import CodeEditTextView import UniformTypeIdentifiers diff --git a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift index c3f4b749be..059d8571ac 100644 --- a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import SwiftUI /// View that fixes [#1158](https://github.com/CodeEditApp/CodeEdit/issues/1158) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index cef7e67262..a0aa4540de 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI +import CodeEditCore import CodeEditLanguages struct FileInspectorView: View { diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 7aa5ea3cf0..da246cbb10 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore class FileSystemTableViewCell: StandardTableViewCell { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index e78029cc61..4aceff98db 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import UniformTypeIdentifiers /// A subclass of `NSMenu` implementing the contextual menu for the project navigator diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 1aa65af926..8dffbca7e9 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditCore import SwiftUI extension ProjectNavigatorMenu { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift index 58e77ebfca..23a0f03cea 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditCore final class ProjectNavigatorNSOutlineView: NSOutlineView, NSMenuItemValidation { override func performKeyEquivalent(with event: NSEvent) -> Bool { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index b3ae44e9b0..cac2b3fb0d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index 82db7b1649..52c1bf30ed 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore protocol OutlineTableViewCellDelegate: AnyObject { func moveFile(file: CEWorkspaceFile, to destination: URL) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift index 0b080127bf..0561f6b73f 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore // MARK: - NSMenuDelegate extension ProjectNavigatorViewController: NSMenuDelegate { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 12891dc430..5c9413d0b0 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditCore extension ProjectNavigatorViewController: NSOutlineViewDataSource { /// Retrieves the children of a given item for the outline view, applying the current filter if necessary. diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index 11278121e7..a70fb58558 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import AppKit // MARK: - OutlineTableViewCellDelegate diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift index 2a191cfdb1..efd35ebbd0 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct OpenQuicklyPreviewView: View { diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift index 2b11a4d9bc..ff7f63ab5a 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore struct OpenQuicklyView: View { @Environment(\.workspaceFileManager) diff --git a/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift b/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift index 795e7bc3a4..23598dc37c 100644 --- a/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift +++ b/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import Combine class WorkspaceNotificationModel: ObservableObject { diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index debd4f4e7e..76e3766946 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditUI import Notifications import UniformTypeIdentifiers diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 45a775a6a0..0e67cbbfca 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditCore @testable import Search @testable import CodeEdit diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 24a09843e6..9d94cddd9e 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditCore @testable import Search @testable import CodeEdit diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index affb2b1536..80a6fa9b6f 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditCore @testable import Search @testable import CodeEdit diff --git a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift b/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift index cfa9619aaf..ba23c8f387 100644 --- a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift +++ b/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift @@ -7,6 +7,7 @@ @testable import CodeEdit import Testing +import CodeEditCore import Foundation import CodeEditTextView diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 2538f90e92..7673bf9456 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditCore import CodeEditTextView import CodeEditSourceEditor import LanguageClient diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index 2fb01159fd..60d4e9a3bc 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -5,6 +5,7 @@ // Created by Marco Carnevali on 16/03/22. // import Combine +import CodeEditCore import Foundation import XCTest @testable import CodeEdit diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift new file mode 100644 index 0000000000..7d72423221 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift @@ -0,0 +1,177 @@ +// +// CEWorkspaceFile.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 07/02/2023. +// + +import Foundation +import UniformTypeIdentifiers +import Combine + +/// A file, folder, or symlink in the workspace. This is the UI-free model; presentation, +/// AppKit intents, name-labeling, and editor-document coupling live in app-side extensions. +public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable { + + /// The id of the ``CEWorkspaceFile``. + public var id: String + + /// Returns the file name (e.g.: `Package.swift`) + public var name: String { url.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) } + + /// The file's ``FileType`` derived from its extension (defaults to `.txt`). + public var type: FileType { + let filename = url.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) + if let type = FileType(rawValue: filename) { + return type + } else { + let extensions = filename.dropFirst().components(separatedBy: ".").reversed() + return extensions.compactMap { FileType(rawValue: $0) }.first ?? .txt + } + } + + /// Returns the URL of the ``CEWorkspaceFile`` + public let url: URL + + /// Returns the resolved symlink url of this object. + public lazy var resolvedURL: URL = { + Self.isSymbolicLink(url) ? url.resolvingSymlinksInPath() : url + }() + + /// Returns a parent ``CEWorkspaceFile``. `nil` for the top-level item. + public weak var parent: CEWorkspaceFile? + + private let fileDocumentSubject = PassthroughSubject() + + /// Type-erased weak reference to the file's open document (a `CodeFileDocument` in the app). + /// The app's `CEWorkspaceFile+Editor` extension provides a typed `fileDocument` accessor. + public weak var fileDocumentObject: AnyObject? { + didSet { fileDocumentSubject.send(fileDocumentObject) } + } + + /// Publisher for ``fileDocumentObject``. + public var fileDocumentObjectPublisher: AnyPublisher { + fileDocumentSubject.eraseToAnyPublisher() + } + + public var fileIdentifier = UUID().uuidString + + /// The Git status of the file. + public var gitStatus: GitStatus? + + /// Whether the file is staged for commit. + public var staged: Bool? + + /// True if the resource is a directory. + public lazy var isFolder: Bool = { + Self.isDirectory(resolvedURL) + }() + + /// True if this directory has no contents. (Check ``isFolder`` first.) + public var isEmptyFolder: Bool { + (try? Self.fileManager.contentsOfDirectory( + at: resolvedURL, + includingPropertiesForKeys: nil, + options: .skipsSubdirectoryDescendants + ).isEmpty) ?? true + } + + /// True if this is the workspace's root folder. + public var isRoot: Bool { parent == nil } + + /// True if the file exists on disk. + public var doesExist: Bool { Self.fileManager.fileExists(atPath: self.url.path) } + + /// The file's UTType. + public var contentType: UTType? { Self.contentType(url) } + + public init( + id: String, + url: URL, + changeType: GitStatus? = nil, + staged: Bool? = false + ) { + self.id = id + self.url = url + self.gitStatus = changeType + self.staged = staged + } + + public convenience init( + url: URL, + changeType: GitStatus? = nil, + staged: Bool? = false + ) { + self.init(id: url.relativePath, url: url, changeType: changeType, staged: staged) + } + + enum CodingKeys: String, CodingKey { + case id, name, url, changeType, staged + } + + public required init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + url = try values.decode(URL.self, forKey: .url) + gitStatus = try values.decode(GitStatus.self, forKey: .changeType) + staged = try values.decode(Bool.self, forKey: .staged) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(url, forKey: .url) + try container.encode(gitStatus, forKey: .changeType) + try container.encode(staged, forKey: .staged) + } + + /// Returns the file name, optionally without its extension. + public func fileName(typeHidden: Bool = false) -> String { + typeHidden + ? url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) + : name + } + + /// Parent directory for a file, or self for a folder. + public var nearestFolder: URL { + isFolder ? url : url.deletingLastPathComponent() + } + + // MARK: Statics + + /// `FileManager.default` is documented thread-safe; the shared instance is only read from here. + nonisolated(unsafe) public static let fileManager = FileManager.default + + private static func resourceValues(_ url: URL) -> URLResourceValues? { + try? url.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey, .contentTypeKey]) + } + + private static func isDirectory(_ url: URL) -> Bool { + resourceValues(url)?.isDirectory ?? false + } + + private static func isSymbolicLink(_ url: URL) -> Bool { + let values = resourceValues(url) + return (values?.isSymbolicLink ?? false) || (values?.contentType ?? .item) == .aliasFile + } + + private static func contentType(_ url: URL) -> UTType? { + resourceValues(url)?.contentType + } + + // MARK: Comparable / Hashable + + public static func == (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { + lhs.id == rhs.id + } + + public static func < (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { + lhs.url.lastPathComponent < rhs.url.lastPathComponent + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(url) + hasher.combine(id) + } +} diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift new file mode 100644 index 0000000000..05e92dd555 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift @@ -0,0 +1,21 @@ +// +// FileType.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import Foundation + +// swiftlint:disable identifier_name +/// File-type discriminator derived from a file's extension. +public enum FileType: String { + case adb, aif, avi, bash, c, cetheme, clj, cls, cs, css, d, dart, elm, entitlements + case env, ex, example, f95, fs, gitignore, go, gs, h, hs, html, ico, java, jl, jpeg + case jpg, js, json, jsx, kt, l, LICENSE, lock, lsp, lua, m, Makefile, md, mid, mjs + case mk, mod, mov, mp3, mp4, pas, pdf, pl, plist, png, py, resolved, rb, rs, rtf, scm + case scpt, sh, ss, strings, sum, svg, swift, ts, tsx + case txt = "text" + case vue, wav, xcconfig, yml, zsh +} +// swiftlint:enable identifier_name From 91ecf65ebfefa241aa2060f558f39794123e3ad9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 5 Jul 2026 22:51:21 +0200 Subject: [PATCH 056/335] Test: Add CodeEditCore CEWorkspaceFile unit tests Cover name/type derivation, id/equality/comparable, Codable round-trip, and parent tree wiring for the relocated Core file model. --- .../CEWorkspaceFileCoreTests.swift | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 CodeEditTests/Features/CEWorkspace/CEWorkspaceFileCoreTests.swift diff --git a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileCoreTests.swift b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileCoreTests.swift new file mode 100644 index 0000000000..dd114434f5 --- /dev/null +++ b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileCoreTests.swift @@ -0,0 +1,70 @@ +// +// CEWorkspaceFileCoreTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import XCTest +import CodeEditCore + +final class CEWorkspaceFileCoreTests: XCTestCase { + + func testNameAndType() { + let file = CEWorkspaceFile(url: URL(filePath: "/tmp/Package.swift")) + XCTAssertEqual(file.name, "Package.swift") + XCTAssertEqual(file.type, .swift) + } + + func testTypeDefaultsToTxt() { + let file = CEWorkspaceFile(url: URL(filePath: "/tmp/no-extension-here")) + XCTAssertEqual(file.type, .txt) + } + + func testFileNameTypeHidden() { + let file = CEWorkspaceFile(url: URL(filePath: "/tmp/Model.swift")) + XCTAssertEqual(file.fileName(typeHidden: true), "Model") + XCTAssertEqual(file.fileName(typeHidden: false), "Model.swift") + } + + func testConvenienceInitUsesRelativePathAsID() { + let url = URL(filePath: "/tmp/a/b.txt") + XCTAssertEqual(CEWorkspaceFile(url: url).id, url.relativePath) + } + + func testEqualityByID() { + let url = URL(filePath: "/tmp/x.swift") + XCTAssertEqual(CEWorkspaceFile(url: url), CEWorkspaceFile(url: url)) + XCTAssertNotEqual( + CEWorkspaceFile(url: URL(filePath: "/tmp/x.swift")), + CEWorkspaceFile(url: URL(filePath: "/tmp/y.swift")) + ) + } + + func testComparableByLastPathComponent() { + let apple = CEWorkspaceFile(url: URL(filePath: "/tmp/a.swift")) + let banana = CEWorkspaceFile(url: URL(filePath: "/tmp/b.swift")) + XCTAssertTrue(apple < banana) + } + + func testCodableRoundTrip() throws { + // Note: the encoder writes `changeType`/`staged` unconditionally and the decoder + // requires them non-null, so a valid round-trip needs a concrete gitStatus. + let original = CEWorkspaceFile(url: URL(filePath: "/tmp/File.swift"), changeType: .modified, staged: true) + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(CEWorkspaceFile.self, from: data) + XCTAssertEqual(decoded.id, original.id) + XCTAssertEqual(decoded.url, original.url) + XCTAssertEqual(decoded.gitStatus, .modified) + XCTAssertEqual(decoded.staged, true) + } + + func testParentWiring() { + let parent = CEWorkspaceFile(url: URL(filePath: "/tmp/folder")) + let child = CEWorkspaceFile(url: URL(filePath: "/tmp/folder/child.swift")) + child.parent = parent + XCTAssertTrue(parent.isRoot) + XCTAssertFalse(child.isRoot) + XCTAssertIdentical(child.parent, parent) + } +} From 0b634fca403aab722846ce24d7018043ff914918 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 11:23:11 +0200 Subject: [PATCH 057/335] Feature: Add WorkspaceFileEvent and GitStatusChangedEvent to CodeEditCore --- .../CEWorkspace/WorkspaceEventsTests.swift | 29 +++++++++++++++++ .../CodeEditCore/Domain/Git/GitStatus.swift | 2 +- .../Events/GitStatusChangedEvent.swift | 25 +++++++++++++++ .../Events/WorkspaceFileEvent.swift | 31 +++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 CodeEditTests/Features/CEWorkspace/WorkspaceEventsTests.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift diff --git a/CodeEditTests/Features/CEWorkspace/WorkspaceEventsTests.swift b/CodeEditTests/Features/CEWorkspace/WorkspaceEventsTests.swift new file mode 100644 index 0000000000..a29ecb7c53 --- /dev/null +++ b/CodeEditTests/Features/CEWorkspace/WorkspaceEventsTests.swift @@ -0,0 +1,29 @@ +// +// WorkspaceEventsTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import XCTest +import CodeEditCore + +final class WorkspaceEventsTests: XCTestCase { + func testWorkspaceFileEventCarriesKindAndURL() { + let url = URL(filePath: "/tmp/ws") + let event = WorkspaceFileEvent(workspaceURL: url, kind: .filesystemChanged(paths: ["a.swift"])) + XCTAssertEqual(event.workspaceURL, url) + if case let .filesystemChanged(paths) = event.kind { + XCTAssertEqual(paths, ["a.swift"]) + } else { + XCTFail("wrong kind") + } + } + + func testGitStatusChangedEventCarriesMap() { + let url = URL(filePath: "/tmp/ws") + let event = GitStatusChangedEvent(workspaceURL: url, changed: ["a.swift": .modified]) + XCTAssertEqual(event.workspaceURL, url) + XCTAssertEqual(event.changed["a.swift"], .modified) + } +} diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift index 251c34e05a..ce46dfc42f 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift @@ -7,7 +7,7 @@ import Foundation -public enum GitStatus: String, Codable { +public enum GitStatus: String, Codable, Sendable { case none = "." case modified = "M" case untracked = "?" diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift new file mode 100644 index 0000000000..8fbd0ebb11 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift @@ -0,0 +1,25 @@ +// +// GitStatusChangedEvent.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import Foundation + +/// A snapshot of the current git status for every changed file in a workspace, +/// published by source control and consumed by the workspace file manager, which +/// applies the statuses onto its cached files and clears any file not present here. +public struct GitStatusChangedEvent: Event { + /// Restricts delivery to the workspace at this URL. + public let workspaceURL: URL + + /// fileKey → status for every currently-changed file. The file manager clears + /// `gitStatus` on any cached file whose key is NOT present in this map. + public let changed: [String: GitStatus] + + public init(workspaceURL: URL, changed: [String: GitStatus]) { + self.workspaceURL = workspaceURL + self.changed = changed + } +} diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift new file mode 100644 index 0000000000..8d754920ed --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift @@ -0,0 +1,31 @@ +// +// WorkspaceFileEvent.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import Foundation + +/// A raw filesystem change under a workspace root, published by the workspace +/// file manager and consumed by features that react to file changes (e.g. source +/// control). The file manager is deliberately git-agnostic: it emits raw paths and +/// lets subscribers interpret them. +public struct WorkspaceFileEvent: Event { + public enum Kind: Sendable { + /// One or more paths under the workspace root changed on disk. + /// Paths are workspace-relative, exactly as reported by the event stream. + case filesystemChanged(paths: [String]) + /// A directory's children were lazily indexed into the file cache. + case childrenIndexed + } + + /// Restricts delivery to the workspace at this URL. + public let workspaceURL: URL + public let kind: Kind + + public init(workspaceURL: URL, kind: Kind) { + self.workspaceURL = workspaceURL + self.kind = kind + } +} From b1c0efad5a7a5f8afe1773cad5bb0655384f6edf Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 11:24:12 +0200 Subject: [PATCH 058/335] Refactor: Move file-delete confirmation out of CEWorkspaceFileManager into the navigator --- ...EWorkspaceFileManager+FileManagement.swift | 44 +++++-------------- .../Models/CEWorkspaceFileManager.swift | 1 - .../ProjectNavigatorMenuActions.swift | 20 ++++++++- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift index 910f30bb9d..5c13b297fe 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift @@ -7,7 +7,6 @@ import Foundation import CodeEditCore -import AppKit extension CEWorkspaceFileManager { /// This function allows creation of folders in the main directory or sub-folders @@ -160,46 +159,23 @@ extension CEWorkspaceFileManager { } /// This function deletes the item or folder from the current project by erasing immediately. - /// - Parameters: - /// - file: The file to delete - /// - confirmDelete: True to present an alert to confirm the delete. + /// - Parameter file: The file to delete + /// - Note: Presenting a confirmation is the caller's responsibility. /// - Authors: Mattijs Eikelenboom, KaiTheRedNinja., Paul Ebose *Moved from 7c27b1e* - public func delete(file: CEWorkspaceFile, confirmDelete: Bool = true) throws { + public func delete(file: CEWorkspaceFile) throws { // This function also has to account for how the // - file system can change outside of the editor - let fileName = file.name - - let deleteConfirmation = NSAlert() - deleteConfirmation.messageText = "Do you want to delete “\(fileName)”?" - deleteConfirmation.informativeText = "This item will be deleted immediately. You can't undo this action." - deleteConfirmation.alertStyle = .critical - deleteConfirmation.addButton(withTitle: "Delete") - deleteConfirmation.buttons.last?.hasDestructiveAction = true - deleteConfirmation.addButton(withTitle: "Cancel") - if !confirmDelete || deleteConfirmation.runModal() == .alertFirstButtonReturn { // "Delete" button - if fileManager.fileExists(atPath: file.url.path) { - try deleteFile(at: file.url) - } + if fileManager.fileExists(atPath: file.url.path) { + try deleteFile(at: file.url) } } /// This function deletes multiple files or folders from the current project by erasing immediately. - /// - Parameters: - /// - files: The files to delete - /// - confirmDelete: True to present an alert to confirm the delete. - public func batchDelete(files: Set, confirmDelete: Bool = true) throws { - let deleteConfirmation = NSAlert() - deleteConfirmation.messageText = "Are you sure you want to delete the \(files.count) selected items?" - // swiftlint:disable:next line_length - deleteConfirmation.informativeText = "\(files.count) items will be deleted immediately. You cannot undo this action." - deleteConfirmation.alertStyle = .critical - deleteConfirmation.addButton(withTitle: "Delete") - deleteConfirmation.buttons.last?.hasDestructiveAction = true - deleteConfirmation.addButton(withTitle: "Cancel") - if !confirmDelete || deleteConfirmation.runModal() == .alertFirstButtonReturn { - for file in files where fileManager.fileExists(atPath: file.url.path) { - try deleteFile(at: file.url) - } + /// - Parameter files: The files to delete + /// - Note: Presenting a confirmation is the caller's responsibility. + public func batchDelete(files: Set) throws { + for file in files where fileManager.fileExists(atPath: file.url.path) { + try deleteFile(at: file.url) } } diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index 138018dc85..a00a96a3c1 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -8,7 +8,6 @@ import Combine import CodeEditCore import Foundation -import AppKit import OSLog protocol CEWorkspaceFileManagerObserver: AnyObject { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 8dffbca7e9..5555dfff8f 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -210,8 +210,26 @@ extension ProjectNavigatorMenu { /// Action that deletes the item immediately. @objc func delete() { + let selectedItems = selectedItems() + + let confirmation = NSAlert() + confirmation.alertStyle = .critical + confirmation.addButton(withTitle: "Delete") + confirmation.buttons.last?.hasDestructiveAction = true + confirmation.addButton(withTitle: "Cancel") + if selectedItems.count == 1, let only = selectedItems.first { + confirmation.messageText = "Do you want to delete \u{201C}\(only.name)\u{201D}?" + confirmation.informativeText = "This item will be deleted immediately. You can't undo this action." + } else { + confirmation.messageText = + "Are you sure you want to delete the \(selectedItems.count) selected items?" + // swiftlint:disable:next line_length + confirmation.informativeText = "\(selectedItems.count) items will be deleted immediately. You cannot undo this action." + } + + guard confirmation.runModal() == .alertFirstButtonReturn else { return } + do { - let selectedItems = selectedItems() if selectedItems.count == 1 { try selectedItems.forEach { item in try workspace?.workspaceFileManager?.delete(file: item) From ff048a32675c9395590cf322f637f8ffc4b4f645 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 11:27:24 +0200 Subject: [PATCH 059/335] Refactor: Publish git status via GitStatusChangedEvent instead of mutating the file cache --- .../Models/CEWorkspaceFileManager.swift | 37 ++++++++ .../ChangedFile/GitChangedFileLabel.swift | 4 +- .../SourceControlManager+FileOperations.swift | 35 ++------ .../SourceControl/SourceControlManager.swift | 7 +- .../Features/Workspace/WorkspaceFactory.swift | 5 +- .../CEWorkspaceFileManagerEventsTests.swift | 87 +++++++++++++++++++ .../Documents/DocumentsUnitTests.swift | 3 +- .../CEWorkspaceFileManagerTests.swift | 8 +- 8 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index a00a96a3c1..cd645cdbdc 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -51,6 +51,8 @@ final class CEWorkspaceFileManager { let folderUrl: URL let workspaceItem: CEWorkspaceFile + let eventBus: EventBus + private var eventCancellables: Set = [] weak var sourceControlManager: SourceControlManager? /// Create a file manager object with a root and a set of files to ignore. @@ -62,6 +64,7 @@ final class CEWorkspaceFileManager { folderUrl: URL, ignoredFilesAndFolders: Set, fileManager: FileManager = FileManager.default, + eventBus: EventBus, sourceControlManager: SourceControlManager? ) { self.folderUrl = folderUrl @@ -71,6 +74,9 @@ final class CEWorkspaceFileManager { self.flattenedFileItems = [workspaceItem.id: workspaceItem] self.sourceControlManager = sourceControlManager self.fileManager = fileManager + self.eventBus = eventBus + + subscribeToGitStatusEvents() self.loadChildrenForFile(self.workspaceItem) @@ -83,6 +89,37 @@ final class CEWorkspaceFileManager { } } + /// Applies git statuses published by source control onto the cached files. + private func subscribeToGitStatusEvents() { + eventBus.subscribe(GitStatusChangedEvent.self) + .filter { [weak self] in $0.workspaceURL == self?.folderUrl } + .receive(on: RunLoop.main) + .sink { [weak self] event in + self?.applyGitStatuses(event.changed) + } + .store(in: &eventCancellables) + } + + /// Applies the given fileKey → status map onto cached files, clears any cached + /// file not present in the map, and notifies observers of the union. + private func applyGitStatuses(_ changed: [String: GitStatus]) { + var updatedStatusFor: Set = [] + for (key, status) in changed { + guard let file = getFile(key) else { continue } + if file.gitStatus != status { + file.gitStatus = status + } + updatedStatusFor.insert(file) + } + for (_, file) in flattenedFileItems + where !updatedStatusFor.contains(file) && file.gitStatus != nil { + file.gitStatus = nil + updatedStatusFor.insert(file) + } + guard !updatedStatusFor.isEmpty else { return } + notifyObservers(updatedItems: updatedStatusFor) + } + // MARK: - Public API /// A function that, given a file's path, returns a `FileItem` if it exists diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index fbc50f2ed9..e777170fc0 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -42,7 +42,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: nil )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient(), eventBus: Container.shared.eventBus())) .environmentObject(Workspace()) GitChangedFileLabel(file: GitChangedFile( @@ -51,7 +51,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: "app2.jsx" )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient(), eventBus: Container.shared.eventBus())) .environmentObject(Workspace()) }.padding() } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift index e16c9c1b48..c72b05a1a2 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift @@ -101,35 +101,14 @@ extension SourceControlManager { self.changedFiles = files } - /// Refresh git status for files in project navigator + /// Publish the current git status snapshot for the workspace's files. The + /// file manager applies these statuses onto its cached files. @MainActor private func refreshStatusInFileManager() { - guard let fileManager = fileManager else { - return - } - - var updatedStatusFor: Set = [] - // Refresh status of file manager files - for changedFile in changedFiles { - guard let file = fileManager.getFile(changedFile.ceFileKey) else { - continue - } - if file.gitStatus != changedFile.anyStatus() { - file.gitStatus = changedFile.anyStatus() - } - updatedStatusFor.insert(file) - } - - for (_, file) in fileManager.flattenedFileItems - where !updatedStatusFor.contains(file) && file.gitStatus != nil { - file.gitStatus = nil - updatedStatusFor.insert(file) - } - - if updatedStatusFor.isEmpty { - return - } - - fileManager.notifyObservers(updatedItems: updatedStatusFor) + let changed = Dictionary( + changedFiles.map { ($0.ceFileKey, $0.anyStatus()) }, + uniquingKeysWith: { _, latest in latest } + ) + eventBus.publish(GitStatusChangedEvent(workspaceURL: workspaceURL, changed: changed)) } } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index e623331ce6..a6d00a7f20 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/05/20. // +import Combine import Foundation import OSLog import CodeEditCore @@ -26,6 +27,8 @@ final class SourceControlManager: ObservableObject { /// The base URL of the workspace let workspaceURL: URL + let eventBus: EventBus + weak var fileManager: CEWorkspaceFileManager? // MARK: - Git State @@ -65,9 +68,11 @@ final class SourceControlManager: ObservableObject { init( workspaceURL: URL, - shellClient: ShellClientProtocol + shellClient: ShellClientProtocol, + eventBus: EventBus ) { self.workspaceURL = workspaceURL + self.eventBus = eventBus gitClient = GitClient(directoryURL: workspaceURL, shellClient: shellClient) } } diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 18cfdedcd5..a77cf1a7b4 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -52,14 +52,17 @@ enum WorkspaceFactory { } let shellClient = Container.shared.shellClient() + let eventBus = Container.shared.eventBus() let sourceControlManager = SourceControlManager( workspaceURL: url, - shellClient: shellClient + shellClient: shellClient, + eventBus: eventBus ) let workspaceFileManager = CEWorkspaceFileManager( folderUrl: url, ignoredFilesAndFolders: ignoredFilesAndDirectories, + eventBus: eventBus, sourceControlManager: sourceControlManager ) diff --git a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift new file mode 100644 index 0000000000..66b09edd51 --- /dev/null +++ b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift @@ -0,0 +1,87 @@ +// +// CEWorkspaceFileManagerEventsTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import XCTest +import CodeEditCore +@testable import CodeEdit + +final class CEWorkspaceFileManagerEventsTests: XCTestCase { + private var directory: URL! + + override func setUpWithError() throws { + directory = FileManager.default.temporaryDirectory + .appending(path: "CEWSFMEvents-\(UUID().uuidString)", directoryHint: .isDirectory) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try Data("x".utf8).write(to: directory.appending(path: "changed.swift")) + } + + override func tearDownWithError() throws { + try? FileManager.default.removeItem(at: directory) + } + + func testAppliesGitStatusChangedEvent() throws { + let bus = EventBus() + let fm = CEWorkspaceFileManager( + folderUrl: directory, + ignoredFilesAndFolders: [], + eventBus: bus, + sourceControlManager: nil + ) + let key = directory.appending(path: "changed.swift").relativePath + XCTAssertNotNil(fm.getFile(key), "file should be cached after init") + + bus.publish(GitStatusChangedEvent(workspaceURL: directory, changed: [key: .modified])) + + let expectation = expectation(description: "status applied") + DispatchQueue.main.async { + XCTAssertEqual(fm.getFile(key)?.gitStatus, .modified) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) + } + + func testClearsStaleGitStatus() throws { + let bus = EventBus() + let fm = CEWorkspaceFileManager( + folderUrl: directory, + ignoredFilesAndFolders: [], + eventBus: bus, + sourceControlManager: nil + ) + let key = directory.appending(path: "changed.swift").relativePath + fm.getFile(key)?.gitStatus = .modified + + bus.publish(GitStatusChangedEvent(workspaceURL: directory, changed: [:])) + + let expectation = expectation(description: "status cleared") + DispatchQueue.main.async { + XCTAssertNil(fm.getFile(key)?.gitStatus) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) + } + + func testIgnoresEventsForOtherWorkspaces() throws { + let bus = EventBus() + let fm = CEWorkspaceFileManager( + folderUrl: directory, + ignoredFilesAndFolders: [], + eventBus: bus, + sourceControlManager: nil + ) + let key = directory.appending(path: "changed.swift").relativePath + + bus.publish(GitStatusChangedEvent(workspaceURL: URL(filePath: "/tmp/other"), changed: [key: .modified])) + + let expectation = expectation(description: "no apply") + DispatchQueue.main.async { + XCTAssertNil(fm.getFile(key)?.gitStatus) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) + } +} diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 45b93d5446..f0e0b6c3ff 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -31,7 +31,8 @@ final class DocumentsUnitTests: XCTestCase { ) workspace.sourceControlManager = SourceControlManager( workspaceURL: URL(filePath: "/tmp"), - shellClient: Container.shared.shellClient() + shellClient: Container.shared.shellClient(), + eventBus: Container.shared.eventBus() ) workspace.sourceControlViewModel = SourceControlViewModel() workspace.searchState = SearchState(workspaceURL: URL(filePath: "/tmp")) diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index 60d4e9a3bc..f3e0fd49ba 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -55,6 +55,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let client = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], + eventBus: EventBus(), sourceControlManager: nil ) @@ -67,6 +68,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let client = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], + eventBus: EventBus(), sourceControlManager: nil ) @@ -119,6 +121,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], + eventBus: EventBus(), sourceControlManager: nil ) @@ -135,11 +138,12 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], + eventBus: EventBus(), sourceControlManager: nil ) XCTAssert(fileManager.getFile(testFileURL.path()) != nil) XCTAssert(FileManager.default.fileExists(atPath: testFileURL.path()) == true) - try fileManager.delete(file: CEWorkspaceFile(url: testFileURL), confirmDelete: false) + try fileManager.delete(file: CEWorkspaceFile(url: testFileURL)) XCTAssert(FileManager.default.fileExists(atPath: testFileURL.path()) == false) } @@ -151,6 +155,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], + eventBus: EventBus(), sourceControlManager: nil ) XCTAssert(fileManager.getFile(testFileURL.path()) != nil) @@ -165,6 +170,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], + eventBus: EventBus(), sourceControlManager: nil ) From e8e2fbff112fadf6cd8f7915b83959c2d41fed43 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 11:32:11 +0200 Subject: [PATCH 060/335] Refactor: Emit WorkspaceFileEvent from the file manager and move git-path handling into SourceControl --- ...WorkspaceFileManager+DirectoryEvents.swift | 10 ++- .../CEWorkspaceFileManager+GitEvents.swift | 84 ------------------- .../Models/CEWorkspaceFileManager.swift | 13 +-- .../Views/ToolbarBranchPicker.swift | 7 +- .../CodeEditWindowController+Toolbar.swift | 3 +- .../SourceControlManager+FileEvents.swift | 82 ++++++++++++++++++ .../SourceControl/SourceControlManager.swift | 3 + .../Features/Workspace/WorkspaceFactory.swift | 5 +- .../CEWorkspaceFileManagerEventsTests.swift | 25 ++++-- .../Features/CodeEditUI/CodeEditUITests.swift | 6 +- .../GitRefreshActionsTests.swift | 51 +++++++++++ .../CEWorkspaceFileManagerTests.swift | 18 ++-- 12 files changed, 180 insertions(+), 127 deletions(-) delete mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift create mode 100644 CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift create mode 100644 CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift index e9cd052d8e..1653596b6f 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift @@ -46,10 +46,12 @@ extension CEWorkspaceFileManager { self.notifyObservers(updatedItems: files) } - if Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled && - Settings.shared.preferences.sourceControl.general.refreshStatusLocally { - self.handleGitEvents(events: events) - } + self.eventBus.publish( + WorkspaceFileEvent( + workspaceURL: self.folderUrl, + kind: .filesystemChanged(paths: events.map(\.path)) + ) + ) } } diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift deleted file mode 100644 index 0d984dd441..0000000000 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+GitEvents.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// CEWorkspaceFileManager+GitEvents.swift -// CodeEdit -// -// Created by Axel Martinez on 5/8/24. -// - -import Foundation - -/// Handles git-specific file system events by detecting changes to git internals -/// and dispatching targeted refreshes to `SourceControlManager`. -extension CEWorkspaceFileManager { - func handleGitEvents(events: [DirectoryEventStream.Event]) { - refreshChangedFilesIfNeeded(events: events) - refreshStashIfNeeded(events: events) - refreshBranchesIfNeeded(events: events) - refreshCurrentBranchIfNeeded(events: events) - refreshRemotesIfNeeded(events: events) - validateRepositoryIfNeeded(events: events) - } - - /// If changes were made to project files or the git index, refresh the changed files list. - private func refreshChangedFilesIfNeeded(events: [DirectoryEventStream.Event]) { - let hasNonGitChanges = events.contains(where: { !$0.path.contains(".git/") }) - let hasIndexChange = events.contains(where: { - $0.path == "\(self.folderUrl.relativePath)/.git/index" - }) - - guard hasNonGitChanges || hasIndexChange else { return } - Task { - await self.sourceControlManager?.refreshAllChangedFiles() - } - } - - /// If changes were stashed, refresh stash entries. - private func refreshStashIfNeeded(events: [DirectoryEventStream.Event]) { - guard events.contains(where: { - $0.path == "\(self.folderUrl.relativePath)/.git/refs/stash" - }) else { return } - Task { - try await self.sourceControlManager?.refreshStashEntries() - } - } - - /// If branches were added or removed, refresh the branches list. - private func refreshBranchesIfNeeded(events: [DirectoryEventStream.Event]) { - guard events.contains(where: { - $0.path.contains("\(self.folderUrl.relativePath)/.git/refs/heads") - }) else { return } - Task { - await self.sourceControlManager?.refreshBranches() - } - } - - /// If HEAD was changed, refresh the current branch. - private func refreshCurrentBranchIfNeeded(events: [DirectoryEventStream.Event]) { - guard events.contains(where: { - $0.path.contains("\(self.folderUrl.relativePath)/.git/HEAD") - }) else { return } - Task { - await self.sourceControlManager?.refreshCurrentBranch() - } - } - - /// If .git/config changed, refresh remotes. - private func refreshRemotesIfNeeded(events: [DirectoryEventStream.Event]) { - guard events.contains(where: { - $0.path == "\(self.folderUrl.relativePath)/.git/config" - }) else { return } - Task { - try await self.sourceControlManager?.refreshRemotes() - } - } - - /// If the .git folder was added or removed, validate the repository. - private func validateRepositoryIfNeeded(events: [DirectoryEventStream.Event]) { - guard events.contains(where: { - $0.path == "\(self.folderUrl.relativePath)/.git" - }) else { return } - Task { - try await self.sourceControlManager?.validate() - } - } -} diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index cd645cdbdc..929e6b24a8 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -53,7 +53,6 @@ final class CEWorkspaceFileManager { let workspaceItem: CEWorkspaceFile let eventBus: EventBus private var eventCancellables: Set = [] - weak var sourceControlManager: SourceControlManager? /// Create a file manager object with a root and a set of files to ignore. /// - Parameters: @@ -64,15 +63,13 @@ final class CEWorkspaceFileManager { folderUrl: URL, ignoredFilesAndFolders: Set, fileManager: FileManager = FileManager.default, - eventBus: EventBus, - sourceControlManager: SourceControlManager? + eventBus: EventBus ) { self.folderUrl = folderUrl self.ignoredFilesAndFolders = ignoredFilesAndFolders self.workspaceItem = CEWorkspaceFile(url: folderUrl) self.flattenedFileItems = [workspaceItem.id: workspaceItem] - self.sourceControlManager = sourceControlManager self.fileManager = fileManager self.eventBus = eventBus @@ -83,10 +80,6 @@ final class CEWorkspaceFileManager { fsEventStream = DirectoryEventStream(directory: self.folderUrl.path) { [weak self] events in self?.fileSystemEventReceived(events: events) } - - Task { - try await self.sourceControlManager?.validate() - } } /// Applies git statuses published by source control onto the cached files. @@ -207,9 +200,7 @@ final class CEWorkspaceFileManager { addedChildrenUrls.append(newFileItem.id) } childrenMap[file.id] = addedChildrenUrls - Task { - await sourceControlManager?.refreshAllChangedFiles() - } + eventBus.publish(WorkspaceFileEvent(workspaceURL: folderUrl, kind: .childrenIndexed)) } /// Creates an ordered array of all files and directories at the given file object. diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index e53b5cf9ad..26cd7e7e0a 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -25,10 +25,11 @@ struct ToolbarBranchPicker: View { /// Initializes the ``ToolbarBranchPicker`` with an instance of a `WorkspaceClient` /// - Parameter workspace: An instance of the current `WorkspaceClient` init( - workspaceFileManager: CEWorkspaceFileManager? + workspaceFileManager: CEWorkspaceFileManager?, + sourceControlManager: SourceControlManager? ) { self.workspaceFileManager = workspaceFileManager - self.sourceControlManager = workspaceFileManager?.sourceControlManager + self.sourceControlManager = sourceControlManager } var body: some View { @@ -52,7 +53,7 @@ struct ToolbarBranchPicker: View { .help(title) if let currentBranch { Menu(content: { - if let sourceControlManager = workspaceFileManager?.sourceControlManager { + if let sourceControlManager { PopoverView(sourceControlManager: sourceControlManager) } }, label: { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index fa762682e6..4454aeb2fa 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -166,7 +166,8 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker) let view = NSHostingView( rootView: ToolbarBranchPicker( - workspaceFileManager: workspace?.workspaceFileManager + workspaceFileManager: workspace?.workspaceFileManager, + sourceControlManager: workspace?.sourceControlManager ) ) toolbarItem.view = view diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift b/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift new file mode 100644 index 0000000000..37400d7b21 --- /dev/null +++ b/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift @@ -0,0 +1,82 @@ +// +// SourceControlManager+FileEvents.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import Combine +import Foundation +import CodeEditCore + +/// Interprets raw workspace filesystem events (published by the file manager) and +/// dispatches targeted git refreshes. This is where git-internals knowledge lives. +extension SourceControlManager { + /// The distinct git refreshes a set of changed paths can imply. + enum GitRefreshAction: CaseIterable { + case changedFiles, stash, branches, currentBranch, remotes, validate + } + + /// Pure classifier: maps changed workspace-relative paths to the set of git + /// refreshes they require. `workspaceRelativePath` is the workspace root's + /// `URL.relativePath` (used to anchor `.git/…` checks). + static func gitRefreshActions( + for paths: [String], + workspaceRelativePath root: String + ) -> Set { + var actions: Set = [] + + let hasNonGitChanges = paths.contains(where: { !$0.contains(".git/") }) + let hasIndexChange = paths.contains("\(root)/.git/index") + if hasNonGitChanges || hasIndexChange { + actions.insert(.changedFiles) + } + if paths.contains("\(root)/.git/refs/stash") { + actions.insert(.stash) + } + if paths.contains(where: { $0.contains("\(root)/.git/refs/heads") }) { + actions.insert(.branches) + } + if paths.contains(where: { $0.contains("\(root)/.git/HEAD") }) { + actions.insert(.currentBranch) + } + if paths.contains("\(root)/.git/config") { + actions.insert(.remotes) + } + if paths.contains("\(root)/.git") { + actions.insert(.validate) + } + return actions + } + + /// Subscribe to workspace file events for this workspace. Call once from `init`. + func subscribeToWorkspaceFileEvents() { + eventBus.subscribe(WorkspaceFileEvent.self) + .filter { [weak self] in $0.workspaceURL == self?.workspaceURL } + .receive(on: RunLoop.main) + .sink { [weak self] event in + self?.handleWorkspaceFileEvent(event) + } + .store(in: &fileEventCancellables) + } + + private func handleWorkspaceFileEvent(_ event: WorkspaceFileEvent) { + switch event.kind { + case .childrenIndexed: + Task { await self.refreshAllChangedFiles() } + case let .filesystemChanged(paths): + let settings = Settings.shared.preferences.sourceControl.general + guard settings.sourceControlIsEnabled && settings.refreshStatusLocally else { return } + dispatch(Self.gitRefreshActions(for: paths, workspaceRelativePath: workspaceURL.relativePath)) + } + } + + private func dispatch(_ actions: Set) { + if actions.contains(.changedFiles) { Task { await self.refreshAllChangedFiles() } } + if actions.contains(.stash) { Task { try await self.refreshStashEntries() } } + if actions.contains(.branches) { Task { await self.refreshBranches() } } + if actions.contains(.currentBranch) { Task { await self.refreshCurrentBranch() } } + if actions.contains(.remotes) { Task { try await self.refreshRemotes() } } + if actions.contains(.validate) { Task { try await self.validate() } } + } +} diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index a6d00a7f20..c55a0736bc 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -28,6 +28,7 @@ final class SourceControlManager: ObservableObject { let workspaceURL: URL let eventBus: EventBus + var fileEventCancellables: Set = [] weak var fileManager: CEWorkspaceFileManager? @@ -74,5 +75,7 @@ final class SourceControlManager: ObservableObject { self.workspaceURL = workspaceURL self.eventBus = eventBus gitClient = GitClient(directoryURL: workspaceURL, shellClient: shellClient) + subscribeToWorkspaceFileEvents() + Task { try? await validate() } } } diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index a77cf1a7b4..db3134b6aa 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -62,12 +62,9 @@ enum WorkspaceFactory { let workspaceFileManager = CEWorkspaceFileManager( folderUrl: url, ignoredFilesAndFolders: ignoredFilesAndDirectories, - eventBus: eventBus, - sourceControlManager: sourceControlManager + eventBus: eventBus ) - sourceControlManager.fileManager = workspaceFileManager - workspace.sourceControlManager = sourceControlManager workspace.sourceControlViewModel = SourceControlViewModel() workspace.workspaceFileManager = workspaceFileManager diff --git a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift index 66b09edd51..41f3f09bef 100644 --- a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift +++ b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift @@ -28,8 +28,7 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { let fm = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: bus, - sourceControlManager: nil + eventBus: bus ) let key = directory.appending(path: "changed.swift").relativePath XCTAssertNotNil(fm.getFile(key), "file should be cached after init") @@ -49,8 +48,7 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { let fm = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: bus, - sourceControlManager: nil + eventBus: bus ) let key = directory.appending(path: "changed.swift").relativePath fm.getFile(key)?.gitStatus = .modified @@ -70,8 +68,7 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { let fm = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: bus, - sourceControlManager: nil + eventBus: bus ) let key = directory.appending(path: "changed.swift").relativePath @@ -84,4 +81,20 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { } wait(for: [expectation], timeout: 2) } + + func testInitPublishesChildrenIndexed() throws { + let bus = EventBus() + var kinds: [String] = [] + let cancellable = bus.subscribe(WorkspaceFileEvent.self) + .sink { event in + if case .childrenIndexed = event.kind { kinds.append("childrenIndexed") } + } + _ = CEWorkspaceFileManager( + folderUrl: directory, + ignoredFilesAndFolders: [], + eventBus: bus + ) + XCTAssertTrue(kinds.contains("childrenIndexed"), "init loads root children and should emit .childrenIndexed") + cancellable.cancel() + } } diff --git a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift index 2774230642..b14a39dbde 100644 --- a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift +++ b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift @@ -88,7 +88,8 @@ final class CodeEditUIUnitTests: XCTestCase { func testBranchPickerLight() throws { let view = ToolbarBranchPicker( - workspaceFileManager: nil + workspaceFileManager: nil, + sourceControlManager: nil ) let hosting = NSHostingView(rootView: view) hosting.appearance = .init(named: .aqua) @@ -98,7 +99,8 @@ final class CodeEditUIUnitTests: XCTestCase { func testBranchPickerDark() throws { let view = ToolbarBranchPicker( - workspaceFileManager: nil + workspaceFileManager: nil, + sourceControlManager: nil ) let hosting = NSHostingView(rootView: view) hosting.appearance = .init(named: .darkAqua) diff --git a/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift b/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift new file mode 100644 index 0000000000..c0c14d2283 --- /dev/null +++ b/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift @@ -0,0 +1,51 @@ +// +// GitRefreshActionsTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import XCTest +@testable import CodeEdit + +final class GitRefreshActionsTests: XCTestCase { + private let root = "MyWorkspace" + + private typealias Action = SourceControlManager.GitRefreshAction + + private func actions(_ paths: [String]) -> Set { + SourceControlManager.gitRefreshActions(for: paths, workspaceRelativePath: root) + } + + func testNonGitChangeRefreshesChangedFiles() { + XCTAssertTrue(actions(["MyWorkspace/Sources/App.swift"]).contains(.changedFiles)) + } + + func testIndexChangeRefreshesChangedFiles() { + XCTAssertTrue(actions(["MyWorkspace/.git/index"]).contains(.changedFiles)) + } + + func testPureGitInternalChangeDoesNotRefreshChangedFiles() { + XCTAssertFalse(actions(["MyWorkspace/.git/logs/HEAD"]).contains(.changedFiles)) + } + + func testStashRefRefreshesStash() { + XCTAssertTrue(actions(["MyWorkspace/.git/refs/stash"]).contains(.stash)) + } + + func testHeadsRefreshesBranches() { + XCTAssertTrue(actions(["MyWorkspace/.git/refs/heads/main"]).contains(.branches)) + } + + func testHeadRefreshesCurrentBranch() { + XCTAssertTrue(actions(["MyWorkspace/.git/HEAD"]).contains(.currentBranch)) + } + + func testConfigRefreshesRemotes() { + XCTAssertTrue(actions(["MyWorkspace/.git/config"]).contains(.remotes)) + } + + func testGitFolderRefreshesValidate() { + XCTAssertTrue(actions(["MyWorkspace/.git"]).contains(.validate)) + } +} diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index f3e0fd49ba..a2bc2a754c 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -55,8 +55,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let client = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: EventBus(), - sourceControlManager: nil + eventBus: EventBus() ) // Compare to flattened files - 1 cause root is in there @@ -68,8 +67,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let client = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: EventBus(), - sourceControlManager: nil + eventBus: EventBus() ) let newFile = generateRandomFiles(amount: 1)[0] @@ -121,8 +119,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: EventBus(), - sourceControlManager: nil + eventBus: EventBus() ) XCTAssert(fileManager.getFile(testFileURL.path()) == nil) @@ -138,8 +135,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: EventBus(), - sourceControlManager: nil + eventBus: EventBus() ) XCTAssert(fileManager.getFile(testFileURL.path()) != nil) XCTAssert(FileManager.default.fileExists(atPath: testFileURL.path()) == true) @@ -155,8 +151,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: EventBus(), - sourceControlManager: nil + eventBus: EventBus() ) XCTAssert(fileManager.getFile(testFileURL.path()) != nil) XCTAssert(FileManager.default.fileExists(atPath: testFileURL.path()) == true) @@ -170,8 +165,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let fileManager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], - eventBus: EventBus(), - sourceControlManager: nil + eventBus: EventBus() ) // This will throw if unsuccessful. From cdd2fb4fcc824e6502f6fb86f069c5daa7e09d86 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 14:45:26 +0200 Subject: [PATCH 061/335] Refactor: Relocate CEWorkspaceFileManager into a CodeEditServices target --- .../Tasks/SchemeDropDownView.swift | 1 + .../Extensions/Array/Array+SortURLs.swift | 41 ---------------- .../Extensions/URL/URL+componentCompare.swift | 17 ------- .../Extensions}/String+ValidFileName.swift | 6 +-- .../Extensions/URL+FileName.swift | 6 +-- .../Services/CodeEditServices/Package.swift | 10 ++-- .../Array+SortURLs.swift | 49 +++++++++++++++++++ .../CEWorkspaceFile+Recursion.swift | 2 +- ...WorkspaceFileManager+DirectoryEvents.swift | 2 +- .../CEWorkspaceFileManager+Error.swift | 2 +- ...EWorkspaceFileManager+FileManagement.swift | 4 +- .../CEWorkspaceFileManager.swift | 10 ++-- .../DirectoryEventStream.swift | 2 +- .../URL+ContainsSubPath.swift | 27 ++++++++++ 14 files changed, 103 insertions(+), 76 deletions(-) rename {CodeEdit/Utils/Extensions/String => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions}/String+ValidFileName.swift (81%) rename CodeEdit/Utils/Extensions/URL/URL+Filename.swift => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift (69%) create mode 100644 Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Array+SortURLs.swift rename {CodeEdit/Features/CEWorkspace/Models => Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager}/CEWorkspaceFile+Recursion.swift (99%) rename {CodeEdit/Features/CEWorkspace/Models => Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager}/CEWorkspaceFileManager+DirectoryEvents.swift (99%) rename {CodeEdit/Features/CEWorkspace/Models => Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager}/CEWorkspaceFileManager+Error.swift (98%) rename {CodeEdit/Features/CEWorkspace/Models => Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager}/CEWorkspaceFileManager+FileManagement.swift (99%) rename {CodeEdit/Features/CEWorkspace/Models => Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager}/CEWorkspaceFileManager.swift (95%) rename {CodeEdit/Features/CEWorkspace/Models => Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager}/DirectoryEventStream.swift (99%) create mode 100644 Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift index 337c1b3249..a205800354 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditUI struct SchemeDropDownView: View { diff --git a/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift b/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift index e3b2887b7c..823b92bd67 100644 --- a/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift +++ b/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift @@ -7,47 +7,6 @@ import Foundation -extension Array where Element == URL { - - /// Sorts the elements in alphabetical order. - /// - Parameter foldersOnTop: if set to `true` folders will always be on top of files. - /// - Returns: A sorted array of `URL` - func sortItems(foldersOnTop: Bool) -> [URL] { - return self.sorted { lhs, rhs in - let lhsIsDir = (try? lhs.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false - let rhsIsDir = (try? rhs.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false - - if foldersOnTop { - if lhsIsDir != rhsIsDir { - return lhsIsDir - } - } - - return compareNaturally(lhs.lastPathComponent, rhs.lastPathComponent) - } - } - - /// Compare two strings using natural sorting. - /// - Parameters: - /// - lhs: The left-hand string. - /// - rhs: The right-hand string. - /// - Returns: `true` if `lhs` should be ordered before `rhs`. - private func compareNaturally(_ lhs: String, _ rhs: String) -> Bool { - let lhsComponents = lhs.components(separatedBy: CharacterSet.decimalDigits.inverted) - let rhsComponents = rhs.components(separatedBy: CharacterSet.decimalDigits.inverted) - - for (lhsPart, rhsPart) in zip(lhsComponents, rhsComponents) where lhsPart != rhsPart { - if let lhsNum = Int(lhsPart), let rhsNum = Int(rhsPart) { - return lhsNum < rhsNum - } else { - return lhsPart < rhsPart - } - } - - return lhs < rhs - } -} - extension Array where Element: Hashable { /// Checks the difference between two given items. diff --git a/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift b/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift index c0d3520986..f5f7949e84 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift @@ -16,23 +16,6 @@ extension URL { return self.pathComponents == other.pathComponents } - /// Determines if another URL is lower in the file system than this URL. - /// - /// Examples: - /// ``` - /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/file.txt")) // true - /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/")) // false - /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/")) // false - /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/Folder")) // true - /// ``` - /// - /// - Parameter other: The URL to compare. - /// - Returns: True, if the other URL is lower in the file system. - func containsSubPath(_ other: URL) -> Bool { - other.absoluteString.starts(with: absoluteString) - && other.pathComponents.count > pathComponents.count - } - /// Compares this url with another, counting the number of shared path components. Stops counting once a /// different component is found. /// diff --git a/CodeEdit/Utils/Extensions/String/String+ValidFileName.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+ValidFileName.swift similarity index 81% rename from CodeEdit/Utils/Extensions/String/String+ValidFileName.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+ValidFileName.swift index 5ab09cb0fb..0680f223de 100644 --- a/CodeEdit/Utils/Extensions/String/String+ValidFileName.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+ValidFileName.swift @@ -1,6 +1,6 @@ // // String+ValidFileName.swift -// CodeEdit +// CodeEditCore // // Created by Khan Winter on 1/13/25. // @@ -9,13 +9,13 @@ import Foundation extension CharacterSet { /// On macOS, valid file names must not contain the `NULL` or `:` characters. - static var invalidFileNameCharacters: CharacterSet = CharacterSet(charactersIn: "\0:") + static let invalidFileNameCharacters: CharacterSet = CharacterSet(charactersIn: "\0:") } extension String { /// On macOS, valid file names must not contain the `NULL` or `:` characters, must be non-empty, and must be less /// than 256 UTF16 characters. - var isValidFilename: Bool { + public var isValidFilename: Bool { !isEmpty && CharacterSet(charactersIn: self).isDisjoint(with: .invalidFileNameCharacters) && utf16.count < 256 } } diff --git a/CodeEdit/Utils/Extensions/URL/URL+Filename.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift similarity index 69% rename from CodeEdit/Utils/Extensions/URL/URL+Filename.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift index be9a2dbd31..30c6f9dcbe 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+Filename.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift @@ -1,6 +1,6 @@ // -// URL+Filename.swift -// CodeEdit +// URL+FileName.swift +// CodeEditCore // // Created by Axel Martinez on 5/8/24. // @@ -8,7 +8,7 @@ import Foundation extension URL { - var fileName: String { + public var fileName: String { self.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) } } diff --git a/Packages/Services/CodeEditServices/Package.swift b/Packages/Services/CodeEditServices/Package.swift index fb5c932e33..544f894f53 100644 --- a/Packages/Services/CodeEditServices/Package.swift +++ b/Packages/Services/CodeEditServices/Package.swift @@ -7,9 +7,9 @@ let package = Package( platforms: [.macOS(.v14)], products: [ // Umbrella product: the app links this once; each service target is - // its own module (`import ShellClient`). Adding a service later is a - // manifest-only change. - .library(name: "CodeEditServices", targets: ["ShellClient"]) + // its own module (`import ShellClient`, `import CEWorkspaceFileManager`). + // Adding a service later is a manifest-only change. + .library(name: "CodeEditServices", targets: ["ShellClient", "CEWorkspaceFileManager"]) ], dependencies: [ .package(path: "../../Foundation/CodeEditCore") @@ -20,6 +20,10 @@ let package = Package( .target( name: "ShellClient", dependencies: [.product(name: "CodeEditCore", package: "CodeEditCore")] + ), + .target( + name: "CEWorkspaceFileManager", + dependencies: [.product(name: "CodeEditCore", package: "CodeEditCore")] ) ] ) diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Array+SortURLs.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Array+SortURLs.swift new file mode 100644 index 0000000000..80f1f67b6b --- /dev/null +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Array+SortURLs.swift @@ -0,0 +1,49 @@ +// +// Array+SortURLs.swift +// CEWorkspaceFileManager +// +// Created by Matthijs Eikelenboom on 07/02/2023. +// + +import Foundation + +extension Array where Element == URL { + + /// Sorts the elements in alphabetical order. + /// - Parameter foldersOnTop: if set to `true` folders will always be on top of files. + /// - Returns: A sorted array of `URL` + func sortItems(foldersOnTop: Bool) -> [URL] { + return self.sorted { lhs, rhs in + let lhsIsDir = (try? lhs.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + let rhsIsDir = (try? rhs.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + + if foldersOnTop { + if lhsIsDir != rhsIsDir { + return lhsIsDir + } + } + + return compareNaturally(lhs.lastPathComponent, rhs.lastPathComponent) + } + } + + /// Compare two strings using natural sorting. + /// - Parameters: + /// - lhs: The left-hand string. + /// - rhs: The right-hand string. + /// - Returns: `true` if `lhs` should be ordered before `rhs`. + private func compareNaturally(_ lhs: String, _ rhs: String) -> Bool { + let lhsComponents = lhs.components(separatedBy: CharacterSet.decimalDigits.inverted) + let rhsComponents = rhs.components(separatedBy: CharacterSet.decimalDigits.inverted) + + for (lhsPart, rhsPart) in zip(lhsComponents, rhsComponents) where lhsPart != rhsPart { + if let lhsNum = Int(lhsPart), let rhsNum = Int(rhsPart) { + return lhsNum < rhsNum + } else { + return lhsPart < rhsPart + } + } + + return lhs < rhs + } +} diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift similarity index 99% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift rename to Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift index 9cb3304bf4..f5722ec6f4 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Recursion.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift @@ -1,6 +1,6 @@ // // CEWorkspaceFile+Recursion.swift -// CodeEdit +// CEWorkspaceFileManager // // Created by Matthijs Eikelenboom on 30/04/2023. // diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift similarity index 99% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift rename to Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift index 1653596b6f..4d745ff4c9 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift @@ -1,6 +1,6 @@ // // CEWorkspaceFileManager+DirectoryEvents.swift -// CodeEdit +// CEWorkspaceFileManager // // Created by Axel Martinez on 5/8/24. // diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+Error.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift similarity index 98% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+Error.swift rename to Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift index c56adc160b..f3385d4bd2 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+Error.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift @@ -1,6 +1,6 @@ // // CEWorkspaceFileManager+Error.swift -// CodeEdit +// CEWorkspaceFileManager // // Created by Khan Winter on 1/13/25. // diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift similarity index 99% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift rename to Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift index 5c13b297fe..63f0311888 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift @@ -1,6 +1,6 @@ // -// CEWorkspaceFileManager+FileSystem.swift -// CodeEdit +// CEWorkspaceFileManager+FileManagement.swift +// CEWorkspaceFileManager // // Created by Khan Winter on 9/30/23. // diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift similarity index 95% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift rename to Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift index 929e6b24a8..f762a31c33 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift @@ -1,6 +1,6 @@ // -// FileSystemClient.swift -// CodeEdit +// CEWorkspaceFileManager.swift +// CEWorkspaceFileManager // // Created by Matthijs Eikelenboom on 04/02/2023. // @@ -38,7 +38,11 @@ protocol CEWorkspaceFileManagerObserver: AnyObject { /// files under the ``CEWorkspaceFileManager/folderUrl`` url. Those can be passed on to listeners that conform to the /// ``CEWorkspaceFileManagerObserver`` protocol. Use the ``CEWorkspaceFileManager/addObserver(_:)`` /// and ``CEWorkspaceFileManager/removeObserver(_:)`` to add or remove observers. Observers are kept as weak references. -final class CEWorkspaceFileManager { +/// `@unchecked Sendable`: the mutable cache (`flattenedFileItems`, `childrenMap`, `observers`) is +/// main-thread-confined — filesystem events hop to the main queue before mutating, and all consumers +/// (UI, use cases) call in on the main thread. The reference is shared across threads (the FSEvents +/// callback thread invokes `fileSystemEventReceived`), which is why the annotation is required. +final class CEWorkspaceFileManager: @unchecked Sendable { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CEWorkspaceFileManager") private(set) var fileManager: FileManager private(set) var ignoredFilesAndFolders: Set diff --git a/CodeEdit/Features/CEWorkspace/Models/DirectoryEventStream.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift similarity index 99% rename from CodeEdit/Features/CEWorkspace/Models/DirectoryEventStream.swift rename to Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift index bfab19f41e..1e1b722938 100644 --- a/CodeEdit/Features/CEWorkspace/Models/DirectoryEventStream.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift @@ -1,6 +1,6 @@ // // DirectoryEventStream.swift -// CodeEdit +// CEWorkspaceFileManager // // Created by Khan Winter on 6/26/23. // diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift new file mode 100644 index 0000000000..19bbbfaf63 --- /dev/null +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift @@ -0,0 +1,27 @@ +// +// URL+ContainsSubPath.swift +// CEWorkspaceFileManager +// +// Created by Khan Winter on 10/22/24. +// + +import Foundation + +extension URL { + /// Determines if another URL is lower in the file system than this URL. + /// + /// Examples: + /// ``` + /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/file.txt")) // true + /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/")) // false + /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/")) // false + /// URL(filePath: "/Users/Bob/Desktop").containsSubPath(URL(filePath: "/Users/Bob/Desktop/Folder")) // true + /// ``` + /// + /// - Parameter other: The URL to compare. + /// - Returns: True, if the other URL is lower in the file system. + func containsSubPath(_ other: URL) -> Bool { + other.absoluteString.starts(with: absoluteString) + && other.pathComponents.count > pathComponents.count + } +} From d14f0b0dee343bcab94953dec918fda08ff8e32a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 14:49:48 +0200 Subject: [PATCH 062/335] Refactor: Expose CEWorkspaceFileManager public API and import it across the app --- .../ActivityViewer/ActivityViewer.swift | 1 + .../Tasks/SchemeDropDownView.swift | 1 + .../Tasks/WorkspaceMenuItemView.swift | 1 + .../UseCases/AcceptDroppedFilesUseCase.swift | 1 + .../UseCases/MoveFileUseCase.swift | 1 + .../Views/ToolbarBranchPicker.swift | 1 + .../CodeEditWindowController+Toolbar.swift | 1 + .../Protocols/WorkspaceManaging.swift | 1 + .../JumpBar/Views/EditorJumpBarMenu.swift | 1 + .../EditorLayout+StateRestoration.swift | 1 + .../Restoration/UndoManagerRegistration.swift | 1 + .../TabBar/Tabs/Tab/EditorTabView.swift | 1 + .../Tab/Models/EditorTabFileObserver.swift | 1 + .../Views/EditorTabBarContextMenu.swift | 1 + .../UseCases/RestoreEditorStateUseCase.swift | 1 + .../FileInspector/FileInspectorView.swift | 1 + .../OutlineView/FileSystemTableViewCell.swift | 1 + .../OutlineView/ProjectNavigatorMenu.swift | 1 + .../ProjectNavigatorMenuActions.swift | 1 + .../ProjectNavigatorOutlineView.swift | 1 + ...ewController+NSOutlineViewDataSource.swift | 1 + ...ViewController+NSOutlineViewDelegate.swift | 1 + ...troller+OutlineTableViewCellDelegate.swift | 1 + .../ProjectNavigatorViewController.swift | 1 + .../ProjectNavigatorToolbarBottom.swift | 1 + .../SourceControlNavigatorChangesList.swift | 1 + .../ChangedFile/GitChangedFileLabel.swift | 1 + .../ChangedFile/GitChangedFileListView.swift | 1 + .../OpenQuickly/Views/OpenQuicklyView.swift | 1 + .../SourceControl/SourceControlManager.swift | 1 + .../Views/SourceControlFetchView.swift | 1 + .../Models/Environment+Workspace.swift | 1 + .../Features/Workspace/Models/Workspace.swift | 1 + .../Services/WorkspaceWindowManager.swift | 1 + .../Features/Workspace/WorkspaceFactory.swift | 1 + ...WorkspaceFileManager+DirectoryEvents.swift | 4 ++-- ...EWorkspaceFileManager+FileManagement.swift | 4 ++-- .../CEWorkspaceFileManager.swift | 20 +++++++++---------- 38 files changed, 49 insertions(+), 14 deletions(-) diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift index 8cd5053932..e64c8a7c5c 100644 --- a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift +++ b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager /// A view that shows the activity bar and the current status of any executed task struct ActivityViewer: View { diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift index a205800354..729965b7d4 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import CodeEditUI diff --git a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift b/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift index 62a3aa8ee3..25b80add2a 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore struct WorkspaceMenuItemView: View { diff --git a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift index 21c0dbcc34..886fb40654 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import CodeEditCore /// Resolves dropped file URLs into copy/move operations, handling source resolution and replace conflicts. diff --git a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift index 108d646d14..20e511b5ef 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import CodeEditCore /// Moves a file within a workspace, closing any open tabs for it and reopening the new location. diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index 26cd7e7e0a..1171baae55 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import CodeEditSymbols import Combine diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index 4454aeb2fa..2832e0ec96 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import SwiftUI import Combine import Notifications diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index a1254cc2af..fd599175b5 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import Notifications import Search diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift index 41aefe9cd7..411590b86f 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import CodeEditCore final class EditorJumpBarMenu: NSMenu, NSMenuDelegate { diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index c56a32acb1..667cfc105f 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import CodeEditCore import Search import SwiftUI diff --git a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift index 1dd0e934e2..43627d2b1a 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift +++ b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import CodeEditTextView diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 6a1d921e45..a5b27da1f6 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore struct EditorTabView: View { diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift index aeff65889e..6b37822d45 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import CodeEditCore import SwiftUI diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index 87056f051c..37e3a97e81 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import Foundation diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift index 2b044bb765..c4e3688a28 100644 --- a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift +++ b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import CodeEditCore import Search import OSLog diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index a0aa4540de..898818eec2 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import CodeEditLanguages diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index da246cbb10..9169f3722c 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore class FileSystemTableViewCell: StandardTableViewCell { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 4aceff98db..66f14951bc 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import UniformTypeIdentifiers diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 5555dfff8f..98d9dd9b9a 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import CodeEditCore import SwiftUI diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index cac2b3fb0d..d79e6ce5d1 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore import Combine diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 5c9413d0b0..d2d25bf8d7 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import CodeEditCore extension ProjectNavigatorViewController: NSOutlineViewDataSource { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index bc09fca118..0755c361c8 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import CodeEditCore extension ProjectNavigatorViewController: NSOutlineViewDelegate { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index a70fb58558..7d1ac09338 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import CodeEditCore import AppKit diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 267e9375e1..b1cc12354f 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import SwiftUI import OSLog import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 30acc94550..cc64ea04cf 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditUI struct ProjectNavigatorToolbarBottom: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 6edac9c2bb..ff3d30bf9a 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import SwiftUI import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index e777170fc0..875d38cc09 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import Factory import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index 5d80fc684c..85b66a8a3c 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore /// A view to display a changed file's information in a list view. Optionally displays the staged status. diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift index ff7f63ab5a..ccb6ad5319 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager import CodeEditCore struct OpenQuicklyView: View { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index c55a0736bc..923f57dc0a 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -6,6 +6,7 @@ // import Combine +import CEWorkspaceFileManager import Foundation import OSLog import CodeEditCore diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift index 02f2c038ff..fc79b3d3a9 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager struct SourceControlFetchView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index 1302a2bd0a..05829d84f6 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CEWorkspaceFileManager private struct WorkspaceFileManagerKey: EnvironmentKey { static let defaultValue: CEWorkspaceFileManager? = nil diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 47f5d14bd5..344b57fb87 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import Notifications import Search import SwiftUI diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 6d59eacb00..e209892fe5 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -6,6 +6,7 @@ // import AppKit +import CEWorkspaceFileManager import CodeEditCore import Factory import Notifications diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index db3134b6aa..8d847d5565 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -6,6 +6,7 @@ // import Foundation +import CEWorkspaceFileManager import Search import Factory diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift index 4d745ff4c9..90e229aa93 100644 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift @@ -120,13 +120,13 @@ extension CEWorkspaceFileManager { /// Add an observer for file system events. /// - Parameter observer: The observer to add. - func addObserver(_ observer: CEWorkspaceFileManagerObserver) { + public func addObserver(_ observer: CEWorkspaceFileManagerObserver) { observers.add(observer as AnyObject) } /// Remove an observer for file system events. /// - Parameter observer: The observer to remove. - func removeObserver(_ observer: CEWorkspaceFileManagerObserver) { + public func removeObserver(_ observer: CEWorkspaceFileManagerObserver) { observers.remove(observer as AnyObject) } } diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift index 63f0311888..6b6d2cc16c 100644 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift @@ -15,7 +15,7 @@ extension CEWorkspaceFileManager { /// - file: The file to add the new folder to. /// - Returns: The ``CEWorkspaceFile`` representing the folder in the file manager's cache. /// - Authors: Mattijs Eikelenboom, KaiTheRedNinja. *Moved from 7c27b1e* - func addFolder(folderName: String, toFile file: CEWorkspaceFile) throws -> CEWorkspaceFile { + public func addFolder(folderName: String, toFile file: CEWorkspaceFile) throws -> CEWorkspaceFile { // Check if folder, if it is create folder under self, else create on same level. var folderUrl = ( file.isFolder ? file.url.appending(path: folderName) @@ -59,7 +59,7 @@ extension CEWorkspaceFileManager { /// - Throws: Throws a `CocoaError.fileWriteUnknown` with the file url if creating the file fails, and calls /// ``rebuildFiles(fromItem:deep:)`` which throws other `FileManager` errors. /// - Returns: The ``CEWorkspaceFile`` representing the new file in the file manager's cache. - func addFile( + public func addFile( fileName: String, toFile file: CEWorkspaceFile, useExtension: String? = nil, diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift index f762a31c33..258514292b 100644 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift @@ -10,7 +10,7 @@ import CodeEditCore import Foundation import OSLog -protocol CEWorkspaceFileManagerObserver: AnyObject { +public protocol CEWorkspaceFileManagerObserver: AnyObject { func fileManagerUpdated(updatedItems: Set) } @@ -42,19 +42,19 @@ protocol CEWorkspaceFileManagerObserver: AnyObject { /// main-thread-confined — filesystem events hop to the main queue before mutating, and all consumers /// (UI, use cases) call in on the main thread. The reference is shared across threads (the FSEvents /// callback thread invokes `fileSystemEventReceived`), which is why the annotation is required. -final class CEWorkspaceFileManager: @unchecked Sendable { +public final class CEWorkspaceFileManager: @unchecked Sendable { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CEWorkspaceFileManager") - private(set) var fileManager: FileManager + public private(set) var fileManager: FileManager private(set) var ignoredFilesAndFolders: Set - var flattenedFileItems: [String: CEWorkspaceFile] + public internal(set) var flattenedFileItems: [String: CEWorkspaceFile] /// Maps all directories to it's children's paths. var childrenMap: [String: [String]] = [:] var fsEventStream: DirectoryEventStream? var observers: NSHashTable = .weakObjects() - let folderUrl: URL - let workspaceItem: CEWorkspaceFile + public let folderUrl: URL + public let workspaceItem: CEWorkspaceFile let eventBus: EventBus private var eventCancellables: Set = [] @@ -63,7 +63,7 @@ final class CEWorkspaceFileManager: @unchecked Sendable { /// - folderUrl: The folder to use as the root of the file manager. /// - ignoredFilesAndFolders: A set of files to ignore. These should not be paths, but rather file names /// like `.DS_Store` - init( + public init( folderUrl: URL, ignoredFilesAndFolders: Set, fileManager: FileManager = FileManager.default, @@ -126,7 +126,7 @@ final class CEWorkspaceFileManager: @unchecked Sendable { /// - createIfNotFound: Set to true if the function should index any intermediate directories to find the file, /// as well as index the file if it is not already. /// - Returns: The file item corresponding to the file - func getFile( + public func getFile( _ path: String, createIfNotFound: Bool = false ) -> CEWorkspaceFile? { @@ -174,7 +174,7 @@ final class CEWorkspaceFileManager: @unchecked Sendable { /// ``CEWorkspaceFileManager/getFile(_:createIfNotFound:)`` to force a file to be loaded. /// - Parameter file: The file to find children for. /// - Returns: An array of children for the file, or `nil` if the file was not a directory. - func childrenOfFile(_ file: CEWorkspaceFile) -> [CEWorkspaceFile]? { + public func childrenOfFile(_ file: CEWorkspaceFile) -> [CEWorkspaceFile]? { if file.isFolder { if childrenMap[file.id] == nil { // Load the children @@ -249,7 +249,7 @@ final class CEWorkspaceFileManager: @unchecked Sendable { /// Run when the owner of the ``CEWorkspaceFileManager`` doesn't need it anymore. /// This de-inits most functions in the ``CEWorkspaceFileManager``, so that in case it isn't de-init'd it does not /// use up significant amounts of RAM, and clears any file system event watchers. - func cleanUp() { + public func cleanUp() { fsEventStream?.cancel() flattenedFileItems = [workspaceItem.id: workspaceItem] } From a0a3f1865782ae1890e9947b4716ee0c186542d4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 15:03:39 +0200 Subject: [PATCH 063/335] Refactor: Import CEWorkspaceFileManager in tests and move containsSubPath to Core Promote URL.containsSubPath to CodeEditCore (it has a standalone test in UnitTests_Extensions), keeping it accessible to both the moved module and the test. --- .../CEWorkspace/CEWorkspaceFileManagerEventsTests.swift | 1 + .../Features/LSP/LanguageServer+CodeFileDocument.swift | 1 + .../CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift | 1 + .../CodeEditCore/Extensions}/URL+ContainsSubPath.swift | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) rename Packages/{Services/CodeEditServices/Sources/CEWorkspaceFileManager => Foundation/CodeEditCore/Sources/CodeEditCore/Extensions}/URL+ContainsSubPath.swift (92%) diff --git a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift index 41f3f09bef..e847ed9649 100644 --- a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift +++ b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 06/07/2026. // +import CEWorkspaceFileManager import XCTest import CodeEditCore @testable import CodeEdit diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 7673bf9456..2633a429cd 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 9/9/24. // +import CEWorkspaceFileManager import XCTest import CodeEditCore import CodeEditTextView diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index a2bc2a754c..7467e7a392 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -4,6 +4,7 @@ // // Created by Marco Carnevali on 16/03/22. // +import CEWorkspaceFileManager import Combine import CodeEditCore import Foundation diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift similarity index 92% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift index 19bbbfaf63..ab4eceb895 100644 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/URL+ContainsSubPath.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift @@ -1,6 +1,6 @@ // // URL+ContainsSubPath.swift -// CEWorkspaceFileManager +// CodeEditCore // // Created by Khan Winter on 10/22/24. // @@ -20,7 +20,7 @@ extension URL { /// /// - Parameter other: The URL to compare. /// - Returns: True, if the other URL is lower in the file system. - func containsSubPath(_ other: URL) -> Bool { + public func containsSubPath(_ other: URL) -> Bool { other.absoluteString.starts(with: absoluteString) && other.pathComponents.count > pathComponents.count } From 47577c964bc5e35d0bca63dabd439f538f326199 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 20:29:08 +0200 Subject: [PATCH 064/335] Feature: Add DocumentRegistry owning the file-document association in EditorManager --- .../Editor/Models/DocumentRegistry.swift | 66 +++++++++++++++++++ .../Editor/Models/EditorManager.swift | 22 +++++++ .../Editor/DocumentRegistryTests.swift | 66 +++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 CodeEdit/Features/Editor/Models/DocumentRegistry.swift create mode 100644 CodeEditTests/Features/Editor/DocumentRegistryTests.swift diff --git a/CodeEdit/Features/Editor/Models/DocumentRegistry.swift b/CodeEdit/Features/Editor/Models/DocumentRegistry.swift new file mode 100644 index 0000000000..d943fd7e86 --- /dev/null +++ b/CodeEdit/Features/Editor/Models/DocumentRegistry.swift @@ -0,0 +1,66 @@ +// +// DocumentRegistry.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import Combine +import Foundation +import CodeEditCore + +/// Owns the workspace's file→document association, keyed by ``CEWorkspaceFile/id``. +/// +/// Storage is intentionally **weak** and change notification uses a `PassthroughSubject`, +/// mirroring the reference that previously lived (type-erased) on `CEWorkspaceFile`. The +/// strong owner of an open document remains the editor view tree; this registry only holds +/// the back-reference that non-view consumers read. +/// +/// Only ever accessed on the main thread, like the rest of the editor state. It is intentionally +/// **not** `@MainActor`: its owner (`EditorManager`) and callers are not yet actor-isolated, and +/// marking only this type does not compile in the app's current Swift 5 mode. It should become +/// `@MainActor` together with `EditorManager` during the eventual app-wide Swift 6 migration. +final class DocumentRegistry { + private final class Box { + weak var document: CodeFileDocument? + let subject = PassthroughSubject() + } + + private var boxes: [String: Box] = [:] + + private func box(for id: String) -> Box { + if let existing = boxes[id] { return existing } + let created = Box() + boxes[id] = created + return created + } + + /// The open document for the file, or `nil` if none is loaded. + func document(for file: CEWorkspaceFile) -> CodeFileDocument? { + boxes[file.id]?.document + } + + /// Associates (or clears, with `nil`) a document for the file and notifies subscribers. + func setDocument(_ document: CodeFileDocument?, for file: CEWorkspaceFile) { + let box = box(for: file.id) + box.document = document + box.subject.send(document) + } + + /// Loads a new `CodeFileDocument` for the file from disk, registers it, and returns it. + @discardableResult + func loadDocument(for file: CEWorkspaceFile) throws -> CodeFileDocument { + let document = try CodeFileDocument( + contentsOf: file.resolvedURL, + ofType: file.contentType?.identifier ?? "" + ) + setDocument(document, for: file) + return document + } + + /// Emits whenever the document association for the file changes. Like the original, + /// this does not replay the current value on subscription. + func documentPublisher(for file: CEWorkspaceFile) -> AnyPublisher { + box(for: file.id).subject.eraseToAnyPublisher() + } +} diff --git a/CodeEdit/Features/Editor/Models/EditorManager.swift b/CodeEdit/Features/Editor/Models/EditorManager.swift index 94c3f9d476..1299dded5e 100644 --- a/CodeEdit/Features/Editor/Models/EditorManager.swift +++ b/CodeEdit/Features/Editor/Models/EditorManager.swift @@ -14,6 +14,9 @@ import os class EditorManager: ObservableObject { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "EditorManager") + /// Owns the file→document association for this workspace. + let documents = DocumentRegistry() + /// The complete editor layout. @Published var editorLayout: EditorLayout @@ -153,4 +156,23 @@ class EditorManager: ObservableObject { } isFocusingActiveEditor.toggle() } + + // MARK: - Documents + + func document(for file: CEWorkspaceFile) -> CodeFileDocument? { + documents.document(for: file) + } + + func setDocument(_ document: CodeFileDocument?, for file: CEWorkspaceFile) { + documents.setDocument(document, for: file) + } + + @discardableResult + func loadDocument(for file: CEWorkspaceFile) throws -> CodeFileDocument { + try documents.loadDocument(for: file) + } + + func documentPublisher(for file: CEWorkspaceFile) -> AnyPublisher { + documents.documentPublisher(for: file) + } } diff --git a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift new file mode 100644 index 0000000000..fd79202260 --- /dev/null +++ b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift @@ -0,0 +1,66 @@ +// +// DocumentRegistryTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import XCTest +import Combine +import CodeEditCore +@testable import CodeEdit + +final class DocumentRegistryTests: XCTestCase { + private func makeFile(_ path: String = "/tmp/reg-\(UUID().uuidString).swift") -> CEWorkspaceFile { + CEWorkspaceFile(url: URL(filePath: path)) + } + + func testSetAndGetRoundTrip() { + let registry = DocumentRegistry() + let file = makeFile() + let doc = CodeFileDocument() + registry.setDocument(doc, for: file) + XCTAssertIdentical(registry.document(for: file), doc) + } + + func testSetNilClears() { + let registry = DocumentRegistry() + let file = makeFile() + registry.setDocument(CodeFileDocument(), for: file) + registry.setDocument(nil, for: file) + XCTAssertNil(registry.document(for: file)) + } + + func testUnknownFileReturnsNil() { + let registry = DocumentRegistry() + XCTAssertNil(registry.document(for: makeFile())) + } + + func testPublisherEmitsOnSet() { + let registry = DocumentRegistry() + let file = makeFile() + var received: [Bool] = [] // whether each emission was non-nil + var cancellables = Set() + registry.documentPublisher(for: file) + .sink { received.append($0 != nil) } + .store(in: &cancellables) + + let doc = CodeFileDocument() + registry.setDocument(doc, for: file) + registry.setDocument(nil, for: file) + + XCTAssertEqual(received, [true, false]) + } + + func testStorageIsWeak() { + let registry = DocumentRegistry() + let file = makeFile() + autoreleasepool { + let doc = CodeFileDocument() + registry.setDocument(doc, for: file) + XCTAssertNotNil(registry.document(for: file)) + } + // No strong owner remains → the weak ref must be nil. + XCTAssertNil(registry.document(for: file), "registry must not retain the document") + } +} From 437f98d567e4936fdfa5a1a7515ca578a64ba71e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 20:30:57 +0200 Subject: [PATCH 065/335] Refactor: Give Editor and UndoManagerRegistration an EditorManager reference --- CodeEdit/Features/Editor/Models/Editor/Editor.swift | 1 + .../Models/EditorLayout/EditorLayout+StateRestoration.swift | 1 + CodeEdit/Features/Editor/Models/EditorManager.swift | 2 ++ .../Editor/Models/Restoration/UndoManagerRegistration.swift | 3 +++ .../Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift | 1 + .../Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift | 1 + CodeEdit/Features/Workspace/WorkspaceFactory.swift | 1 + 7 files changed, 10 insertions(+) diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index 8956239306..4d75e9bcaf 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -63,6 +63,7 @@ final class Editor: ObservableObject, Identifiable { weak var parent: SplitViewData? weak var searchState: SearchState? + weak var editorManager: EditorManager? /// Whether this editor is attached to a workspace. Used to guard file loading operations. var isAttachedToWorkspace: Bool = false diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 667cfc105f..13e674014e 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -27,6 +27,7 @@ extension EditorManager { // No matter what, set up each editor. Even if we fail to read data. flattenedEditors.forEach { editor in editor.searchState = searchState + editor.editorManager = self editor.isAttachedToWorkspace = true } } diff --git a/CodeEdit/Features/Editor/Models/EditorManager.swift b/CodeEdit/Features/Editor/Models/EditorManager.swift index 1299dded5e..d2268732a5 100644 --- a/CodeEdit/Features/Editor/Models/EditorManager.swift +++ b/CodeEdit/Features/Editor/Models/EditorManager.swift @@ -56,6 +56,7 @@ class EditorManager: ObservableObject { self.activeEditorHistory.prepend { [weak tab] in tab } self.editorLayout = .horizontal(.init(.horizontal, editorLayouts: [.one(tab)])) self.isFocusingActiveEditor = false + tab.editorManager = self switchToActiveEditor() } @@ -68,6 +69,7 @@ class EditorManager: ObservableObject { self.activeEditorHistory.prepend { [weak tab] in tab } self.editorLayout = .horizontal(.init(.horizontal, editorLayouts: [.one(tab)])) self.isFocusingActiveEditor = false + tab.editorManager = self switchToActiveEditor() } diff --git a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift index 43627d2b1a..3f07bc0692 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift +++ b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift @@ -20,6 +20,9 @@ import CodeEditTextView final class UndoManagerRegistration: ObservableObject { private var managerMap: [String: CEUndoManager] = [:] + /// Used to check whether a file still has an open document. Wired by `WorkspaceFactory`. + weak var editorManager: EditorManager? + init() { } /// Find or create a new undo manager. diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index 37e3a97e81..bb3a602554 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -145,6 +145,7 @@ struct EditorTabBarContextMenu: ViewModifier { func moveToNewSplit(_ edge: Edge) { let newEditor = Editor(files: [item], searchState: tabs.searchState) + newEditor.editorManager = editorManager splitEditor(edge, newEditor) tabs.closeTab(file: item) editorManager.activeEditor = newEditor diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift index f5a1809fd5..4d8d45cc04 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -102,6 +102,7 @@ struct EditorTabBarTrailingAccessories: View { } else { newEditor = .init() } + newEditor.editorManager = editorManager splitEditor(edge, newEditor) editorManager.updateCachedFlattenedEditors = true editorManager.activeEditor = newEditor diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 8d847d5565..6d408a9095 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -85,6 +85,7 @@ enum WorkspaceFactory { // --- Phase 3: Observer registration --- workspaceFileManager.addObserver(workspace.undoRegistration) + workspace.undoRegistration.editorManager = editorManager // --- Phase 4: State restoration --- if let statePersistence = workspace.statePersistence { From b5e2c8a81498e6514559ed92c416d7850819be60 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 20:38:31 +0200 Subject: [PATCH 066/335] Refactor: Route file-document access through EditorManager's DocumentRegistry --- .../CodeEditWindowController.swift | 4 ++- .../CodeEditWindowControllerExtensions.swift | 18 +++++++----- .../Editor/Models/Editor/Editor.swift | 14 ++++----- .../EditorLayout+StateRestoration.swift | 3 +- .../Restoration/UndoManagerRegistration.swift | 2 +- .../Tabs/Tab/EditorFileTabCloseButton.swift | 9 ++++-- .../UseCases/RestoreEditorStateUseCase.swift | 29 ++++++++++++++----- .../Editor/Views/EditorAreaView.swift | 11 ++++--- .../FileInspector/FileInspectorView.swift | 20 ++++++++----- .../Features/Workspace/Models/Workspace.swift | 20 +++++++------ .../LSP/LanguageServer+CodeFileDocument.swift | 4 +-- 11 files changed, 81 insertions(+), 53 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index eab10c6e15..6de1a08d8e 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -107,7 +107,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } private func getSelectedCodeFile() -> CodeFileDocument? { - workspace?.editorManager?.activeEditor.selectedTab?.file.fileDocument + guard let editorManager = workspace?.editorManager, + let file = editorManager.activeEditor.selectedTab?.file else { return nil } + return editorManager.document(for: file) } @IBAction func saveDocument(_ sender: Any) { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index fab6c6cf31..be7b4e8f49 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -37,12 +37,13 @@ extension CodeEditWindowController { // Listen to changes in all tabs/files internal func listenToDocumentEdited(workspace: Workspace) { - workspace.editorManager?.$activeEditor + guard let editorManager = workspace.editorManager else { return } + editorManager.$activeEditor .flatMap({ editor in editor.$tabs }) .compactMap({ tab in - Publishers.MergeMany(tab.elements.compactMap({ $0.file.fileDocumentPublisher })) + Publishers.MergeMany(tab.elements.map({ editorManager.documentPublisher(for: $0.file) })) }) .switchToLatest() .compactMap({ fileDocument in @@ -61,7 +62,7 @@ extension CodeEditWindowController { // Listen to change of tabs, if closed tab without saving content, // we also need to recalculate isDocumentEdited - workspace.editorManager?.$activeEditor + editorManager.$activeEditor .flatMap({ editor in editor.$tabs }) @@ -74,10 +75,13 @@ extension CodeEditWindowController { // Recalculate documentEdited by checking if any tab/file is edited private func updateDocumentEdited(workspace: Workspace) { let hasEditedDocuments = !(workspace - .editorManager? - .editorLayout - .gatherOpenFiles() - .filter({ $0.fileDocument?.isDocumentEdited == true }) + .editorManager + .map({ editorManager in + editorManager + .editorLayout + .gatherOpenFiles() + .filter({ editorManager.document(for: $0)?.isDocumentEdited == true }) + })? .isEmpty ?? true) self.setDocumentEdited(hasEditedDocuments) } diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index 4d75e9bcaf..2730f7d4b6 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -132,7 +132,7 @@ final class Editor: ObservableObject, Identifiable { return } self.selectedTab = tab - if tab.file.fileDocument == nil { + if editorManager?.document(for: tab.file) == nil { do { // Ignore this error for simpler API usage. try openFile(item: tab) } catch { @@ -165,12 +165,12 @@ final class Editor: ObservableObject, Identifiable { addToHistory(selectedTab) } // Reset change count to 0 - file.fileDocument?.updateChangeCount(.changeCleared) - if let codeFile = file.fileDocument { + editorManager?.document(for: file)?.updateChangeCount(.changeCleared) + if let codeFile = editorManager?.document(for: file) { codeFile.close() } // remove file from memory - file.fileDocument = nil + editorManager?.setDocument(nil, for: file) } /// Closes the currently opened tab in the tab group. @@ -271,7 +271,7 @@ final class Editor: ObservableObject, Identifiable { private func openFile(item: Tab) throws { // If this isn't attached to a workspace, loading a new NSDocument will cause a loose document we can't close - guard item.file.fileDocument == nil else { + guard editorManager?.document(for: item.file) == nil else { return } @@ -279,14 +279,14 @@ final class Editor: ObservableObject, Identifiable { throw EditorError.noWorkspaceAttached } - try item.file.loadCodeFile() + try editorManager?.loadDocument(for: item.file) } /// Check if tab can be closed /// /// If document edited it will show dialog where user can save document before closing or cancel. private func canCloseTab(file: CEWorkspaceFile) -> Bool { - guard let codeFile = file.fileDocument else { return true } + guard let codeFile = editorManager?.document(for: file) else { return true } if codeFile.isDocumentEdited { let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 13e674014e..7221a09a38 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -36,7 +36,8 @@ extension EditorManager { switch useCase.execute( statePersistence: statePersistence, fileManager: fileManager, - searchState: searchState + searchState: searchState, + editorManager: self ) { case .restored(let layout, let activeEditor): self.editorLayout = layout diff --git a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift index 3f07bc0692..6f521f639b 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift +++ b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift @@ -61,7 +61,7 @@ extension UndoManagerRegistration: CEWorkspaceFileManagerObserver { /// To handle this? /// - When we receive a file update, if the file is not open in any editors we clear the undo stack func fileManagerUpdated(updatedItems: Set) { - for file in updatedItems where file.fileDocument == nil { + for file in updatedItems where editorManager?.document(for: file) == nil { managerMap.removeValue(forKey: file.url.absolutePath) } } diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift index 0e8a7c47f4..10506c7df6 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift @@ -22,6 +22,8 @@ struct EditorFileTabCloseButton: View { @State private var isDocumentEdited: Bool = false @State private var id: Int = 0 + @EnvironmentObject private var editorManager: EditorManager + var body: some View { EditorTabCloseButton( isActive: isActive, @@ -33,13 +35,14 @@ struct EditorFileTabCloseButton: View { isHoveringClose: $isHoveringClose ) .id(id) - // Detects if file document changed, when this view created item.fileDocument is nil - .onReceive(item.fileDocumentPublisher, perform: { _ in + // Detects if the file's document changed; when this view is created the document may be nil + .onReceive(editorManager.documentPublisher(for: item), perform: { _ in // Force re-render so isDocumentEdited publisher is updated self.id += 1 }) .onReceive( - item.fileDocument?.isDocumentEditedPublisher.eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + editorManager.document(for: item)?.isDocumentEditedPublisher.eraseToAnyPublisher() + ?? Empty().eraseToAnyPublisher() ) { newValue in self.isDocumentEdited = newValue } diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift index c4e3688a28..77acd2ae1b 100644 --- a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift +++ b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -30,7 +30,8 @@ final class RestoreEditorStateUseCase { func execute( statePersistence: any WorkspaceStatePersisting, fileManager: CEWorkspaceFileManager?, - searchState: SearchState? + searchState: SearchState?, + editorManager: EditorManager ) -> Outcome { guard let data = statePersistence.get(.openTabs) as? Data else { return .noChange @@ -51,7 +52,12 @@ final class RestoreEditorStateUseCase { return .shouldInitCleanState } - try fixRestoredEditorLayout(state.groups, fileManager: fileManager, searchState: searchState) + try fixRestoredEditorLayout( + state.groups, + fileManager: fileManager, + searchState: searchState, + editorManager: editorManager + ) return .restored(layout: state.groups, activeEditor: activeEditor) } catch { @@ -66,18 +72,23 @@ final class RestoreEditorStateUseCase { private func fixRestoredEditorLayout( _ group: EditorLayout, fileManager: CEWorkspaceFileManager?, - searchState: SearchState? + searchState: SearchState?, + editorManager: EditorManager ) throws { switch group { case let .one(data): - try fixEditor(data, fileManager: fileManager, searchState: searchState) + try fixEditor(data, fileManager: fileManager, searchState: searchState, editorManager: editorManager) case let .vertical(splitData): try splitData.editorLayouts.forEach { group in - try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) + try fixRestoredEditorLayout( + group, fileManager: fileManager, searchState: searchState, editorManager: editorManager + ) } case let .horizontal(splitData): try splitData.editorLayouts.forEach { group in - try fixRestoredEditorLayout(group, fileManager: fileManager, searchState: searchState) + try fixRestoredEditorLayout( + group, fileManager: fileManager, searchState: searchState, editorManager: editorManager + ) } } } @@ -87,7 +98,8 @@ final class RestoreEditorStateUseCase { private func fixEditor( _ editor: Editor, fileManager: CEWorkspaceFileManager?, - searchState: SearchState? + searchState: SearchState?, + editorManager: EditorManager ) throws { guard let fileManager else { return } let resolvedTabs = editor @@ -96,10 +108,11 @@ final class RestoreEditorStateUseCase { .map({ EditorInstance(searchState: searchState, file: $0) }) for tab in resolvedTabs { - try tab.file.loadCodeFile() + try editorManager.loadDocument(for: tab.file) } editor.searchState = searchState + editor.editorManager = editorManager editor.isAttachedToWorkspace = true editor.tabs = OrderedSet(resolvedTabs) diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index b324496e7d..8f787e46b0 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -38,9 +38,8 @@ struct EditorAreaView: View { init(editor: Editor, focus: FocusState.Binding) { self.editor = editor self._focus = focus - if let file = editor.selectedTab?.file.fileDocument { - self.codeFile = { [weak file] in file } - } + // `codeFile` is seeded from the environment's document registry in `body` + // (via `.onAppear` / the document publisher) — the environment is unavailable in `init`. } var body: some View { @@ -74,11 +73,11 @@ struct EditorAreaView: View { } else { LoadingFileView(selected.file.name) .onAppear { - if let file = selected.file.fileDocument { + if let file = editorManager.document(for: selected.file) { self.codeFile = { [weak file] in file } } } - .onReceive(selected.file.fileDocumentPublisher) { latestValue in + .onReceive(editorManager.documentPublisher(for: selected.file)) { latestValue in self.codeFile = { [weak latestValue] in latestValue } } } @@ -196,7 +195,7 @@ struct EditorAreaView: View { } } .onChange(of: editor.selectedTab) { _, newValue in - if let file = newValue?.file.fileDocument { + if let newValue, let file = editorManager.document(for: newValue.file) { codeFile = { [weak file] in file } } } diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 898818eec2..4801aed223 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -34,15 +34,16 @@ struct FileInspectorView: View { func updateFileOptions(_ textEditingOverride: SettingsData.TextEditingSettings? = nil) { let textEditingSettings = textEditingOverride ?? textEditing - indentOption = file?.fileDocument?.indentOption ?? textEditingSettings.indentOption - defaultTabWidth = file?.fileDocument?.defaultTabWidth ?? textEditingSettings.defaultTabWidth - wrapLines = file?.fileDocument?.wrapLines ?? textEditingSettings.wrapLinesToEditorWidth + let document = file.flatMap { editorManager.document(for: $0) } + indentOption = document?.indentOption ?? textEditingSettings.indentOption + defaultTabWidth = document?.defaultTabWidth ?? textEditingSettings.defaultTabWidth + wrapLines = document?.wrapLines ?? textEditingSettings.wrapLinesToEditorWidth } func updateInspectorSource() { file = editorManager.activeEditor.selectedTab?.file fileName = file?.name ?? "" - language = file?.fileDocument?.language + language = file.flatMap { editorManager.document(for: $0) }?.language updateFileOptions() } @@ -130,7 +131,7 @@ struct FileInspectorView: View { } } .onChange(of: language) { _, newValue in - file?.fileDocument?.language = newValue + file.flatMap { editorManager.document(for: $0) }?.language = newValue } } @@ -175,7 +176,8 @@ struct FileInspectorView: View { Text("Tabs").tag(SettingsData.TextEditingSettings.IndentOption.IndentType.tab) } .onChange(of: indentOption) { _, newValue in - file?.fileDocument?.indentOption = newValue == textEditing.indentOption ? nil : newValue + file.flatMap { editorManager.document(for: $0) }?.indentOption = + newValue == textEditing.indentOption ? nil : newValue } } @@ -219,14 +221,16 @@ struct FileInspectorView: View { } } .onChange(of: defaultTabWidth) { _, newValue in - file?.fileDocument?.defaultTabWidth = newValue == textEditing.defaultTabWidth ? nil : newValue + file.flatMap { editorManager.document(for: $0) }?.defaultTabWidth = + newValue == textEditing.defaultTabWidth ? nil : newValue } } private var wrapLinesToggle: some View { Toggle("Wrap lines", isOn: $wrapLines) .onChange(of: wrapLines) { _, newValue in - file?.fileDocument?.wrapLines = newValue == textEditing.wrapLinesToEditorWidth ? nil : newValue + file.flatMap { editorManager.document(for: $0) }?.wrapLines = + newValue == textEditing.wrapLinesToEditorWidth ? nil : newValue } } diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 344b57fb87..b00936204b 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -87,20 +87,22 @@ final class Workspace: ObservableObject, WorkspaceManaging { // MARK: - Unsaved Changes func hasUnsavedChanges() -> Bool { - let editedFiles = editorManager?.editorLayout + guard let editorManager else { return false } + let editedFiles = editorManager.editorLayout .gatherOpenFiles() - .compactMap(\.fileDocument) - .filter(\.isDocumentEdited) ?? [] + .compactMap { editorManager.document(for: $0) } + .filter(\.isDocumentEdited) return !editedFiles.isEmpty } /// Prompts the user to save any unsaved files before closing. /// Returns `true` if all files are clean and the workspace can close, `false` if the user cancelled. func promptSaveUnsavedFiles() -> Bool { - let editedCodeFiles = editorManager?.editorLayout + guard let editorManager else { return true } + let editedCodeFiles = editorManager.editorLayout .gatherOpenFiles() - .compactMap(\.fileDocument) - .filter(\.isDocumentEdited) ?? [] + .compactMap { editorManager.document(for: $0) } + .filter(\.isDocumentEdited) for editedCodeFile in editedCodeFiles { let shouldClose = UnsafeMutablePointer.allocate(capacity: 1) @@ -119,9 +121,9 @@ final class Workspace: ObservableObject, WorkspaceManaging { } } - let areAllClean = editorManager?.editorLayout.gatherOpenFiles() - .compactMap(\.fileDocument) - .allSatisfy { !$0.isDocumentEdited } ?? true + let areAllClean = editorManager.editorLayout.gatherOpenFiles() + .compactMap { editorManager.document(for: $0) } + .allSatisfy { !$0.isDocumentEdited } return areAllClean } diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 2633a429cd..98d0dc8271 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -160,7 +160,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { lspService.languageClients[.init(.swift, tempTestDir.path() + "/")] = server // Set up workspace. Registers it with the workspace window manager. - let (_, fileManager) = try makeTestWorkspace() + let (workspace, fileManager) = try makeTestWorkspace() // Add a CEWorkspaceFile _ = try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") @@ -175,7 +175,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { withContentsOf: file.url, ofType: "public.swift-source" ) - file.fileDocument = codeFile + workspace.editorManager?.setDocument(codeFile, for: file) NSDocumentController.shared.addDocument(codeFile) await waitForClientState( From a3676853dc15aa1f0e4be612875e26b30487483f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 20:39:52 +0200 Subject: [PATCH 067/335] Refactor: Remove type-erased document reference from Core CEWorkspaceFile --- .../Models/CEWorkspaceFile+Editor.swift | 19 ------------------- .../Domain/Workspace/CEWorkspaceFile.swift | 14 -------------- 2 files changed, 33 deletions(-) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift index 34909f2fa7..0ef3302e40 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift @@ -5,28 +5,9 @@ // Created by Matthijs Eikelenboom on 05/07/2026. // -import SwiftUI -import Combine import CodeEditCore extension CEWorkspaceFile: EditorTabRepresentable { /// The `id` in `EditorTabID` form. var tabID: EditorTabID { .codeEditor(id) } - - /// The file's open document, if any. Bridges the Core type-erased ``fileDocumentObject``. - var fileDocument: CodeFileDocument? { - get { fileDocumentObject as? CodeFileDocument } - set { fileDocumentObject = newValue } - } - - /// Publisher for ``fileDocument``. - var fileDocumentPublisher: AnyPublisher { - fileDocumentObjectPublisher.map { $0 as? CodeFileDocument }.eraseToAnyPublisher() - } - - /// Loads ``fileDocument`` with a new `CodeFileDocument`. - func loadCodeFile() throws { - let codeFile = try CodeFileDocument(contentsOf: resolvedURL, ofType: contentType?.identifier ?? "") - self.fileDocument = codeFile - } } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift index 7d72423221..26bab146a8 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift @@ -7,7 +7,6 @@ import Foundation import UniformTypeIdentifiers -import Combine /// A file, folder, or symlink in the workspace. This is the UI-free model; presentation, /// AppKit intents, name-labeling, and editor-document coupling live in app-side extensions. @@ -41,19 +40,6 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable /// Returns a parent ``CEWorkspaceFile``. `nil` for the top-level item. public weak var parent: CEWorkspaceFile? - private let fileDocumentSubject = PassthroughSubject() - - /// Type-erased weak reference to the file's open document (a `CodeFileDocument` in the app). - /// The app's `CEWorkspaceFile+Editor` extension provides a typed `fileDocument` accessor. - public weak var fileDocumentObject: AnyObject? { - didSet { fileDocumentSubject.send(fileDocumentObject) } - } - - /// Publisher for ``fileDocumentObject``. - public var fileDocumentObjectPublisher: AnyPublisher { - fileDocumentSubject.eraseToAnyPublisher() - } - public var fileIdentifier = UUID().uuidString /// The Git status of the file. From 31e78ba0a6f86cf13bbf599b90ef8926b521f110 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 23:02:35 +0200 Subject: [PATCH 068/335] Feature: LSPService owns per-document language-server objects keyed by URI --- .../Features/LSP/Service/LSPService.swift | 25 ++++++++++ .../LSP/LSPServiceDocumentObjectsTests.swift | 47 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 57f7feb299..eaefb4da29 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -123,12 +123,36 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// Holds all the event listeners for each active language client var eventListeningTasks: [ClientKey: Task] = [:] + /// Per-document language-server objects (content coordinator + highlight provider), keyed by + /// document URI. Owned here so `CodeFileDocument` need not depend on LSP types. Entries are + /// created on demand and removed when the document closes. + private var documentObjects: [String: LanguageServerDocumentObjects] = [:] + @AppSettings(\.developerSettings.lspBinaries) var lspBinaries @Environment(\.openWindow) private var openWindow + /// Returns the language-server objects for a document, creating and storing them on first use. + /// A document without a URI (e.g. untitled) gets a fresh, unstored instance — it has no server. + func languageServerObjects(for document: CodeFileDocument) -> LanguageServerDocumentObjects { + guard let uri = document.languageServerURI else { + return LanguageServerDocumentObjects() + } + if let existing = documentObjects[uri] { + return existing + } + let created = LanguageServerDocumentObjects() + documentObjects[uri] = created + return created + } + + /// Drops the stored objects for a document URI. Called when a document closes. + func removeLanguageServerObjects(for uri: String) { + documentObjects[uri] = nil + } + init() { // Load the LSP binaries from the developer menu for binary in lspBinaries { @@ -223,6 +247,7 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// Notify all relevant language clients that a document was closed. /// - Parameter url: The url of the document that was closed func closeDocument(_ url: URL) { + removeLanguageServerObjects(for: url.lspURI) guard let languageClient = languageClient(forDocument: url) else { return } Task { do { diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift new file mode 100644 index 0000000000..cbcbabf8f9 --- /dev/null +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -0,0 +1,47 @@ +// +// LSPServiceDocumentObjectsTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 06/07/2026. +// + +import XCTest +@testable import CodeEdit + +@MainActor +final class LSPServiceDocumentObjectsTests: XCTestCase { + private func makeService() -> LSPService { LSPService() } + + private func makeDocument(path: String) throws -> CodeFileDocument { + let url = FileManager.default.temporaryDirectory + .appending(path: "lsp-objs-\(UUID().uuidString)-\(path)") + try Data("x".utf8).write(to: url) + return try CodeFileDocument(contentsOf: url, ofType: "public.swift-source") + } + + func testSameURIReturnsSameInstance() throws { + let service = makeService() + let doc = try makeDocument(path: "a.swift") + let first = service.languageServerObjects(for: doc) + let second = service.languageServerObjects(for: doc) + XCTAssertTrue(first.textCoordinator === second.textCoordinator) + } + + func testDifferentURIsReturnDistinctInstances() throws { + let service = makeService() + let docA = try makeDocument(path: "a.swift") + let docB = try makeDocument(path: "b.swift") + let objectsA = service.languageServerObjects(for: docA) + let objectsB = service.languageServerObjects(for: docB) + XCTAssertFalse(objectsA.textCoordinator === objectsB.textCoordinator) + } + + func testRemoveDropsStoredInstance() throws { + let service = makeService() + let doc = try makeDocument(path: "a.swift") + let first = service.languageServerObjects(for: doc) + service.removeLanguageServerObjects(for: doc.languageServerURI!) + let afterRemove = service.languageServerObjects(for: doc) + XCTAssertFalse(first.textCoordinator === afterRemove.textCoordinator) + } +} From 4c4b5994a93559818d506904f16d47e8d0dca448 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 23:03:51 +0200 Subject: [PATCH 069/335] Refactor: Inject document-objects provider into LanguageServer from LSPService --- .../LSP/LanguageServer/LanguageServer.swift | 22 ++++++++++++++++--- .../Features/LSP/Service/LSPService.swift | 8 ++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 7f855bbf01..8db31d832b 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -30,6 +30,12 @@ class LanguageServer { /// language server and a document. For example, the content coordinator. let openFiles: LanguageServerFileMap + /// Resolves the per-document LSP objects (owned by `LSPService`). Injected at creation so the + /// document itself need not store them. + let provideObjects: @MainActor (DocumentType) -> LanguageServerDocumentObjects + /// Drops the per-document LSP objects for a URI when a document closes. + let clearObjects: @MainActor (String) -> Void + /// Maps the language server's highlight config to one CodeEdit can read. See ``SemanticTokenMap``. let highlightMap: SemanticTokenMap? @@ -52,8 +58,13 @@ class LanguageServer { lspPid: pid_t, serverCapabilities: ServerCapabilities, rootPath: URL, - logContainer: LanguageServerLogContainer + logContainer: LanguageServerLogContainer, + provideObjects: @escaping @MainActor (DocumentType) -> LanguageServerDocumentObjects + = { _ in LanguageServerDocumentObjects() }, + clearObjects: @escaping @MainActor (String) -> Void = { _ in } ) { + self.provideObjects = provideObjects + self.clearObjects = clearObjects self.languageId = languageId self.binary = binary self.lspInstance = lspInstance @@ -82,7 +93,10 @@ class LanguageServer { static func createServer( for languageId: LanguageIdentifier, with binary: LanguageServerBinary, - workspacePath: String + workspacePath: String, + provideObjects: @escaping @MainActor (DocumentType) -> LanguageServerDocumentObjects + = { _ in LanguageServerDocumentObjects() }, + clearObjects: @escaping @MainActor (String) -> Void = { _ in } ) async throws -> LanguageServer { let executionParams = Process.ExecutionParameters( path: binary.execPath, @@ -109,7 +123,9 @@ class LanguageServer { lspPid: process.processIdentifier, serverCapabilities: initializationResponse.capabilities, rootPath: URL(filePath: workspacePath), - logContainer: logContainer + logContainer: logContainer, + provideObjects: provideObjects, + clearObjects: clearObjects ) } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index eaefb4da29..d43c981361 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -200,7 +200,13 @@ final class LSPService: ObservableObject, LSPServiceProtocol { let server = try await LanguageServerType.createServer( for: languageId, with: serverBinary, - workspacePath: workspacePath + workspacePath: workspacePath, + provideObjects: { [weak self] document in + self?.languageServerObjects(for: document) ?? LanguageServerDocumentObjects() + }, + clearObjects: { [weak self] uri in + self?.removeLanguageServerObjects(for: uri) + } ) languageClients[ClientKey(languageId, workspacePath)] = server logger.info("Successfully started \(languageId.rawValue) language server") From 2f28e0896edbd1cb5c13ecd0ee403984031ddd70 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 23:05:49 +0200 Subject: [PATCH 070/335] Refactor: Move languageServerObjects ownership to LSPService; thin LanguageServerDocument --- .../CodeFileDocument/CodeFileDocument.swift | 3 --- CodeEdit/Features/Editor/Views/CodeFileView.swift | 9 +++++++-- .../LanguageServer+DocumentSync.swift | 15 ++++++++++++--- .../Features/LSP/LanguageServerDocument.swift | 7 +++++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index f37f168717..b79599abe8 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -64,9 +64,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Document-specific overridden line wrap preference. @Published var wrapLines: Bool? - /// Set up by ``LanguageServer``, conforms this type to ``LanguageServerDocument``. - @Published var languageServerObjects: LanguageServerDocumentObjects = .init() - /// The type of data this file document contains. /// /// If its text content is not nil, a `text` UTType is returned. diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index a0b16da6af..d6c51b69d3 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -12,6 +12,7 @@ import CodeEditSourceEditor import CodeEditTextView import CodeEditLanguages import Combine +import Factory /// CodeFileView is just a wrapper of the `CodeEditor` dependency struct CodeFileView: View { @@ -83,10 +84,14 @@ struct CodeFileView: View { self._editorInstance = .init(wrappedValue: editorInstance) self._codeFile = .init(wrappedValue: codeFile) + // The per-document LSP objects are owned by `LSPService` (keyed by URI); fetch the same + // instance the language server configures via `setUp`. + let lspObjects = Container.shared.lspService().languageServerObjects(for: codeFile) + self.textViewCoordinators = textViewCoordinators + [editorInstance.rangeTranslator] + [codeFile.contentCoordinator] - + [codeFile.languageServerObjects.textCoordinator] + + [lspObjects.textCoordinator] self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -94,7 +99,7 @@ struct CodeFileView: View { editorInstance.cursorPositions = openOptions.cursorPositions } - highlightProviders = [codeFile.languageServerObjects.highlightProvider] + [treeSitterClient] + highlightProviders = [lspObjects.highlightProvider] + [treeSitterClient] codeFile .contentCoordinator diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index 2c30e6935f..5ca5843630 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -108,7 +108,7 @@ extension LanguageServer { // Let the semantic token provider know about the update. // Note for future: If a related LSP object need notifying about document changes, do it here. - try await document.languageServerObjects.highlightProvider.documentDidChange() + try await notifyHighlightProviderDidChange(document) } catch { logger.warning("closeDocument: Error \(error)") throw error @@ -129,12 +129,21 @@ extension LanguageServer { @MainActor private func updateIsolatedDocument(_ document: DocumentType) { - document.languageServerObjects.setUp(server: self, document: document) + provideObjects(document).setUp(server: self, document: document) } @MainActor private func clearIsolatedDocument(_ document: DocumentType) { - document.languageServerObjects = LanguageServerDocumentObjects() + if let uri = document.languageServerURI { + clearObjects(uri) + } + } + + /// Notifies the document's highlight provider of a change. Kept `@MainActor` so the + /// non-`Sendable` `LanguageServerDocumentObjects` never crosses an actor boundary. + @MainActor + private func notifyHighlightProviderDidChange(_ document: DocumentType) async throws { + try await provideObjects(document).highlightProvider.documentDidChange() } // swiftlint:disable line_length diff --git a/CodeEdit/Features/LSP/LanguageServerDocument.swift b/CodeEdit/Features/LSP/LanguageServerDocument.swift index 2953d08fc2..877af1917e 100644 --- a/CodeEdit/Features/LSP/LanguageServerDocument.swift +++ b/CodeEdit/Features/LSP/LanguageServerDocument.swift @@ -21,10 +21,13 @@ struct LanguageServerDocumentObjects { } } -/// A protocol that allows a language server to register objects on a text document. +/// A protocol that allows a language server to work with a text document. +/// +/// Deliberately holds no LSP-typed state: the per-document objects +/// (``LanguageServerDocumentObjects``) are owned by `LSPService`, keyed by URI, so conformers +/// (e.g. `CodeFileDocument`) need not depend on LSP types. protocol LanguageServerDocument: AnyObject { var content: NSTextStorage? { get } var languageServerURI: String? { get } - var languageServerObjects: LanguageServerDocumentObjects { get set } func getLanguage() -> CodeLanguage } From 7cdfbbf6784e0d7d04da2fec3255dc0132f35ff6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 6 Jul 2026 23:12:37 +0200 Subject: [PATCH 071/335] Test: Update LSP tests for service-owned languageServerObjects --- .../LSP/LanguageServer+CodeFileDocument.swift | 18 +++++++++++------- .../LSP/LanguageServer+DocumentObjects.swift | 12 ++++++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 98d0dc8271..16a3754d7d 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -75,7 +75,9 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { lspPid: -1, serverCapabilities: capabilities, rootPath: tempTestDir, - logContainer: LanguageServerLogContainer(language: .swift) + logContainer: LanguageServerLogContainer(language: .swift), + provideObjects: { Container.shared.lspService().languageServerObjects(for: $0) }, + clearObjects: { Container.shared.lspService().removeLanguageServerObjects(for: $0) } ) _ = try await server.lspInstance.initializeIfNeeded() return (connection: bufferingConnection, server: server) @@ -241,13 +243,14 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { let (connection, server) = try await makeTestServer() // Create a CodeFileDocument to test with, attach it to the workspace and file let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) - XCTAssertNotNil(codeFile.languageServerObjects.textCoordinator.languageServer) - codeFile.languageServerObjects.textCoordinator.setUpUpdatesTask() + let lspObjects = Container.shared.lspService().languageServerObjects(for: codeFile) + XCTAssertNotNil(lspObjects.textCoordinator.languageServer) + lspObjects.textCoordinator.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) let textView = TextView(string: "") textView.setTextStorage(codeFile.content!) - textView.delegate = codeFile.languageServerObjects.textCoordinator + textView.delegate = lspObjects.textCoordinator textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "Worlld") textView.replaceCharacters(in: NSRange(location: 39, length: 6), with: "") @@ -298,14 +301,15 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { // Set up test server let (connection, server) = try await makeTestServer() let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) + let lspObjects = Container.shared.lspService().languageServerObjects(for: codeFile) - XCTAssertNotNil(codeFile.languageServerObjects.textCoordinator.languageServer) - codeFile.languageServerObjects.textCoordinator.setUpUpdatesTask() + XCTAssertNotNil(lspObjects.textCoordinator.languageServer) + lspObjects.textCoordinator.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) let textView = TextView(string: "") textView.setTextStorage(codeFile.content!) - textView.delegate = codeFile.languageServerObjects.textCoordinator + textView.delegate = lspObjects.textCoordinator textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "Worlld") textView.replaceCharacters(in: NSRange(location: 39, length: 6), with: "") textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift index 9b7738a7d8..bcb5502639 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift @@ -18,12 +18,13 @@ final class LanguageServerDocumentObjectsTests: XCTestCase { final class MockDocumentType: LanguageServerDocument { var content: NSTextStorage? var languageServerURI: String? - var languageServerObjects: LanguageServerDocumentObjects + /// Test-local store (the protocol no longer requires it; `LSPService` owns it in production). + /// The server's `provideObjects`/`clearObjects` closures are wired to this in `setUp`. + var languageServerObjects = LanguageServerDocumentObjects() init() { self.content = NSTextStorage(string: "hello world") self.languageServerURI = "/test/file/path" - self.languageServerObjects = .init() } func getLanguage() -> CodeLanguage { @@ -42,6 +43,8 @@ final class LanguageServerDocumentObjectsTests: XCTestCase { var capabilities = ServerCapabilities() capabilities.textDocumentSync = .optionA(.init(openClose: true, change: .full)) capabilities.semanticTokensProvider = .optionA(.init(legend: .init(tokenTypes: [], tokenModifiers: []))) + let document = MockDocumentType() + self.document = document server = LanguageServerType( languageId: .swift, binary: .init(execPath: "", args: [], env: nil), @@ -52,10 +55,11 @@ final class LanguageServerDocumentObjectsTests: XCTestCase { lspPid: -1, serverCapabilities: capabilities, rootPath: URL(fileURLWithPath: ""), - logContainer: LanguageServerLogContainer(language: .swift) + logContainer: LanguageServerLogContainer(language: .swift), + provideObjects: { $0.languageServerObjects }, + clearObjects: { [weak document] _ in document?.languageServerObjects = .init() } ) _ = try await server.lspInstance.initializeIfNeeded() - document = MockDocumentType() } // MARK: - Tests From bce9fe1cdf3c95fa2738bbf0222e4d1c3a7c46d3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 11:17:43 +0200 Subject: [PATCH 072/335] Refactor: Move IndentOption into CodeEditCore and re-export from Settings --- .../CodeFileDocument/CodeFileDocument.swift | 3 ++- .../Features/Editor/Views/CodeFileView.swift | 7 ++--- .../Models/TextEditingSettings.swift | 15 +++-------- .../CodeFile/CodeFileDocumentTests.swift | 9 +++++++ .../Domain/Editor/IndentOption.swift | 27 +++++++++++++++++++ 5 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/IndentOption.swift diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index b79599abe8..dad01f3ba1 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -12,6 +12,7 @@ import UniformTypeIdentifiers import CodeEditSourceEditor import CodeEditTextView import CodeEditLanguages +import CodeEditCore import Combine import OSLog import TextStory @@ -56,7 +57,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { @Published var language: CodeLanguage? /// Document-specific overridden indent option. - @Published var indentOption: SettingsData.TextEditingSettings.IndentOption? + @Published var indentOption: CodeEditCore.IndentOption? /// Document-specific overridden tab width. @Published var defaultTabWidth: Int? diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index d6c51b69d3..360c8ee735 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -11,6 +11,7 @@ import CodeEditUI import CodeEditSourceEditor import CodeEditTextView import CodeEditLanguages +import CodeEditCore import Combine import Factory @@ -218,12 +219,12 @@ struct CodeFileView: View { // This extension is kept here because it should not be used elsewhere in the app and may cause confusion // due to the similar type name from the CETV module. private extension SettingsData.TextEditingSettings.IndentOption { - func textViewOption() -> IndentOption { + func textViewOption() -> CodeEditSourceEditor.IndentOption { switch self.indentType { case .spaces: - return IndentOption.spaces(count: spaceCount) + return CodeEditSourceEditor.IndentOption.spaces(count: spaceCount) case .tab: - return IndentOption.tab + return CodeEditSourceEditor.IndentOption.tab } } } diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift index fa55d6433c..7fdecb7cd1 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditCore import Factory import Foundation @@ -213,17 +214,9 @@ extension SettingsData { } } - struct IndentOption: Codable, Hashable { - var indentType: IndentType - // Kept even when `indentType` is `.tab` to retain the user's - // settings when changing `indentType`. - var spaceCount: Int = 4 - - enum IndentType: String, Codable { - case tab - case spaces - } - } + /// Re-exported from `CodeEditCore`. Keeps `SettingsData.TextEditingSettings.IndentOption` + /// valid for all existing call sites while the underlying type lives in the Core package. + typealias IndentOption = CodeEditCore.IndentOption struct BracketPairEmphasis: Codable, Hashable { /// The type of highlight to use diff --git a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift index b5b8fc0408..656363982f 100644 --- a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift +++ b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Testing +import CodeEditCore @testable import CodeEdit @Suite @@ -29,6 +30,14 @@ struct CodeFileDocumentTests { } } + @Test + func indentOptionOverrideUsesCoreType() { + let codeFile = CodeFileDocument() + codeFile.indentOption = CodeEditCore.IndentOption(indentType: .spaces, spaceCount: 2) + #expect(codeFile.indentOption?.indentType == .spaces) + #expect(codeFile.indentOption?.spaceCount == 2) + } + @Test func testLoadUTF8Encoding() throws { try withFile { fileURL in diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/IndentOption.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/IndentOption.swift new file mode 100644 index 0000000000..d42f7b5a2f --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/IndentOption.swift @@ -0,0 +1,27 @@ +// +// IndentOption.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +/// The behavior of a `tab` keypress. If `.tab`, inserts a tab character; if `.spaces`, +/// inserts `spaceCount` spaces instead. +/// +/// A pure value type shared by the Settings feature and `CodeFileDocument`. +public struct IndentOption: Codable, Hashable, Sendable { + public var indentType: IndentType + // Kept even when `indentType` is `.tab` to retain the user's + // settings when changing `indentType`. + public var spaceCount: Int + + public init(indentType: IndentType, spaceCount: Int = 4) { + self.indentType = indentType + self.spaceCount = spaceCount + } + + public enum IndentType: String, Codable, Sendable { + case tab + case spaces + } +} From c6163037473e7645faf8e29d10d83b5c4365c6fc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 11:21:40 +0200 Subject: [PATCH 073/335] Refactor: Read autosave preference via injected provider on CodeFileDocument --- CodeEdit/AppDelegate.swift | 3 +++ .../CodeFileDocument/CodeFileDocument.swift | 11 +++++++---- .../Features/CodeFile/CodeFileDocumentTests.swift | 12 ++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 7053265276..3860484c58 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -35,6 +35,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var cancellables = Set() func applicationDidFinishLaunching(_ notification: Notification) { + CodeFileDocument.isAutoSaveOnProvider = { + Settings.shared.preferences.general.isAutoSaveOn + } enableWindowSizeSaveOnQuit() Settings.shared.preferences.general.appAppearance.applyAppearance() checkForFilesToOpen() diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index dad01f3ba1..2d04b93221 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -99,16 +99,19 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Timer used to schedule autosave intervals. private var autosaveTimer: Timer? + /// Provides the current "autosave enabled" preference without coupling this type to the + /// Settings feature. Wired by the app at launch (see `AppDelegate`). Defaults to `false` + /// so the type stays self-contained for packaging and predictable in tests that don't wire it. + static var isAutoSaveOnProvider: () -> Bool = { false } + // MARK: - NSDocument override static var autosavesInPlace: Bool { - Settings.shared.preferences.general.isAutoSaveOn + isAutoSaveOnProvider() } override var autosavingFileType: String? { - Settings.shared.preferences.general.isAutoSaveOn - ? fileType - : nil + Self.isAutoSaveOnProvider() ? fileType : nil } override func makeWindowControllers() { diff --git a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift index 656363982f..c69d82538e 100644 --- a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift +++ b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift @@ -30,6 +30,18 @@ struct CodeFileDocumentTests { } } + @Test + func autosavesInPlaceReflectsProvider() { + let original = CodeFileDocument.isAutoSaveOnProvider + defer { CodeFileDocument.isAutoSaveOnProvider = original } + + CodeFileDocument.isAutoSaveOnProvider = { true } + #expect(CodeFileDocument.autosavesInPlace == true) + + CodeFileDocument.isAutoSaveOnProvider = { false } + #expect(CodeFileDocument.autosavesInPlace == false) + } + @Test func indentOptionOverrideUsesCoreType() { let codeFile = CodeFileDocument() From 9e4e56649606718673fc2e9ba16c28273ba79023 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 13:58:44 +0200 Subject: [PATCH 074/335] Refactor: Route CodeFileDocument LSP lifecycle through CodeFileDocumentDelegate --- CodeEdit/CodeEditApp.swift | 1 + .../AppCodeFileDocumentDelegate.swift | 41 +++++++++++++++++++ .../CodeFileDocument/CodeFileDocument.swift | 19 +++++---- .../CodeFileDocumentDelegate.swift | 36 ++++++++++++++++ .../Features/LSP/Service/LSPService.swift | 2 +- .../CodeFile/CodeFileDocumentTests.swift | 32 +++++++++++++++ .../LSP/LanguageServer+CodeFileDocument.swift | 1 + 7 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift create mode 100644 CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index a5d08de68e..3c95e26e4f 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -22,6 +22,7 @@ struct CodeEditApp: App { NSMenuItem.swizzle() NSSplitViewItem.swizzle() Container.shared.workspaceFileOpener.register { AppWorkspaceFileOpener() } + Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } } var body: some Scene { diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift new file mode 100644 index 0000000000..03caa9112c --- /dev/null +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -0,0 +1,41 @@ +// +// AppCodeFileDocumentDelegate.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import AppKit +import SwiftUI +import Factory +import CodeEditTextView + +/// App-side implementation of ``CodeFileDocumentDelegate``. Bridges a packaged +/// `CodeFileDocument` back to the app's `Workspace` undo registry, Settings-injected +/// standalone-window view, and `LSPService` lifecycle notifications. +@MainActor +final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { + @LazyInjected(\.lspService) private var lspService + + /// `nonisolated` so the Factory registration closure can construct it from any context + /// (e.g. a non-isolated test `setUp`); the init touches no main-actor state. + nonisolated init() {} + + func undoManager(forFile url: URL) -> CEUndoManager? { + url.findWorkspace()?.undoRegistration.managerIfExists(forFile: url) + } + + func makeWindowContentView(for document: CodeFileDocument) -> NSView { + NSHostingView(rootView: SettingsInjector { + WindowCodeFileView(codeFile: document) + }) + } + + func documentDidOpen(_ document: CodeFileDocument) { + lspService.openDocument(document) + } + + func documentDidClose(at url: URL) { + lspService.closeDocument(url) + } +} diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 2d04b93221..b2383cca56 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -32,10 +32,11 @@ final class CodeFileDocument: NSDocument, ObservableObject { static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") - /// Notified when this document is opened (contents available) or closed, - /// so language servers can track the document's lifecycle. - @LazyInjected(\.lspService) - private var lspService + /// The app-registered delegate (see ``CodeFileDocumentDelegate``). Provides LSP lifecycle + /// notifications, the standalone-window content view, and undo-manager lookup, keeping this + /// document free of app-tier types. `nil` when unhosted (e.g. tests that don't register one). + @LazyInjected(\.codeFileDocumentDelegate) + private var delegate /// The text content of the document, stored as a text storage /// @@ -178,22 +179,22 @@ final class CodeFileDocument: NSDocument, ObservableObject { notifyLSPDidOpen() } - /// `LSPService` is main-actor isolated, but document reads and closes can happen off the main + /// The delegate is main-actor isolated, but document reads and closes can happen off the main /// thread (AppKit concurrent reads, Swift Testing). Mirrors the NotificationCenter `queue: .main` /// delivery this replaced: synchronous on main, async hop otherwise. private func notifyLSPDidOpen() { if Thread.isMainThread { - MainActor.assumeIsolated { lspService.openDocument(self) } + MainActor.assumeIsolated { delegate?.documentDidOpen(self) } } else { - DispatchQueue.main.async { self.lspService.openDocument(self) } + DispatchQueue.main.async { self.delegate?.documentDidOpen(self) } } } private func notifyLSPDidClose(_ url: URL) { if Thread.isMainThread { - MainActor.assumeIsolated { lspService.closeDocument(url) } + MainActor.assumeIsolated { delegate?.documentDidClose(at: url) } } else { - DispatchQueue.main.async { self.lspService.closeDocument(url) } + DispatchQueue.main.async { self.delegate?.documentDidClose(at: url) } } } diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift new file mode 100644 index 0000000000..1cb7629b95 --- /dev/null +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift @@ -0,0 +1,36 @@ +// +// CodeFileDocumentDelegate.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import AppKit +import Factory +import CodeEditTextView + +/// The app-provided capabilities a `CodeFileDocument` needs from its host application +/// (undo registry, standalone-window content, and language-server lifecycle). Keeps the +/// document free of `Workspace`, Settings-view, and `LSPService` references. +/// +/// A single shared delegate is registered by the app in `CodeEditApp.init`; it is delivered +/// via Factory rather than a per-instance `weak var delegate` so that framework-created +/// documents (`init()` → `read()` fires `documentDidOpen` before any completion handler could +/// set a per-instance delegate) get correct timing without a custom `NSDocumentController`. +@MainActor +protocol CodeFileDocumentDelegate: AnyObject { + /// The undo manager already registered for a file, if any (nil if none exists yet). + func undoManager(forFile url: URL) -> CEUndoManager? + /// The content view for a standalone single-file window (`makeWindowControllers`). + func makeWindowContentView(for document: CodeFileDocument) -> NSView + /// The document finished reading its contents and is now open. + func documentDidOpen(_ document: CodeFileDocument) + /// The document at `url` closed. + func documentDidClose(at url: URL) +} + +extension Container { + /// App shell registers the real delegate in `CodeEditApp.init`; defaults to `nil` + /// (no-op) so tests and pre-launch contexts are safe. + var codeFileDocumentDelegate: Factory { self { nil } } +} diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index d43c981361..b6d5f07fa9 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -221,7 +221,7 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// - Note: Must be invoked after the contents of the file are available. /// - Parameter document: The code document that was opened. func openDocument(_ document: CodeFileDocument) { - guard let workspace = document.findWorkspace(), + guard let workspace = document.fileURL?.findWorkspace(), let workspacePath = workspace.fileURL?.absolutePath, let lspLanguage = document.getLanguage().lspLanguage else { return diff --git a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift index c69d82538e..27676afa84 100644 --- a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift +++ b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift @@ -7,14 +7,46 @@ import Foundation import SwiftUI +import AppKit import Testing import CodeEditCore +import Factory +import CodeEditTextView @testable import CodeEdit @Suite struct CodeFileDocumentTests { let defaultString = "func test() { }" + @MainActor + final class MockDelegate: CodeFileDocumentDelegate { + var openedDocuments: [CodeFileDocument] = [] + var closedURLs: [URL] = [] + var undoRequestedURLs: [URL] = [] + func undoManager(forFile url: URL) -> CEUndoManager? { + undoRequestedURLs.append(url) + return nil + } + func makeWindowContentView(for document: CodeFileDocument) -> NSView { NSView() } + func documentDidOpen(_ document: CodeFileDocument) { openedDocuments.append(document) } + func documentDidClose(at url: URL) { closedURLs.append(url) } + } + + @MainActor + @Test + func delegateReceivesOpenAndCloseNotifications() throws { + let mock = MockDelegate() + Container.shared.codeFileDocumentDelegate.register { mock } + defer { Container.shared.codeFileDocumentDelegate.reset() } + + try withCodeFile { codeFile in + #expect(mock.openedDocuments.contains { $0 === codeFile }) + let url = codeFile.fileURL + codeFile.close() + #expect(url != nil && mock.closedURLs.contains(url!)) + } + } + private func withFile(_ operation: (URL) throws -> Void) throws { try withTempDir { dir in let fileURL = dir.appending(path: "file.swift") diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 16a3754d7d..b12d5cb0af 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -29,6 +29,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { var tempTestDir: URL! override func setUp() { + Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } continueAfterFailure = false do { let tempDir = FileManager.default.temporaryDirectory.appending( From 939844c5c5beb9f4498d84f9dcbe662e5da644c0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 14:00:52 +0200 Subject: [PATCH 075/335] Refactor: Route CodeFileDocument undo and window creation through its delegate --- .../CodeFileDocument/CodeFileDocument.swift | 12 ++++-------- .../CodeFile/CodeFileDocumentTests.swift | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index b2383cca56..8c58300cec 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -128,9 +128,9 @@ final class CodeFileDocument: NSDocument, ObservableObject { } addWindowController(windowController) - window.contentView = NSHostingView(rootView: SettingsInjector { - WindowCodeFileView(codeFile: self) - }) + if let delegate { + window.contentView = delegate.makeWindowContentView(for: self) + } window.makeKeyAndOrderFront(nil) @@ -213,7 +213,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { range: NSRange(location: 0, length: content.length), limit: content.length ) - let undoManager = self.findWorkspace()?.undoRegistration.managerIfExists(forFile: fileURL) + let undoManager = delegate?.undoManager(forFile: fileURL) undoManager?.registerMutation(mutation) } @@ -356,10 +356,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { ) } - @MainActor - func findWorkspace() -> Workspace? { - fileURL?.findWorkspace() - } } // MARK: LanguageServerDocument diff --git a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift index 27676afa84..b7c1f7fdc4 100644 --- a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift +++ b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift @@ -32,6 +32,22 @@ struct CodeFileDocumentTests { func documentDidClose(at url: URL) { closedURLs.append(url) } } + @MainActor + @Test + func delegateConsultedForUndoOnReread() throws { + let mock = MockDelegate() + Container.shared.codeFileDocumentDelegate.register { mock } + defer { Container.shared.codeFileDocumentDelegate.reset() } + + try withCodeFile { codeFile in + // First read happened in `withCodeFile` (content now loaded). A second read + // takes the re-read branch, which consults the delegate for an undo manager. + let data = Data("different contents".utf8) + try codeFile.read(from: data, ofType: "public.source-code") + #expect(codeFile.fileURL != nil && mock.undoRequestedURLs.contains(codeFile.fileURL!)) + } + } + @MainActor @Test func delegateReceivesOpenAndCloseNotifications() throws { From cd54349b952eaed592eea97e14a51ecdca60223f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 15:46:26 +0200 Subject: [PATCH 076/335] Refactor: Scaffold CodeEditDocument package, relocate FileEncoding as seed --- CodeEdit.xcodeproj/project.pbxproj | 7 ++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 ++ .../CodeFileDocument/CodeFileDocument.swift | 1 + .../CodeFile/CodeFileDocumentTests.swift | 1 + .../Foundation/CodeEditDocument/Package.swift | 33 +++++++++++++++++++ .../CodeEditDocument}/FileEncoding.swift | 6 ++-- 6 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 Packages/Foundation/CodeEditDocument/Package.swift rename {CodeEdit/Features/Documents/CodeFileDocument => Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument}/FileEncoding.swift (86%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index ff00f20deb..e0ee6a53eb 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; + 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; @@ -182,6 +183,7 @@ 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */, + 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, @@ -354,6 +356,7 @@ 58CF9F392F86D64F009F4AA7 /* Factory */, 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, 5800E2F72FF843390085ECF1 /* CodeEditUI */, + 5AD0C0DE2D00000000000002 /* CodeEditDocument */, 588950C42FFA5C05004BE116 /* Search */, 588957122FFA679E004BE116 /* CodeEditServices */, 5889639D2FFA9A87004BE116 /* Notifications */, @@ -1903,6 +1906,10 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditUI; }; + 5AD0C0DE2D00000000000002 /* CodeEditDocument */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditDocument; + }; 583E529B29361BAB001AB554 /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 1202877b10..cb690a0af4 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -13,6 +13,9 @@ + + Date: Tue, 7 Jul 2026 15:59:19 +0200 Subject: [PATCH 077/335] Refactor: Move CodeFileDocument and its delegate into CodeEditDocument package --- CodeEdit.xcodeproj/project.pbxproj | 10 +- CodeEdit/AppDelegate.swift | 1 + CodeEdit/CodeEditApp.swift | 1 + .../AppCodeFileDocumentDelegate.swift | 1 + .../CodeEditWindowController.swift | 1 + .../JumpBar/Views/EditorJumpBarView.swift | 1 + .../Editor/Models/DocumentRegistry.swift | 1 + .../Editor/Models/EditorManager.swift | 1 + .../Restoration/UndoManagerRegistration.swift | 1 + .../EditorTabBarTrailingAccessories.swift | 1 + .../TabBar/Views/EditorTabBarView.swift | 1 + .../Features/Editor/Views/CodeFileView.swift | 1 + .../Editor/Views/EditorAreaFileView.swift | 1 + .../Editor/Views/EditorAreaView.swift | 1 + .../Editor/Views/NonTextFileView.swift | 1 + .../Editor/Views/WindowCodeFileView.swift | 1 + ...eFileDocument+LanguageServerDocument.swift | 17 ++++ .../DocumentSync/LSPContentCoordinator.swift | 1 + .../Features/LSP/LanguageServerDocument.swift | 1 + .../Features/LSP/Service/LSPService.swift | 1 + .../LSP/Service/LSPServiceProtocol.swift | 1 + .../Views/OpenQuicklyPreviewView.swift | 1 + .../CodeFileDocument+UTTypeTests.swift | 1 + .../Editor/DocumentRegistryTests.swift | 1 + .../LSP/LSPServiceDocumentObjectsTests.swift | 1 + .../LSP/LanguageServer+CodeFileDocument.swift | 1 + .../CodeEditDocument}/CodeFileDocument.swift | 97 ++++++++++--------- .../CodeFileDocumentDelegate.swift | 4 +- .../CodeEditDocument}/String+Lines.swift | 0 .../CodeEditDocument/URL+AbsolutePath.swift | 16 +++ 30 files changed, 114 insertions(+), 54 deletions(-) create mode 100644 CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift rename {CodeEdit/Features/Documents/CodeFileDocument => Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument}/CodeFileDocument.swift (81%) rename {CodeEdit/Features/Documents/CodeFileDocument => Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument}/CodeFileDocumentDelegate.swift (90%) rename {CodeEdit/Utils/Extensions/String => Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument}/String+Lines.swift (100%) create mode 100644 Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index e0ee6a53eb..996a6f4519 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; - 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; @@ -25,6 +24,7 @@ 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; + 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5E4485602DF600D9008BBE69 /* AboutWindow */; }; 5EACE6222DF4BF08005E08B8 /* WelcomeWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5EACE6212DF4BF08005E08B8 /* WelcomeWindow */; }; 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0617D52BDB4432008C9C42 /* LogStream */; }; @@ -1906,10 +1906,6 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditUI; }; - 5AD0C0DE2D00000000000002 /* CodeEditDocument */ = { - isa = XCSwiftPackageProductDependency; - productName = CodeEditDocument; - }; 583E529B29361BAB001AB554 /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; @@ -1946,6 +1942,10 @@ package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + 5AD0C0DE2D00000000000002 /* CodeEditDocument */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditDocument; + }; 5E4485602DF600D9008BBE69 /* AboutWindow */ = { isa = XCSwiftPackageProductDependency; package = 5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */; diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 3860484c58..acdc80ba77 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditDocument import SwiftUI import Factory import CodeEditCore diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 3c95e26e4f..2fa159e5b1 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument import CodeEditCore import Factory import WelcomeWindow diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift index 03caa9112c..7db85fc808 100644 --- a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -9,6 +9,7 @@ import AppKit import SwiftUI import Factory import CodeEditTextView +import CodeEditDocument /// App-side implementation of ``CodeFileDocumentDelegate``. Bridges a packaged /// `CodeFileDocument` back to the app's `Workspace` undo registry, Settings-injected diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 6de1a08d8e..49b68ca0f7 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -6,6 +6,7 @@ // import Cocoa +import CodeEditDocument import SwiftUI import CodeEditUI import Factory diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift index b6a7188d8b..4b7f82de88 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument import CodeEditCore struct EditorJumpBarView: View { diff --git a/CodeEdit/Features/Editor/Models/DocumentRegistry.swift b/CodeEdit/Features/Editor/Models/DocumentRegistry.swift index d943fd7e86..b88b9b7397 100644 --- a/CodeEdit/Features/Editor/Models/DocumentRegistry.swift +++ b/CodeEdit/Features/Editor/Models/DocumentRegistry.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditDocument import Foundation import CodeEditCore diff --git a/CodeEdit/Features/Editor/Models/EditorManager.swift b/CodeEdit/Features/Editor/Models/EditorManager.swift index d2268732a5..cf51083948 100644 --- a/CodeEdit/Features/Editor/Models/EditorManager.swift +++ b/CodeEdit/Features/Editor/Models/EditorManager.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditDocument import CodeEditCore import Foundation import DequeModule diff --git a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift index 6f521f639b..988f3a4b9f 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift +++ b/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument import CEWorkspaceFileManager import CodeEditCore import CodeEditTextView diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 4d8d45cc04..177b8a1c5e 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument import CodeEditUI struct EditorTabBarTrailingAccessories: View { diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarView.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarView.swift index e080d1dffc..cb13743083 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarView.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument struct EditorTabBarView: View { let hasTopInsets: Bool diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 360c8ee735..a12cd367cc 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDocument import SwiftUI import CodeEditUI import CodeEditSourceEditor diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index e4367dcc0a..837ffa043d 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditDocument import AVKit import CodeEditSourceEditor import SwiftUI diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index 8f787e46b0..65c31bbad2 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument import CodeEditCore import CodeEditUI import CodeEditTextView diff --git a/CodeEdit/Features/Editor/Views/NonTextFileView.swift b/CodeEdit/Features/Editor/Views/NonTextFileView.swift index 36ca0cf5ef..e52f385cd1 100644 --- a/CodeEdit/Features/Editor/Views/NonTextFileView.swift +++ b/CodeEdit/Features/Editor/Views/NonTextFileView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument /// Determines what type of file is passed in, and previews it accordingly. /// diff --git a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift index 059d8571ac..c1aa1243ef 100644 --- a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDocument import CodeEditCore import SwiftUI diff --git a/CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift b/CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift new file mode 100644 index 0000000000..819fa48a2f --- /dev/null +++ b/CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift @@ -0,0 +1,17 @@ +// +// CodeFileDocument+LanguageServerDocument.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import AppKit +import CodeEditDocument + +extension CodeFileDocument: LanguageServerDocument { + /// A stable string to use when identifying documents with language servers. + /// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI. + var languageServerURI: String? { + fileURL?.lspURI + } +} diff --git a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift index b2c2e75b7c..8411d4e4ce 100644 --- a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditDocument import AsyncAlgorithms import CodeEditSourceEditor import CodeEditTextView diff --git a/CodeEdit/Features/LSP/LanguageServerDocument.swift b/CodeEdit/Features/LSP/LanguageServerDocument.swift index 877af1917e..652afc17f9 100644 --- a/CodeEdit/Features/LSP/LanguageServerDocument.swift +++ b/CodeEdit/Features/LSP/LanguageServerDocument.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditDocument import CodeEditLanguages /// A set of properties a language server sets when a document is registered. diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index b6d5f07fa9..64ef2cddfb 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -6,6 +6,7 @@ // import os.log +import CodeEditDocument import JSONRPC import SwiftUI import Foundation diff --git a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift b/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift index 8f1297a0af..006e458fb6 100644 --- a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift +++ b/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditDocument /// Protocol for managing Language Server Protocol services. /// diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift index efd35ebbd0..50873e6688 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditDocument import CodeEditCore struct OpenQuicklyPreviewView: View { diff --git a/CodeEditTests/Features/CodeFile/CodeFileDocument+UTTypeTests.swift b/CodeEditTests/Features/CodeFile/CodeFileDocument+UTTypeTests.swift index cd0149457a..c0e7d9c55a 100644 --- a/CodeEditTests/Features/CodeFile/CodeFileDocument+UTTypeTests.swift +++ b/CodeEditTests/Features/CodeFile/CodeFileDocument+UTTypeTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditDocument @testable import CodeEdit diff --git a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift index fd79202260..28de22808a 100644 --- a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift +++ b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditDocument import Combine import CodeEditCore @testable import CodeEdit diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift index cbcbabf8f9..e77065fb34 100644 --- a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditDocument @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index b12d5cb0af..1ad4fe6c3a 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -6,6 +6,7 @@ // import CEWorkspaceFileManager +import CodeEditDocument import XCTest import CodeEditCore import CodeEditTextView diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift similarity index 81% rename from CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift rename to Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift index 285ad26614..a5c69826a5 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift @@ -13,7 +13,6 @@ import CodeEditSourceEditor import CodeEditTextView import CodeEditLanguages import CodeEditCore -import CodeEditDocument import Combine import OSLog import TextStory @@ -26,9 +25,13 @@ enum CodeFileError: Error { } @objc(CodeFileDocument) -final class CodeFileDocument: NSDocument, ObservableObject { - struct OpenOptions { - let cursorPositions: [CursorPosition] +public final class CodeFileDocument: NSDocument, ObservableObject { + public struct OpenOptions { + public let cursorPositions: [CursorPosition] + + public init(cursorPositions: [CursorPosition]) { + self.cursorPositions = cursorPositions + } } static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") @@ -46,26 +49,26 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// enough. /// /// To receive notifications for content updates, subscribe to one of the publishers on ``contentCoordinator``. - var content: NSTextStorage? + public var content: NSTextStorage? /// The string encoding of the original file. Used to save the file back to the encoding it was loaded from. - var sourceEncoding: FileEncoding? + public var sourceEncoding: FileEncoding? /// The coordinator to use to subscribe to edit events and cursor location events. /// See ``CodeEditSourceEditor/CombineCoordinator``. - @Published var contentCoordinator: CombineCoordinator = CombineCoordinator() + @Published public var contentCoordinator: CombineCoordinator = CombineCoordinator() /// Used to override detected languages. - @Published var language: CodeLanguage? + @Published public var language: CodeLanguage? /// Document-specific overridden indent option. - @Published var indentOption: CodeEditCore.IndentOption? + @Published public var indentOption: CodeEditCore.IndentOption? /// Document-specific overridden tab width. - @Published var defaultTabWidth: Int? + @Published public var defaultTabWidth: Int? /// Document-specific overridden line wrap preference. - @Published var wrapLines: Bool? + @Published public var wrapLines: Bool? /// The type of data this file document contains. /// @@ -73,7 +76,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// /// - Note: The UTType doesn't necessarily mean the file extension, it can be the MIME /// type or any other form of data representation. - var utType: UTType? { + public var utType: UTType? { if content != nil { return .text } @@ -87,12 +90,12 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Specify options for opening the file such as the initial cursor positions. /// Nulled by ``CodeFileView`` on first load. - var openOptions: OpenOptions? + public var openOptions: OpenOptions? private let isDocumentEditedSubject = PassthroughSubject() /// Publisher for isDocumentEdited property - var isDocumentEditedPublisher: AnyPublisher { + public var isDocumentEditedPublisher: AnyPublisher { isDocumentEditedSubject.eraseToAnyPublisher() } @@ -104,19 +107,19 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Provides the current "autosave enabled" preference without coupling this type to the /// Settings feature. Wired by the app at launch (see `AppDelegate`). Defaults to `false` /// so the type stays self-contained for packaging and predictable in tests that don't wire it. - static var isAutoSaveOnProvider: () -> Bool = { false } + nonisolated(unsafe) public static var isAutoSaveOnProvider: () -> Bool = { false } // MARK: - NSDocument - override static var autosavesInPlace: Bool { + public override static var autosavesInPlace: Bool { isAutoSaveOnProvider() } - override var autosavingFileType: String? { + public override var autosavingFileType: String? { Self.isAutoSaveOnProvider() ? fileType : nil } - override func makeWindowControllers() { + public override func makeWindowControllers() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 750, height: 800), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], @@ -142,7 +145,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - Data - override func data(ofType _: String) throws -> Data { + public override func data(ofType _: String) throws -> Data { guard let sourceEncoding, let data = (content?.string as NSString?)?.data(using: sourceEncoding.nsValue) else { Self.logger.error("Failed to encode contents to \(self.sourceEncoding.debugDescription)") throw CodeFileError.failedToEncode @@ -154,7 +157,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// This function is used for decoding files. /// It should not throw error as unsupported files can still be opened by QLPreviewView. - override func read(from data: Data, ofType _: String) throws { + public override func read(from data: Data, ofType _: String) throws { var nsString: NSString? let rawEncoding = NSString.stringEncoding( for: data, @@ -206,22 +209,32 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// - Note: This is inefficient memory-wise. We could do a diff of the file and only register the /// mutations that would recreate the diff. However, that would instead be CPU intensive. /// Tradeoffs. - private func registerContentChangeUndo(fileURL: URL?, nsString: NSString, content: NSTextStorage) { + private nonisolated func registerContentChangeUndo(fileURL: URL?, nsString: NSString, content: NSTextStorage) { guard let fileURL else { return } - // If there's an undo manager, register a mutation replacing the entire contents. - let mutation = TextMutation( - string: nsString as String, - range: NSRange(location: 0, length: content.length), - limit: content.length - ) - let undoManager = delegate?.undoManager(forFile: fileURL) - undoManager?.registerMutation(mutation) + // The delegate's undo registry is main-actor isolated. Capture only Sendable primitives and build + // the (non-Sendable) `TextMutation` on the main actor so nothing non-Sendable crosses the boundary. + // Re-reads reach here on the main thread; mirror the `queue: .main` bridge used for LSP notifications. + let string = nsString as String + let length = content.length + let register: @MainActor () -> Void = { [weak self] in + let mutation = TextMutation( + string: string, + range: NSRange(location: 0, length: length), + limit: length + ) + self?.delegate?.undoManager(forFile: fileURL)?.registerMutation(mutation) + } + if Thread.isMainThread { + MainActor.assumeIsolated { register() } + } else { + DispatchQueue.main.async { register() } + } } // MARK: - Autosave /// Triggered when change occurred - override func updateChangeCount(_ change: NSDocument.ChangeType) { + public override func updateChangeCount(_ change: NSDocument.ChangeType) { super.updateChangeCount(change) if CodeFileDocument.autosavesInPlace { @@ -232,7 +245,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { } /// Triggered when changes saved - override func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) { + public override func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) { super.updateChangeCount(withToken: changeCountToken, for: saveOperation) if CodeFileDocument.autosavesInPlace { @@ -247,7 +260,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// /// All operations are done with the ``autosaveTimerLock`` acquired (including the scheduled autosave) to ensure /// correct timing when scheduling or cancelling timers. - override func scheduleAutosaving() { + public override func scheduleAutosaving() { autosaveTimerLock.withLock { if self.hasUnautosavedChanges { guard autosaveTimer == nil else { return } @@ -274,7 +287,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// we continue. /// To determine if we can reload the file, we check if the document has outstanding edits. If not, we reload the /// file. - override func presentedItemDidChange() { + public override func presentedItemDidChange() { if fileModificationDate != getModificationDate() { guard isDocumentEdited else { fileModificationDate = getModificationDate() @@ -308,14 +321,14 @@ final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - Close - override func close() { + public override func close() { super.close() if let fileURL { notifyLSPDidClose(fileURL) } } - override func save(_ sender: Any?) { + public override func save(_ sender: Any?) { guard let fileURL else { super.save(sender) return @@ -332,7 +345,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { } } - override func fileNameExtension( + public override func fileNameExtension( forType typeName: String, saveOperation: NSDocument.SaveOperationType ) -> String? { @@ -346,7 +359,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Use ``CodeFileDocument/language`` for the default value before using this. That property is used to override /// the file's language. /// - Returns: The detected code language. - func getLanguage() -> CodeLanguage { + public func getLanguage() -> CodeLanguage { guard let url = fileURL else { return .default } @@ -359,16 +372,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { } -// MARK: LanguageServerDocument - -extension CodeFileDocument: LanguageServerDocument { - /// A stable string to use when identifying documents with language servers. - /// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI. - var languageServerURI: String? { - fileURL?.lspURI - } -} - private extension CodeFileDocument { static let fileTypeExtension: [String: String?] = [ diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift similarity index 90% rename from CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift rename to Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift index 1cb7629b95..054051be4d 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocumentDelegate.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift @@ -18,7 +18,7 @@ import CodeEditTextView /// documents (`init()` → `read()` fires `documentDidOpen` before any completion handler could /// set a per-instance delegate) get correct timing without a custom `NSDocumentController`. @MainActor -protocol CodeFileDocumentDelegate: AnyObject { +public protocol CodeFileDocumentDelegate: AnyObject { /// The undo manager already registered for a file, if any (nil if none exists yet). func undoManager(forFile url: URL) -> CEUndoManager? /// The content view for a standalone single-file window (`makeWindowControllers`). @@ -32,5 +32,5 @@ protocol CodeFileDocumentDelegate: AnyObject { extension Container { /// App shell registers the real delegate in `CodeEditApp.init`; defaults to `nil` /// (no-op) so tests and pre-launch contexts are safe. - var codeFileDocumentDelegate: Factory { self { nil } } + public var codeFileDocumentDelegate: Factory { self { nil } } } diff --git a/CodeEdit/Utils/Extensions/String/String+Lines.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/String+Lines.swift similarity index 100% rename from CodeEdit/Utils/Extensions/String/String+Lines.swift rename to Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/String+Lines.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift new file mode 100644 index 0000000000..170a19b53e --- /dev/null +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift @@ -0,0 +1,16 @@ +// +// URL+AbsolutePath.swift +// CodeEditDocument +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +extension URL { + /// The non-percent-encoded absolute path. Package-internal copy of the app's helper + /// (kept private to this module to avoid a cross-module import ripple). + var absolutePath: String { + absoluteURL.path(percentEncoded: false) + } +} From bc56d1c5a21bbff167ac48263601cde1a4e2c970 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 18:44:42 +0200 Subject: [PATCH 078/335] Feature: Add WorkspaceNavigator open command interface to CodeEditCore --- .../Infrastructure/CoreContainer.swift | 4 ++++ .../Infrastructure/WorkspaceNavigator.swift | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift index 2f15354a32..635bdd98e6 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift @@ -18,4 +18,8 @@ extension Container { public var workspaceFileOpener: Factory { self { NoOpWorkspaceFileOpener() }.singleton } + + public var workspaceNavigator: Factory { + self { NoOpWorkspaceNavigator() }.singleton + } } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift new file mode 100644 index 0000000000..0ca3f1af9d --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift @@ -0,0 +1,23 @@ +// +// WorkspaceNavigator.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +/// Command interface for opening a file in the workspace that owns it. +/// One rightful handler (the app shell binds it); features request, never resolve. +public protocol WorkspaceNavigator: AnyObject { + /// Open `file` in its owning workspace's active editor. + /// - Parameter asTemporary: open as a temporary (preview) tab, replaced by the next + /// temporary open, rather than a pinned tab. + @MainActor func open(file: CEWorkspaceFile, asTemporary: Bool) +} + +/// Default no-op used until the app registers a real implementation. +public final class NoOpWorkspaceNavigator: WorkspaceNavigator { + public init() {} + @MainActor public func open(file: CEWorkspaceFile, asTemporary: Bool) {} +} From 0ed6139f427ff6944ad7bc2c9afa77efa9c19c78 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 18:49:34 +0200 Subject: [PATCH 079/335] Feature: Bind WorkspaceNavigator via AppWorkspaceNavigator and asTemporary openFileInWorkspace --- CodeEdit/CodeEditApp.swift | 1 + .../Protocols/WorkspaceWindowManaging.swift | 9 +++- .../Services/AppWorkspaceNavigator.swift | 27 ++++++++++++ .../Services/WorkspaceWindowManager.swift | 4 +- .../AppWorkspaceNavigatorTests.swift | 41 +++++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift create mode 100644 CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 2fa159e5b1..0c063c492d 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -24,6 +24,7 @@ struct CodeEditApp: App { NSSplitViewItem.swizzle() Container.shared.workspaceFileOpener.register { AppWorkspaceFileOpener() } Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } + Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } } var body: some Scene { diff --git a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift index 2680880903..a7a850a238 100644 --- a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift +++ b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift @@ -14,5 +14,12 @@ protocol WorkspaceWindowManaging: AnyObject { func openWorkspace(at url: URL) throws func closeWorkspace(_ workspace: Workspace) func workspace(containing url: URL) -> Workspace? - func openFileInWorkspace(url: URL) -> Bool + func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool +} + +extension WorkspaceWindowManaging { + /// Convenience: open non-temporarily. Keeps existing `openFileInWorkspace(url:)` call sites working. + func openFileInWorkspace(url: URL) -> Bool { + openFileInWorkspace(url: url, asTemporary: false) + } } diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift new file mode 100644 index 0000000000..2e2deb61fe --- /dev/null +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -0,0 +1,27 @@ +// +// AppWorkspaceNavigator.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import CodeEditCore +import CEWorkspaceFileManager +import Factory + +/// App-shell binding of the `WorkspaceNavigator` command interface. +/// Delegates to `WorkspaceWindowManager.openFileInWorkspace(url:asTemporary:)`, which maps the +/// file's URL to the workspace that owns it, opens the tab, and focuses that workspace. +final class AppWorkspaceNavigator: WorkspaceNavigator { + private let windowManager: WorkspaceWindowManaging + + init(windowManager: WorkspaceWindowManaging = Container.shared.workspaceWindowManager()) { + self.windowManager = windowManager + } + + @MainActor + func open(file: CEWorkspaceFile, asTemporary: Bool) { + _ = windowManager.openFileInWorkspace(url: file.url, asTemporary: asTemporary) + } +} diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index e209892fe5..93a9150a56 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -78,14 +78,14 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { /// Attempts to open a file URL in an existing workspace, finding the nearest workspace. /// Returns `true` if the file was opened in a workspace. - func openFileInWorkspace(url: URL) -> Bool { + func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool { guard !url.isFolder else { return false } for workspace in openWorkspaces.sorted(by: { ($0.fileURL?.sharedComponents(url) ?? 0) > ($1.fileURL?.sharedComponents(url) ?? 0) }) { if let newFile = workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) { - workspace.editorManager?.openTab(item: newFile) + workspace.editorManager?.openTab(item: newFile, asTemporary: asTemporary) focusWorkspace(workspace) return true } diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift new file mode 100644 index 0000000000..4fdb03d63d --- /dev/null +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -0,0 +1,41 @@ +// +// AppWorkspaceNavigatorTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Testing +import CodeEditCore +@testable import CodeEdit + +@Suite +struct AppWorkspaceNavigatorTests { + @MainActor + final class MockWindowManager: WorkspaceWindowManaging { + var opened: [(url: URL, asTemporary: Bool)] = [] + var openWorkspaces: [Workspace] = [] + func openWorkspace(at url: URL) throws {} + func closeWorkspace(_ workspace: Workspace) {} + func workspace(containing url: URL) -> Workspace? { nil } + func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool { + opened.append((url, asTemporary)) + return true + } + } + + @MainActor + @Test + func openDelegatesToWindowManagerWithTemporaryFlag() { + let mock = MockWindowManager() + let navigator = AppWorkspaceNavigator(windowManager: mock) + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/example.swift")) + + navigator.open(file: file, asTemporary: true) + + #expect(mock.opened.count == 1) + #expect(mock.opened.first?.url == file.url) + #expect(mock.opened.first?.asTemporary == true) + } +} From f04e6c754b6e3608b9c37facab336361374b8875 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 18:59:40 +0200 Subject: [PATCH 080/335] Refactor: Open files via WorkspaceNavigator from Navigator, SourceControl, and Inspector --- .../InspectorArea/FileInspector/FileInspectorView.swift | 5 +++-- .../OutlineView/ProjectNavigatorMenuActions.swift | 7 ++++--- ...jectNavigatorViewController+NSOutlineViewDelegate.swift | 3 ++- .../OutlineView/ProjectNavigatorViewController.swift | 3 ++- .../ProjectNavigator/ProjectNavigatorToolbarBottom.swift | 4 +++- .../Changes/Views/SourceControlNavigatorChangesList.swift | 4 ++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 4801aed223..2dc48a8bd4 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -8,6 +8,7 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore import CodeEditLanguages +import Factory struct FileInspectorView: View { @Environment(\.workspaceFileManager) @@ -104,7 +105,7 @@ struct FileInspectorView: View { ), !newItem.isFolder { editorManager.editorLayout.closeAllTabs(of: file) - editorManager.openTab(item: newItem) + Container.shared.workspaceNavigator().open(file: newItem, asTemporary: false) } } catch { let alert = NSAlert(error: error) @@ -152,7 +153,7 @@ struct FileInspectorView: View { return } editorManager.editorLayout.closeAllTabs(of: file) - editorManager.openTab(item: newItem) + Container.shared.workspaceNavigator().open(file: newItem, asTemporary: false) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 98d9dd9b9a..8b4b28d434 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -9,6 +9,7 @@ import AppKit import CEWorkspaceFileManager import CodeEditCore import SwiftUI +import Factory extension ProjectNavigatorMenu { /// - Returns: the currently selected `CEWorkspaceFile` items in the outline view. @@ -69,7 +70,7 @@ extension ProjectNavigatorMenu { /// Open the items in order. sortedItems.forEach { item in - workspace?.editorManager?.openTab(item: item) + Container.shared.workspaceNavigator().open(file: item, asTemporary: false) } } @@ -91,7 +92,7 @@ extension ProjectNavigatorMenu { do { if let newFile = try workspace?.workspaceFileManager?.addFile(fileName: "untitled", toFile: item) { workspace?.listenerModel.highlightedFileItem = newFile - workspace?.editorManager?.openTab(item: newFile) + Container.shared.workspaceNavigator().open(file: newFile, asTemporary: false) } } catch { let alert = NSAlert(error: error) @@ -131,7 +132,7 @@ extension ProjectNavigatorMenu { contents: clipBoardContent ) { workspace?.listenerModel.highlightedFileItem = newFile - workspace?.editorManager?.openTab(item: newFile) + Container.shared.workspaceNavigator().open(file: newFile, asTemporary: false) renameFile() } } catch { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 0755c361c8..099a7afe79 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -8,6 +8,7 @@ import AppKit import CEWorkspaceFileManager import CodeEditCore +import Factory extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView( @@ -49,7 +50,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { if !item.isFolder && shouldSendSelectionUpdate { shouldSendSelectionUpdate = false if workspace?.editorManager?.activeEditor.selectedTab?.file != item { - workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: true) + Container.shared.workspaceNavigator().open(file: item, asTemporary: true) } shouldSendSelectionUpdate = true } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index b1cc12354f..e01f9441c8 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -10,6 +10,7 @@ import CEWorkspaceFileManager import SwiftUI import OSLog import CodeEditCore +import Factory /// A `NSViewController` that handles the **ProjectNavigatorView** in the **NavigatorArea**. /// @@ -180,7 +181,7 @@ final class ProjectNavigatorViewController: NSViewController { outlineView.expandItem(item) } } else if Settings[\.navigation].navigationStyle == .openInTabs { - workspace?.editorManager?.activeEditor.openTab(file: item, asTemporary: false) + Container.shared.workspaceNavigator().open(file: item, asTemporary: false) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index cc64ea04cf..4e0a6e92c9 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -8,6 +8,8 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditUI +import CodeEditCore +import Factory struct ProjectNavigatorToolbarBottom: View { @Environment(\.controlActiveState) @@ -115,7 +117,7 @@ struct ProjectNavigatorToolbarBottom: View { toFile: rootFile ) { listenerModel.highlightedFileItem = newFile - editorManager.openTab(item: newFile) + Container.shared.workspaceNavigator().open(file: newFile, asTemporary: false) } } catch { let alert = NSAlert(error: error) diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index ff3d30bf9a..3ed92e87b1 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -9,10 +9,10 @@ import AppKit import CEWorkspaceFileManager import SwiftUI import CodeEditCore +import Factory struct SourceControlNavigatorChangesList: View { @EnvironmentObject var sourceControlManager: SourceControlManager - @EnvironmentObject var editorManager: EditorManager @Environment(\.workspaceFileManager) private var workspaceFileManager @@ -81,7 +81,7 @@ struct SourceControlNavigatorChangesList: View { return } DispatchQueue.main.async { - editorManager.openTab(item: ceFile, asTemporary: true) + Container.shared.workspaceNavigator().open(file: ceFile, asTemporary: true) } } } From 50dce91ec0416a0e465615125dee2c70e9a24c15 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 21:31:38 +0200 Subject: [PATCH 081/335] Feature: Add ActiveEditorState read-model interface to CodeEditCore --- .../Infrastructure/ActiveEditorState.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift new file mode 100644 index 0000000000..03a2029647 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift @@ -0,0 +1,26 @@ +// +// ActiveEditorState.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Combine + +/// Read-model for the workspace's currently active file. Lets StatusBar / Inspector / +/// Navigator observe "which file is active" without depending on the Editor feature's +/// `EditorManager`/`Editor`/`EditorInstance`. Workspace-scoped: one per window. +public protocol ActiveEditorState: AnyObject { + @MainActor var selectedFile: CEWorkspaceFile? { get } + @MainActor var selectedFilePublisher: AnyPublisher { get } +} + +/// Default used when no editor state is injected (tests, previews); reports no active file. +public final class NoOpActiveEditorState: ActiveEditorState { + public init() {} + public var selectedFile: CEWorkspaceFile? { nil } + public var selectedFilePublisher: AnyPublisher { + Just(nil).eraseToAnyPublisher() + } +} From c39e11a182eb4fa844afb08a2b8d66142adde2cb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 21:41:58 +0200 Subject: [PATCH 082/335] Feature: Provide AppActiveEditorState via the activeEditorState environment key --- .../CodeEditSplitViewController.swift | 8 ++++ .../Editor/Models/AppActiveEditorState.swift | 30 +++++++++++++++ .../Models/Environment+Workspace.swift | 10 +++++ .../Editor/AppActiveEditorStateTests.swift | 38 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 CodeEdit/Features/Editor/Models/AppActiveEditorState.swift create mode 100644 CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index c31f52036e..3a160f76e5 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -21,6 +21,9 @@ final class CodeEditSplitViewController: NSSplitViewController { private weak var statePersistence: (any WorkspaceStatePersisting)? private unowned var hapticPerformer: NSHapticFeedbackPerformer + /// Per-window active-file read-model, retained so its Combine subscription lives with the window. + private var activeEditorState: AppActiveEditorState? + // MARK: - Initialization init( @@ -67,6 +70,9 @@ final class CodeEditSplitViewController: NSSplitViewController { splitView.translatesAutoresizingMaskIntoConstraints = false + let activeEditorState = AppActiveEditorState(editorManager: editorManager) + self.activeEditorState = activeEditorState + let navigator = makeNavigator(view: SettingsInjector { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) @@ -78,6 +84,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(searchState) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) + .environment(\.activeEditorState, activeEditorState) }) addSplitViewItem(navigator) @@ -97,6 +104,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.workspaceStatePersistence, workspace.statePersistence) + .environment(\.activeEditorState, activeEditorState) } } diff --git a/CodeEdit/Features/Editor/Models/AppActiveEditorState.swift b/CodeEdit/Features/Editor/Models/AppActiveEditorState.swift new file mode 100644 index 0000000000..893fad386d --- /dev/null +++ b/CodeEdit/Features/Editor/Models/AppActiveEditorState.swift @@ -0,0 +1,30 @@ +// +// AppActiveEditorState.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Combine +import CodeEditCore +import CEWorkspaceFileManager + +/// App-side `ActiveEditorState` over a window's `EditorManager`. Emits the active editor's +/// selected file across both active-editor switches and within-editor tab changes. +final class AppActiveEditorState: ActiveEditorState { + private let subject: CurrentValueSubject + private var cancellable: AnyCancellable? + + @MainActor + init(editorManager: EditorManager) { + subject = CurrentValueSubject(editorManager.activeEditor.selectedTab?.file) + cancellable = editorManager.$activeEditor + .flatMap { $0.$selectedTab } + .map { $0?.file } + .sink { [weak subject] file in subject?.send(file) } + } + + var selectedFile: CEWorkspaceFile? { subject.value } + var selectedFilePublisher: AnyPublisher { subject.eraseToAnyPublisher() } +} diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index 05829d84f6..60c176aac2 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -7,11 +7,16 @@ import SwiftUI import CEWorkspaceFileManager +import CodeEditCore private struct WorkspaceFileManagerKey: EnvironmentKey { static let defaultValue: CEWorkspaceFileManager? = nil } +private struct ActiveEditorStateKey: EnvironmentKey { + static let defaultValue: ActiveEditorState = NoOpActiveEditorState() +} + private struct WorkspaceFileURLKey: EnvironmentKey { static let defaultValue: URL? = nil } @@ -35,4 +40,9 @@ extension EnvironmentValues { get { self[WorkspaceStatePersistenceKey.self] } set { self[WorkspaceStatePersistenceKey.self] = newValue } } + + var activeEditorState: ActiveEditorState { + get { self[ActiveEditorStateKey.self] } + set { self[ActiveEditorStateKey.self] = newValue } + } } diff --git a/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift b/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift new file mode 100644 index 0000000000..6af3aef184 --- /dev/null +++ b/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift @@ -0,0 +1,38 @@ +// +// AppActiveEditorStateTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Combine +import Testing +import CodeEditCore +import CEWorkspaceFileManager +@testable import CodeEdit + +@Suite +struct AppActiveEditorStateTests { + @MainActor + @Test + func reflectsAndPublishesActiveFile() { + let editorManager = EditorManager() + let fileA = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/a.swift")) + editorManager.activeEditor.openTab(file: fileA) + + let state = AppActiveEditorState(editorManager: editorManager) + #expect(state.selectedFile?.url == fileA.url) + + var received: [URL?] = [] + let cancellable = state.selectedFilePublisher.sink { received.append($0?.url) } + + let fileB = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/b.swift")) + editorManager.activeEditor.openTab(file: fileB) + cancellable.cancel() + + // Current value replayed on subscribe (fileA) then fileB after the switch. + #expect(received.first == fileA.url) + #expect(received.last == fileB.url) + } +} From 77373e47cc155af69632fa9246c9763108c90922 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 21:48:40 +0200 Subject: [PATCH 083/335] Refactor: Observe active file via ActiveEditorState in StatusBar, Inspector, and Navigator --- .../FileInspector/FileInspectorView.swift | 12 ++++-------- .../HistoryInspectorView.swift | 18 ++++-------------- .../Views/InspectorAreaView.swift | 1 - .../ProjectNavigatorToolbarBottom.swift | 10 +++++----- .../ViewModifiers/UpdateStatusBarInfo.swift | 9 +++++---- 5 files changed, 18 insertions(+), 32 deletions(-) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 2dc48a8bd4..17d179998e 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -16,6 +16,8 @@ struct FileInspectorView: View { @EnvironmentObject private var editorManager: EditorManager + @Environment(\.activeEditorState) private var activeEditorState + @AppSettings(\.textEditing) private var textEditing @@ -42,7 +44,7 @@ struct FileInspectorView: View { } func updateInspectorSource() { - file = editorManager.activeEditor.selectedTab?.file + file = activeEditorState.selectedFile fileName = file?.name ?? "" language = file.flatMap { editorManager.document(for: $0) }?.language updateFileOptions() @@ -72,13 +74,7 @@ struct FileInspectorView: View { .onAppear { updateInspectorSource() } - .onReceive(editorManager.activeEditor.objectWillChange) { _ in - updateInspectorSource() - } - .onChange(of: editorManager.activeEditor) { _, _ in - updateInspectorSource() - } - .onChange(of: editorManager.activeEditor.selectedTab) { _, _ in + .onReceive(activeEditorState.selectedFilePublisher) { _ in updateInspectorSource() } .onChange(of: textEditing) { _, newValue in diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 2d4e022026..29ce156644 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -14,7 +14,7 @@ struct HistoryInspectorView: View { @EnvironmentObject private var sourceControlManager: SourceControlManager - @EnvironmentObject private var editorManager: EditorManager + @Environment(\.activeEditorState) private var activeEditorState @ObservedObject private var model: HistoryInspectorModel @@ -46,24 +46,14 @@ struct HistoryInspectorView: View { NoSelectionInspectorView() } } - .onReceive(editorManager.activeEditor.objectWillChange) { _ in + .onReceive(activeEditorState.selectedFilePublisher) { file in Task { - await model.setFile(url: editorManager.activeEditor.selectedTab?.file.url.path()) - } - } - .onChange(of: editorManager.activeEditor) { _, _ in - Task { - await model.setFile(url: editorManager.activeEditor.selectedTab?.file.url.path()) - } - } - .onChange(of: editorManager.activeEditor.selectedTab) { _, _ in - Task { - await model.setFile(url: editorManager.activeEditor.selectedTab?.file.url.path()) + await model.setFile(url: file?.url.path()) } } .task { await model.setWorkspace(sourceControlManager: sourceControlManager) - await model.setFile(url: editorManager.activeEditor.selectedTab?.file.url.path) + await model.setFile(url: activeEditorState.selectedFile?.url.path()) } .onChange(of: showMergeCommitsPerFileLog) { _, _ in Task { diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index 62f98ca616..14da61c0e4 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -8,7 +8,6 @@ import SwiftUI struct InspectorAreaView: View { - @EnvironmentObject private var editorManager: EditorManager @ObservedObject private var extensionManager = ExtensionManager.shared @ObservedObject public var viewModel: InspectorAreaViewModel diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 4e0a6e92c9..6d6a78c57c 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -18,7 +18,7 @@ struct ProjectNavigatorToolbarBottom: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject var editorManager: EditorManager + @Environment(\.activeEditorState) private var activeEditorState @EnvironmentObject var listenerModel: WorkspaceNotificationModel @EnvironmentObject var projectNavigatorViewModel: ProjectNavigatorViewModel @@ -85,14 +85,14 @@ struct ProjectNavigatorToolbarBottom: View { /// Retrieves the active tab URL from the underlying editor instance, if theres no /// active tab, fallbacks to the workspace's root directory private func activeTabURL() -> URL { - if let selectedTab = editorManager.activeEditor.selectedTab { - if selectedTab.file.isFolder { - return selectedTab.file.url + if let file = activeEditorState.selectedFile { + if file.isFolder { + return file.url } // If the current active tab belongs to a file, pop the filename from // the path URL to retrieve the folder URL - let activeTabFileURL = selectedTab.file.url + let activeTabFileURL = file.url if URLComponents(url: activeTabFileURL, resolvingAgainstBaseURL: false) != nil { var pathComponents = activeTabFileURL.pathComponents diff --git a/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift b/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift index 271141b721..b56c11ae5c 100644 --- a/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift +++ b/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore /// Updates ``StatusBarFileInfoView``'s `fileSize` and `dimensions`. /// ```swift @@ -21,7 +22,7 @@ struct UpdateStatusBarInfo: ViewModifier { self.fileURL = fileURL } - @EnvironmentObject private var editorManager: EditorManager + @Environment(\.activeEditorState) private var activeEditorState @EnvironmentObject private var statusBarViewModel: StatusBarViewModel /// This is returned by ``UpdateStatusBarInfo`` `.computeStatusBarInfo`. @@ -57,9 +58,9 @@ struct UpdateStatusBarInfo: ViewModifier { statusBarViewModel.fileSize = statusBarInfo?.fileSize statusBarViewModel.dimensions = statusBarInfo?.dimensions } - .onChange(of: editorManager.activeEditor.selectedTab) { _, newTab in - guard let newTab else { return } - let statusBarInfo = computeStatusBarInfo(with: newTab.file.url) + .onReceive(activeEditorState.selectedFilePublisher) { newFile in + guard let newFile else { return } + let statusBarInfo = computeStatusBarInfo(with: newFile.url) statusBarViewModel.fileSize = statusBarInfo?.fileSize statusBarViewModel.dimensions = statusBarInfo?.dimensions } From f64a714d8ea96e4c29652ac145f3deee39935aef Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 21:58:25 +0200 Subject: [PATCH 084/335] Refactor: Drive Project Navigator selection from ActiveEditorState --- .../ProjectNavigatorOutlineView.swift | 26 ++++++++++++++----- ...ViewController+NSOutlineViewDelegate.swift | 4 +-- .../ProjectNavigatorViewController.swift | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index d79e6ce5d1..41321b8d53 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -16,6 +16,8 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @EnvironmentObject var workspace: Workspace @EnvironmentObject var editorManager: EditorManager + @Environment(\.activeEditorState) private var activeEditorState + @StateObject var prefs: Settings = .shared typealias NSViewControllerType = ProjectNavigatorViewController @@ -25,9 +27,11 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { controller.workspace = workspace controller.iconColor = prefs.preferences.general.fileIconStyle controller.editor = editorManager.activeEditor + controller.activeEditorState = activeEditorState workspace.workspaceFileManager?.addObserver(context.coordinator) context.coordinator.controller = controller + context.coordinator.observeActiveFile(activeEditorState) return controller } @@ -39,7 +43,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { nsViewController.shownFileExtensions = prefs.preferences.general.shownFileExtensions nsViewController.hiddenFileExtensions = prefs.preferences.general.hiddenFileExtensions /// if the window becomes active from background, it will restore the selection to outline view. - nsViewController.updateSelection(itemID: workspace.editorManager?.activeEditor.selectedTab?.file.id) + nsViewController.updateSelection(itemID: activeEditorState.selectedFile?.id) return } @@ -62,11 +66,6 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { self?.controller?.reveal(fileItem) }) .store(in: &cancellables) - workspace.editorManager?.tabBarTabIdSubject - .sink { [weak self] editorInstance in - self?.controller?.updateSelection(itemID: editorInstance?.file.id) - } - .store(in: &cancellables) if let projectNavigatorViewModel = workspace.projectNavigatorViewModel { projectNavigatorViewModel.$navigatorFilter .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) @@ -87,10 +86,25 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } var cancellables: Set = [] + private var selectionCancellable: AnyCancellable? weak var workspace: Workspace? weak var fileManager: CEWorkspaceFileManager? weak var controller: ProjectNavigatorViewController? + /// Subscribe to the active-file read-model so the outline highlights the active file. + /// Wired from `makeNSViewController`, where the `@Environment` value is reliably populated. + func observeActiveFile(_ state: ActiveEditorState) { + // React to *changes* only. `selectedFilePublisher` is a `CurrentValueSubject` that + // replays the current value on subscribe; skip it so we don't call `updateSelection` + // (which touches the IUO `outlineView`) during `makeNSViewController`, before the view + // has loaded. The initial selection is set by `updateNSViewController`. + selectionCancellable = state.selectedFilePublisher + .dropFirst() + .sink { [weak self] file in + self?.controller?.updateSelection(itemID: file?.id) + } + } + func fileManagerUpdated(updatedItems: Set) { guard let outlineView = controller?.outlineView else { return } let selectedRows = outlineView.selectedRowIndexes.compactMap({ outlineView.item(atRow: $0) }) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 099a7afe79..f3fbf99514 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -49,7 +49,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { if !item.isFolder && shouldSendSelectionUpdate { shouldSendSelectionUpdate = false - if workspace?.editorManager?.activeEditor.selectedTab?.file != item { + if activeEditorState?.selectedFile != item { Container.shared.workspaceNavigator().open(file: item, asTemporary: true) } shouldSendSelectionUpdate = true @@ -68,7 +68,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { expandedItems.insert(item) } - guard let id = workspace.editorManager?.activeEditor.selectedTab?.file.id, + guard let id = activeEditorState?.selectedFile?.id, let item = workspace.workspaceFileManager?.getFile(id, createIfNotFound: true), /// update outline selection only if the parent of selected item match with expanded item item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index e01f9441c8..e784fa2665 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -40,6 +40,7 @@ final class ProjectNavigatorViewController: NSViewController { weak var workspace: Workspace? weak var editor: Editor? + weak var activeEditorState: (any ActiveEditorState)? var iconColor: SettingsData.FileIconStyle = .color { willSet { @@ -150,7 +151,7 @@ final class ProjectNavigatorViewController: NSViewController { /// Forces to reveal the selected file through the command regardless of the auto reveal setting @objc func revealFile(_ sender: Any) { - updateSelection(itemID: workspace?.editorManager?.activeEditor.selectedTab?.file.id, forcesReveal: true) + updateSelection(itemID: activeEditorState?.selectedFile?.id, forcesReveal: true) } /// Updates the selection of the ``outlineView`` whenever it changes. From 1216f47a91733cb9aa25024d3d82a7c8f86caeab Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 23:06:15 +0200 Subject: [PATCH 085/335] Fix: Apply per-file editor overrides (indent, tab width, wrap) from the document --- CodeEdit/Features/Editor/Views/CodeFileView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index a12cd367cc..fb6f4036e1 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -132,14 +132,14 @@ struct CodeFileView: View { font: font, lineHeightMultiple: lineHeightMultiple, letterSpacing: letterSpacing, - wrapLines: wrapLinesToEditorWidth, + wrapLines: codeFile.wrapLines ?? wrapLinesToEditorWidth, useSystemCursor: useSystemCursor, - tabWidth: defaultTabWidth, + tabWidth: codeFile.defaultTabWidth ?? defaultTabWidth, bracketPairEmphasis: getBracketPairEmphasis() ), behavior: .init( isEditable: isEditable, - indentOption: indentOption.textViewOption(), + indentOption: (codeFile.indentOption ?? indentOption).textViewOption(), reformatAtColumn: reformatAtColumn ), layout: .init( From 481ef3262283a400d949aa8ace586de3f7cb65b9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 7 Jul 2026 23:09:43 +0200 Subject: [PATCH 086/335] Fix: Inject activeEditorState into the inspector subtree so File Inspector sees the active file --- .../Documents/Controllers/CodeEditSplitViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 3a160f76e5..2ed653e871 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -119,6 +119,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(editorManager) .environmentObject(sourceControlManager) .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.activeEditorState, activeEditorState) }) addSplitViewItem(inspector) From f5122c6fde3619f0226079f417a84df1b58a9ad9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 11:31:00 +0200 Subject: [PATCH 087/335] Add ActiveCursorState Core abstraction and EditorCursorPosition value type --- .../Domain/Editor/EditorCursorPosition.swift | 25 +++++++++++++++ .../Infrastructure/ActiveCursorState.swift | 31 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift new file mode 100644 index 0000000000..d5fb81b163 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift @@ -0,0 +1,25 @@ +// +// EditorCursorPosition.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +/// The subset of an editor cursor/selection that the status bar renders, mirrored +/// as a plain value type so `CodeEditCore` need not depend on `CodeEditSourceEditor`. +public struct EditorCursorPosition: Sendable, Equatable { + /// 1-indexed line at the start of the selection (from `CursorPosition.start.line`). + public let line: Int + /// 1-indexed column at the start of the selection (from `CursorPosition.start.column`). + public let column: Int + /// The selection range in the document. + public let range: NSRange + + public init(line: Int, column: Int, range: NSRange) { + self.line = line + self.column = column + self.range = range + } +} diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift new file mode 100644 index 0000000000..87a5143b5a --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift @@ -0,0 +1,31 @@ +// +// ActiveCursorState.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Combine + +/// Read-model for the currently active editor's cursor/selection. Lets the status +/// bar render cursor position without depending on the Editor feature's +/// `EditorManager` / `EditorInstance` or the `CodeEditSourceEditor` widget. +/// Workspace-scoped: one per window. +public protocol ActiveCursorState: AnyObject { + @MainActor var cursorPositions: [EditorCursorPosition] { get } + @MainActor var cursorPositionsPublisher: AnyPublisher<[EditorCursorPosition], Never> { get } + /// Number of lines contained by `range` in the active editor's live text view. + /// Returns 0 when there is no active editor or the lines cannot be resolved. + @MainActor func linesInRange(_ range: NSRange) -> Int +} + +/// Default used when no cursor state is injected (tests, previews); reports no cursor. +public final class NoOpActiveCursorState: ActiveCursorState { + public init() {} + public var cursorPositions: [EditorCursorPosition] { [] } + public var cursorPositionsPublisher: AnyPublisher<[EditorCursorPosition], Never> { + Just([]).eraseToAnyPublisher() + } + public func linesInRange(_ range: NSRange) -> Int { 0 } +} From e14f52d57d4ae1c241faa61087ed93bd733a8e6d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 11:32:48 +0200 Subject: [PATCH 088/335] Add AppActiveCursorState adapter bridging EditorManager to ActiveCursorState --- .../Editor/Models/AppActiveCursorState.swift | 63 +++++++++++++++++++ .../Editor/AppActiveCursorStateTests.swift | 58 +++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 CodeEdit/Features/Editor/Models/AppActiveCursorState.swift create mode 100644 CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift diff --git a/CodeEdit/Features/Editor/Models/AppActiveCursorState.swift b/CodeEdit/Features/Editor/Models/AppActiveCursorState.swift new file mode 100644 index 0000000000..eeae1fc134 --- /dev/null +++ b/CodeEdit/Features/Editor/Models/AppActiveCursorState.swift @@ -0,0 +1,63 @@ +// +// AppActiveCursorState.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Combine +import CodeEditCore +import CodeEditSourceEditor + +/// App-side `ActiveCursorState` over a window's `EditorManager`. Publishes the +/// active editor's cursor positions across both active-editor switches and +/// within-editor tab changes, and forwards `linesInRange(_:)` to the live +/// `EditorInstance.rangeTranslator`. +final class AppActiveCursorState: ActiveCursorState { + private let subject: CurrentValueSubject<[EditorCursorPosition], Never> + private weak var currentTab: EditorInstance? + private var editorCancellable: AnyCancellable? + private var cursorCancellable: AnyCancellable? + + @MainActor + init(editorManager: EditorManager) { + let initialTab = editorManager.activeEditor.selectedTab + currentTab = initialTab + subject = CurrentValueSubject(Self.map(initialTab?.cursorPositions ?? [])) + + editorCancellable = editorManager.$activeEditor + .flatMap { $0.$selectedTab } + .sink { [weak self] tab in + self?.bind(to: tab) + } + } + + @MainActor + private func bind(to tab: EditorInstance?) { + currentTab = tab + guard let tab else { + cursorCancellable = nil + subject.send([]) + return + } + cursorCancellable = tab.$cursorPositions + .sink { [weak subject] positions in + subject?.send(Self.map(positions)) + } + } + + private static func map(_ positions: [CursorPosition]) -> [EditorCursorPosition] { + positions.map { EditorCursorPosition(line: $0.start.line, column: $0.start.column, range: $0.range) } + } + + var cursorPositions: [EditorCursorPosition] { subject.value } + + var cursorPositionsPublisher: AnyPublisher<[EditorCursorPosition], Never> { + subject.eraseToAnyPublisher() + } + + func linesInRange(_ range: NSRange) -> Int { + currentTab?.rangeTranslator.linesInRange(range) ?? 0 + } +} diff --git a/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift b/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift new file mode 100644 index 0000000000..3006c87ba3 --- /dev/null +++ b/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift @@ -0,0 +1,58 @@ +// +// AppActiveCursorStateTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Combine +import Testing +import CodeEditCore +import CEWorkspaceFileManager +import CodeEditSourceEditor +@testable import CodeEdit + +@Suite +struct AppActiveCursorStateTests { + @MainActor + @Test + func reflectsAndPublishesActiveTabCursorPositions() { + let editorManager = EditorManager() + let fileA = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/a.swift")) + editorManager.activeEditor.openTab(file: fileA) + + let state = AppActiveCursorState(editorManager: editorManager) + + // The freshly opened tab seeds a default cursor at line 1, column 1. + #expect(state.cursorPositions.first?.line == 1) + #expect(state.cursorPositions.first?.column == 1) + + var received: [[EditorCursorPosition]] = [] + let cancellable = state.cursorPositionsPublisher.sink { received.append($0) } + + // Drive a new cursor position on the active tab. + let tab = editorManager.activeEditor.selectedTab + tab?.cursorPositions = [CursorPosition(line: 5, column: 3)] + cancellable.cancel() + + #expect(received.last?.first?.line == 5) + #expect(received.last?.first?.column == 3) + } + + @MainActor + @Test + func mapsRangeAndForwardsLinesInRangeSafelyWithoutTextView() { + let editorManager = EditorManager() + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/a.swift")) + editorManager.activeEditor.openTab(file: file) + + let state = AppActiveCursorState(editorManager: editorManager) + let tab = editorManager.activeEditor.selectedTab + tab?.cursorPositions = [CursorPosition(range: NSRange(location: 10, length: 4))] + + #expect(state.cursorPositions.first?.range == NSRange(location: 10, length: 4)) + // No live text view is attached in a unit test, so the forwarded query returns 0. + #expect(state.linesInRange(NSRange(location: 10, length: 4)) == 0) + } +} From 363a4d2098a153a916295b1735cb4f24bc58b270 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 11:33:42 +0200 Subject: [PATCH 089/335] Inject activeCursorState into the workspace subtree via env key --- .../Controllers/CodeEditSplitViewController.swift | 7 +++++++ .../Workspace/Models/Environment+Workspace.swift | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 2ed653e871..02e19f5bab 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -24,6 +24,9 @@ final class CodeEditSplitViewController: NSSplitViewController { /// Per-window active-file read-model, retained so its Combine subscription lives with the window. private var activeEditorState: AppActiveEditorState? + /// Per-window active-cursor read-model, retained so its Combine subscription lives with the window. + private var activeCursorState: AppActiveCursorState? + // MARK: - Initialization init( @@ -73,6 +76,9 @@ final class CodeEditSplitViewController: NSSplitViewController { let activeEditorState = AppActiveEditorState(editorManager: editorManager) self.activeEditorState = activeEditorState + let activeCursorState = AppActiveCursorState(editorManager: editorManager) + self.activeCursorState = activeCursorState + let navigator = makeNavigator(view: SettingsInjector { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) @@ -105,6 +111,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.workspaceStatePersistence, workspace.statePersistence) .environment(\.activeEditorState, activeEditorState) + .environment(\.activeCursorState, activeCursorState) } } diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index 60c176aac2..6c677dac8c 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -17,6 +17,10 @@ private struct ActiveEditorStateKey: EnvironmentKey { static let defaultValue: ActiveEditorState = NoOpActiveEditorState() } +private struct ActiveCursorStateKey: EnvironmentKey { + static let defaultValue: ActiveCursorState = NoOpActiveCursorState() +} + private struct WorkspaceFileURLKey: EnvironmentKey { static let defaultValue: URL? = nil } @@ -45,4 +49,9 @@ extension EnvironmentValues { get { self[ActiveEditorStateKey.self] } set { self[ActiveEditorStateKey.self] = newValue } } + + var activeCursorState: ActiveCursorState { + get { self[ActiveCursorStateKey.self] } + set { self[ActiveCursorStateKey.self] = newValue } + } } From 81ef05ba5b9b546d7029bb29b09e6555c965f4a4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 11:34:22 +0200 Subject: [PATCH 090/335] Rewrite StatusBarCursorPositionLabel onto the ActiveCursorState abstraction --- .../StatusBarCursorPositionLabel.swift | 134 +++++++----------- 1 file changed, 54 insertions(+), 80 deletions(-) diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift index 6939fca4ec..42b9d7321e 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift @@ -6,116 +6,90 @@ // import SwiftUI -import Combine -import CodeEditSourceEditor +import CodeEditCore struct StatusBarCursorPositionLabel: View { - @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel - @EnvironmentObject private var editorManager: EditorManager + @Environment(\.activeCursorState) + private var activeCursorState - @State private var tab: EditorInstance? + @Environment(\.modifierKeys) + private var modifierKeys + @Environment(\.controlActiveState) + private var controlActive - /// Updates the source of cursor position notifications. - func updateSource() { - tab = editorManager.activeEditor.selectedTab - } + @EnvironmentObject private var statusBarViewModel: StatusBarViewModel + + @State private var cursorPositions: [EditorCursorPosition] = [] var body: some View { Group { - if let currentTab = tab { - LineLabel(editorInstance: currentTab) - } else { + if cursorPositions.isEmpty { Text("").accessibilityLabel("No Selection") + } else { + Text(getLabel()) + .font(statusBarViewModel.statusBarFont) + .foregroundColor(foregroundColor) + .lineLimit(1) } } .fixedSize() .accessibilityIdentifier("CursorPositionLabel") .accessibilityAddTraits(.updatesFrequently) .onHover { isHovering($0) } - .onAppear { - updateSource() - } - .onReceive(editorManager.tabBarTabIdSubject) { _ in - updateSource() + .onReceive(activeCursorState.cursorPositionsPublisher) { newValue in + self.cursorPositions = newValue } } - struct LineLabel: View { - @Environment(\.modifierKeys) - private var modifierKeys - @Environment(\.controlActiveState) - private var controlActive - - @EnvironmentObject private var statusBarViewModel: StatusBarViewModel - - let editorInstance: EditorInstance - - @State private var cursorPositions: [CursorPosition] = [] - - init(editorInstance: EditorInstance) { - self.editorInstance = editorInstance + private var foregroundColor: Color { + if controlActive == .inactive { + Color(nsColor: .disabledControlTextColor) + } else { + Color(nsColor: .secondaryLabelColor) } + } - var body: some View { - Text(getLabel()) - .font(statusBarViewModel.statusBarFont) - .foregroundColor(foregroundColor) - .lineLimit(1) - .onReceive(editorInstance.$cursorPositions) { newValue in - self.cursorPositions = newValue - } - } + /// Finds the number of lines contained by a range in the currently active document. + /// - Parameter range: The range to query. + /// - Returns: The number of lines in the range. + private func getLines(_ range: NSRange) -> Int { + activeCursorState.linesInRange(range) + } - private var foregroundColor: Color { - if controlActive == .inactive { - Color(nsColor: .disabledControlTextColor) - } else { - Color(nsColor: .secondaryLabelColor) - } + /// Create a label string for cursor positions. + /// - Returns: A string describing the user's location in a document. + private func getLabel() -> String { + if cursorPositions.isEmpty { + return "" } - /// Finds the lines contained by a range in the currently selected document. - /// - Parameter range: The range to query. - /// - Returns: The number of lines in the range. - func getLines(_ range: NSRange) -> Int { - return editorInstance.rangeTranslator.linesInRange(range) + // More than one selection, display the number of selections. + if cursorPositions.count > 1 { + return "\(cursorPositions.count) selected ranges" } - /// Create a label string for cursor positions. - /// - Returns: A string describing the user's location in a document. - func getLabel() -> String { - if cursorPositions.isEmpty { - return "" - } - - // More than one selection, display the number of selections. - if cursorPositions.count > 1 { - return "\(cursorPositions.count) selected ranges" + // If the selection is more than just a cursor, return the length. + if cursorPositions[0].range.length > 0 { + // When the option key is pressed display the character range. + if modifierKeys.contains(.option) { + return "Char: \(cursorPositions[0].range.location) Len: \(cursorPositions[0].range.length)" } - // If the selection is more than just a cursor, return the length. - if cursorPositions[0].range.length > 0 { - // When the option key is pressed display the character range. - if modifierKeys.contains(.option) { - return "Char: \(cursorPositions[0].range.location) Len: \(cursorPositions[0].range.length)" - } + let lineCount = getLines(cursorPositions[0].range) - let lineCount = getLines(cursorPositions[0].range) - - if lineCount > 1 { - return "\(lineCount) lines" - } - - return "\(cursorPositions[0].range.length) characters" + if lineCount > 1 { + return "\(lineCount) lines" } - // When the option key is pressed display the character offset. - if modifierKeys.contains(.option) { - return "Char: \(cursorPositions[0].range.location) Len: 0" - } + return "\(cursorPositions[0].range.length) characters" + } - // When there's a single cursor, display the line and column. - return "Line: \(cursorPositions[0].start.line) Col: \(cursorPositions[0].start.column)" + // When the option key is pressed display the character offset. + if modifierKeys.contains(.option) { + return "Char: \(cursorPositions[0].range.location) Len: 0" } + + // When there's a single cursor, display the line and column. + return "Line: \(cursorPositions[0].line) Col: \(cursorPositions[0].column)" } } From d5bd27c8803d126068c3cddecaaecaf4a1b29e50 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:49:10 +0200 Subject: [PATCH 091/335] Add FileEditorOverrides Core seam and FileEditorOverrideValues --- .../Editor/FileEditorOverrideValues.swift | 31 +++++++++++++++++++ .../Infrastructure/FileEditorOverrides.swift | 30 ++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift new file mode 100644 index 0000000000..f667633045 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift @@ -0,0 +1,31 @@ +// +// FileEditorOverrideValues.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +/// Per-file editor setting overrides, as plain values so `CodeEditCore` need not +/// name `CodeFileDocument` (which depends on Core) or `CodeLanguage` (heavy dep). +/// A `nil` field means "no override — use the global setting"; the language +/// override is carried as `CodeLanguage.id.rawValue`. +public struct FileEditorOverrideValues: Sendable, Equatable { + public var indentOption: IndentOption? + public var defaultTabWidth: Int? + public var wrapLines: Bool? + public var languageId: String? + + public init( + indentOption: IndentOption? = nil, + defaultTabWidth: Int? = nil, + wrapLines: Bool? = nil, + languageId: String? = nil + ) { + self.indentOption = indentOption + self.defaultTabWidth = defaultTabWidth + self.wrapLines = wrapLines + self.languageId = languageId + } +} diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift new file mode 100644 index 0000000000..c6082b4938 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift @@ -0,0 +1,30 @@ +// +// FileEditorOverrides.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +/// Read/write seam for a file's editor setting overrides. Lets the File Inspector +/// edit per-file overrides without depending on the Editor feature's +/// `EditorManager` / `CodeFileDocument`. Workspace-scoped: one per window. +public protocol FileEditorOverrides: AnyObject { + /// Current overrides for `file`; all fields `nil` when the file has no open document. + @MainActor func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues + @MainActor func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) + @MainActor func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) + @MainActor func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) + @MainActor func setLanguageId(_ value: String?, for file: CEWorkspaceFile) +} + +/// Default used when no editor is injected (tests, previews); no overrides, no-op writes. +public final class NoOpFileEditorOverrides: FileEditorOverrides { + public init() {} + public func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues { .init() } + public func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) {} + public func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) {} + public func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) {} + public func setLanguageId(_ value: String?, for file: CEWorkspaceFile) {} +} From 37292a1ff21297638c75b4c12cf3ac54328f17c7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:50:48 +0200 Subject: [PATCH 092/335] Add AppFileEditorOverrides adapter bridging EditorManager document overrides --- .../Models/AppFileEditorOverrides.swift | 56 ++++++++++++ .../Editor/AppFileEditorOverridesTests.swift | 85 +++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift create mode 100644 CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift diff --git a/CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift b/CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift new file mode 100644 index 0000000000..26eac9b8e6 --- /dev/null +++ b/CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift @@ -0,0 +1,56 @@ +// +// AppFileEditorOverrides.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import CodeEditCore +import CodeEditLanguages +import CodeEditDocument + +/// App-side `FileEditorOverrides` over a window's `EditorManager`. Reads and writes +/// the per-file overrides on the file's open `CodeFileDocument`, translating the +/// language override to/from `CodeLanguage.id.rawValue`. +final class AppFileEditorOverrides: FileEditorOverrides { + private let editorManager: EditorManager + + @MainActor + init(editorManager: EditorManager) { + self.editorManager = editorManager + } + + @MainActor + func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues { + let document = editorManager.document(for: file) + return FileEditorOverrideValues( + indentOption: document?.indentOption, + defaultTabWidth: document?.defaultTabWidth, + wrapLines: document?.wrapLines, + languageId: document?.language?.id.rawValue + ) + } + + @MainActor + func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) { + editorManager.document(for: file)?.indentOption = value + } + + @MainActor + func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) { + editorManager.document(for: file)?.defaultTabWidth = value + } + + @MainActor + func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) { + editorManager.document(for: file)?.wrapLines = value + } + + @MainActor + func setLanguageId(_ value: String?, for file: CEWorkspaceFile) { + editorManager.document(for: file)?.language = value.flatMap { id in + CodeLanguage.allLanguages.first { $0.id.rawValue == id } + } + } +} diff --git a/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift b/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift new file mode 100644 index 0000000000..dbb4a4697e --- /dev/null +++ b/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift @@ -0,0 +1,85 @@ +// +// AppFileEditorOverridesTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Testing +import CodeEditCore +import CEWorkspaceFileManager +import CodeEditDocument +import CodeEditLanguages +@testable import CodeEdit + +@Suite +struct AppFileEditorOverridesTests { + @MainActor + @Test + func readsAndWritesDocumentOverrides() { + let editorManager = EditorManager() + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/a.swift")) + let document = CodeFileDocument() + editorManager.documents.setDocument(document, for: file) + + let overrides = AppFileEditorOverrides(editorManager: editorManager) + + // No overrides set yet. + #expect(overrides.overrides(for: file).indentOption == nil) + + // Writes propagate to the document. + let indent = IndentOption(indentType: .spaces, spaceCount: 8) + overrides.setIndentOption(indent, for: file) + overrides.setDefaultTabWidth(3, for: file) + overrides.setWrapLines(true, for: file) + #expect(document.indentOption == indent) + #expect(document.defaultTabWidth == 3) + #expect(document.wrapLines == true) + + // Reads reflect the document. + let values = overrides.overrides(for: file) + #expect(values.indentOption == indent) + #expect(values.defaultTabWidth == 3) + #expect(values.wrapLines == true) + + _ = document // registry holds documents weakly; keep alive for the test + } + + @MainActor + @Test + func languageIdRoundTrips() { + let editorManager = EditorManager() + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/a.swift")) + let document = CodeFileDocument() + editorManager.documents.setDocument(document, for: file) + let overrides = AppFileEditorOverrides(editorManager: editorManager) + + guard let swift = CodeLanguage.allLanguages.first(where: { $0.id.rawValue == "swift" }) else { + Issue.record("swift language not found in allLanguages") + return + } + + overrides.setLanguageId(swift.id.rawValue, for: file) + #expect(document.language?.id.rawValue == "swift") + #expect(overrides.overrides(for: file).languageId == "swift") + + overrides.setLanguageId(nil, for: file) + #expect(document.language == nil) + #expect(overrides.overrides(for: file).languageId == nil) + + _ = document + } + + @MainActor + @Test + func noDocumentYieldsEmptyOverridesAndNoOpWrites() { + let editorManager = EditorManager() + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/none.swift")) + let overrides = AppFileEditorOverrides(editorManager: editorManager) + + #expect(overrides.overrides(for: file) == FileEditorOverrideValues()) + overrides.setIndentOption(IndentOption(indentType: .tab), for: file) // no crash + #expect(overrides.overrides(for: file).indentOption == nil) + } +} From 734923d0071d8008362633318d40807dc8896c81 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:51:45 +0200 Subject: [PATCH 093/335] Inject fileEditorOverrides into the inspector subtree via env key --- .../Controllers/CodeEditSplitViewController.swift | 7 +++++++ .../Workspace/Models/Environment+Workspace.swift | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 02e19f5bab..fade182dd5 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -27,6 +27,9 @@ final class CodeEditSplitViewController: NSSplitViewController { /// Per-window active-cursor read-model, retained so its Combine subscription lives with the window. private var activeCursorState: AppActiveCursorState? + /// Per-window file-override read/write seam, retained for the window's lifetime. + private var fileEditorOverrides: AppFileEditorOverrides? + // MARK: - Initialization init( @@ -79,6 +82,9 @@ final class CodeEditSplitViewController: NSSplitViewController { let activeCursorState = AppActiveCursorState(editorManager: editorManager) self.activeCursorState = activeCursorState + let fileEditorOverrides = AppFileEditorOverrides(editorManager: editorManager) + self.fileEditorOverrides = fileEditorOverrides + let navigator = makeNavigator(view: SettingsInjector { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) @@ -127,6 +133,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(sourceControlManager) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.activeEditorState, activeEditorState) + .environment(\.fileEditorOverrides, fileEditorOverrides) }) addSplitViewItem(inspector) diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index 6c677dac8c..546b0eb528 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -21,6 +21,10 @@ private struct ActiveCursorStateKey: EnvironmentKey { static let defaultValue: ActiveCursorState = NoOpActiveCursorState() } +private struct FileEditorOverridesKey: EnvironmentKey { + static let defaultValue: FileEditorOverrides = NoOpFileEditorOverrides() +} + private struct WorkspaceFileURLKey: EnvironmentKey { static let defaultValue: URL? = nil } @@ -54,4 +58,9 @@ extension EnvironmentValues { get { self[ActiveCursorStateKey.self] } set { self[ActiveCursorStateKey.self] = newValue } } + + var fileEditorOverrides: FileEditorOverrides { + get { self[FileEditorOverridesKey.self] } + set { self[FileEditorOverridesKey.self] = newValue } + } } From 84427c13d2b2859bf73865689d74b30337a79c91 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:52:56 +0200 Subject: [PATCH 094/335] Route File Inspector per-file overrides through the FileEditorOverrides seam --- .../FileInspector/FileInspectorView.swift | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 17d179998e..477f39a852 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -18,6 +18,8 @@ struct FileInspectorView: View { @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.fileEditorOverrides) private var fileEditorOverrides + @AppSettings(\.textEditing) private var textEditing @@ -27,7 +29,7 @@ struct FileInspectorView: View { // File settings overrides - @State private var language: CodeLanguage? + @State private var languageId: String? @State var indentOption: SettingsData.TextEditingSettings.IndentOption = .init(indentType: .tab) @@ -37,16 +39,16 @@ struct FileInspectorView: View { func updateFileOptions(_ textEditingOverride: SettingsData.TextEditingSettings? = nil) { let textEditingSettings = textEditingOverride ?? textEditing - let document = file.flatMap { editorManager.document(for: $0) } - indentOption = document?.indentOption ?? textEditingSettings.indentOption - defaultTabWidth = document?.defaultTabWidth ?? textEditingSettings.defaultTabWidth - wrapLines = document?.wrapLines ?? textEditingSettings.wrapLinesToEditorWidth + let values = file.map { fileEditorOverrides.overrides(for: $0) } + indentOption = values?.indentOption ?? textEditingSettings.indentOption + defaultTabWidth = values?.defaultTabWidth ?? textEditingSettings.defaultTabWidth + wrapLines = values?.wrapLines ?? textEditingSettings.wrapLinesToEditorWidth } func updateInspectorSource() { file = activeEditorState.selectedFile fileName = file?.name ?? "" - language = file.flatMap { editorManager.document(for: $0) }?.language + languageId = file.flatMap { fileEditorOverrides.overrides(for: $0).languageId } updateFileOptions() } @@ -119,16 +121,18 @@ struct FileInspectorView: View { @ViewBuilder private var fileType: some View { Picker( "Type", - selection: $language + selection: $languageId ) { - Text("Default - Detected").tag(nil as CodeLanguage?) + Text("Default - Detected").tag(nil as String?) Divider() ForEach(CodeLanguage.allLanguages, id: \.id) { language in - Text(language.id.rawValue.capitalized).tag(language as CodeLanguage?) + Text(language.id.rawValue.capitalized).tag(language.id.rawValue as String?) } } - .onChange(of: language) { _, newValue in - file.flatMap { editorManager.document(for: $0) }?.language = newValue + .onChange(of: languageId) { _, newValue in + if let file { + fileEditorOverrides.setLanguageId(newValue, for: file) + } } } @@ -173,8 +177,9 @@ struct FileInspectorView: View { Text("Tabs").tag(SettingsData.TextEditingSettings.IndentOption.IndentType.tab) } .onChange(of: indentOption) { _, newValue in - file.flatMap { editorManager.document(for: $0) }?.indentOption = - newValue == textEditing.indentOption ? nil : newValue + if let file { + fileEditorOverrides.setIndentOption(newValue == textEditing.indentOption ? nil : newValue, for: file) + } } } @@ -218,16 +223,18 @@ struct FileInspectorView: View { } } .onChange(of: defaultTabWidth) { _, newValue in - file.flatMap { editorManager.document(for: $0) }?.defaultTabWidth = - newValue == textEditing.defaultTabWidth ? nil : newValue + if let file { + fileEditorOverrides.setDefaultTabWidth(newValue == textEditing.defaultTabWidth ? nil : newValue, for: file) + } } } private var wrapLinesToggle: some View { Toggle("Wrap lines", isOn: $wrapLines) .onChange(of: wrapLines) { _, newValue in - file.flatMap { editorManager.document(for: $0) }?.wrapLines = - newValue == textEditing.wrapLinesToEditorWidth ? nil : newValue + if let file { + fileEditorOverrides.setWrapLines(newValue == textEditing.wrapLinesToEditorWidth ? nil : newValue, for: file) + } } } From f35da29f749189c6c3e0100ce0670512c6d6b5b4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:53:42 +0200 Subject: [PATCH 095/335] Add FileRelocator Core command and Factory key --- .../Infrastructure/CoreContainer.swift | 4 ++++ .../Infrastructure/FileRelocator.swift | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift index 635bdd98e6..b80a89d1a8 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift @@ -22,4 +22,8 @@ extension Container { public var workspaceNavigator: Factory { self { NoOpWorkspaceNavigator() }.singleton } + + public var fileRelocator: Factory { + self { NoOpFileRelocator() }.singleton + } } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift new file mode 100644 index 0000000000..9fcb4e88fa --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift @@ -0,0 +1,22 @@ +// +// FileRelocator.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +/// Command to move a file within its workspace and reconcile open editor tabs +/// (close tabs for the old location, reopen at the new one). Lets the File +/// Inspector rename/relocate without depending on the Editor feature or holding +/// a `Workspace`. Returns the resolved new file (nil for folders or unresolved workspace). +public protocol FileRelocator: AnyObject { + @MainActor func relocate(file: CEWorkspaceFile, to destination: URL) throws -> CEWorkspaceFile? +} + +/// Default used when no app shell is present (tests, previews); performs no move. +public final class NoOpFileRelocator: FileRelocator { + public init() {} + public func relocate(file: CEWorkspaceFile, to destination: URL) throws -> CEWorkspaceFile? { nil } +} From 2b618f031c0d30acfcd7ee1cb9c480407d4b29fc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:55:14 +0200 Subject: [PATCH 096/335] Add AppFileRelocator adapter backed by MoveFileUseCase and register it --- CodeEdit/CodeEditApp.swift | 1 + .../Workspace/Services/AppFileRelocator.swift | 28 +++++++++++++ .../Workspace/AppFileRelocatorTests.swift | 40 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 CodeEdit/Features/Workspace/Services/AppFileRelocator.swift create mode 100644 CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 0c063c492d..6d61be0dbc 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -25,6 +25,7 @@ struct CodeEditApp: App { Container.shared.workspaceFileOpener.register { AppWorkspaceFileOpener() } Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } + Container.shared.fileRelocator.register { AppFileRelocator() } } var body: some Scene { diff --git a/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift b/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift new file mode 100644 index 0000000000..6721820621 --- /dev/null +++ b/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift @@ -0,0 +1,28 @@ +// +// AppFileRelocator.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import CodeEditCore +import CEWorkspaceFileManager +import Factory + +/// App-shell binding of the `FileRelocator` command. Resolves the workspace that +/// owns the file and delegates to `MoveFileUseCase`, which moves the file and +/// reconciles open tabs. +final class AppFileRelocator: FileRelocator { + private let windowManager: WorkspaceWindowManaging + + init(windowManager: WorkspaceWindowManaging = Container.shared.workspaceWindowManager()) { + self.windowManager = windowManager + } + + @MainActor + func relocate(file: CEWorkspaceFile, to destination: URL) throws -> CEWorkspaceFile? { + guard let workspace = windowManager.workspace(containing: file.url) else { return nil } + return try MoveFileUseCase().execute(file: file, to: destination, in: workspace) + } +} diff --git a/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift b/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift new file mode 100644 index 0000000000..63a07e8884 --- /dev/null +++ b/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift @@ -0,0 +1,40 @@ +// +// AppFileRelocatorTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Testing +import CodeEditCore +@testable import CodeEdit + +@Suite +struct AppFileRelocatorTests { + @MainActor + final class MockWindowManager: WorkspaceWindowManaging { + var queried: [URL] = [] + var openWorkspaces: [Workspace] = [] + func openWorkspace(at url: URL) throws {} + func closeWorkspace(_ workspace: Workspace) {} + func workspace(containing url: URL) -> Workspace? { + queried.append(url) + return nil + } + func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool { false } + } + + @MainActor + @Test + func returnsNilAndQueriesByURLWhenWorkspaceUnresolved() throws { + let mock = MockWindowManager() + let relocator = AppFileRelocator(windowManager: mock) + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/a.swift")) + + let result = try relocator.relocate(file: file, to: URL(fileURLWithPath: "/tmp/b.swift")) + + #expect(result == nil) + #expect(mock.queried == [file.url]) + } +} From 8b0b75a680339158eaec042a2a1738cebdbafff2 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 15:56:56 +0200 Subject: [PATCH 097/335] Route File Inspector rename/relocate through FileRelocator and drop EditorManager dependency --- .../FileInspector/FileInspectorView.swift | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 477f39a852..c0616bca7e 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -5,17 +5,11 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI -import CEWorkspaceFileManager import CodeEditCore import CodeEditLanguages import Factory struct FileInspectorView: View { - @Environment(\.workspaceFileManager) - private var workspaceFileManager - - @EnvironmentObject private var editorManager: EditorManager - @Environment(\.activeEditorState) private var activeEditorState @Environment(\.fileEditorOverrides) private var fileEditorOverrides @@ -95,16 +89,9 @@ struct FileInspectorView: View { let destinationURL = file.url .deletingLastPathComponent() .appending(path: fileName) - DispatchQueue.main.async { [weak workspaceFileManager] in + DispatchQueue.main.async { do { - if let newItem = try workspaceFileManager?.move( - file: file, - to: destinationURL - ), - !newItem.isFolder { - editorManager.editorLayout.closeAllTabs(of: file) - Container.shared.workspaceNavigator().open(file: newItem, asTemporary: false) - } + _ = try Container.shared.fileRelocator().relocate(file: file, to: destinationURL) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") @@ -146,14 +133,9 @@ struct FileInspectorView: View { } // This is ugly but if the tab is opened at the same time as closing the others, it doesn't open // And if the files are re-built at the same time as the tab is opened, it causes a memory error - DispatchQueue.main.async { [weak workspaceFileManager] in + DispatchQueue.main.async { do { - guard let newItem = try workspaceFileManager?.move(file: file, to: newURL), - !newItem.isFolder else { - return - } - editorManager.editorLayout.closeAllTabs(of: file) - Container.shared.workspaceNavigator().open(file: newItem, asTemporary: false) + _ = try Container.shared.fileRelocator().relocate(file: file, to: newURL) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") From 52ad052a6f5cafcd5c6cf5155e74e046e22dc02b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 23:01:51 +0200 Subject: [PATCH 098/335] Refactor: Lift text-editing command registration out of settings decode --- CodeEdit/CodeEditApp.swift | 1 + .../SettingsData+CommandRegistration.swift | 52 ++++++++++++++++++ .../Models/TextEditingSettings.swift | 55 +------------------ .../Domain/Search/FuzzySearchModels.swift | 15 +++-- 4 files changed, 65 insertions(+), 58 deletions(-) create mode 100644 CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 6d61be0dbc..74ec8ce1a7 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -26,6 +26,7 @@ struct CodeEditApp: App { Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } Container.shared.fileRelocator.register { AppFileRelocator() } + SettingsData.TextEditingSettings.registerCommands() } var body: some Scene { diff --git a/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift b/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift new file mode 100644 index 0000000000..587e4cf27f --- /dev/null +++ b/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift @@ -0,0 +1,52 @@ +// +// SettingsData+CommandRegistration.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Factory + +extension SettingsData.TextEditingSettings { + /// Registers toggle-able text-editing preferences with the command palette. + /// Invoked once at app startup (previously ran as a side effect of decoding). + static func registerCommands() { + let mgr = Container.shared.commandManager() + + mgr.addCommand( + name: "Toggle Type-Over Completion", + title: "Toggle Type-Over Completion", + id: "prefs.text_editing.type_over_completion" + ) { + Settings[\.textEditing].enableTypeOverCompletion.toggle() + } + mgr.addCommand( + name: "Toggle Autocomplete Braces", + title: "Toggle Autocomplete Braces", + id: "prefs.text_editing.autocomplete_braces" + ) { + Settings[\.textEditing].autocompleteBraces.toggle() + } + mgr.addCommand( + name: "Toggle Word Wrap", + title: "Toggle Word Wrap", + id: "prefs.text_editing.wrap_lines_to_editor_width" + ) { + Settings[\.textEditing].wrapLinesToEditorWidth.toggle() + } + mgr.addCommand(name: "Toggle Minimap", title: "Toggle Minimap", id: "prefs.text_editing.toggle_minimap") { + Settings[\.textEditing].showMinimap.toggle() + } + mgr.addCommand(name: "Toggle Gutter", title: "Toggle Gutter", id: "prefs.text_editing.toggle_gutter") { + Settings[\.textEditing].showGutter.toggle() + } + mgr.addCommand( + name: "Toggle Folding Ribbon", + title: "Toggle Folding Ribbon", + id: "prefs.text_editing.toggle_folding_ribbon" + ) { + Settings[\.textEditing].showFoldingRibbon.toggle() + } + } +} diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift index 7fdecb7cd1..3062a5a465 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift @@ -7,7 +7,6 @@ import AppKit import CodeEditCore -import Factory import Foundation extension SettingsData { @@ -99,9 +98,7 @@ extension SettingsData { var warningCharacters: WarningCharacters = .default /// Default initializer - init() { - self.populateCommands() - } + init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` init(from decoder: Decoder) throws { // swiftlint:disable:this function_body_length @@ -162,56 +159,6 @@ extension SettingsData { WarningCharacters.self, forKey: .warningCharacters ) ?? .default - - self.populateCommands() - } - - /// Adds toggle-able preferences to the command palette via shared `CommandManager` - private func populateCommands() { - let mgr = Container.shared.commandManager() - - mgr.addCommand( - name: "Toggle Type-Over Completion", - title: "Toggle Type-Over Completion", - id: "prefs.text_editing.type_over_completion", - command: { - Settings[\.textEditing].enableTypeOverCompletion.toggle() - } - ) - - mgr.addCommand( - name: "Toggle Autocomplete Braces", - title: "Toggle Autocomplete Braces", - id: "prefs.text_editing.autocomplete_braces", - command: { - Settings[\.textEditing].autocompleteBraces.toggle() - } - ) - - mgr.addCommand( - name: "Toggle Word Wrap", - title: "Toggle Word Wrap", - id: "prefs.text_editing.wrap_lines_to_editor_width", - command: { - Settings[\.textEditing].wrapLinesToEditorWidth.toggle() - } - ) - - mgr.addCommand(name: "Toggle Minimap", title: "Toggle Minimap", id: "prefs.text_editing.toggle_minimap") { - Settings[\.textEditing].showMinimap.toggle() - } - - mgr.addCommand(name: "Toggle Gutter", title: "Toggle Gutter", id: "prefs.text_editing.toggle_gutter") { - Settings[\.textEditing].showGutter.toggle() - } - - mgr.addCommand( - name: "Toggle Folding Ribbon", - title: "Toggle Folding Ribbon", - id: "prefs.text_editing.toggle_folding_ribbon" - ) { - Settings[\.textEditing].showFoldingRibbon.toggle() - } } /// Re-exported from `CodeEditCore`. Keeps `SettingsData.TextEditingSettings.IndentOption` diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift index 27d2ef3887..c69cc6e8a2 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift @@ -7,32 +7,39 @@ import Foundation -/// FuzzySearchCharacters is used to normalise strings +/// A single character in a fuzzy search string, storing both original and normalised forms. public struct FuzzySearchCharacter { + /// The original character content. public let content: String - // normalised content is referring to a string that is case- and accent-insensitive + /// The case- and accent-insensitive form of ``content``. public let normalisedContent: String + /// Creates a ``FuzzySearchCharacter`` with the given original and normalised content. public init(content: String, normalisedContent: String) { self.content = content self.normalisedContent = normalisedContent } } -/// FuzzySearchString is just made up by multiple characters, similar to a string, but also with normalised characters +/// A sequence of ``FuzzySearchCharacter`` values representing a string prepared for fuzzy matching. public struct FuzzySearchString { + /// The individual characters that make up this string. public var characters: [FuzzySearchCharacter] + /// Creates a ``FuzzySearchString`` from an array of ``FuzzySearchCharacter`` values. public init(characters: [FuzzySearchCharacter]) { self.characters = characters } } -/// FuzzySearchMatchResult represents an object that has undergone a fuzzy search using the fuzzyMatch function. +/// The result of a fuzzy match operation, containing a relevance weight and the matched ranges. public struct FuzzySearchMatchResult { + /// A score indicating how closely the input matched; higher values mean a better match. public let weight: Int + /// The ranges within the original string that were matched. public let matchedParts: [NSRange] + /// Creates a ``FuzzySearchMatchResult`` with the given weight and matched ranges. public init(weight: Int, matchedParts: [NSRange]) { self.weight = weight self.matchedParts = matchedParts From cfbf911bb8aab3af03da7f7206e9f1d374ce31bd Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 23:02:48 +0200 Subject: [PATCH 099/335] Refactor: Lift keybinding default-seeding into an app-startup reconcile --- CodeEdit/CodeEditApp.swift | 1 + .../SettingsData+KeybindingReconcile.swift | 24 +++++++++++++++++++ .../Models/KeybindingsSettings.swift | 19 +++------------ 3 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 74ec8ce1a7..6af9ec8d2d 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -27,6 +27,7 @@ struct CodeEditApp: App { Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } Container.shared.fileRelocator.register { AppFileRelocator() } SettingsData.TextEditingSettings.registerCommands() + SettingsData.reconcileDefaultKeybindings() } var body: some Scene { diff --git a/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift b/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift new file mode 100644 index 0000000000..3ed50ed8d4 --- /dev/null +++ b/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift @@ -0,0 +1,24 @@ +// +// SettingsData+KeybindingReconcile.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import Factory + +extension SettingsData { + /// Merges bundled-default keybindings (from `default_keybindings.json`, owned by + /// `KeybindingManager`) into the persisted settings, adding only keys the user does + /// not already have. Preserves user overrides. Invoked once at app startup — + /// previously ran as a side effect of decoding `KeybindingsSettings`. + static func reconcileDefaultKeybindings() { + let defaults = Container.shared.keybindingManager().keyboardShortcuts + var current = Settings.shared.preferences.keybindings.keybindings + for (key, _) in defaults where current[key] == nil { + current[key] = Container.shared.keybindingManager().named(with: key) + } + Settings.shared.preferences.keybindings.keybindings = current + } +} diff --git a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift index ddeba525f0..95ea214c60 100644 --- a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift @@ -6,7 +6,6 @@ // import Foundation -import Factory extension SettingsData { @@ -16,10 +15,9 @@ extension SettingsData { /// An integer indicating how many spaces a `tab` will generate var keybindings: [String: KeyboardShortcutWrapper] = .init() - /// Default initializer - init() { - self.keybindings = Container.shared.keybindingManager().keyboardShortcuts - } + /// Default initializer — empty; bundled defaults are seeded by the app at + /// startup via `SettingsData.reconcileDefaultKeybindings()`. + init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` init(from decoder: Decoder) throws { @@ -28,17 +26,6 @@ extension SettingsData { [String: KeyboardShortcutWrapper].self, forKey: .keybindings ) ?? .init() - appendNew() - } - - /// Adds new keybindings if they were added to default_keybindings.json. - /// To ensure users will get new keybindings with new app version releases - private mutating func appendNew() { - let newKeybindings = Container.shared.keybindingManager() - .keyboardShortcuts.filter { !keybindings.keys.contains($0.key) } - for keybinding in newKeybindings { - self.keybindings[keybinding.key] = Container.shared.keybindingManager().named(with: keybinding.key) - } } } } From c4b5bcea9361678638eca2c8c41f13d224f8ef06 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 23:06:52 +0200 Subject: [PATCH 100/335] Refactor: Move settings search keys and propertiesOf to app-side extensions --- .../Models}/SearchableSettingsPage.swift | 0 .../Settings/Models/SettingsData+Search.swift | 229 ++++++++++++++++++ .../Settings/Models/SettingsData.swift | 39 --- .../Models/AccountsSettings.swift | 12 +- .../Models/DeveloperSettings.swift | 13 +- .../Models/LanguageServerSettings.swift | 15 +- .../Models/GeneralSettings.swift | 32 +-- .../Models/NavigationSettings.swift | 10 +- .../Models/SearchSettings.swift | 11 +- .../Models/SourceControlSettings.swift | 24 +- .../Models/TerminalSettings.swift | 16 +- .../Models/TextEditingSettings.swift | 30 +-- .../ThemeSettings/Models/ThemeSettings.swift | 19 +- 13 files changed, 239 insertions(+), 211 deletions(-) rename CodeEdit/{Utils/Protocols => Features/Settings/Models}/SearchableSettingsPage.swift (100%) create mode 100644 CodeEdit/Features/Settings/Models/SettingsData+Search.swift diff --git a/CodeEdit/Utils/Protocols/SearchableSettingsPage.swift b/CodeEdit/Features/Settings/Models/SearchableSettingsPage.swift similarity index 100% rename from CodeEdit/Utils/Protocols/SearchableSettingsPage.swift rename to CodeEdit/Features/Settings/Models/SearchableSettingsPage.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsData+Search.swift b/CodeEdit/Features/Settings/Models/SettingsData+Search.swift new file mode 100644 index 0000000000..0926100237 --- /dev/null +++ b/CodeEdit/Features/Settings/Models/SettingsData+Search.swift @@ -0,0 +1,229 @@ +// +// SettingsData+Search.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +// App-side settings-search support. `searchKeys` are localized UI labels for the +// settings search feature (presentation, not preference data), so they live in the +// app rather than the CodeEditSettings package. One conformance extension per +// persisted settings page, plus `propertiesOf`. + +extension SettingsData.GeneralSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Appearance", + "File Icon Style", + "Tab Bar Style", + "Show Jump Bar", + "Dim editors without focus", + "Navigator Tab Bar Position", + "Inspector Tab Bar Position", + "Show Issues", + "Show Live Issues", + "Automatically save change to disk", + "Automatically reveal in project navigator", + "Reopen Behavior", + "After the last window is closed", + "File Extensions", + "Project Navigator Size", + "Find Navigator Detail", + "Issue Navigator Detail", + "Show “Open With CodeEdit“ option in Finder", + "'codeedit' Shell command", + "Dialog Warnings", + "Check for updates", + "Automatically check for app updates", + "Include pre-release versions" + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.AccountsSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Accounts", + "Delete Account...", + "Add Account..." + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.NavigationSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Navigation Style", + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.ThemeSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Automatically Change theme based on system appearance", + "Always use dark terminal appearance", + "Use theme background", + "Light Appearance", + "GitHub Light", + "Xcode Light", + "Solarized Light", + "Solarized Dark", + "Midnight", + "Xcode Dark", + "GitHub Dark" + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.TextEditingSettings: SearchableSettingsPage { + var searchKeys: [String] { + var keys = [ + "Prefer Indent Using", + "Tab Width", + "Wrap lines to editor width", + "Editor Overscroll", + "Font", + "Font Size", + "Font Weight", + "Line Height", + "Letter Spacing", + "Autocomplete braces", + "Enable type-over completion", + "Bracket Pair Emphasis", + "Bracket Pair Highlight", + "Show Gutter", + "Show Minimap", + "Reformat at Column", + "Show Reformatting Guide", + "Invisibles", + "Warning Characters" + ] + if #available(macOS 14.0, *) { + keys.append("System Cursor") + } + return keys.map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.TerminalSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Shell", + "Use \"Option\" key as \"Meta\"", + "Use text editor font", + "Font", + "Font Size", + "Terminal Cursor Style", + "Blink Cursor" + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.SourceControlSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "General", + "Enable source control", + "Refresh local status automatically", + "Fetch and refresh server status automatically", + "Add and remove files automatically", + "Select files to commit automatically", + "Show source control changes", + "Include upstream changes", + "Comparison view", + "Source control navigator", + "Default branch name", + "Git", + "Author Name", + "Author Email", + "Prefer to rebase when pulling", + "Show merge commits in per-file log" + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.SearchSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Ignore Glob Patterns", + "Ignore Patterns" + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.LanguageServerSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Language Servers", + "LSP Binaries", + "Linters", + "Formatters", + "Debug Protocol", + "DAP", + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData.DeveloperSettings: SearchableSettingsPage { + var searchKeys: [String] { + [ + "Developer", + "Language Server Protocol", + "LSP Binaries", + "Show Internal Development Inspector" + ] + .map { NSLocalizedString($0, comment: "") } + } +} + +extension SettingsData { + // swiftlint:disable cyclomatic_complexity + func propertiesOf(_ name: SettingsPage.Name) -> [SettingsPage] { + var settings: [SettingsPage] = [] + + switch name { + case .general: + general.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .accounts: + accounts.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .navigation: + navigation.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .theme: + theme.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .textEditing: + textEditing.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .terminal: + terminal.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .search: + search.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + case .sourceControl: + 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 .languageServers: + LanguageServerSettings().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")] + case .advanced: return [.init(name, settingName: "Error")] + } + + return settings + } + // swiftlint:enable cyclomatic_complexity +} diff --git a/CodeEdit/Features/Settings/Models/SettingsData.swift b/CodeEdit/Features/Settings/Models/SettingsData.swift index cd860c7e43..e842f71059 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData.swift @@ -84,43 +84,4 @@ struct SettingsData: Codable, Hashable { DeveloperSettings.self, forKey: .developerSettings ) ?? .init() } - - // swiftlint:disable cyclomatic_complexity - func propertiesOf(_ name: SettingsPage.Name) -> [SettingsPage] { - var settings: [SettingsPage] = [] - - switch name { - case .general: - general.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .accounts: - accounts.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .navigation: - navigation.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .theme: - theme.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .textEditing: - textEditing.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .terminal: - terminal.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .search: - search.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } - case .sourceControl: - 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 .languageServers: - LanguageServerSettings().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")] - case .advanced: return [.init(name, settingName: "Error")] - } - - return settings - } - // swiftlint:enable cyclomatic_complexity } diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift index 18ff529e6d..c3a9fddb92 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift @@ -10,20 +10,10 @@ import Foundation extension SettingsData { /// The global settings for source control accounts - struct AccountsSettings: Codable, Hashable, SearchableSettingsPage { + struct AccountsSettings: Codable, Hashable { /// The list of git accounts the user has saved @CodableDefault var sourceControlAccounts: GitAccounts = .init() - /// The search keys - var searchKeys: [String] { - [ - "Accounts", - "Delete Account...", - "Add Account..." - ] - .map { NSLocalizedString($0, comment: "") } - } - /// Default initializer init() {} } diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift index 2b6eb9d645..f7aa80242f 100644 --- a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift @@ -8,18 +8,7 @@ import Foundation extension SettingsData { - struct DeveloperSettings: Codable, Hashable, SearchableSettingsPage { - - /// The search keys - var searchKeys: [String] { - [ - "Developer", - "Language Server Protocol", - "LSP Binaries", - "Show Internal Development Inspector" - ] - .map { NSLocalizedString($0, comment: "") } - } + struct DeveloperSettings: Codable, Hashable { /// A dictionary that stores a file type and a path to an LSP binary @CodableDefault var lspBinaries: [String: String] = [:] diff --git a/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift b/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift index 60a0765309..d58747383a 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift @@ -8,20 +8,7 @@ import Foundation extension SettingsData { - struct LanguageServerSettings: Codable, Hashable, SearchableSettingsPage { - - /// The search keys - var searchKeys: [String] { - [ - "Language Servers", - "LSP Binaries", - "Linters", - "Formatters", - "Debug Protocol", - "DAP", - ] - .map { NSLocalizedString($0, comment: "") } - } + struct LanguageServerSettings: Codable, Hashable { /// Stores the currently installed language servers. The key is the name of the language server. @CodableDefault diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift b/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift index 1e67727763..37a781fd36 100644 --- a/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift +++ b/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift @@ -10,7 +10,7 @@ import SwiftUI extension SettingsData { /// The general global setting - struct GeneralSettings: Codable, Hashable, SearchableSettingsPage { + struct GeneralSettings: Codable, Hashable { /// The appearance of the app @CodableDefault var appAppearance: Appearances = .system @@ -21,36 +21,6 @@ extension SettingsData { /// The show live issues behavior of the app @CodableDefault var showLiveIssues = true - /// The search keys - var searchKeys: [String] { - [ - "Appearance", - "File Icon Style", - "Tab Bar Style", - "Show Jump Bar", - "Dim editors without focus", - "Navigator Tab Bar Position", - "Inspector Tab Bar Position", - "Show Issues", - "Show Live Issues", - "Automatically save change to disk", - "Automatically reveal in project navigator", - "Reopen Behavior", - "After the last window is closed", - "File Extensions", - "Project Navigator Size", - "Find Navigator Detail", - "Issue Navigator Detail", - "Show “Open With CodeEdit“ option in Finder", - "'codeedit' Shell command", - "Dialog Warnings", - "Check for updates", - "Automatically check for app updates", - "Include pre-release versions" - ] - .map { NSLocalizedString($0, comment: "") } - } - /// Show editor jump bar @CodableDefault var showEditorJumpBar = true diff --git a/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift b/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift index 55a4f39e71..fdb6eb7d6e 100644 --- a/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift +++ b/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift @@ -10,15 +10,7 @@ import Foundation extension SettingsData { /// The global settings for the terminal emulator - struct NavigationSettings: Codable, Hashable, SearchableSettingsPage { - - /// The search keys - var searchKeys: [String] { - [ - "Navigation Style", - ] - .map { NSLocalizedString($0, comment: "") } - } + struct NavigationSettings: Codable, Hashable { /// Navigation style used @CodableDefault var navigationStyle: NavigationStyle = .openInTabs diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift index cc93d0bb1d..e4de7ee75d 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift +++ b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift @@ -8,16 +8,7 @@ import Foundation extension SettingsData { - struct SearchSettings: Codable, Hashable, SearchableSettingsPage { - - /// The search keys - var searchKeys: [String] { - [ - "Ignore Glob Patterns", - "Ignore Patterns" - ] - .map { NSLocalizedString($0, comment: "") } - } + struct SearchSettings: Codable, Hashable { /// List of Glob Patterns that determine which files or directories to ignore @CodableDefault var ignoreGlobPatterns: [GlobPattern] = [] diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift index 309bdf0294..982acf8d72 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift @@ -9,29 +9,7 @@ import Foundation extension SettingsData { /// The global settings for source control - struct SourceControlSettings: Codable, Hashable, SearchableSettingsPage { - - var searchKeys: [String] { - [ - "General", - "Enable source control", - "Refresh local status automatically", - "Fetch and refresh server status automatically", - "Add and remove files automatically", - "Select files to commit automatically", - "Show source control changes", - "Include upstream changes", - "Comparison view", - "Source control navigator", - "Default branch name", - "Git", - "Author Name", - "Author Email", - "Prefer to rebase when pulling", - "Show merge commits in per-file log" - ] - .map { NSLocalizedString($0, comment: "") } - } + struct SourceControlSettings: Codable, Hashable { /// The general source control settings var general: SourceControlGeneral = .init() diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift index d0844f03c6..df17b4103a 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift @@ -11,21 +11,7 @@ import Foundation extension SettingsData { /// The global settings for the terminal emulator - struct TerminalSettings: Codable, Hashable, SearchableSettingsPage { - - /// The search keys - var searchKeys: [String] { - [ - "Shell", - "Use \"Option\" key as \"Meta\"", - "Use text editor font", - "Font", - "Font Size", - "Terminal Cursor Style", - "Blink Cursor" - ] - .map { NSLocalizedString($0, comment: "") } - } + struct TerminalSettings: Codable, Hashable { /// If true terminal will use editor theme. @CodableDefault var useEditorTheme = true diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift index 3062a5a465..6ed9e68011 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift @@ -12,35 +12,7 @@ import Foundation extension SettingsData { /// The global settings for text editing - struct TextEditingSettings: Codable, Hashable, SearchableSettingsPage { - - var searchKeys: [String] { - var keys = [ - "Prefer Indent Using", - "Tab Width", - "Wrap lines to editor width", - "Editor Overscroll", - "Font", - "Font Size", - "Font Weight", - "Line Height", - "Letter Spacing", - "Autocomplete braces", - "Enable type-over completion", - "Bracket Pair Emphasis", - "Bracket Pair Highlight", - "Show Gutter", - "Show Minimap", - "Reformat at Column", - "Show Reformatting Guide", - "Invisibles", - "Warning Characters" - ] - if #available(macOS 14.0, *) { - keys.append("System Cursor") - } - return keys.map { NSLocalizedString($0, comment: "") } - } + struct TextEditingSettings: Codable, Hashable { /// An integer indicating how many spaces a `tab` will appear as visually. var defaultTabWidth: Int = 4 diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift index b37f986be3..2cb52f7b67 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift @@ -30,24 +30,7 @@ extension SettingsData { typealias ThemeOverrides = [String: [String: Theme.Attributes]] /// The global settings for themes - struct ThemeSettings: Codable, Hashable, SearchableSettingsPage { - - var searchKeys: [String] { - [ - "Automatically Change theme based on system appearance", - "Always use dark terminal appearance", - "Use theme background", - "Light Appearance", - "GitHub Light", - "Xcode Light", - "Solarized Light", - "Solarized Dark", - "Midnight", - "Xcode Dark", - "GitHub Dark" - ] - .map { NSLocalizedString($0, comment: "") } - } + struct ThemeSettings: Codable, Hashable { /// The name of the currently selected dark theme var selectedDarkTheme: String = "Default (Dark)" From 28fc0a4a1d562e03c6db13a61bfd5cbbbcf827bc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 8 Jul 2026 23:50:10 +0200 Subject: [PATCH 101/335] Chore: Scaffold and wire empty CodeEditSettings Foundation package --- CodeEdit.xcodeproj/project.pbxproj | 7 ++++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 +++ .../Foundation/CodeEditSettings/Package.swift | 25 +++++++++++++++++++ .../CodeEditSettings/Placeholder.swift | 8 ++++++ 4 files changed, 43 insertions(+) create mode 100644 Packages/Foundation/CodeEditSettings/Package.swift create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Placeholder.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 996a6f4519..d26a69ffbb 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; + 5AD0C0DE2D00000000000011 /* CodeEditSettings in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000012 /* CodeEditSettings */; }; 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5E4485602DF600D9008BBE69 /* AboutWindow */; }; 5EACE6222DF4BF08005E08B8 /* WelcomeWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 5EACE6212DF4BF08005E08B8 /* WelcomeWindow */; }; 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0617D52BDB4432008C9C42 /* LogStream */; }; @@ -184,6 +185,7 @@ 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */, 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */, + 5AD0C0DE2D00000000000011 /* CodeEditSettings in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, @@ -357,6 +359,7 @@ 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, 5800E2F72FF843390085ECF1 /* CodeEditUI */, 5AD0C0DE2D00000000000002 /* CodeEditDocument */, + 5AD0C0DE2D00000000000012 /* CodeEditSettings */, 588950C42FFA5C05004BE116 /* Search */, 588957122FFA679E004BE116 /* CodeEditServices */, 5889639D2FFA9A87004BE116 /* Notifications */, @@ -1946,6 +1949,10 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditDocument; }; + 5AD0C0DE2D00000000000012 /* CodeEditSettings */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditSettings; + }; 5E4485602DF600D9008BBE69 /* AboutWindow */ = { isa = XCSwiftPackageProductDependency; package = 5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index cb690a0af4..9e4ef2ffd7 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -16,6 +16,9 @@ + + Date: Thu, 9 Jul 2026 09:59:39 +0200 Subject: [PATCH 102/335] Refactor: Move settings leaf value types into CodeEditSettings package --- .../Features/Editor/Views/CodeFileView.swift | 1 + .../Keybindings/KeybindingManager.swift | 57 +------- .../Protocols/KeybindingManaging.swift | 1 + .../Models/CodableDefault+Providers.swift | 1 + .../Models/KeybindingsSettings.swift | 1 + .../Models/SearchSettings.swift | 1 + .../Models/SearchSettingsModel.swift | 1 + .../Models/IgnorePatternModel.swift | 1 + .../Models/TerminalSettings.swift | 1 + .../Models/TextEditingSettings.swift | 1 + .../ThemeSettings/Models/Theme+Color.swift | 1 + .../Models/Theme+FuzzySearchable.swift | 1 + .../Models/ThemeModel+CRUD.swift | 1 + .../Models/ThemeModel+Export.swift | 1 + .../ThemeSettings/Models/ThemeModel.swift | 1 + .../Models/ThemeRepository.swift | 1 + .../ThemeSettings/Models/ThemeSettings.swift | 1 + .../ThemeSettings/ThemeSettingThemeRow.swift | 1 + .../ThemeSettingsColorPreview.swift | 1 + .../ThemeSettingsThemeDetails.swift | 1 + .../ThemeSettings/ThemeSettingsView.swift | 1 + .../Settings/Views/FontWeightPicker.swift | 1 + .../Settings/Views/GlobPatternList.swift | 1 + .../Settings/Views/GlobPatternListItem.swift | 1 + CodeEdit/WorkspaceView.swift | 1 + .../CodeEditSettings}/GlobPattern.swift | 11 +- .../KeyboardShortcutWrapper.swift | 65 +++++++++ .../Sources/CodeEditSettings}/Loopable.swift | 4 +- .../CodeEditSettings}/NSFont+WithWeight.swift | 2 +- .../CodeEditSettings/Placeholder.swift | 8 - .../Sources/CodeEditSettings}/Theme.swift | 138 +++++++++--------- 31 files changed, 170 insertions(+), 139 deletions(-) rename {CodeEdit/Features/Settings/Models => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/GlobPattern.swift (65%) create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift rename {CodeEdit/Utils/Protocols => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Loopable.swift (93%) rename {CodeEdit/Features/Settings/Pages/TextEditingSettings/Models => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/NSFont+WithWeight.swift (95%) delete mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Placeholder.swift rename {CodeEdit/Features/Settings/Pages/ThemeSettings/Models => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Theme.swift (80%) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index fb6f4036e1..7530354034 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import CodeEditDocument import SwiftUI import CodeEditUI diff --git a/CodeEdit/Features/Keybindings/KeybindingManager.swift b/CodeEdit/Features/Keybindings/KeybindingManager.swift index 95bc15804d..5121121ba0 100644 --- a/CodeEdit/Features/Keybindings/KeybindingManager.swift +++ b/CodeEdit/Features/Keybindings/KeybindingManager.swift @@ -6,6 +6,7 @@ import Foundation import SwiftUI +import CodeEditSettings final class KeybindingManager: KeybindingManaging { /// Array which contains all available keyboard shortcuts @@ -55,59 +56,3 @@ final class KeybindingManager: KeybindingManaging { } } - -/// Wrapper for KeyboardShortcut. It contains name, keybindings. -struct KeyboardShortcutWrapper: Codable, Hashable { - var keyboardShortcut: KeyboardShortcut { - return KeyboardShortcut.init(.init(Character(keybinding)), modifiers: parsedModifier) - } - - var parsedModifier: EventModifiers { - switch modifier { - case "command": - return EventModifiers.command - case "shift": - return EventModifiers.shift - case "option": - return EventModifiers.option - case "control": - return EventModifiers.control - default: - return EventModifiers.command - } - } - var name: String - var description: String - var context: String - var keybinding: String - var modifier: String - var id: String - - enum CodingKeys: String, CodingKey { - case name - case description - case context - case keybinding - case modifier - case id - } - - init(name: String, description: String, context: String, keybinding: String, modifier: String, id: String) { - self.name = name - self.description = description - self.context = context - self.keybinding = keybinding - self.modifier = modifier - self.id = id - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - name = try container.decode(String.self, forKey: .name) - description = try container.decode(String.self, forKey: .description) - context = try container.decode(String.self, forKey: .context) - keybinding = try container.decode(String.self, forKey: .keybinding) - modifier = try container.decode(String.self, forKey: .modifier) - id = try container.decode(String.self, forKey: .id) - } -} diff --git a/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift b/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift index da6dc841d6..15565f16b2 100644 --- a/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift +++ b/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings /// Protocol for managing keyboard shortcuts. protocol KeybindingManaging: AnyObject { diff --git a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift index 6ea0f2cc0b..a7363b5fc6 100644 --- a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift +++ b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings // MARK: - Bool Defaults diff --git a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift index 95ea214c60..adfad00f14 100644 --- a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings extension SettingsData { diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift index e4de7ee75d..9cdd338561 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift +++ b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings extension SettingsData { struct SearchSettings: Codable, Hashable { diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift index 28cf47818e..7d84a2d84a 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift +++ b/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings /// The Search Settings View Model. Accessible via the singleton "``SearchSettings/shared``". /// diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift index 04d8800401..7abf6c1554 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import Factory /// A model to manage Git ignore patterns for a file, including loading, saving, and monitoring changes. diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift index df17b4103a..55f057bba3 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import Foundation extension SettingsData { diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift index 6ed9e68011..1dde55dc54 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import CodeEditCore import Foundation diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift index 42eb3be90a..f0c7b0a645 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditSourceEditor /// Color conversion extensions for Theme types. diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift index 88826b20f8..ae17ba8c55 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings extension Theme: FuzzySearchable { var searchableString: String { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 16d66467e5..a0a51dcb54 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import UniformTypeIdentifiers extension ThemeModel { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift index 07875bcc1d..ca899c6176 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import UniformTypeIdentifiers /// Export dialog methods for themes. diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index 303217c91c..8f3b82262d 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import UniformTypeIdentifiers /// The Theme View Model. Accessible via the singleton "``ThemeModel/shared``". diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift index 39d6672dc2..2967e4ce22 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings /// Handles all file I/O operations for themes. /// diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift index 2cb52f7b67..7ff87ab8a5 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings extension SettingsData { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index 864802745b..e1ac3fb8c2 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct ThemeSettingsThemeRow: View { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift index 42e2f97526..f943c40b40 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct ThemeSettingsColorPreview: View { var theme: Theme diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index d6fc59657e..b06c0b5114 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct ThemeSettingsThemeDetails: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index f56fdced00..09049fa89a 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI /// A view that implements the `Theme` preference section diff --git a/CodeEdit/Features/Settings/Views/FontWeightPicker.swift b/CodeEdit/Features/Settings/Views/FontWeightPicker.swift index 92b50fccfb..6720acfa08 100644 --- a/CodeEdit/Features/Settings/Views/FontWeightPicker.swift +++ b/CodeEdit/Features/Settings/Views/FontWeightPicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct FontWeightPicker: View { @Binding var selection: NSFont.Weight diff --git a/CodeEdit/Features/Settings/Views/GlobPatternList.swift b/CodeEdit/Features/Settings/Views/GlobPatternList.swift index 80f216a4b8..9e455963e0 100644 --- a/CodeEdit/Features/Settings/Views/GlobPatternList.swift +++ b/CodeEdit/Features/Settings/Views/GlobPatternList.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct GlobPatternList: View { @Binding var patterns: [GlobPattern] diff --git a/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift b/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift index f6d4d91a3d..d3c161d531 100644 --- a/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift +++ b/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct GlobPatternListItem: View { @Binding var pattern: GlobPattern diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 76e3766946..10dd303726 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditCore import CodeEditUI import Notifications diff --git a/CodeEdit/Features/Settings/Models/GlobPattern.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/GlobPattern.swift similarity index 65% rename from CodeEdit/Features/Settings/Models/GlobPattern.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/GlobPattern.swift index 7eb16409fe..bc516a6364 100644 --- a/CodeEdit/Features/Settings/Models/GlobPattern.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/GlobPattern.swift @@ -11,10 +11,15 @@ import Foundation /// /// This type does not interpret or validate the glob pattern itself. /// It is simply an identifier (`id`) and the glob pattern string (`value`) associated with it. -struct GlobPattern: Identifiable, Hashable, Decodable, Encodable { +public struct GlobPattern: Identifiable, Hashable, Decodable, Encodable { /// Ephemeral UUID used to uniquely identify this instance in the UI - var id = UUID() + public var id = UUID() /// The Glob Pattern string - var value: String + public var value: String + + public init(id: UUID = UUID(), value: String) { + self.id = id + self.value = value + } } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift new file mode 100644 index 0000000000..e5aafdbf10 --- /dev/null +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift @@ -0,0 +1,65 @@ +// +// KeyboardShortcutWrapper.swift +// CodeEditSettings +// +// Created by Matthijs Eikelenboom. +// + +import Foundation +import SwiftUI + +/// Wrapper for KeyboardShortcut. It contains name, keybindings. +public struct KeyboardShortcutWrapper: Codable, Hashable { + public var keyboardShortcut: KeyboardShortcut { + return KeyboardShortcut.init(.init(Character(keybinding)), modifiers: parsedModifier) + } + + public var parsedModifier: EventModifiers { + switch modifier { + case "command": + return EventModifiers.command + case "shift": + return EventModifiers.shift + case "option": + return EventModifiers.option + case "control": + return EventModifiers.control + default: + return EventModifiers.command + } + } + public var name: String + public var description: String + public var context: String + public var keybinding: String + public var modifier: String + public var id: String + + enum CodingKeys: String, CodingKey { + case name + case description + case context + case keybinding + case modifier + case id + } + + public init(name: String, description: String, context: String, keybinding: String, modifier: String, id: String) { + self.name = name + self.description = description + self.context = context + self.keybinding = keybinding + self.modifier = modifier + self.id = id + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + description = try container.decode(String.self, forKey: .description) + context = try container.decode(String.self, forKey: .context) + keybinding = try container.decode(String.self, forKey: .keybinding) + modifier = try container.decode(String.self, forKey: .modifier) + id = try container.decode(String.self, forKey: .id) + } +} diff --git a/CodeEdit/Utils/Protocols/Loopable.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift similarity index 93% rename from CodeEdit/Utils/Protocols/Loopable.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift index 85a8af3e5b..ffc105e20d 100644 --- a/CodeEdit/Utils/Protocols/Loopable.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift @@ -9,7 +9,7 @@ import Foundation /// Loopable protocol implements a method that will return all child /// properties and their associated values of a `Type` -protocol Loopable { +public protocol Loopable { func allProperties() throws -> [String: Any] } @@ -30,7 +30,7 @@ extension Loopable { /// // returns /// ["name": "Steve", "books": 4] /// ``` - func allProperties() throws -> [String: Any] { + public func allProperties() throws -> [String: Any] { var result: [String: Any] = [:] let mirror = Mirror(reflecting: self) diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/NSFont+WithWeight.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/NSFont+WithWeight.swift similarity index 95% rename from CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/NSFont+WithWeight.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/NSFont+WithWeight.swift index e3b7871f1b..e79d283900 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/NSFont+WithWeight.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/NSFont+WithWeight.swift @@ -10,7 +10,7 @@ import SwiftUI extension NSFont { /// Rough mapping from behavior of .systemFont(…weight:) /// to NSFontManager's Int-based weight, as of 13.4 Ventura - func withWeight(weight: NSFont.Weight) -> NSFont? { + public func withWeight(weight: NSFont.Weight) -> NSFont? { let fontManager = NSFontManager.shared var intWeight: Int diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Placeholder.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Placeholder.swift deleted file mode 100644 index 36638ee0b9..0000000000 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Placeholder.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// Placeholder.swift -// CodeEditSettings -// -// Created by Matthijs Eikelenboom. -// - -// Temporary placeholder so the empty target compiles. Removed once real sources land. diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift similarity index 80% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift index 77fc9bb86f..ba086ec162 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift @@ -12,59 +12,59 @@ import Foundation /// # Theme /// /// The model structure of themes for the editor & terminal emulator -struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable { +public struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable { enum CodingKeys: String, CodingKey { case author, license, distributionURL, name, displayName, editor, terminal, version case appearance = "type" case metadataDescription = "description" } - static func == (lhs: Theme, rhs: Theme) -> Bool { + public static func == (lhs: Theme, rhs: Theme) -> Bool { lhs.id == rhs.id } /// The `id` of the theme - var id: String { self.name } + public var id: String { self.name } /// The `author` of the theme - var author: String + public var author: String /// The `license` of the theme - var license: String + public var license: String /// A short `description` of the theme - var metadataDescription: String + public var metadataDescription: String /// An URL for reference - var distributionURL: String + public var distributionURL: String /// If the theme is bundled with CodeEdit or not - var isBundled: Bool = false + public var isBundled: Bool = false /// The URL for the theme file - var fileURL: URL? + public var fileURL: URL? /// The `unique name` of the theme - var name: String + public var name: String /// The `display name` of the theme - var displayName: String + public var displayName: String /// The `version` of the theme - var version: String + public var version: String /// The ``ThemeType`` of the theme /// /// Appears as `"type"` in the `settings.json` - var appearance: ThemeType + public var appearance: ThemeType /// Editor colors of the theme - var editor: EditorColors + public var editor: EditorColors /// Terminal colors of the theme - var terminal: TerminalColors + public var terminal: TerminalColors - init( + public init( editor: EditorColors, terminal: TerminalColors, author: String, @@ -95,7 +95,7 @@ extension Theme { /// The type of the theme /// - **dark**: this is a theme for dark system appearance /// - **light**: this is a theme for light system appearance - enum ThemeType: String, Codable, Hashable { + public enum ThemeType: String, Codable, Hashable { case dark case light } @@ -107,27 +107,27 @@ extension Theme { /// /// As of now it only includes the colors `hex` string and /// an accessor for a `SwiftUI` `Color`. - struct Attributes: Codable, Equatable, Hashable, Loopable { + public struct Attributes: Codable, Equatable, Hashable, Loopable { /// The 24-bit hex string of the color (e.g. #123456) - var color: String - var bold: Bool - var italic: Bool + public var color: String + public var bold: Bool + public var italic: Bool - init(color: String, bold: Bool = false, italic: Bool = false) { + public init(color: String, bold: Bool = false, italic: Bool = false) { self.color = color self.bold = bold self.italic = italic } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.color = try container.decode(String.self, forKey: .color) self.bold = try container.decodeIfPresent(Bool.self, forKey: .bold) ?? false self.italic = try container.decodeIfPresent(Bool.self, forKey: .italic) ?? false } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(color, forKey: .color) @@ -150,23 +150,23 @@ extension Theme { extension Theme { /// The editor colors of the theme - struct EditorColors: Codable, Hashable, Loopable { - var text: Attributes - var insertionPoint: Attributes - var invisibles: Attributes - var background: Attributes - var lineHighlight: Attributes - var selection: Attributes - var keywords: Attributes - var commands: Attributes - var types: Attributes - var attributes: Attributes - var variables: Attributes - var values: Attributes - var numbers: Attributes - var strings: Attributes - var characters: Attributes - var comments: Attributes + public struct EditorColors: Codable, Hashable, Loopable { + public var text: Attributes + public var insertionPoint: Attributes + public var invisibles: Attributes + public var background: Attributes + public var lineHighlight: Attributes + public var selection: Attributes + public var keywords: Attributes + public var commands: Attributes + public var types: Attributes + public var attributes: Attributes + public var variables: Attributes + public var values: Attributes + public var numbers: Attributes + public var strings: Attributes + public var characters: Attributes + public var comments: Attributes /// Allows to look up properties by their name /// @@ -176,7 +176,7 @@ extension Theme { /// // equal to calling /// editor.text /// ``` - subscript(key: String) -> Attributes { + public subscript(key: String) -> Attributes { get { switch key { case "text": return self.text @@ -221,7 +221,7 @@ extension Theme { } } - init( + public init( text: Attributes, insertionPoint: Attributes, invisibles: Attributes, @@ -261,30 +261,30 @@ extension Theme { extension Theme { /// The terminal emulator colors of the theme - struct TerminalColors: Codable, Hashable, Loopable { - var text: Attributes - var boldText: Attributes - var cursor: Attributes - var background: Attributes - var selection: Attributes - var black: Attributes - var red: Attributes - var green: Attributes - var yellow: Attributes - var blue: Attributes - var magenta: Attributes - var cyan: Attributes - var white: Attributes - var brightBlack: Attributes - var brightRed: Attributes - var brightGreen: Attributes - var brightYellow: Attributes - var brightBlue: Attributes - var brightMagenta: Attributes - var brightCyan: Attributes - var brightWhite: Attributes - - var ansiColors: [String] { + public struct TerminalColors: Codable, Hashable, Loopable { + public var text: Attributes + public var boldText: Attributes + public var cursor: Attributes + public var background: Attributes + public var selection: Attributes + public var black: Attributes + public var red: Attributes + public var green: Attributes + public var yellow: Attributes + public var blue: Attributes + public var magenta: Attributes + public var cyan: Attributes + public var white: Attributes + public var brightBlack: Attributes + public var brightRed: Attributes + public var brightGreen: Attributes + public var brightYellow: Attributes + public var brightBlue: Attributes + public var brightMagenta: Attributes + public var brightCyan: Attributes + public var brightWhite: Attributes + + public var ansiColors: [String] { [ black.color, red.color, @@ -313,7 +313,7 @@ extension Theme { /// // equal to calling /// terminal.text /// ``` - subscript(key: String) -> Attributes { + public subscript(key: String) -> Attributes { get { switch key { case "text": return self.text @@ -368,7 +368,7 @@ extension Theme { } } - init( + public init( text: Attributes, boldText: Attributes, cursor: Attributes, From 1ad0ebbd7771b779e3c58a6a3e30fe83a27ed718 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 10:18:52 +0200 Subject: [PATCH 103/335] Refactor: Move settings data model and store into CodeEditSettings package --- CodeEdit/AppDelegate.swift | 1 + CodeEdit/CodeEditApp.swift | 1 + .../Models/CEWorkspaceFile+Presentation.swift | 1 + .../Views/ToolbarBranchPicker.swift | 1 + .../Views/WorkspacePanelTabBar.swift | 1 + .../CodeEditUI/Views/WorkspacePanelView.swift | 1 + .../JumpBar/Views/EditorJumpBarMenu.swift | 1 + .../TabBar/Tabs/Tab/EditorTabView.swift | 1 + .../EditorTabBarTrailingAccessories.swift | 1 + .../Editor/Views/EditorAreaView.swift | 1 + .../Feedback/Model/FeedbackModel.swift | 1 + .../FileInspector/FileInspectorView.swift | 1 + .../HistoryInspectorModel.swift | 1 + .../HistoryInspectorView.swift | 1 + .../Views/InspectorAreaView.swift | 1 + .../Registry/Protocols/RegistryManaging.swift | 1 + .../LSP/Registry/RegistryManager.swift | 1 + .../Features/LSP/Service/LSPService.swift | 1 + .../FindNavigator/FindNavigatorTab.swift | 1 + .../OutlineView/FileSystemTableViewCell.swift | 1 + .../ProjectNavigatorOutlineView.swift | 3 +- ...ViewController+NSOutlineViewDelegate.swift | 1 + .../ProjectNavigatorViewController.swift | 1 + .../SourceControlNavigatorHistoryView.swift | 1 + ...SourceControlNavigatorRepositoryItem.swift | 1 + .../ChangedFile/GitChangedFileListView.swift | 1 + .../Views/SourceControlNavigatorView.swift | 1 + .../Views/NavigatorAreaView.swift | 1 + .../Models/CodableDefault+Providers.swift | 109 ------------------ .../Settings/Models/PageAndSettings.swift | 1 + .../SettingsData+CommandRegistration.swift | 1 + .../SettingsData+KeybindingReconcile.swift | 1 + .../Settings/Models/SettingsData+Search.swift | 1 + .../Settings/Models/SettingsInjector.swift | 1 + .../AccountSelectionView.swift | 1 + .../AccountsSettingsAccountLink.swift | 1 + .../AccountsSettingsDetailsView.swift | 1 + .../AccountsSettingsSigninView.swift | 1 + .../AccountsSettingsView.swift | 1 + .../Models/AccountsSettings.swift | 30 ----- .../Models/SourceControlAccount+Icon.swift | 24 ++++ .../DeveloperSettingsView.swift | 1 + .../Models/LanguageServerSettings.swift | 26 ----- .../GeneralSettings/GeneralSettingsView.swift | 1 + .../LocationsSettingsView.swift | 1 + .../Models/LocationsSettings.swift | 1 + .../NavigationSettingsView.swift | 1 + .../SourceControlGeneralView.swift | 1 + .../SourceControlGitView.swift | 1 + .../SourceControlSettingsView.swift | 1 + .../TerminalSettingsView.swift | 1 + .../InvisiblesSettingsView.swift | 1 + .../TextEditingSettingsView.swift | 1 + CodeEdit/Features/Settings/SettingsView.swift | 3 +- .../Views/InvisibleCharacterWarningList.swift | 1 + .../Views/WarningCharactersView.swift | 1 + .../BitBucketOAuthConfiguration.swift | 1 + .../BitBucketTokenConfiguration.swift | 1 + .../Accounts/GitHub/GitHubConfiguration.swift | 1 + .../Accounts/GitLab/GitLabConfiguration.swift | 1 + .../GitLab/GitLabOAuthConfiguration.swift | 1 + .../SourceControlManager+FileEvents.swift | 1 + .../StatusBarIndentSelector.swift | 1 + .../Views/CEActiveTaskTerminalView.swift | 1 + .../Views/CELocalShellTerminalView.swift | 1 + .../Views/TerminalEmulatorView.swift | 1 + .../DebugUtility/UtilityAreaDebugView.swift | 1 + .../View/UtilityAreaOutputSourcePicker.swift | 1 + .../UtilityAreaTerminalView.swift | 1 + .../WindowCommands/CodeEditCommands.swift | 1 + .../WindowCommands/ViewCommands.swift | 1 + .../Services/WorkspaceWindowManager.swift | 1 + .../Models/AccountsSettings.swift | 30 +++++ .../Models/DeveloperSettings.swift | 8 +- .../Models/GeneralSettings.swift | 72 ++++++------ .../Models/KeybindingsSettings.swift | 9 +- .../Models/LanguageServerSettings.swift | 32 +++++ .../Models/NavigationSettings.swift | 8 +- .../Models/SearchSettings.swift | 7 +- .../Models/SettingsData.swift | 28 ++--- .../Models/SourceControlAccount.swift | 69 ++++++----- .../Models/SourceControlSettings.swift | 48 ++++---- .../Models/TerminalSettings.swift | 45 ++++---- .../Models/TextEditingSettings.swift | 105 +++++++++-------- .../Models/ThemeSettings.swift | 21 ++-- .../CodeEditSettings/Store}/AppSettings.swift | 14 +-- .../Store/CodableDefault+Providers.swift | 108 +++++++++++++++++ .../Store}/CodableDefault.swift | 12 +- .../CodeEditSettings/Store}/Settings.swift | 10 +- 89 files changed, 494 insertions(+), 393 deletions(-) delete mode 100644 CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift delete mode 100644 CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift create mode 100644 CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount+Icon.swift delete mode 100644 CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/AccountsSettings.swift rename {CodeEdit/Features/Settings/Pages/DeveloperSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/DeveloperSettings.swift (53%) rename {CodeEdit/Features/Settings/Pages/GeneralSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/GeneralSettings.swift (63%) rename {CodeEdit/Features/Settings/Pages/Keybindings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/KeybindingsSettings.swift (79%) create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift rename {CodeEdit/Features/Settings/Pages/NavigationSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/NavigationSettings.swift (56%) rename {CodeEdit/Features/Settings/Pages/SearchSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/SearchSettings.swift (58%) rename {CodeEdit/Features/Settings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/SettingsData.swift (79%) rename {CodeEdit/Features/Settings/Pages/AccountsSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/SourceControlAccount.swift (75%) rename {CodeEdit/Features/Settings/Pages/SourceControlSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/SourceControlSettings.swift (76%) rename {CodeEdit/Features/Settings/Pages/TerminalSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/TerminalSettings.swift (63%) rename {CodeEdit/Features/Settings/Pages/TextEditingSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/TextEditingSettings.swift (70%) rename {CodeEdit/Features/Settings/Pages/ThemeSettings => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings}/Models/ThemeSettings.swift (82%) rename {CodeEdit/Features/Settings/Models => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store}/AppSettings.swift (71%) create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift rename {CodeEdit/Features/Settings/Models => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store}/CodableDefault.swift (84%) rename {CodeEdit/Features/Settings/Models => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store}/Settings.swift (89%) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index acdc80ba77..2f75ad0070 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditSettings import CodeEditDocument import SwiftUI import Factory diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 6af9ec8d2d..81ec4ae5a5 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditDocument import CodeEditCore import Factory diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift index f5177158a8..7cbab34c96 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditCore extension CEWorkspaceFile { diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index 1171baae55..4979fd1673 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore import CodeEditSymbols diff --git a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelTabBar.swift b/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelTabBar.swift index 83053c8dd2..8de0393f69 100644 --- a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelTabBar.swift +++ b/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelTabBar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings protocol WorkspacePanelTab: View, Identifiable, Hashable { var title: String { get } diff --git a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift b/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift index 71ff220821..028056977d 100644 --- a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift +++ b/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct WorkspacePanelView: View { diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift index 411590b86f..4d0012ee8d 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index a5b27da1f6..290c45c502 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 177b8a1c5e..6d9fe32a7c 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditDocument import CodeEditUI diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index 65c31bbad2..ef4e1c9b81 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditDocument import CodeEditCore import CodeEditUI diff --git a/CodeEdit/Features/Feedback/Model/FeedbackModel.swift b/CodeEdit/Features/Feedback/Model/FeedbackModel.swift index 589e762694..4aa2f73add 100644 --- a/CodeEdit/Features/Feedback/Model/FeedbackModel.swift +++ b/CodeEdit/Features/Feedback/Model/FeedbackModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings public class FeedbackModel: ObservableObject { diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index c0616bca7e..168937ce73 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI +import CodeEditSettings import CodeEditCore import CodeEditLanguages import Factory diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index 77c3de99e1..584110ba08 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import CodeEditCore final class HistoryInspectorModel: ObservableObject { diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 29ce156644..a29df2ab6f 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/03/24. // import SwiftUI +import CodeEditSettings import CodeEditUI import CodeEditCore diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index 14da61c0e4..494fdc182a 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct InspectorAreaView: View { @ObservedObject private var extensionManager = ExtensionManager.shared diff --git a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift index 5b30d10d89..69b6cb3d35 100644 --- a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift +++ b/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import CodeEditCore /// Protocol for managing the language server registry. diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index 2d4c013d35..95c21cfac8 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -6,6 +6,7 @@ // import OSLog +import CodeEditSettings import Foundation import ZIPFoundation import Combine diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 64ef2cddfb..1c9862345f 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -6,6 +6,7 @@ // import os.log +import CodeEditSettings import CodeEditDocument import JSONRPC import SwiftUI diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift index 798d5332d4..7c576799d0 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import Search /// App-side wrapper for the Search package's find navigator: reads Settings diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index 9169f3722c..cac5eee5ae 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 41321b8d53..81a4cf2716 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -8,6 +8,7 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore +import CodeEditSettings import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` @@ -18,7 +19,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @Environment(\.activeEditorState) private var activeEditorState - @StateObject var prefs: Settings = .shared + @StateObject var prefs: CodeEditSettings.Settings = .shared typealias NSViewControllerType = ProjectNavigatorViewController diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index f3fbf99514..4b9c13c05b 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore import Factory diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index e784fa2665..f0ec50723d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import CEWorkspaceFileManager import SwiftUI import OSLog diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index 6ee98a7730..dbbb978f7c 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI import CodeEditCore import CodeEditSymbols diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift index 003b285394..073566df12 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct SourceControlNavigatorRepositoryItem: View { @AppSettings(\.general.fileIconStyle) diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index 85b66a8a3c..481d2a3bd6 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 90df569130..8d859209b3 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct SourceControlNavigatorView: View { diff --git a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift b/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift index 85321b60cd..3d41f3e717 100644 --- a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift +++ b/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct NavigatorAreaView: View { @ObservedObject private var workspace: Workspace diff --git a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift b/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift deleted file mode 100644 index a7363b5fc6..0000000000 --- a/CodeEdit/Features/Settings/Models/CodableDefault+Providers.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// CodableDefault+Providers.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 07.04.26. -// - -import AppKit -import CodeEditSettings - -// MARK: - Bool Defaults - -enum DefaultTrue: DefaultValueProvider { - static let defaultValue = true -} - -enum DefaultFalse: DefaultValueProvider { - static let defaultValue = false -} - -// MARK: - Terminal Defaults - -enum DefaultTerminalShell: DefaultValueProvider { - static let defaultValue = SettingsData.TerminalShell.system -} - -enum DefaultTerminalCursorStyle: DefaultValueProvider { - static let defaultValue = SettingsData.TerminalCursorStyle.block -} - -enum DefaultTerminalFont: DefaultValueProvider { - static let defaultValue = SettingsData.TerminalFont() -} - -// MARK: - Navigation Defaults - -enum DefaultNavigationStyle: DefaultValueProvider { - static let defaultValue = SettingsData.NavigationStyle.openInTabs -} - -// MARK: - Collection Defaults - -enum DefaultEmptyGlobPatterns: DefaultValueProvider { - static let defaultValue: [GlobPattern] = [] -} - -enum DefaultEmptyStringDictionary: DefaultValueProvider { - static let defaultValue: [String: String] = [:] -} - -enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { - static let defaultValue: [String: SettingsData.InstalledLanguageServer] = [:] -} - -// MARK: - Account Defaults - -enum DefaultGitAccounts: DefaultValueProvider { - static let defaultValue = SettingsData.GitAccounts() -} - -enum DefaultEmptySourceControlAccounts: DefaultValueProvider { - static let defaultValue: [SourceControlAccount] = [] -} - -enum DefaultEmptyString: DefaultValueProvider { - static let defaultValue = "" -} - -// MARK: - General Settings Defaults - -enum DefaultAppearance: DefaultValueProvider { - static let defaultValue = SettingsData.Appearances.system -} - -enum DefaultIssues: DefaultValueProvider { - static let defaultValue = SettingsData.Issues.inline -} - -enum DefaultFileExtensionsVisibility: DefaultValueProvider { - static let defaultValue = SettingsData.FileExtensionsVisibility.showAll -} - -enum DefaultFileExtensions: DefaultValueProvider { - static let defaultValue = SettingsData.FileExtensions.default -} - -enum DefaultFileIconStyle: DefaultValueProvider { - static let defaultValue = SettingsData.FileIconStyle.color -} - -enum DefaultSidebarTabBarPositionTop: DefaultValueProvider { - static let defaultValue = SettingsData.SidebarTabBarPosition.top -} - -enum DefaultReopenBehavior: DefaultValueProvider { - static let defaultValue = SettingsData.ReopenBehavior.welcome -} - -enum DefaultReopenWindowBehavior: DefaultValueProvider { - static let defaultValue = SettingsData.ReopenWindowBehavior.doNothing -} - -enum DefaultProjectNavigatorSize: DefaultValueProvider { - static let defaultValue = SettingsData.ProjectNavigatorSize.medium -} - -enum DefaultNavigatorDetail: DefaultValueProvider { - static let defaultValue = SettingsData.NavigatorDetail.upTo3 -} diff --git a/CodeEdit/Features/Settings/Models/PageAndSettings.swift b/CodeEdit/Features/Settings/Models/PageAndSettings.swift index 3297fcb06d..0414a1b201 100644 --- a/CodeEdit/Features/Settings/Models/PageAndSettings.swift +++ b/CodeEdit/Features/Settings/Models/PageAndSettings.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings struct PageAndSettings: Identifiable, Equatable { let id: UUID = UUID() diff --git a/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift b/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift index 587e4cf27f..0125c6395a 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import Factory extension SettingsData.TextEditingSettings { diff --git a/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift b/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift index 3ed50ed8d4..6857e67167 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import Factory extension SettingsData { diff --git a/CodeEdit/Features/Settings/Models/SettingsData+Search.swift b/CodeEdit/Features/Settings/Models/SettingsData+Search.swift index 0926100237..665cc11a9a 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData+Search.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData+Search.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings // App-side settings-search support. `searchKeys` are localized UI labels for the // settings search feature (presentation, not preference data), so they live in the diff --git a/CodeEdit/Features/Settings/Models/SettingsInjector.swift b/CodeEdit/Features/Settings/Models/SettingsInjector.swift index 301991273d..ec7c81ed44 100644 --- a/CodeEdit/Features/Settings/Models/SettingsInjector.swift +++ b/CodeEdit/Features/Settings/Models/SettingsInjector.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct SettingsInjector: View { diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountSelectionView.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountSelectionView.swift index 0437b716d5..a0b867286d 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountSelectionView.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountSelectionView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct AccountSelectionView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift index 709b7f01e0..a824116528 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct AccountsSettingsAccountLink: View { diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift index 52596cfcef..95c33791eb 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct AccountsSettingsDetailsView: View { @Environment(\.dismiss) diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift index 4de2a2f996..9799a2215e 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct AccountsSettingsSigninView: View { diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsView.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsView.swift index 92926889c6..1fc762f053 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct AccountsSettingsView: View { @AppSettings(\.accounts.sourceControlAccounts.gitAccounts) diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift deleted file mode 100644 index c3a9fddb92..0000000000 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/AccountsSettings.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// AccountsPreferences.swift -// CodeEditModules/Settings -// -// Created by Nanashi Li on 2022/04/08. -// - -import Foundation - -extension SettingsData { - - /// The global settings for source control accounts - struct AccountsSettings: Codable, Hashable { - /// The list of git accounts the user has saved - @CodableDefault var sourceControlAccounts: GitAccounts = .init() - - /// Default initializer - init() {} - } - - struct GitAccounts: Codable, Hashable { - /// This id will store the account name as the identifiable - @CodableDefault var gitAccounts: [SourceControlAccount] = [] - - @CodableDefault var sshKey = "" - - /// Default initializer - init() {} - } -} diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount+Icon.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount+Icon.swift new file mode 100644 index 0000000000..710e08155c --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount+Icon.swift @@ -0,0 +1,24 @@ +// +// SourceControlAccount+Icon.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom. +// + +import SwiftUI +import CodeEditSettings + +extension SourceControlAccount.Provider { + /// The provider's icon from the app asset catalog. Lives app-side because + /// `ImageResource` symbols are generated in the app target, not the package. + var iconResource: ImageResource { + switch self { + case .bitbucketCloud, .bitbucketServer: + return .bitBucketIcon + case .github, .githubEnterprise: + return .gitHubIcon + case .gitlab, .gitlabSelfHosted: + return .gitLabIcon + } + } +} diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift index bf9e3eaf58..efa77644fb 100644 --- a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI import LanguageServerProtocol diff --git a/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift b/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift deleted file mode 100644 index d58747383a..0000000000 --- a/CodeEdit/Features/Settings/Pages/Extensions/Models/LanguageServerSettings.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// LanguageServerSettings.swift -// CodeEdit -// -// Created by Abe Malla on 2/2/25. -// - -import Foundation - -extension SettingsData { - struct LanguageServerSettings: Codable, Hashable { - - /// Stores the currently installed language servers. The key is the name of the language server. - @CodableDefault - var installedLanguageServers: [String: InstalledLanguageServer] = [:] - - /// Default initializer - init() {} - } - - struct InstalledLanguageServer: Codable, Hashable { - let packageName: String - var isEnabled: Bool - let version: String - } -} diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/GeneralSettingsView.swift b/CodeEdit/Features/Settings/Pages/GeneralSettings/GeneralSettingsView.swift index 0dbc5cc6e7..769e38897d 100644 --- a/CodeEdit/Features/Settings/Pages/GeneralSettings/GeneralSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/GeneralSettings/GeneralSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings /// A view that implements the `General` settings page struct GeneralSettingsView: View { diff --git a/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettingsView.swift b/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettingsView.swift index d797cc1f6b..9b3399c04d 100644 --- a/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings /// A view that implements the `Locations` settings section struct LocationsSettingsView: View { diff --git a/CodeEdit/Features/Settings/Pages/LocationsSettings/Models/LocationsSettings.swift b/CodeEdit/Features/Settings/Pages/LocationsSettings/Models/LocationsSettings.swift index 9481b1f019..cf9332cc33 100644 --- a/CodeEdit/Features/Settings/Pages/LocationsSettings/Models/LocationsSettings.swift +++ b/CodeEdit/Features/Settings/Pages/LocationsSettings/Models/LocationsSettings.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings extension SettingsData { diff --git a/CodeEdit/Features/Settings/Pages/NavigationSettings/NavigationSettingsView.swift b/CodeEdit/Features/Settings/Pages/NavigationSettings/NavigationSettingsView.swift index 552eb4a075..7431fd47c4 100644 --- a/CodeEdit/Features/Settings/Pages/NavigationSettings/NavigationSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/NavigationSettings/NavigationSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct NavigationSettingsView: View { @AppSettings(\.navigation) diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index e2b03620d7..336572aab5 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import Factory struct SourceControlGeneralView: View { diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index 57f780f0b9..d6dac8c257 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import Factory struct SourceControlGitView: View { diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift index 5fc1ae18e8..a6b6a465e1 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct SourceControlSettingsView: View { diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift index 99c90fdd74..defc690143 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct TerminalSettingsView: View { @AppSettings(\.terminal) diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift index d7c885d13e..59247cbeab 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct InvisiblesSettingsView: View { typealias Config = SettingsData.TextEditingSettings.InvisibleCharactersConfig diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift b/CodeEdit/Features/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift index 73d9eca772..01e5e28745 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings /// A view that implements the `Text Editing` settings page struct TextEditingSettingsView: View { diff --git a/CodeEdit/Features/Settings/SettingsView.swift b/CodeEdit/Features/Settings/SettingsView.swift index 9caf15464c..712d5620f6 100644 --- a/CodeEdit/Features/Settings/SettingsView.swift +++ b/CodeEdit/Features/Settings/SettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings /// A struct for settings struct SettingsView: View { @@ -101,7 +102,7 @@ struct SettingsView: View { ), ] - @ObservedObject private var settings: Settings = .shared + @ObservedObject private var settings: CodeEditSettings.Settings = .shared let updater: SoftwareUpdater diff --git a/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift b/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift index ef0fae3f6e..4a6fd7a7e3 100644 --- a/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift +++ b/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct InvisibleCharacterWarningList: View { diff --git a/CodeEdit/Features/Settings/Views/WarningCharactersView.swift b/CodeEdit/Features/Settings/Views/WarningCharactersView.swift index bc2c21133b..9a7b8f0d66 100644 --- a/CodeEdit/Features/Settings/Views/WarningCharactersView.swift +++ b/CodeEdit/Features/Settings/Views/WarningCharactersView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct WarningCharactersView: View { typealias Config = SettingsData.TextEditingSettings.WarningCharacters diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift b/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift index 57620ee824..542467d422 100644 --- a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift +++ b/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings struct BitBucketOAuthConfiguration: GitRouterConfiguration { let provider = SourceControlAccount.Provider.bitbucketCloud diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift b/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift index 46173eac19..84bb764ee5 100644 --- a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift +++ b/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings struct BitBucketTokenConfiguration: GitRouterConfiguration { let provider = SourceControlAccount.Provider.bitbucketCloud diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift b/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift index 4194b49baf..5566f0f41e 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift +++ b/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings #if canImport(FoundationNetworking) import FoundationNetworking #endif diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift b/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift index 756f0a6dac..00f65eecd6 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift +++ b/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings struct GitLabTokenConfiguration: GitRouterConfiguration { let provider = SourceControlAccount.Provider.gitlab diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift b/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift index 8570631013..e9c858d9fe 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift +++ b/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings struct GitLabOAuthConfiguration: GitRouterConfiguration { let provider = SourceControlAccount.Provider.gitlab diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift b/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift index 37400d7b21..6c41169c1e 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift @@ -6,6 +6,7 @@ // import Combine +import CodeEditSettings import Foundation import CodeEditCore diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarIndentSelector.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarIndentSelector.swift index fdae627cfd..a5d4537d2a 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarIndentSelector.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarIndentSelector.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct StatusBarIndentSelector: View { @AppSettings(\.textEditing.defaultTabWidth) diff --git a/CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift b/CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift index 1e721d6a34..437e5b405d 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import SwiftTerm class CEActiveTaskTerminalView: CELocalShellTerminalView { diff --git a/CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift b/CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift index e755839c37..d401cfa0db 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import SwiftTerm import Foundation diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift index 11683c9ce1..cac7fc610a 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import SwiftTerm /// # TerminalEmulatorView diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift b/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift index 211a080d40..dede677f6a 100644 --- a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift +++ b/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI struct UtilityAreaDebugView: View { diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index 962e46f5ff..ed6d16514b 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import Factory struct UtilityAreaOutputSourcePicker: View { diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 611944a657..d040e9185f 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditUI import Cocoa diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift index 5e2d664134..e1107077ce 100644 --- a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift +++ b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings struct CodeEditCommands: Commands { @AppSettings(\.sourceControl.general.sourceControlIsEnabled) diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 7dcfadf20e..23d599a4c1 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import Factory import Combine diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 93a9150a56..81e038d516 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore import Factory diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/AccountsSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/AccountsSettings.swift new file mode 100644 index 0000000000..1fe6ddd48a --- /dev/null +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/AccountsSettings.swift @@ -0,0 +1,30 @@ +// +// AccountsPreferences.swift +// CodeEditModules/Settings +// +// Created by Nanashi Li on 2022/04/08. +// + +import Foundation + +extension SettingsData { + + /// The global settings for source control accounts + public struct AccountsSettings: Codable, Hashable { + /// The list of git accounts the user has saved + @CodableDefault public var sourceControlAccounts: GitAccounts = .init() + + /// Default initializer + public init() {} + } + + public struct GitAccounts: Codable, Hashable { + /// This id will store the account name as the identifiable + @CodableDefault public var gitAccounts: [SourceControlAccount] = [] + + @CodableDefault public var sshKey = "" + + /// Default initializer + public init() {} + } +} diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/DeveloperSettings.swift similarity index 53% rename from CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/DeveloperSettings.swift index f7aa80242f..21ba35dac4 100644 --- a/CodeEdit/Features/Settings/Pages/DeveloperSettings/Models/DeveloperSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/DeveloperSettings.swift @@ -8,15 +8,15 @@ import Foundation extension SettingsData { - struct DeveloperSettings: Codable, Hashable { + public struct DeveloperSettings: Codable, Hashable { /// A dictionary that stores a file type and a path to an LSP binary - @CodableDefault var lspBinaries: [String: String] = [:] + @CodableDefault public var lspBinaries: [String: String] = [:] /// Toggle for showing the internal development inspector - @CodableDefault var showInternalDevelopmentInspector = false + @CodableDefault public var showInternalDevelopmentInspector = false /// Default initializer - init() {} + public init() {} } } diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift similarity index 63% rename from CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift index 37a781fd36..6af057da2f 100644 --- a/CodeEdit/Features/Settings/Pages/GeneralSettings/Models/GeneralSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift @@ -10,77 +10,77 @@ import SwiftUI extension SettingsData { /// The general global setting - struct GeneralSettings: Codable, Hashable { + public struct GeneralSettings: Codable, Hashable { /// The appearance of the app - @CodableDefault var appAppearance: Appearances = .system + @CodableDefault public var appAppearance: Appearances = .system /// The show issues behavior of the app - @CodableDefault var showIssues: Issues = .inline + @CodableDefault public var showIssues: Issues = .inline /// The show live issues behavior of the app - @CodableDefault var showLiveIssues = true + @CodableDefault public var showLiveIssues = true /// Show editor jump bar - @CodableDefault var showEditorJumpBar = true + @CodableDefault public var showEditorJumpBar = true /// Dims editors without focus - @CodableDefault var dimEditorsWithoutFocus = false + @CodableDefault public var dimEditorsWithoutFocus = false /// The show file extensions behavior of the app - @CodableDefault var fileExtensionsVisibility: FileExtensionsVisibility = .showAll + @CodableDefault public var fileExtensionsVisibility: FileExtensionsVisibility = .showAll /// The file extensions collection to display - @CodableDefault var shownFileExtensions: FileExtensions = .default + @CodableDefault public var shownFileExtensions: FileExtensions = .default /// The file extensions collection to hide - @CodableDefault var hiddenFileExtensions: FileExtensions = .default + @CodableDefault public var hiddenFileExtensions: FileExtensions = .default /// The style for file icons - @CodableDefault var fileIconStyle: FileIconStyle = .color + @CodableDefault public var fileIconStyle: FileIconStyle = .color /// The position for the navigator sidebar tab bar - @CodableDefault var navigatorTabBarPosition: SidebarTabBarPosition = .top + @CodableDefault public var navigatorTabBarPosition: SidebarTabBarPosition = .top /// The position for the inspector sidebar tab bar - @CodableDefault var inspectorTabBarPosition: SidebarTabBarPosition = .top + @CodableDefault public var inspectorTabBarPosition: SidebarTabBarPosition = .top /// The reopen behavior of the app - @CodableDefault var reopenBehavior: ReopenBehavior = .welcome + @CodableDefault public var reopenBehavior: ReopenBehavior = .welcome /// Decides what the app does after a workspace is closed - @CodableDefault var reopenWindowAfterClose: ReopenWindowBehavior = .doNothing + @CodableDefault public var reopenWindowAfterClose: ReopenWindowBehavior = .doNothing /// The size of the project navigator - @CodableDefault var projectNavigatorSize: ProjectNavigatorSize = .medium + @CodableDefault public var projectNavigatorSize: ProjectNavigatorSize = .medium /// The Find Navigator Detail line limit - @CodableDefault var findNavigatorDetail: NavigatorDetail = .upTo3 + @CodableDefault public var findNavigatorDetail: NavigatorDetail = .upTo3 /// The Issue Navigator Detail line limit - @CodableDefault var issueNavigatorDetail: NavigatorDetail = .upTo3 + @CodableDefault public var issueNavigatorDetail: NavigatorDetail = .upTo3 /// The reveal file in navigator when focus changes behavior of the app. - @CodableDefault var revealFileOnFocusChange = false + @CodableDefault public var revealFileOnFocusChange = false /// Auto save behavior toggle - @CodableDefault var isAutoSaveOn = true + @CodableDefault public var isAutoSaveOn = true /// Default initializer - init() {} + public init() {} } /// The appearance of the app /// - **system**: uses the system appearance /// - **dark**: always uses dark appearance /// - **light**: always uses light appearance - enum Appearances: String, Codable { + public enum Appearances: String, Codable { case system case light case dark /// Applies the selected appearance - func applyAppearance() { + public func applyAppearance() { switch self { case .system: NSApp.appearance = nil @@ -97,7 +97,7 @@ extension SettingsData { /// The style for issues display /// - **inline**: Issues show inline /// - **minimized** Issues show minimized - enum Issues: String, Codable { + public enum Issues: String, Codable { case inline case minimized } @@ -107,7 +107,7 @@ extension SettingsData { /// - **showAll** File extensions are visible /// - **showOnly** Specific file extensions are visible /// - **hideOnly** Specific file extensions are hidden - enum FileExtensionsVisibility: Codable, Hashable { + public enum FileExtensionsVisibility: Codable, Hashable { case hideAll case showAll case showOnly @@ -116,10 +116,10 @@ extension SettingsData { /// The collection of file extensions used by /// ``FileExtensionsVisibility/showOnly`` or ``FileExtensionsVisibility/hideOnly`` preference - struct FileExtensions: Codable, Hashable { - var extensions: [String] + public struct FileExtensions: Codable, Hashable { + public var extensions: [String] - var string: String { + public var string: String { get { extensions.joined(separator: ", ") } @@ -131,7 +131,7 @@ extension SettingsData { } } - static var `default` = FileExtensions(extensions: [ + nonisolated(unsafe) public static var `default` = FileExtensions(extensions: [ "c", "cc", "cpp", "h", "hpp", "m", "mm", "gif", "icns", "jpeg", "jpg", "png", "tiff", "swift" ]) @@ -139,7 +139,7 @@ extension SettingsData { /// The style for file icons /// - **color**: File icons appear in their default colors /// - **monochrome**: File icons appear monochromatic - enum FileIconStyle: String, Codable { + public enum FileIconStyle: String, Codable { case color case monochrome } @@ -147,7 +147,7 @@ extension SettingsData { /// The position for a sidebar tab bar /// - **top**: Tab bar is positioned at the top of the sidebar /// - **side**: Tab bar is positioned to the side of the sidebar - enum SidebarTabBarPosition: String, Codable { + public enum SidebarTabBarPosition: String, Codable { case top, side } @@ -155,19 +155,19 @@ extension SettingsData { /// - **welcome**: On restart the app will show the welcome screen /// - **openPanel**: On restart the app will show an open panel /// - **newDocument**: On restart a new empty document will be created - enum ReopenBehavior: String, Codable { + public enum ReopenBehavior: String, Codable { case welcome case openPanel case newDocument } - enum ReopenWindowBehavior: String, Codable { + public enum ReopenWindowBehavior: String, Codable { case showWelcomeWindow case doNothing case quit } - enum ProjectNavigatorSize: String, Codable { + public enum ProjectNavigatorSize: String, Codable { case small case medium case large @@ -177,7 +177,7 @@ extension SettingsData { /// * `small`: 20 /// * `medium`: 22 /// * `large`: 24 - var rowHeight: Double { + public var rowHeight: Double { switch self { case .small: return 20 case .medium: return 22 @@ -188,7 +188,7 @@ extension SettingsData { /// The Navigation Detail behavior of the app /// - Use **rawValue** to set lineLimit - enum NavigatorDetail: Int, Codable, CaseIterable { + public enum NavigatorDetail: Int, Codable, CaseIterable { case upTo1 = 1 case upTo2 = 2 case upTo3 = 3 @@ -197,7 +197,7 @@ extension SettingsData { case upTo10 = 10 case upTo30 = 30 - var label: String { + public var label: String { switch self { case .upTo1: return "One Line" diff --git a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/KeybindingsSettings.swift similarity index 79% rename from CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/KeybindingsSettings.swift index adfad00f14..2cb51d391c 100644 --- a/CodeEdit/Features/Settings/Pages/Keybindings/Models/KeybindingsSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/KeybindingsSettings.swift @@ -6,22 +6,21 @@ // import Foundation -import CodeEditSettings extension SettingsData { /// The global settings for text editing - struct KeybindingsSettings: Codable, Hashable { + public struct KeybindingsSettings: Codable, Hashable { /// An integer indicating how many spaces a `tab` will generate - var keybindings: [String: KeyboardShortcutWrapper] = .init() + public var keybindings: [String: KeyboardShortcutWrapper] = .init() /// Default initializer — empty; bundled defaults are seeded by the app at /// startup via `SettingsData.reconcileDefaultKeybindings()`. - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.keybindings = try container.decodeIfPresent( [String: KeyboardShortcutWrapper].self, diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift new file mode 100644 index 0000000000..531ebf5474 --- /dev/null +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift @@ -0,0 +1,32 @@ +// +// LanguageServerSettings.swift +// CodeEdit +// +// Created by Abe Malla on 2/2/25. +// + +import Foundation + +extension SettingsData { + public struct LanguageServerSettings: Codable, Hashable { + + /// Stores the currently installed language servers. The key is the name of the language server. + @CodableDefault + public var installedLanguageServers: [String: InstalledLanguageServer] = [:] + + /// Default initializer + public init() {} + } + + public struct InstalledLanguageServer: Codable, Hashable { + public let packageName: String + public var isEnabled: Bool + public let version: String + + public init(packageName: String, isEnabled: Bool, version: String) { + self.packageName = packageName + self.isEnabled = isEnabled + self.version = version + } + } +} diff --git a/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/NavigationSettings.swift similarity index 56% rename from CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/NavigationSettings.swift index fdb6eb7d6e..57126e13a2 100644 --- a/CodeEdit/Features/Settings/Pages/NavigationSettings/Models/NavigationSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/NavigationSettings.swift @@ -10,16 +10,16 @@ import Foundation extension SettingsData { /// The global settings for the terminal emulator - struct NavigationSettings: Codable, Hashable { + public struct NavigationSettings: Codable, Hashable { /// Navigation style used - @CodableDefault var navigationStyle: NavigationStyle = .openInTabs + @CodableDefault public var navigationStyle: NavigationStyle = .openInTabs /// Default initializer - init() {} + public init() {} } - enum NavigationStyle: String, Codable, Hashable { + public enum NavigationStyle: String, Codable, Hashable { case openInTabs case openInPlace } diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SearchSettings.swift similarity index 58% rename from CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SearchSettings.swift index 9cdd338561..6a8d427ce4 100644 --- a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SearchSettings.swift @@ -6,15 +6,14 @@ // import Foundation -import CodeEditSettings extension SettingsData { - struct SearchSettings: Codable, Hashable { + public struct SearchSettings: Codable, Hashable { /// List of Glob Patterns that determine which files or directories to ignore - @CodableDefault var ignoreGlobPatterns: [GlobPattern] = [] + @CodableDefault public var ignoreGlobPatterns: [GlobPattern] = [] /// Default initializer - init() {} + public init() {} } } diff --git a/CodeEdit/Features/Settings/Models/SettingsData.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SettingsData.swift similarity index 79% rename from CodeEdit/Features/Settings/Models/SettingsData.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SettingsData.swift index e842f71059..d3207231ea 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SettingsData.swift @@ -21,46 +21,46 @@ import Foundation /// all properties with /// [`decodeIfPresent`](https://developer.apple.com/documentation/swift/keyeddecodingcontainer/2921389-decodeifpresent) /// and providing a default value. Otherwise all settings get overridden. -struct SettingsData: Codable, Hashable { +public struct SettingsData: Codable, Hashable { /// The general global settings - var general: GeneralSettings = .init() + public var general: GeneralSettings = .init() /// The global settings for accounts - var accounts: AccountsSettings = .init() + public var accounts: AccountsSettings = .init() /// The global settings for themes - var navigation: NavigationSettings = .init() + public var navigation: NavigationSettings = .init() /// The global settings for themes - var theme: ThemeSettings = .init() + public var theme: ThemeSettings = .init() /// The global settings for text editing - var textEditing: TextEditingSettings = .init() + public var textEditing: TextEditingSettings = .init() /// The global settings for the terminal emulator - var terminal: TerminalSettings = .init() + public var terminal: TerminalSettings = .init() /// The global settings for source control - var sourceControl: SourceControlSettings = .init() + public var sourceControl: SourceControlSettings = .init() /// The global settings for keybindings - var keybindings: KeybindingsSettings = .init() + public var keybindings: KeybindingsSettings = .init() /// Search Settings - var search: SearchSettings = .init() + public var search: SearchSettings = .init() /// Language Server Settings - var languageServers: LanguageServerSettings = .init() + public var languageServers: LanguageServerSettings = .init() /// Developer settings for CodeEdit developers - var developerSettings: DeveloperSettings = .init() + public var developerSettings: DeveloperSettings = .init() /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.general = try container.decodeIfPresent(GeneralSettings.self, forKey: .general) ?? .init() self.accounts = try container.decodeIfPresent(AccountsSettings.self, forKey: .accounts) ?? .init() diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlAccount.swift similarity index 75% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlAccount.swift index f7b67898a7..ca7ce941c6 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlAccount.swift @@ -5,27 +5,47 @@ // Created by Austin Condiff on 4/6/23. // -import SwiftUI +import Foundation -struct SourceControlAccount: Codable, Identifiable, Hashable { +public struct SourceControlAccount: Codable, Identifiable, Hashable { - var id: String - var name: String - var description: String - var provider: Provider - var serverURL: String + public var id: String + public var name: String + public var description: String + public var provider: Provider + public var serverURL: String // TODO: Should we use an enum instead of a boolean here: // If true we use the HTTP protocol else if false we use SSH - var urlProtocol: URLProtocol - var sshKey: String - var isTokenValid: Bool + public var urlProtocol: URLProtocol + public var sshKey: String + public var isTokenValid: Bool - enum URLProtocol: String, Codable, CaseIterable { + public init( + id: String, + name: String, + description: String, + provider: Provider, + serverURL: String, + urlProtocol: URLProtocol, + sshKey: String, + isTokenValid: Bool + ) { + self.id = id + self.name = name + self.description = description + self.provider = provider + self.serverURL = serverURL + self.urlProtocol = urlProtocol + self.sshKey = sshKey + self.isTokenValid = isTokenValid + } + + public enum URLProtocol: String, Codable, CaseIterable { case https = "HTTPS" case ssh = "SSH" } - enum Provider: Codable, CaseIterable, Identifiable { + public enum Provider: Codable, CaseIterable, Identifiable { case bitbucketCloud case bitbucketServer case github @@ -33,7 +53,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { case gitlab case gitlabSelfHosted - var id: String { + public var id: String { switch self { case .bitbucketCloud: return "bitbucketCloud" @@ -50,7 +70,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { } } - var name: String { + public var name: String { switch self { case .bitbucketCloud: return "BitBucket Cloud" @@ -67,7 +87,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { } } - var baseURL: URL? { + public var baseURL: URL? { switch self { case .bitbucketCloud: return URL(string: "https://www.bitbucket.com/")! @@ -84,7 +104,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { } } - var apiURL: URL? { + public var apiURL: URL? { switch self { case .bitbucketCloud: return URL(string: "https://api.bitbucket.org/2.0/")! @@ -101,18 +121,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { } } - var iconResource: ImageResource { - switch self { - case .bitbucketCloud, .bitbucketServer: - return .bitBucketIcon - case .github, .githubEnterprise: - return .gitHubIcon - case .gitlab, .gitlabSelfHosted: - return .gitLabIcon - } - } - - var authHelpURL: URL { + public var authHelpURL: URL { switch self { case .bitbucketCloud: return URL(string: "https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/")! @@ -130,7 +139,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { } } - var authType: AuthType { + public var authType: AuthType { switch self { case .bitbucketCloud: return .password @@ -148,7 +157,7 @@ struct SourceControlAccount: Codable, Identifiable, Hashable { } } - enum AuthType { + public enum AuthType { case token case password } diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlSettings.swift similarity index 76% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlSettings.swift index 982acf8d72..97c099a737 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlSettings.swift @@ -9,50 +9,50 @@ import Foundation extension SettingsData { /// The global settings for source control - struct SourceControlSettings: Codable, Hashable { + public struct SourceControlSettings: Codable, Hashable { /// The general source control settings - var general: SourceControlGeneral = .init() + public var general: SourceControlGeneral = .init() /// The source control git settings - var git: SourceControlGit = .init() + public var git: SourceControlGit = .init() /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.general = try container.decodeIfPresent(SourceControlGeneral.self, forKey: .general) ?? .init() self.git = try container.decodeIfPresent(SourceControlGit.self, forKey: .git) ?? .init() } } - struct SourceControlGeneral: Codable, Hashable { + public struct SourceControlGeneral: Codable, Hashable { /// Indicates whether or not the source control is active - var sourceControlIsEnabled: Bool = true + public var sourceControlIsEnabled: Bool = true /// Indicates whether the status should be refreshed locally without fetching updates from the server. - var refreshStatusLocally: Bool = true + public var refreshStatusLocally: Bool = true /// Indicates whether the application should automatically fetch updates from the server and refresh the status. - var fetchRefreshServerStatus: Bool = true + public var fetchRefreshServerStatus: Bool = true /// Indicates whether new and deleted files should be automatically staged for commit. - var addRemoveAutomatically: Bool = true + public var addRemoveAutomatically: Bool = true /// Indicates whether the application should automatically select files to commit. - var selectFilesToCommit: Bool = true + public var selectFilesToCommit: Bool = true /// Indicates whether or not to show the source control changes - var showSourceControlChanges: Bool = true + public var showSourceControlChanges: Bool = true /// Indicates whether or not we should include the upstream - var includeUpstreamChanges: Bool = true + public var includeUpstreamChanges: Bool = true /// Indicates whether or not we should open the reported feedback in the browser - var openFeedbackInBrowser: Bool = true + public var openFeedbackInBrowser: Bool = true /// The selected value of the comparison view - var revisionComparisonLayout: RevisionComparisonLayout = .localLeft + public var revisionComparisonLayout: RevisionComparisonLayout = .localLeft /// The selected value of the control navigator - var controlNavigatorOrder: ControlNavigatorOrder = .sortByName + public var controlNavigatorOrder: ControlNavigatorOrder = .sortByName /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.sourceControlIsEnabled = try container.decodeIfPresent( Bool.self, @@ -94,7 +94,7 @@ extension SettingsData { /// The style for comparison View /// - **localLeft**: Local Revision on Left Side /// - **localRight**: Local Revision on Right Side - enum RevisionComparisonLayout: String, Codable { + public enum RevisionComparisonLayout: String, Codable { case localLeft case localRight } @@ -102,18 +102,18 @@ extension SettingsData { /// The style for control Navigator /// - **sortName**: They are sorted by Name /// - **sortDate**: They are sorted by Date - enum ControlNavigatorOrder: String, Codable { + public enum ControlNavigatorOrder: String, Codable { case sortByName case sortByDate } - struct SourceControlGit: Codable, Hashable { + public struct SourceControlGit: Codable, Hashable { /// Indicates whether we should rebase when pulling commits - var showMergeCommitsPerFileLog: Bool = false + public var showMergeCommitsPerFileLog: Bool = false /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.showMergeCommitsPerFileLog = try container.decodeIfPresent( Bool.self, diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TerminalSettings.swift similarity index 63% rename from CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TerminalSettings.swift index 55f057bba3..ec6321b32d 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TerminalSettings.swift @@ -6,82 +6,81 @@ // import AppKit -import CodeEditSettings import Foundation extension SettingsData { /// The global settings for the terminal emulator - struct TerminalSettings: Codable, Hashable { + public struct TerminalSettings: Codable, Hashable { /// If true terminal will use editor theme. - @CodableDefault var useEditorTheme = true + @CodableDefault public var useEditorTheme = true /// If true terminal appearance will always be `dark`. Otherwise it adapts to the system setting. - @CodableDefault var darkAppearance = false + @CodableDefault public var darkAppearance = false /// If true, the terminal uses the background color of the theme, otherwise it is clear - @CodableDefault var useThemeBackground = true + @CodableDefault public var useThemeBackground = true /// If true, the terminal treats the `Option` key as the `Meta` key - @CodableDefault var optionAsMeta = false + @CodableDefault public var optionAsMeta = false /// The selected shell to use. - @CodableDefault var shell: TerminalShell = .system + @CodableDefault public var shell: TerminalShell = .system /// The font to use in terminal. - @CodableDefault var font: TerminalFont = .init() + @CodableDefault public var font: TerminalFont = .init() // The cursor style to use in terminal - @CodableDefault var cursorStyle: TerminalCursorStyle = .block + @CodableDefault public var cursorStyle: TerminalCursorStyle = .block // Toggle for blinking cursor or not - @CodableDefault var cursorBlink = false + @CodableDefault public var cursorBlink = false // Use font settings from Text Editing - @CodableDefault var useTextEditorFont = true + @CodableDefault public var useTextEditorFont = true /// If `true`, use injection scripts for terminal features like automatic tab title. - @CodableDefault var useShellIntegration = true + @CodableDefault public var useShellIntegration = true /// If `true`, use a login shell. - @CodableDefault var useLoginShell = true + @CodableDefault public var useLoginShell = true /// Default initializer - init() {} + public init() {} } /// The shell options. /// - **bash**: uses the default bash shell /// - **zsh**: uses the ZSH shell /// - **system**: uses the system default shell (most likely ZSH) - enum TerminalShell: String, Codable, Hashable { + public enum TerminalShell: String, Codable, Hashable { case bash case zsh case system } - enum TerminalCursorStyle: String, Codable, Hashable { + public enum TerminalCursorStyle: String, Codable, Hashable { case block case underline case bar } - struct TerminalFont: Codable, Hashable { + public struct TerminalFont: Codable, Hashable { /// The font size for the custom font - var size: Double = 12 + public var size: Double = 12 /// The name of the custom font - var name: String = "SF Mono" + public var name: String = "SF Mono" /// The weight of the custom font - var weight: NSFont.Weight = .medium + public var weight: NSFont.Weight = .medium /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.size = try container.decodeIfPresent(Double.self, forKey: .size) ?? size self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? name @@ -92,7 +91,7 @@ extension SettingsData { /// /// Returns the custom font, if enabled and able to be instantiated. /// Otherwise returns a default system font monospaced. - var current: NSFont { + public var current: NSFont { let customFont = NSFont(name: name, size: size)?.withWeight(weight: weight) return customFont ?? NSFont.monospacedSystemFont(ofSize: size, weight: .medium) } diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift similarity index 70% rename from CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift index 1dde55dc54..e2101d771a 100644 --- a/CodeEdit/Features/Settings/Pages/TextEditingSettings/Models/TextEditingSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift @@ -6,75 +6,74 @@ // import AppKit -import CodeEditSettings import CodeEditCore import Foundation extension SettingsData { /// The global settings for text editing - struct TextEditingSettings: Codable, Hashable { + public struct TextEditingSettings: Codable, Hashable { /// An integer indicating how many spaces a `tab` will appear as visually. - var defaultTabWidth: Int = 4 + public var defaultTabWidth: Int = 4 /// The behavior of a `tab` keypress. If `.tab`, will insert a tab character. If `.spaces` will insert /// `.spaceCount` spaces instead. - var indentOption: IndentOption = IndentOption(indentType: .spaces, spaceCount: 4) + public var indentOption: IndentOption = IndentOption(indentType: .spaces, spaceCount: 4) /// The font to use in editor. - var font: EditorFont = .init() + public var font: EditorFont = .init() /// A flag indicating whether type-over completion is enabled - var enableTypeOverCompletion: Bool = true + public var enableTypeOverCompletion: Bool = true /// A flag indicating whether braces are automatically completed - var autocompleteBraces: Bool = true + public var autocompleteBraces: Bool = true /// A flag indicating whether to wrap lines to editor width - var wrapLinesToEditorWidth: Bool = true + public var wrapLinesToEditorWidth: Bool = true /// The percentage of overscroll to apply to the text view - var overscroll: OverscrollOption = .medium + public var overscroll: OverscrollOption = .medium /// A multiplier for setting the line height. Defaults to `1.2` - var lineHeightMultiple: Double = 1.2 + public var lineHeightMultiple: Double = 1.2 /// A multiplier for setting the letter spacing, `1` being no spacing and /// `2` is one character of spacing between letters, defaults to `1`. - var letterSpacing: Double = 1.0 + public var letterSpacing: Double = 1.0 /// The behavior of bracket pair highlights. - var bracketEmphasis: BracketPairEmphasis = BracketPairEmphasis() + public var bracketEmphasis: BracketPairEmphasis = BracketPairEmphasis() /// Use the system cursor for the source editor. - var useSystemCursor: Bool = true + public var useSystemCursor: Bool = true /// Toggle the gutter in the editor. - var showGutter: Bool = true + public var showGutter: Bool = true /// Toggle the minimap in the editor. - var showMinimap: Bool = true + public var showMinimap: Bool = true /// Toggle the code folding ribbon. - var showFoldingRibbon: Bool = true + public var showFoldingRibbon: Bool = true /// The column at which to reformat text - var reformatAtColumn: Int = 80 + public var reformatAtColumn: Int = 80 /// Show the reformatting guide in the editor - var showReformattingGuide: Bool = false + public var showReformattingGuide: Bool = false - var invisibleCharacters: InvisibleCharactersConfig = .default + public var invisibleCharacters: InvisibleCharactersConfig = .default /// Map of unicode character codes to a note about them - var warningCharacters: WarningCharacters = .default + public var warningCharacters: WarningCharacters = .default /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { // swiftlint:disable:this function_body_length + public init(from decoder: Decoder) throws { // swiftlint:disable:this function_body_length let container = try decoder.container(keyedBy: CodingKeys.self) self.defaultTabWidth = try container.decodeIfPresent(Int.self, forKey: .defaultTabWidth) ?? 4 self.indentOption = try container.decodeIfPresent( @@ -136,16 +135,16 @@ extension SettingsData { /// Re-exported from `CodeEditCore`. Keeps `SettingsData.TextEditingSettings.IndentOption` /// valid for all existing call sites while the underlying type lives in the Core package. - typealias IndentOption = CodeEditCore.IndentOption + public typealias IndentOption = CodeEditCore.IndentOption - struct BracketPairEmphasis: Codable, Hashable { + public struct BracketPairEmphasis: Codable, Hashable { /// The type of highlight to use - var highlightType: HighlightType = .flash - var useCustomColor: Bool = false + public var highlightType: HighlightType = .flash + public var useCustomColor: Bool = false /// The color to use for the highlight. - var color: Theme.Attributes = Theme.Attributes(color: "FFFFFF", bold: false, italic: false) + public var color: Theme.Attributes = Theme.Attributes(color: "FFFFFF", bold: false, italic: false) - enum HighlightType: String, Codable { + public enum HighlightType: String, Codable { case disabled case bordered case flash @@ -153,13 +152,13 @@ extension SettingsData { } } - enum OverscrollOption: String, Codable { + public enum OverscrollOption: String, Codable { case none case small case medium case large - var overscrollPercentage: CGFloat { + public var overscrollPercentage: CGFloat { switch self { case .none: return 0 case .small: return 0.25 @@ -169,8 +168,8 @@ extension SettingsData { } } - struct InvisibleCharactersConfig: Equatable, Hashable, Codable { - static var `default`: InvisibleCharactersConfig = { + public struct InvisibleCharactersConfig: Equatable, Hashable, Codable { + nonisolated(unsafe) public static var `default`: InvisibleCharactersConfig = { InvisibleCharactersConfig( enabled: false, showSpaces: true, @@ -179,24 +178,24 @@ extension SettingsData { ) }() - var enabled: Bool + public var enabled: Bool - var showSpaces: Bool - var showTabs: Bool - var showLineEndings: Bool + public var showSpaces: Bool + public var showTabs: Bool + public var showLineEndings: Bool - var spaceReplacement: String = "·" - var tabReplacement: String = "→" + public var spaceReplacement: String = "·" + public var tabReplacement: String = "→" // Controlled by `showLineEndings` - var carriageReturnReplacement: String = "↵" - var lineFeedReplacement: String = "¬" - var paragraphSeparatorReplacement: String = "¶" - var lineSeparatorReplacement: String = "⏎" + public var carriageReturnReplacement: String = "↵" + public var lineFeedReplacement: String = "¬" + public var paragraphSeparatorReplacement: String = "¶" + public var lineSeparatorReplacement: String = "⏎" } - struct WarningCharacters: Equatable, Hashable, Codable { - static let `default`: WarningCharacters = WarningCharacters(enabled: true, characters: [ + public struct WarningCharacters: Equatable, Hashable, Codable { + nonisolated(unsafe) public static let `default`: WarningCharacters = WarningCharacters(enabled: true, characters: [ 0x0003: "End of text", 0x00A0: "Non-breaking space", @@ -216,26 +215,26 @@ extension SettingsData { 0x037E: "Greek Question Mark" ]) - var enabled: Bool - var characters: [UInt16: String] + public var enabled: Bool + public var characters: [UInt16: String] } } - struct EditorFont: Codable, Hashable { + public struct EditorFont: Codable, Hashable { /// The font size for the font - var size: Double = 12 + public var size: Double = 12 /// The name of the custom font - var name: String = "SF Mono" + public var name: String = "SF Mono" /// The weight of the custom font - var weight: NSFont.Weight = .medium + public var weight: NSFont.Weight = .medium /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.size = try container.decodeIfPresent(Double.self, forKey: .size) ?? size self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? name @@ -246,7 +245,7 @@ extension SettingsData { /// /// Returns the custom font, if enabled and able to be instantiated. /// Otherwise returns a default system font monospaced. - var current: NSFont { + public var current: NSFont { let customFont = NSFont(name: name, size: size)?.withWeight(weight: weight) return customFont ?? NSFont.monospacedSystemFont(ofSize: size, weight: .medium) } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/ThemeSettings.swift similarity index 82% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/ThemeSettings.swift index 7ff87ab8a5..337a4f5cdb 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/ThemeSettings.swift @@ -6,7 +6,6 @@ // import Foundation -import CodeEditSettings extension SettingsData { @@ -28,25 +27,25 @@ extension SettingsData { /// } /// } /// ``` - typealias ThemeOverrides = [String: [String: Theme.Attributes]] + public typealias ThemeOverrides = [String: [String: Theme.Attributes]] /// The global settings for themes - struct ThemeSettings: Codable, Hashable { + public struct ThemeSettings: Codable, Hashable { /// The name of the currently selected dark theme - var selectedDarkTheme: String = "Default (Dark)" + public var selectedDarkTheme: String = "Default (Dark)" /// The name of the currently selected light theme - var selectedLightTheme: String = "Default (Light)" + public var selectedLightTheme: String = "Default (Light)" /// The name of the currently selected theme - var selectedTheme: String? + public var selectedTheme: String? /// Use the system background that matches the appearance setting - var useThemeBackground: Bool = true + public var useThemeBackground: Bool = true /// Automatically change theme based on system appearance - var matchAppearance: Bool = true + public var matchAppearance: Bool = true /// Dictionary of themes containing overrides /// @@ -73,13 +72,13 @@ extension SettingsData { /// ... /// } /// ``` - var overrides: [String: ThemeOverrides] = [:] + public var overrides: [String: ThemeOverrides] = [:] /// Default initializer - init() {} + public init() {} /// Explicit decoder init for setting default values when key is not present in `JSON` - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.selectedDarkTheme = try container.decodeIfPresent( String.self, forKey: .selectedDarkTheme diff --git a/CodeEdit/Features/Settings/Models/AppSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift similarity index 71% rename from CodeEdit/Features/Settings/Models/AppSettings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift index d7a115df37..fa06e4a843 100644 --- a/CodeEdit/Features/Settings/Models/AppSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift @@ -9,19 +9,19 @@ import Foundation import SwiftUI @propertyWrapper -struct AppSettings: DynamicProperty where T: Equatable { +public struct AppSettings: DynamicProperty where T: Equatable { var settings: Environment let keyPath: WritableKeyPath - init(_ keyPath: WritableKeyPath) { + public init(_ keyPath: WritableKeyPath) { self.keyPath = keyPath let settingsKeyPath = (\EnvironmentValues.settings).appending(path: keyPath) self.settings = Environment(settingsKeyPath) } - var wrappedValue: T { + public var wrappedValue: T { get { Settings.shared.preferences[keyPath: keyPath] } @@ -30,7 +30,7 @@ struct AppSettings: DynamicProperty where T: Equatable { } } - var projectedValue: Binding { + public var projectedValue: Binding { Binding { Settings.shared.preferences[keyPath: keyPath] } set: { @@ -39,11 +39,11 @@ struct AppSettings: DynamicProperty where T: Equatable { } } -struct SettingsDataEnvironmentKey: EnvironmentKey { - static var defaultValue: SettingsData = .init() +public struct SettingsDataEnvironmentKey: EnvironmentKey { + nonisolated(unsafe) public static var defaultValue: SettingsData = .init() } -extension EnvironmentValues { +public extension EnvironmentValues { var settings: SettingsDataEnvironmentKey.Value { get { self[SettingsDataEnvironmentKey.self] } set { self[SettingsDataEnvironmentKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift new file mode 100644 index 0000000000..f537ea1ac0 --- /dev/null +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift @@ -0,0 +1,108 @@ +// +// CodableDefault+Providers.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 07.04.26. +// + +import AppKit + +// MARK: - Bool Defaults + +public enum DefaultTrue: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = true +} + +public enum DefaultFalse: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = false +} + +// MARK: - Terminal Defaults + +public enum DefaultTerminalShell: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.TerminalShell.system +} + +public enum DefaultTerminalCursorStyle: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.TerminalCursorStyle.block +} + +public enum DefaultTerminalFont: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.TerminalFont() +} + +// MARK: - Navigation Defaults + +public enum DefaultNavigationStyle: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.NavigationStyle.openInTabs +} + +// MARK: - Collection Defaults + +public enum DefaultEmptyGlobPatterns: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue: [GlobPattern] = [] +} + +public enum DefaultEmptyStringDictionary: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue: [String: String] = [:] +} + +public enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue: [String: SettingsData.InstalledLanguageServer] = [:] +} + +// MARK: - Account Defaults + +public enum DefaultGitAccounts: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.GitAccounts() +} + +public enum DefaultEmptySourceControlAccounts: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue: [SourceControlAccount] = [] +} + +public enum DefaultEmptyString: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = "" +} + +// MARK: - General Settings Defaults + +public enum DefaultAppearance: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.Appearances.system +} + +public enum DefaultIssues: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.Issues.inline +} + +public enum DefaultFileExtensionsVisibility: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.FileExtensionsVisibility.showAll +} + +public enum DefaultFileExtensions: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.FileExtensions.default +} + +public enum DefaultFileIconStyle: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.FileIconStyle.color +} + +public enum DefaultSidebarTabBarPositionTop: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.SidebarTabBarPosition.top +} + +public enum DefaultReopenBehavior: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.ReopenBehavior.welcome +} + +public enum DefaultReopenWindowBehavior: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.ReopenWindowBehavior.doNothing +} + +public enum DefaultProjectNavigatorSize: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.ProjectNavigatorSize.medium +} + +public enum DefaultNavigatorDetail: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = SettingsData.NavigatorDetail.upTo3 +} diff --git a/CodeEdit/Features/Settings/Models/CodableDefault.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift similarity index 84% rename from CodeEdit/Features/Settings/Models/CodableDefault.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift index ba3752aef8..52f149822b 100644 --- a/CodeEdit/Features/Settings/Models/CodableDefault.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift @@ -11,7 +11,7 @@ import Foundation /// /// Conform to this protocol to define a default value that will be used /// when decoding fails or the key is missing from the JSON. -protocol DefaultValueProvider { +public protocol DefaultValueProvider { associatedtype Value: Codable & Hashable static var defaultValue: Value { get } } @@ -30,19 +30,19 @@ protocol DefaultValueProvider { /// With this wrapper, you no longer need a custom `init(from:)` for handling /// missing keys — Swift's auto-synthesized decoder handles it automatically. @propertyWrapper -struct CodableDefault: Codable, Hashable { - var wrappedValue: Provider.Value +public struct CodableDefault: Codable, Hashable { + public var wrappedValue: Provider.Value - init(wrappedValue: Provider.Value) { + public init(wrappedValue: Provider.Value) { self.wrappedValue = wrappedValue } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = (try? container.decode(Provider.Value.self)) ?? Provider.defaultValue } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(wrappedValue) } diff --git a/CodeEdit/Features/Settings/Models/Settings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Settings.swift similarity index 89% rename from CodeEdit/Features/Settings/Models/Settings.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Settings.swift index 0d638c0ec0..3c66ca9106 100644 --- a/CodeEdit/Features/Settings/Models/Settings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Settings.swift @@ -16,10 +16,10 @@ import Combine /// @StateObject /// private var prefs: SettingsModel = .shared /// ``` -final class Settings: ObservableObject { +public final class Settings: ObservableObject { /// The publicly available singleton instance of ``SettingsModel`` - static let shared: Settings = .init() + nonisolated(unsafe) public static let shared: Settings = .init() private var storeTask: AnyCancellable! @@ -32,7 +32,7 @@ final class Settings: ObservableObject { } } - static subscript(_ path: WritableKeyPath, suite: Settings = .shared) -> T { + public static subscript(_ path: WritableKeyPath, suite: Settings = .shared) -> T { get { suite.preferences[keyPath: path] } @@ -44,7 +44,7 @@ final class Settings: ObservableObject { /// Published instance of the ``Settings`` model. /// /// Changes are saved automatically. - @Published var preferences: SettingsData + @Published public var preferences: SettingsData /// Load and construct ``Settings`` model from /// `~/Library/Application Support/CodeEdit/settings.json` @@ -77,7 +77,7 @@ final class Settings: ObservableObject { /// The base URL of settings. /// /// Points to `~/Library/Application Support/CodeEdit/` - internal var baseURL: URL { + public var baseURL: URL { filemanager .homeDirectoryForCurrentUser .appending(path: "Library/Application Support/CodeEdit", directoryHint: .isDirectory) From 722cc60038df443ce22d35a2b80396ccec6202e9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 10:34:48 +0200 Subject: [PATCH 104/335] Test: Import CodeEditSettings in TaskManagerTests after settings extraction --- CodeEditTests/Features/Tasks/TaskManagerTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index ab1a1b4b6e..729fd08cc7 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditSettings import Testing import CodeEditCore @testable import CodeEdit From 076a326f58356be9a54896b438eb29678314141c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 13:10:38 +0200 Subject: [PATCH 105/335] Feat: Add FindReplaceQuery primitive to CodeEditCore --- .../Infrastructure/FindReplaceQuery.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift new file mode 100644 index 0000000000..f5ae4a4126 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift @@ -0,0 +1,18 @@ +// +// FindReplaceQuery.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom. +// + +import Foundation + +/// Shared find/replace query text, editable by both the Search and Editor features without +/// either depending on the other. Owned by Search's `SearchState` (workspace-scoped); Editor +/// mirrors it into each open file's find panel. +public final class FindReplaceQuery: ObservableObject { + @Published public var searchQuery: String = "" + @Published public var replaceText: String = "" + + public init() {} +} From 99fe92f46b82123d74cdaa7dfd55a301594b1136 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 13:13:32 +0200 Subject: [PATCH 106/335] Feat: Bridge SearchState to the shared FindReplaceQuery primitive --- ...nt+SearchState+FindReplaceQueryTests.swift | 64 +++++++++++++++++++ .../Search/SearchState/SearchState.swift | 46 +++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift new file mode 100644 index 0000000000..5b88774d03 --- /dev/null +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift @@ -0,0 +1,64 @@ +// +// WorkspaceDocument+SearchState+FindReplaceQueryTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom. +// + +import XCTest +import CodeEditCore +import Search + +final class FindReplaceQueryBridgeTests: XCTestCase { + private var directory: URL! + private var searchState: SearchState! + + override func setUp() async throws { + directory = try FileManager.default.url( + for: .developerApplicationDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + .appending(path: "CodeEdit", directoryHint: .isDirectory) + .appending(path: "FindReplaceQueryBridgeTests", directoryHint: .isDirectory) + try? FileManager.default.removeItem(at: directory) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + + searchState = SearchState(workspaceURL: directory) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: directory) + searchState = nil + } + + func testSearchQueryStartsEmptyOnBothSides() { + XCTAssertEqual(searchState.searchQuery, "") + XCTAssertEqual(searchState.query.searchQuery, "") + } + + func testSearchQuerySyncsToFindReplaceQuery() async { + searchState.searchQuery = "hello" + try? await Task.sleep(nanoseconds: 100_000_000) + XCTAssertEqual(searchState.query.searchQuery, "hello") + } + + func testFindReplaceQuerySyncsBackToSearchQuery() async { + searchState.query.searchQuery = "world" + try? await Task.sleep(nanoseconds: 100_000_000) + XCTAssertEqual(searchState.searchQuery, "world") + } + + func testReplaceTextSyncsToFindReplaceQuery() async { + searchState.replaceText = "replacement" + try? await Task.sleep(nanoseconds: 100_000_000) + XCTAssertEqual(searchState.query.replaceText, "replacement") + } + + func testFindReplaceQueryReplaceTextSyncsBack() async { + searchState.query.replaceText = "other" + try? await Task.sleep(nanoseconds: 100_000_000) + XCTAssertEqual(searchState.replaceText, "other") + } +} diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift index 5cea43cef8..91dfbb0399 100644 --- a/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift +++ b/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift @@ -7,6 +7,7 @@ import Foundation import CodeEditCore +import Combine import Factory /// Manages the search/find state for a workspace, including indexing, search results, @@ -35,6 +36,12 @@ public final class SearchState: ObservableObject { @Published public var searchQuery: String = "" @Published public var replaceText: String = "" + /// The find/replace primitive shared with the Editor feature, kept in sync with + /// `searchQuery`/`replaceText` below. See `Packages/Foundation/CodeEditCore`. + public let query = FindReplaceQuery() + + private var queryBridgeCancellables: Set = [] + @Published public var indexStatus: IndexStatus = .none @Published public var findNavigatorStatus: FindNavigatorStatus = .none @@ -59,6 +66,45 @@ public final class SearchState: ObservableObject { self.workspaceURL = workspaceURL self.indexer = SearchIndexer.Memory.create() addProjectToIndex() + bridgeFindReplaceQuery() + } + + /// Keeps `searchQuery`/`replaceText` and `query` in sync in both directions, so Editor can + /// depend on `query` (a `CodeEditCore` type) without importing this feature. + private func bridgeFindReplaceQuery() { + query.$searchQuery + .receive(on: RunLoop.main) + .sink { [weak self] newQuery in + if self?.searchQuery != newQuery { + self?.searchQuery = newQuery + } + } + .store(in: &queryBridgeCancellables) + $searchQuery + .receive(on: RunLoop.main) + .sink { [weak self] newQuery in + if self?.query.searchQuery != newQuery { + self?.query.searchQuery = newQuery + } + } + .store(in: &queryBridgeCancellables) + + query.$replaceText + .receive(on: RunLoop.main) + .sink { [weak self] newText in + if self?.replaceText != newText { + self?.replaceText = newText + } + } + .store(in: &queryBridgeCancellables) + $replaceText + .receive(on: RunLoop.main) + .sink { [weak self] newText in + if self?.query.replaceText != newText { + self?.query.replaceText = newText + } + } + .store(in: &queryBridgeCancellables) } /// Represents the compare options to be used for find and replace. From 0f8869978fbf79a66f5e55acb9d953dfc8cb64df Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 13:37:58 +0200 Subject: [PATCH 107/335] Fix: Wait for indexing to settle in FindReplaceQueryBridgeTests setUp Racing a fresh SearchState's background indexing against the next test's directory teardown/recreate on the same path caused intermittent host process crashes. Poll-until-timeout also replaces fixed sleeps for the sync assertions themselves. --- ...nt+SearchState+FindReplaceQueryTests.swift | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift index 5b88774d03..52cf9da6b5 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift @@ -26,6 +26,18 @@ final class FindReplaceQueryBridgeTests: XCTestCase { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) searchState = SearchState(workspaceURL: directory) + + // Wait for indexing to settle before the test starts mutating state, to avoid racing + // background indexing work against the next test's setUp/tearDown on the same directory. + let startTime = Date() + let timeoutInSeconds = 2.0 + while searchState.indexStatus != .done { + try? await Task.sleep(nanoseconds: 100_000_000) + if Date().timeIntervalSince(startTime) > timeoutInSeconds { + XCTFail("TIMEOUT: Indexing took too long or did not complete.") + return + } + } } override func tearDown() { @@ -40,25 +52,38 @@ final class FindReplaceQueryBridgeTests: XCTestCase { func testSearchQuerySyncsToFindReplaceQuery() async { searchState.searchQuery = "hello" - try? await Task.sleep(nanoseconds: 100_000_000) + await waitUntil { self.searchState.query.searchQuery == "hello" } XCTAssertEqual(searchState.query.searchQuery, "hello") } func testFindReplaceQuerySyncsBackToSearchQuery() async { searchState.query.searchQuery = "world" - try? await Task.sleep(nanoseconds: 100_000_000) + await waitUntil { self.searchState.searchQuery == "world" } XCTAssertEqual(searchState.searchQuery, "world") } func testReplaceTextSyncsToFindReplaceQuery() async { searchState.replaceText = "replacement" - try? await Task.sleep(nanoseconds: 100_000_000) + await waitUntil { self.searchState.query.replaceText == "replacement" } XCTAssertEqual(searchState.query.replaceText, "replacement") } func testFindReplaceQueryReplaceTextSyncsBack() async { searchState.query.replaceText = "other" - try? await Task.sleep(nanoseconds: 100_000_000) + await waitUntil { self.searchState.replaceText == "other" } XCTAssertEqual(searchState.replaceText, "other") } + + /// Polls `condition` until it's true or 2 seconds elapse, yielding to the run loop between checks so + /// `.receive(on: RunLoop.main)`-scheduled Combine work actually gets a chance to run. + private func waitUntil(timeout: TimeInterval = 2, _ condition: @escaping () -> Bool) async { + let startTime = Date() + while !condition() { + try? await Task.sleep(nanoseconds: 20_000_000) + if Date().timeIntervalSince(startTime) > timeout { + XCTFail("TIMEOUT: Condition did not become true within \(timeout) seconds.") + return + } + } + } } From 9a4cf3ff11bb776ecc054b72560a5fa5c049fef6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 13:38:06 +0200 Subject: [PATCH 108/335] Refactor: Sever Editor's import of Search via the shared FindReplaceQuery primitive --- .../Editor/Models/Editor/Editor.swift | 25 ++++++------- .../Editor/Models/EditorInstance.swift | 35 +++++++++---------- .../EditorLayout+StateRestoration.swift | 13 ++++--- .../TabBar/Tabs/Tab/EditorTabView.swift | 2 +- .../Views/EditorTabBarContextMenu.swift | 2 +- .../EditorTabBarTrailingAccessories.swift | 2 +- .../UseCases/RestoreEditorStateUseCase.swift | 21 +++++------ .../Editor/Views/WindowCodeFileView.swift | 2 +- .../Views/OpenQuicklyPreviewView.swift | 2 +- .../Features/Workspace/WorkspaceFactory.swift | 2 +- 10 files changed, 53 insertions(+), 53 deletions(-) diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/CodeEdit/Features/Editor/Models/Editor/Editor.swift index 2730f7d4b6..011b8ac591 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor/Editor.swift @@ -7,7 +7,6 @@ import Foundation import CodeEditCore -import Search import OrderedCollections import DequeModule import AppKit @@ -62,7 +61,7 @@ final class Editor: ObservableObject, Identifiable { var id = UUID() weak var parent: SplitViewData? - weak var searchState: SearchState? + weak var findReplaceQuery: FindReplaceQuery? weak var editorManager: EditorManager? /// Whether this editor is attached to a workspace. Used to guard file loading operations. @@ -74,7 +73,7 @@ final class Editor: ObservableObject, Identifiable { self.tabs = [] self.temporaryTab = nil self.parent = nil - self.searchState = nil + self.findReplaceQuery = nil } init( @@ -82,17 +81,19 @@ final class Editor: ObservableObject, Identifiable { selectedTab: Tab? = nil, temporaryTab: Tab? = nil, parent: SplitViewData? = nil, - searchState: SearchState? = nil + findReplaceQuery: FindReplaceQuery? = nil ) { self.parent = parent - self.searchState = searchState + self.findReplaceQuery = findReplaceQuery // If we open the files without a valid workspace, we risk creating a file we lose track of but stays in memory if isAttachedToWorkspace { files.forEach { openTab(file: $0) } } else { - self.tabs = OrderedSet(files.map { EditorInstance(searchState: searchState, file: $0) }) + self.tabs = OrderedSet(files.map { EditorInstance(findReplaceQuery: findReplaceQuery, file: $0) }) } - self.selectedTab = selectedTab ?? (files.isEmpty ? nil : Tab(searchState: searchState, file: files.first!)) + self.selectedTab = selectedTab ?? ( + files.isEmpty ? nil : Tab(findReplaceQuery: findReplaceQuery, file: files.first!) + ) self.temporaryTab = temporaryTab } @@ -101,11 +102,11 @@ final class Editor: ObservableObject, Identifiable { selectedTab: Tab? = nil, temporaryTab: Tab? = nil, parent: SplitViewData? = nil, - searchState: SearchState? = nil + findReplaceQuery: FindReplaceQuery? = nil ) { self.tabs = [] self.parent = parent - self.searchState = searchState + self.findReplaceQuery = findReplaceQuery files.forEach { openTab(file: $0.file) } self.selectedTab = selectedTab ?? tabs.first self.temporaryTab = temporaryTab @@ -158,7 +159,7 @@ final class Editor: ObservableObject, Identifiable { clearFuture() } if file != selectedTab?.file { - addToHistory(EditorInstance(searchState: searchState, file: file)) + addToHistory(EditorInstance(findReplaceQuery: findReplaceQuery, file: file)) } removeTab(file) if let selectedTab { @@ -188,7 +189,7 @@ final class Editor: ObservableObject, Identifiable { /// - file: the file to open. /// - asTemporary: indicates whether the tab should be opened as a temporary tab or a permanent tab. func openTab(file: CEWorkspaceFile, asTemporary: Bool) { - let item = EditorInstance(searchState: searchState, file: file) + let item = EditorInstance(findReplaceQuery: findReplaceQuery, file: file) // Item is already opened in a tab. guard !tabs.contains(item) || !asTemporary else { selectedTab = item @@ -246,7 +247,7 @@ final class Editor: ObservableObject, Identifiable { /// - index: Index where the tab needs to be added. If nil, it is added to the back. /// - fromHistory: Indicates whether the tab has been opened from going back in history. func openTab(file: CEWorkspaceFile, at index: Int? = nil, fromHistory: Bool = false) { - let item = Tab(searchState: searchState, file: file) + let item = Tab(findReplaceQuery: findReplaceQuery, file: file) if let index { tabs.insert(item, at: index) } else { diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index e7f422ec9d..72b49f8dad 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -7,7 +7,6 @@ import Foundation import CodeEditCore -import Search import AppKit import Combine import CodeEditTextView @@ -35,14 +34,14 @@ class EditorInstance: ObservableObject, Hashable { // MARK: - Init - init(searchState: SearchState?, file: CEWorkspaceFile, cursorPositions: [CursorPosition]? = nil) { + init(findReplaceQuery: FindReplaceQuery?, file: CEWorkspaceFile, cursorPositions: [CursorPosition]? = nil) { self.file = file let url = file.url let editorState = EditorStateRestoration.shared?.restorationState(for: url) - findText = searchState?.searchQuery + findText = findReplaceQuery?.searchQuery findTextSubject = PassthroughSubject() - replaceText = searchState?.replaceText + replaceText = findReplaceQuery?.replaceText replaceTextSubject = PassthroughSubject() self.cursorPositions = ( @@ -66,14 +65,14 @@ class EditorInstance: ObservableObject, Hashable { } .store(in: &cancellables) - listenToFindText(searchState: searchState) - listenToReplaceText(searchState: searchState) + listenToFindText(findReplaceQuery: findReplaceQuery) + listenToReplaceText(findReplaceQuery: findReplaceQuery) } // MARK: - Find/Replace Listeners - func listenToFindText(searchState: SearchState?) { - searchState?.$searchQuery + func listenToFindText(findReplaceQuery: FindReplaceQuery?) { + findReplaceQuery?.$searchQuery .receive(on: RunLoop.main) .sink { [weak self] newQuery in if self?.findText != newQuery { @@ -83,17 +82,17 @@ class EditorInstance: ObservableObject, Hashable { .store(in: &cancellables) findTextSubject .receive(on: RunLoop.main) - .sink { [weak searchState, weak self] newFindText in - if let newFindText, searchState?.searchQuery != newFindText { - searchState?.searchQuery = newFindText + .sink { [weak findReplaceQuery, weak self] newFindText in + if let newFindText, findReplaceQuery?.searchQuery != newFindText { + findReplaceQuery?.searchQuery = newFindText } - self?.findText = searchState?.searchQuery + self?.findText = findReplaceQuery?.searchQuery } .store(in: &cancellables) } - func listenToReplaceText(searchState: SearchState?) { - searchState?.$replaceText + func listenToReplaceText(findReplaceQuery: FindReplaceQuery?) { + findReplaceQuery?.$replaceText .receive(on: RunLoop.main) .sink { [weak self] newText in if self?.replaceText != newText { @@ -103,11 +102,11 @@ class EditorInstance: ObservableObject, Hashable { .store(in: &cancellables) replaceTextSubject .receive(on: RunLoop.main) - .sink { [weak searchState, weak self] newReplaceText in - if let newReplaceText, searchState?.replaceText != newReplaceText { - searchState?.replaceText = newReplaceText + .sink { [weak findReplaceQuery, weak self] newReplaceText in + if let newReplaceText, findReplaceQuery?.replaceText != newReplaceText { + findReplaceQuery?.replaceText = newReplaceText } - self?.replaceText = searchState?.replaceText + self?.replaceText = findReplaceQuery?.replaceText } .store(in: &cancellables) } diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 7221a09a38..993d38d592 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -8,7 +8,6 @@ import Foundation import CEWorkspaceFileManager import CodeEditCore -import Search import SwiftUI import OrderedCollections @@ -17,16 +16,16 @@ extension EditorManager { /// - Parameters: /// - statePersistence: The persistence service to retrieve saved state from. /// - fileManager: The file manager to resolve file references. - /// - searchState: The search state for editor instances. + /// - findReplaceQuery: The shared find/replace query for editor instances. func restoreFromState( statePersistence: any WorkspaceStatePersisting, fileManager: CEWorkspaceFileManager?, - searchState: SearchState? + findReplaceQuery: FindReplaceQuery? ) { defer { // No matter what, set up each editor. Even if we fail to read data. flattenedEditors.forEach { editor in - editor.searchState = searchState + editor.findReplaceQuery = findReplaceQuery editor.editorManager = self editor.isAttachedToWorkspace = true } @@ -36,7 +35,7 @@ extension EditorManager { switch useCase.execute( statePersistence: statePersistence, fileManager: fileManager, - searchState: searchState, + findReplaceQuery: findReplaceQuery, editorManager: self ) { case .restored(let layout, let activeEditor): @@ -163,11 +162,11 @@ extension Editor: Codable { self.init( files: OrderedSet(fileURLs.map { CEWorkspaceFile(url: $0) }), selectedTab: selectedTab == nil ? nil : EditorInstance( - searchState: nil, + findReplaceQuery: nil, file: CEWorkspaceFile(url: selectedTab!) ), parent: nil, - searchState: nil + findReplaceQuery: nil ) self.id = id } diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift index 290c45c502..e90dd79bbd 100644 --- a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift @@ -97,7 +97,7 @@ struct EditorTabView: View { // Only set the `selectedId` when they are not equal to avoid performance issue for now. editorManager.activeEditor = editor if editor.selectedTab?.file != tabFile { - let tabItem = EditorInstance(searchState: editor.searchState, file: tabFile) + let tabItem = EditorInstance(findReplaceQuery: editor.findReplaceQuery, file: tabFile) editor.setSelectedTab(tabFile) editor.clearFuture() editor.addToHistory(tabItem) diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index bb3a602554..6b2a5f6174 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -144,7 +144,7 @@ struct EditorTabBarContextMenu: ViewModifier { } func moveToNewSplit(_ edge: Edge) { - let newEditor = Editor(files: [item], searchState: tabs.searchState) + let newEditor = Editor(files: [item], findReplaceQuery: tabs.findReplaceQuery) newEditor.editorManager = editorManager splitEditor(edge, newEditor) tabs.closeTab(file: item) diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 6d9fe32a7c..996f220f76 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -100,7 +100,7 @@ struct EditorTabBarTrailingAccessories: View { func split(edge: Edge) { let newEditor: Editor if let tab = editor.selectedTab { - newEditor = .init(files: [tab], temporaryTab: tab, searchState: editor.searchState) + newEditor = .init(files: [tab], temporaryTab: tab, findReplaceQuery: editor.findReplaceQuery) } else { newEditor = .init() } diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift index 77acd2ae1b..24909f982b 100644 --- a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift +++ b/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -8,7 +8,6 @@ import Foundation import CEWorkspaceFileManager import CodeEditCore -import Search import OSLog import OrderedCollections @@ -30,7 +29,7 @@ final class RestoreEditorStateUseCase { func execute( statePersistence: any WorkspaceStatePersisting, fileManager: CEWorkspaceFileManager?, - searchState: SearchState?, + findReplaceQuery: FindReplaceQuery?, editorManager: EditorManager ) -> Outcome { guard let data = statePersistence.get(.openTabs) as? Data else { @@ -55,7 +54,7 @@ final class RestoreEditorStateUseCase { try fixRestoredEditorLayout( state.groups, fileManager: fileManager, - searchState: searchState, + findReplaceQuery: findReplaceQuery, editorManager: editorManager ) @@ -72,22 +71,24 @@ final class RestoreEditorStateUseCase { private func fixRestoredEditorLayout( _ group: EditorLayout, fileManager: CEWorkspaceFileManager?, - searchState: SearchState?, + findReplaceQuery: FindReplaceQuery?, editorManager: EditorManager ) throws { switch group { case let .one(data): - try fixEditor(data, fileManager: fileManager, searchState: searchState, editorManager: editorManager) + try fixEditor( + data, fileManager: fileManager, findReplaceQuery: findReplaceQuery, editorManager: editorManager + ) case let .vertical(splitData): try splitData.editorLayouts.forEach { group in try fixRestoredEditorLayout( - group, fileManager: fileManager, searchState: searchState, editorManager: editorManager + group, fileManager: fileManager, findReplaceQuery: findReplaceQuery, editorManager: editorManager ) } case let .horizontal(splitData): try splitData.editorLayouts.forEach { group in try fixRestoredEditorLayout( - group, fileManager: fileManager, searchState: searchState, editorManager: editorManager + group, fileManager: fileManager, findReplaceQuery: findReplaceQuery, editorManager: editorManager ) } } @@ -98,20 +99,20 @@ final class RestoreEditorStateUseCase { private func fixEditor( _ editor: Editor, fileManager: CEWorkspaceFileManager?, - searchState: SearchState?, + findReplaceQuery: FindReplaceQuery?, editorManager: EditorManager ) throws { guard let fileManager else { return } let resolvedTabs = editor .tabs .compactMap({ fileManager.getFile($0.file.url.path(percentEncoded: false), createIfNotFound: true) }) - .map({ EditorInstance(searchState: searchState, file: $0) }) + .map({ EditorInstance(findReplaceQuery: findReplaceQuery, file: $0) }) for tab in resolvedTabs { try editorManager.loadDocument(for: tab.file) } - editor.searchState = searchState + editor.findReplaceQuery = findReplaceQuery editor.editorManager = editorManager editor.isAttachedToWorkspace = true editor.tabs = OrderedSet(resolvedTabs) diff --git a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift index c1aa1243ef..a2ea975ec1 100644 --- a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift @@ -20,7 +20,7 @@ struct WindowCodeFileView: View { init(codeFile: CodeFileDocument) { self._editorInstance = .init( wrappedValue: EditorInstance( - searchState: nil, + findReplaceQuery: nil, file: CEWorkspaceFile(url: codeFile.fileURL ?? URL(fileURLWithPath: "")) ) ) diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift index 50873e6688..818f745a07 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift @@ -26,7 +26,7 @@ struct OpenQuicklyPreviewView: View { withContentsOf: item.url, ofType: item.contentType?.identifier ?? "public.source-code" ) - self._editorInstance = .init(wrappedValue: EditorInstance(searchState: nil, file: item)) + self._editorInstance = .init(wrappedValue: EditorInstance(findReplaceQuery: nil, file: item)) self._document = .init(wrappedValue: doc ?? .init()) } diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 6d408a9095..2f66aed53c 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -92,7 +92,7 @@ enum WorkspaceFactory { editorManager.restoreFromState( statePersistence: statePersistence, fileManager: workspaceFileManager, - searchState: workspace.searchState + findReplaceQuery: workspace.searchState?.query ) workspace.utilityAreaModel?.restoreFromState(statePersistence) } From 08664a6da52a8aa892d6adb50abde4896bad80b7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 16:25:55 +0200 Subject: [PATCH 109/335] Feat: Add reveal(file:) to WorkspaceNavigator protocol --- .../Services/AppWorkspaceNavigator.swift | 5 +++++ .../Workspace/AppWorkspaceNavigatorTests.swift | 17 ++++++++++++++++- .../Infrastructure/WorkspaceNavigator.swift | 4 ++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift index 2e2deb61fe..8b58c9067a 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -24,4 +24,9 @@ final class AppWorkspaceNavigator: WorkspaceNavigator { func open(file: CEWorkspaceFile, asTemporary: Bool) { _ = windowManager.openFileInWorkspace(url: file.url, asTemporary: asTemporary) } + + @MainActor + func reveal(file: CEWorkspaceFile) { + windowManager.workspace(containing: file.url)?.listenerModel.highlightedFileItem = file + } } diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index 4fdb03d63d..86acfe9f90 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -16,9 +16,10 @@ struct AppWorkspaceNavigatorTests { final class MockWindowManager: WorkspaceWindowManaging { var opened: [(url: URL, asTemporary: Bool)] = [] var openWorkspaces: [Workspace] = [] + var stubbedWorkspace: Workspace? func openWorkspace(at url: URL) throws {} func closeWorkspace(_ workspace: Workspace) {} - func workspace(containing url: URL) -> Workspace? { nil } + func workspace(containing url: URL) -> Workspace? { stubbedWorkspace } func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool { opened.append((url, asTemporary)) return true @@ -38,4 +39,18 @@ struct AppWorkspaceNavigatorTests { #expect(mock.opened.first?.url == file.url) #expect(mock.opened.first?.asTemporary == true) } + + @MainActor + @Test + func revealSetsHighlightedFileItemOnCorrectWorkspace() { + let workspace = Workspace() + let mock = MockWindowManager() + mock.stubbedWorkspace = workspace + let navigator = AppWorkspaceNavigator(windowManager: mock) + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/example.swift")) + + navigator.reveal(file: file) + + #expect(workspace.listenerModel.highlightedFileItem === file) + } } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift index 0ca3f1af9d..7af60238f0 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift @@ -14,10 +14,14 @@ public protocol WorkspaceNavigator: AnyObject { /// - Parameter asTemporary: open as a temporary (preview) tab, replaced by the next /// temporary open, rather than a pinned tab. @MainActor func open(file: CEWorkspaceFile, asTemporary: Bool) + + /// Highlight `file` in the project navigator without opening it. + @MainActor func reveal(file: CEWorkspaceFile) } /// Default no-op used until the app registers a real implementation. public final class NoOpWorkspaceNavigator: WorkspaceNavigator { public init() {} @MainActor public func open(file: CEWorkspaceFile, asTemporary: Bool) {} + @MainActor public func reveal(file: CEWorkspaceFile) {} } From feaa0a91db81214709f8157fa53b3f13e59b2db2 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 16:26:27 +0200 Subject: [PATCH 110/335] Refactor: Sever Editor's WorkspaceNotificationModel dependency via WorkspaceNavigator.reveal --- .../Editor/TabBar/Views/EditorTabBarContextMenu.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift index 6b2a5f6174..f7d76781f9 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -8,6 +8,7 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore +import Factory import Foundation extension View { @@ -27,8 +28,6 @@ struct EditorTabBarContextMenu: ViewModifier { @EnvironmentObject var editorManager: EditorManager - @EnvironmentObject var listenerModel: WorkspaceNotificationModel - @Environment(\.workspaceFileManager) private var workspaceFileManager @@ -108,7 +107,7 @@ struct EditorTabBarContextMenu: ViewModifier { } Button("Reveal in Project Navigator") { - listenerModel.highlightedFileItem = item + Container.shared.workspaceNavigator().reveal(file: item) } Button("Open in New Window") { From 832e7c02774eb5e43c03d1141f9a22804770427c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 16:28:12 +0200 Subject: [PATCH 111/335] Refactor: Promote StatusBarView.height to CodeEditUI.LayoutMetrics.statusBarHeight --- .../Features/Editor/Views/EditorAreaFileView.swift | 3 ++- .../Features/Editor/Views/EditorLayoutView.swift | 5 +++-- .../Features/StatusBar/Views/StatusBarView.swift | 3 ++- .../Sources/CodeEditUI/LayoutMetrics.swift | 12 ++++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index 837ffa043d..bddd7e2058 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -7,6 +7,7 @@ import AppKit import CodeEditDocument +import CodeEditUI import AVKit import CodeEditSourceEditor import SwiftUI @@ -32,7 +33,7 @@ struct EditorAreaFileView: View { } else { NonTextFileView(fileDocument: codeFile) .padding(.top, edgeInsets.top - 1.74) - .padding(.bottom, StatusBarView.height + 1.26) + .padding(.bottom, LayoutMetrics.statusBarHeight + 1.26) .modifier(UpdateStatusBarInfo(with: codeFile.fileURL)) .onDisappear { statusBarViewModel.dimensions = nil diff --git a/CodeEdit/Features/Editor/Views/EditorLayoutView.swift b/CodeEdit/Features/Editor/Views/EditorLayoutView.swift index aa7fa3252c..026da785bd 100644 --- a/CodeEdit/Features/Editor/Views/EditorLayoutView.swift +++ b/CodeEdit/Features/Editor/Views/EditorLayoutView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct EditorLayoutView: View { var layout: EditorLayout @@ -31,11 +32,11 @@ struct EditorLayoutView: View { switch isAtEdge { case .all: insets.top += toolbarHeight - insets.bottom += StatusBarView.height + 5 + insets.bottom += LayoutMetrics.statusBarHeight + 5 case .top: insets.top += toolbarHeight case .bottom: - insets.bottom += StatusBarView.height + 5 + insets.bottom += LayoutMetrics.statusBarHeight + 5 default: return } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarView.swift b/CodeEdit/Features/StatusBar/Views/StatusBarView.swift index cb73012d8a..a8ce41ff0c 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarView.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI /// # StatusBarView /// @@ -22,7 +23,7 @@ struct StatusBarView: View { @Environment(\.controlActiveState) private var controlActive - static let height = 28.0 + static let height = LayoutMetrics.statusBarHeight @Environment(\.colorScheme) private var colorScheme diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift new file mode 100644 index 0000000000..0b68692593 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift @@ -0,0 +1,12 @@ +// +// LayoutMetrics.swift +// CodeEditUI +// +// Created by Matthijs Eikelenboom. +// + +import CoreGraphics + +public enum LayoutMetrics { + public static let statusBarHeight: CGFloat = 28.0 +} From 4b2351d93e800d15849e8b7a01f8c724ad0f77dd Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 16:29:19 +0200 Subject: [PATCH 112/335] Refactor: Move StatusBar file-info computation into StatusBarFileInfoView and sever Editor dependency --- .../Editor/Views/EditorAreaFileView.swift | 6 -- .../ViewModifiers/UpdateStatusBarInfo.swift | 72 ------------------- .../StatusBarFileInfoView.swift | 36 ++++++++-- 3 files changed, 32 insertions(+), 82 deletions(-) delete mode 100644 CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index bddd7e2058..30cf6dcaae 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -16,7 +16,6 @@ struct EditorAreaFileView: View { @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var editor: Editor - @EnvironmentObject private var statusBarViewModel: StatusBarViewModel @Environment(\.edgeInsets) private var edgeInsets @@ -34,11 +33,6 @@ struct EditorAreaFileView: View { NonTextFileView(fileDocument: codeFile) .padding(.top, edgeInsets.top - 1.74) .padding(.bottom, LayoutMetrics.statusBarHeight + 1.26) - .modifier(UpdateStatusBarInfo(with: codeFile.fileURL)) - .onDisappear { - statusBarViewModel.dimensions = nil - statusBarViewModel.fileSize = nil - } } } diff --git a/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift b/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift deleted file mode 100644 index b56c11ae5c..0000000000 --- a/CodeEdit/Features/StatusBar/ViewModifiers/UpdateStatusBarInfo.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// UpdateStatusBarInfo.swift -// CodeEdit -// -// Created by Paul Ebose on 2024/5/12. -// - -import SwiftUI -import CodeEditCore - -/// Updates ``StatusBarFileInfoView``'s `fileSize` and `dimensions`. -/// ```swift -/// FileView -/// .modifier(UpdateStatusBarInfo(withURL)) -/// ``` -struct UpdateStatusBarInfo: ViewModifier { - - /// The URL of the file to compute information from. - let fileURL: URL? - - init(with fileURL: URL?) { - self.fileURL = fileURL - } - - @Environment(\.activeEditorState) private var activeEditorState - @EnvironmentObject private var statusBarViewModel: StatusBarViewModel - - /// This is returned by ``UpdateStatusBarInfo`` `.computeStatusBarInfo`. - private struct ComputedStatusBarInfo { - let fileSize: Int - let dimensions: ImageDimensions? - } - - /// Compute information that can be used to update properties in ``StatusBarFileInfoView``. - /// - Parameter with fileURL: URL of the file to compute information from. - /// - Returns: The file size and its image dimensions (if any). - private func computeStatusBarInfo(with fileURL: URL) -> ComputedStatusBarInfo? { - guard let resourceValues = try? fileURL.resourceValues(forKeys: [.contentTypeKey, .fileSizeKey]), - let contentType = resourceValues.contentType, - let fileSize = resourceValues.fileSize - else { - return nil - } - - if contentType.conforms(to: .image), let imageReps = NSImage(contentsOf: fileURL)?.representations.first { - let dimensions = ImageDimensions(width: imageReps.pixelsWide, height: imageReps.pixelsHigh) - return ComputedStatusBarInfo(fileSize: fileSize, dimensions: dimensions) - } else { // non-image file - return ComputedStatusBarInfo(fileSize: fileSize, dimensions: nil) - } - } - - func body(content: Content) -> some View { - if let fileURL { - content - .onAppear { - let statusBarInfo = computeStatusBarInfo(with: fileURL) - statusBarViewModel.fileSize = statusBarInfo?.fileSize - statusBarViewModel.dimensions = statusBarInfo?.dimensions - } - .onReceive(activeEditorState.selectedFilePublisher) { newFile in - guard let newFile else { return } - let statusBarInfo = computeStatusBarInfo(with: newFile.url) - statusBarViewModel.fileSize = statusBarInfo?.fileSize - statusBarViewModel.dimensions = statusBarInfo?.dimensions - } - } else { - content - } - } - -} diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift index 58d6896d64..255daf37ab 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift @@ -6,13 +6,14 @@ // import SwiftUI +import AppKit +import CodeEditCore +import UniformTypeIdentifiers -/// Shows media information about the currently opened file. -/// -/// This currently shows the file size and image dimensions, if available. struct StatusBarFileInfoView: View { @EnvironmentObject private var statusBarViewModel: StatusBarViewModel + @Environment(\.activeEditorState) private var activeEditorState private let dimensionsNumberStyle = IntegerFormatStyle(locale: Locale(identifier: "en_US")).grouping(.never) @@ -24,7 +25,7 @@ struct StatusBarFileInfoView: View { let width = dimensionsNumberStyle.format(dimensions.width) let height = dimensionsNumberStyle.format(dimensions.height) - Text("\(width) × \(height)") + Text("\(width) × \(height)") } if let fileSize = statusBarViewModel.fileSize { @@ -34,6 +35,33 @@ struct StatusBarFileInfoView: View { } .font(statusBarViewModel.statusBarFont) .foregroundStyle(statusBarViewModel.foregroundStyle) + .onReceive(activeEditorState.selectedFilePublisher) { file in + updateFileInfo(for: file) + } } + private func updateFileInfo(for file: CEWorkspaceFile?) { + guard let file, + let resourceValues = try? file.url.resourceValues(forKeys: [.contentTypeKey, .fileSizeKey]), + let contentType = resourceValues.contentType, + let fileSize = resourceValues.fileSize, + !contentType.conforms(to: .text) + else { + statusBarViewModel.fileSize = nil + statusBarViewModel.dimensions = nil + return + } + + statusBarViewModel.fileSize = fileSize + + if contentType.conforms(to: .image), + let imageReps = NSImage(contentsOf: file.url)?.representations.first { + statusBarViewModel.dimensions = ImageDimensions( + width: imageReps.pixelsWide, + height: imageReps.pixelsHigh + ) + } else { + statusBarViewModel.dimensions = nil + } + } } From 355774ed518aaa4bf09b09fddbf24ae6f76eced1 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 16:40:51 +0200 Subject: [PATCH 113/335] Fix: Use @State instead of @EnvironmentObject mutation to break re-render loop in StatusBarFileInfoView --- .../StatusBarFileInfoView.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift index 255daf37ab..1eb01d1cf2 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift @@ -15,20 +15,23 @@ struct StatusBarFileInfoView: View { @EnvironmentObject private var statusBarViewModel: StatusBarViewModel @Environment(\.activeEditorState) private var activeEditorState + @State private var fileSize: Int? + @State private var dimensions: ImageDimensions? + private let dimensionsNumberStyle = IntegerFormatStyle(locale: Locale(identifier: "en_US")).grouping(.never) var body: some View { HStack(spacing: 15) { - if let dimensions = statusBarViewModel.dimensions { + if let dimensions { let width = dimensionsNumberStyle.format(dimensions.width) let height = dimensionsNumberStyle.format(dimensions.height) Text("\(width) × \(height)") } - if let fileSize = statusBarViewModel.fileSize { + if let fileSize { Text(fileSize.formatted(.byteCount(style: .memory))) } @@ -44,24 +47,24 @@ struct StatusBarFileInfoView: View { guard let file, let resourceValues = try? file.url.resourceValues(forKeys: [.contentTypeKey, .fileSizeKey]), let contentType = resourceValues.contentType, - let fileSize = resourceValues.fileSize, + let newFileSize = resourceValues.fileSize, !contentType.conforms(to: .text) else { - statusBarViewModel.fileSize = nil - statusBarViewModel.dimensions = nil + fileSize = nil + dimensions = nil return } - statusBarViewModel.fileSize = fileSize + fileSize = newFileSize if contentType.conforms(to: .image), let imageReps = NSImage(contentsOf: file.url)?.representations.first { - statusBarViewModel.dimensions = ImageDimensions( + dimensions = ImageDimensions( width: imageReps.pixelsWide, height: imageReps.pixelsHigh ) } else { - statusBarViewModel.dimensions = nil + dimensions = nil } } } From 4b66d342565a90d1362d83e5ac0d1d73f79867a2 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 16:41:41 +0200 Subject: [PATCH 114/335] Chore: Remove unused fileSize and dimensions from StatusBarViewModel --- .../Features/StatusBar/ViewModels/StatusBarViewModel.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift b/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift index c7f00ae929..7abca6d29e 100644 --- a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift +++ b/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift @@ -9,12 +9,6 @@ import SwiftUI final class StatusBarViewModel: ObservableObject { - /// The file size of the currently opened file. - @Published var fileSize: Int? - - /// The dimensions (width x height) of the currently opened image. - @Published var dimensions: ImageDimensions? - /// Indicates whether the breakpoint is enabled or not. @Published var isBreakpointEnabled = true From 8926ae66cd564316f854fa882f5d50de62af0359 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 17:55:20 +0200 Subject: [PATCH 115/335] Refactor: Sever NavigatorArea's Editor dependency via WorkspaceNavigator.closeTab --- .../ProjectNavigatorMenuActions.swift | 4 ++-- .../ProjectNavigatorOutlineView.swift | 1 - .../ProjectNavigatorViewController.swift | 1 - .../Services/AppWorkspaceNavigator.swift | 5 +++++ .../Workspace/AppWorkspaceNavigatorTests.swift | 18 ++++++++++++++++++ .../Infrastructure/WorkspaceNavigator.swift | 4 ++++ 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 8b4b28d434..65bac51161 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -193,7 +193,7 @@ extension ProjectNavigatorMenu { do { try selectedItems().forEach { item in withAnimation { - sender.editor?.closeTab(file: item) + Container.shared.workspaceNavigator().closeTab(file: item) } guard FileManager.default.fileExists(atPath: item.url.path) else { // Was likely already trashed (eg selecting files in a folder and deleting the folder and files) @@ -242,7 +242,7 @@ extension ProjectNavigatorMenu { withAnimation { selectedItems.forEach { item in - sender.editor?.closeTab(file: item) + Container.shared.workspaceNavigator().closeTab(file: item) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 81a4cf2716..0209ccbe41 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -27,7 +27,6 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { let controller = ProjectNavigatorViewController() controller.workspace = workspace controller.iconColor = prefs.preferences.general.fileIconStyle - controller.editor = editorManager.activeEditor controller.activeEditorState = activeEditorState workspace.workspaceFileManager?.addObserver(context.coordinator) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index f0ec50723d..46b51302a0 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -40,7 +40,6 @@ final class ProjectNavigatorViewController: NSViewController { var expandedItems: Set = [] weak var workspace: Workspace? - weak var editor: Editor? weak var activeEditorState: (any ActiveEditorState)? var iconColor: SettingsData.FileIconStyle = .color { diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift index 8b58c9067a..da8bf9179c 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -29,4 +29,9 @@ final class AppWorkspaceNavigator: WorkspaceNavigator { func reveal(file: CEWorkspaceFile) { windowManager.workspace(containing: file.url)?.listenerModel.highlightedFileItem = file } + + @MainActor + func closeTab(file: CEWorkspaceFile) { + windowManager.workspace(containing: file.url)?.editorManager?.editorLayout.closeAllTabs(of: file) + } } diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index 86acfe9f90..d2b09cd183 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -53,4 +53,22 @@ struct AppWorkspaceNavigatorTests { #expect(workspace.listenerModel.highlightedFileItem === file) } + + @MainActor + @Test + func closeTabClosesFileInEditorLayout() { + let workspace = Workspace() + let editorManager = EditorManager() + workspace.editorManager = editorManager + let mock = MockWindowManager() + mock.stubbedWorkspace = workspace + let navigator = AppWorkspaceNavigator(windowManager: mock) + let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/example.swift")) + + editorManager.activeEditor.openTab(file: file, asTemporary: false) + #expect(editorManager.activeEditor.tabs.contains(where: { $0.file == file })) + + navigator.closeTab(file: file) + #expect(!editorManager.activeEditor.tabs.contains(where: { $0.file == file })) + } } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift index 7af60238f0..ec77fc33ec 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift @@ -17,6 +17,9 @@ public protocol WorkspaceNavigator: AnyObject { /// Highlight `file` in the project navigator without opening it. @MainActor func reveal(file: CEWorkspaceFile) + + /// Close all tabs showing `file` across every editor split. + @MainActor func closeTab(file: CEWorkspaceFile) } /// Default no-op used until the app registers a real implementation. @@ -24,4 +27,5 @@ public final class NoOpWorkspaceNavigator: WorkspaceNavigator { public init() {} @MainActor public func open(file: CEWorkspaceFile, asTemporary: Bool) {} @MainActor public func reveal(file: CEWorkspaceFile) {} + @MainActor public func closeTab(file: CEWorkspaceFile) {} } From 11b17dd7aa5912f66ccaac08d98f3aea0b4eaf4d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 17:57:39 +0200 Subject: [PATCH 116/335] Refactor: Decouple OpenQuickly from Editor via environment-based file preview factory --- .../CodeEditWindowController.swift | 4 +- .../Editor/Views/FilePreviewView.swift | 38 +++++++++++++++++++ .../Views/OpenQuicklyPreviewView.swift | 30 ++------------- .../Models/Environment+Workspace.swift | 9 +++++ 4 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 CodeEdit/Features/Editor/Views/FilePreviewView.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 49b68ca0f7..e183bd543a 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -184,7 +184,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs self.panelOpen = false } openFile: { file in workspace.editorManager?.openTab(item: file) - }.environment(\.workspaceFileManager, workspace.workspaceFileManager) + } + .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.filePreview) { file in AnyView(FilePreviewView(item: file)) } panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) window?.addChildWindow(panel, ordered: .above) diff --git a/CodeEdit/Features/Editor/Views/FilePreviewView.swift b/CodeEdit/Features/Editor/Views/FilePreviewView.swift new file mode 100644 index 0000000000..8b1daca878 --- /dev/null +++ b/CodeEdit/Features/Editor/Views/FilePreviewView.swift @@ -0,0 +1,38 @@ +// +// FilePreviewView.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 2026/07/09. +// + +import SwiftUI +import CodeEditDocument +import CodeEditCore + +struct FilePreviewView: View { + private let item: CEWorkspaceFile + + @StateObject private var editorInstance: EditorInstance + @StateObject private var document: CodeFileDocument + @StateObject private var undoRegistration = UndoManagerRegistration() + + init(item: CEWorkspaceFile) { + self.item = item + let doc = try? CodeFileDocument( + for: item.url, + withContentsOf: item.url, + ofType: item.contentType?.identifier ?? "public.source-code" + ) + self._editorInstance = .init(wrappedValue: EditorInstance(findReplaceQuery: nil, file: item)) + self._document = .init(wrappedValue: doc ?? .init()) + } + + var body: some View { + if let utType = document.utType, utType.conforms(to: .text) { + CodeFileView(editorInstance: editorInstance, codeFile: document, isEditable: false) + .environmentObject(undoRegistration) + } else { + NonTextFileView(fileDocument: document) + } + } +} diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift index 818f745a07..e47c5c43ed 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift @@ -1,41 +1,19 @@ // // OpenQuicklyPreviewView.swift -// CodeEditModules/QuickOpen +// CodeEdit // // Created by Pavel Kasila on 20.03.22. // import SwiftUI -import CodeEditDocument import CodeEditCore struct OpenQuicklyPreviewView: View { + let item: CEWorkspaceFile - private let queue = DispatchQueue(label: "app.codeedit.CodeEdit.quickOpen.preview") - private let item: CEWorkspaceFile - - @StateObject var editorInstance: EditorInstance - @StateObject var document: CodeFileDocument - - @StateObject var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() - - init(item: CEWorkspaceFile) { - self.item = item - let doc = try? CodeFileDocument( - for: item.url, - withContentsOf: item.url, - ofType: item.contentType?.identifier ?? "public.source-code" - ) - self._editorInstance = .init(wrappedValue: EditorInstance(findReplaceQuery: nil, file: item)) - self._document = .init(wrappedValue: doc ?? .init()) - } + @Environment(\.filePreview) private var filePreview var body: some View { - if let utType = document.utType, utType.conforms(to: .text) { - CodeFileView(editorInstance: editorInstance, codeFile: document, isEditable: false) - .environmentObject(undoRegistration) - } else { - NonTextFileView(fileDocument: document) - } + filePreview(item) } } diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index 546b0eb528..cbbd79343a 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -33,6 +33,10 @@ private struct WorkspaceStatePersistenceKey: EnvironmentKey { static let defaultValue: (any WorkspaceStatePersisting)? = nil } +private struct FilePreviewFactoryKey: EnvironmentKey { + static let defaultValue: (CEWorkspaceFile) -> AnyView = { _ in AnyView(EmptyView()) } +} + extension EnvironmentValues { var workspaceFileManager: CEWorkspaceFileManager? { get { self[WorkspaceFileManagerKey.self] } @@ -63,4 +67,9 @@ extension EnvironmentValues { get { self[FileEditorOverridesKey.self] } set { self[FileEditorOverridesKey.self] = newValue } } + + var filePreview: (CEWorkspaceFile) -> AnyView { + get { self[FilePreviewFactoryKey.self] } + set { self[FilePreviewFactoryKey.self] = newValue } + } } From 26a347f0a379960858fef92bc07ea822524ba327 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 21:05:26 +0200 Subject: [PATCH 117/335] Feat: Add LanguageServicesProvider protocol to CodeEditDocument --- .../LanguageServicesProvider.swift | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift new file mode 100644 index 0000000000..c04e4f0e59 --- /dev/null +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift @@ -0,0 +1,66 @@ +// +// LanguageServicesProvider.swift +// CodeEditDocument +// +// Created by Matthijs Eikelenboom on 2026/07/09. +// + +@preconcurrency import CodeEditSourceEditor +import CodeEditTextView +import CodeEditLanguages +import AppKit + +public struct LanguageServices { + public let textCoordinator: TextViewCoordinator + public let highlightProvider: any HighlightProviding + + public init(textCoordinator: TextViewCoordinator, highlightProvider: any HighlightProviding) { + self.textCoordinator = textCoordinator + self.highlightProvider = highlightProvider + } +} + +@MainActor +public protocol LanguageServicesProvider: AnyObject { + func languageServices(for document: CodeFileDocument) -> LanguageServices +} + +public final class NoOpLanguageServicesProvider: LanguageServicesProvider { + public init() {} + + @MainActor + public func languageServices(for document: CodeFileDocument) -> LanguageServices { + LanguageServices( + textCoordinator: NoOpTextViewCoordinator(), + highlightProvider: NoOpHighlightProvider() + ) + } +} + +final class NoOpTextViewCoordinator: TextViewCoordinator { + func prepareCoordinator(controller: TextViewController) {} +} + +final class NoOpHighlightProvider: HighlightProviding { + @MainActor + func setUp(textView: TextView, codeLanguage: CodeLanguage) {} + + @MainActor + func applyEdit( + textView: TextView, + range: NSRange, + delta: Int, + completion: @escaping @MainActor (Result) -> Void + ) { + completion(.success(IndexSet())) + } + + @MainActor + func queryHighlightsFor( + textView: TextView, + range: NSRange, + completion: @escaping @MainActor (Result<[HighlightRange], Error>) -> Void + ) { + completion(.success([])) + } +} From 530cef80d1b56b8cda1c5ceef27e80d68040aa5d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 9 Jul 2026 21:06:31 +0200 Subject: [PATCH 118/335] Refactor: Decouple Editor from LSPService via LanguageServicesProvider --- CodeEdit/CodeEditContainer.swift | 5 +++++ .../Features/Editor/Views/CodeFileView.swift | 8 +++---- .../Service/AppLanguageServicesProvider.swift | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 82364a9f01..c4ba0bf040 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -6,6 +6,7 @@ // import CodeEditCore +import CodeEditDocument import ShellClient import Factory @@ -14,6 +15,10 @@ extension Container { self { @MainActor in LSPService() }.singleton } + var languageServicesProvider: Factory { + self { @MainActor in AppLanguageServicesProvider() as LanguageServicesProvider }.singleton + } + var workspaceWindowManager: Factory { self { @MainActor in WorkspaceWindowManager() }.singleton } diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 7530354034..2af051c92a 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -87,14 +87,12 @@ struct CodeFileView: View { self._editorInstance = .init(wrappedValue: editorInstance) self._codeFile = .init(wrappedValue: codeFile) - // The per-document LSP objects are owned by `LSPService` (keyed by URI); fetch the same - // instance the language server configures via `setUp`. - let lspObjects = Container.shared.lspService().languageServerObjects(for: codeFile) + let languageServices = Container.shared.languageServicesProvider().languageServices(for: codeFile) self.textViewCoordinators = textViewCoordinators + [editorInstance.rangeTranslator] + [codeFile.contentCoordinator] - + [lspObjects.textCoordinator] + + [languageServices.textCoordinator] self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -102,7 +100,7 @@ struct CodeFileView: View { editorInstance.cursorPositions = openOptions.cursorPositions } - highlightProviders = [lspObjects.highlightProvider] + [treeSitterClient] + highlightProviders = [languageServices.highlightProvider] + [treeSitterClient] codeFile .contentCoordinator diff --git a/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift b/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift new file mode 100644 index 0000000000..543f1019a8 --- /dev/null +++ b/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift @@ -0,0 +1,22 @@ +// +// AppLanguageServicesProvider.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 2026/07/09. +// + +import CodeEditDocument +import Factory + +@MainActor +final class AppLanguageServicesProvider: LanguageServicesProvider { + @LazyInjected(\.lspService) private var lspService + + func languageServices(for document: CodeFileDocument) -> LanguageServices { + let objects = lspService.languageServerObjects(for: document) + return LanguageServices( + textCoordinator: objects.textCoordinator, + highlightProvider: objects.highlightProvider + ) + } +} From c2c13096cef3684a93a050503b92207d7aa11645 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 09:05:34 +0200 Subject: [PATCH 119/335] Refactor: Move WorkspaceStatePersisting and WorkspaceStateKey to CodeEditCore Pre-move decoupling for Editor package extraction: relocate the workspace state persistence protocol and key enum from the app target to CodeEditCore so the Editor package can depend on them. --- .../Documents/Controllers/CodeEditSplitViewController.swift | 1 + .../WorkspaceDocument/WorkspaceStatePersistence.swift | 1 + .../UtilityArea/ViewModels/UtilityAreaViewModel.swift | 1 + .../CodeEditCore/Infrastructure}/WorkspaceStateKey.swift | 6 ++++-- .../Infrastructure}/WorkspaceStatePersisting.swift | 5 ++--- 5 files changed, 9 insertions(+), 5 deletions(-) rename {CodeEdit/Features/Documents/WorkspaceDocument => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure}/WorkspaceStateKey.swift (80%) rename {CodeEdit/Features/Documents/Protocols => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure}/WorkspaceStatePersisting.swift (58%) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index fade182dd5..827c742460 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -6,6 +6,7 @@ // import Cocoa +import CodeEditCore import SwiftUI import Notifications diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift index d85f1adfad..8006437052 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 25.03.26. // +import CodeEditCore import Foundation /// A standalone service for persisting workspace-specific UI state (window size, collapsed panels, etc.) diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 6806ca11ac..b2ea6bc88e 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -5,6 +5,7 @@ // Created by Lukas Pistrol on 20.03.22. // +import CodeEditCore import SwiftUI /// # UtilityAreaViewModel diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStateKey.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift similarity index 80% rename from CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStateKey.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift index 7a233fe4da..2381eaee0f 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStateKey.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift @@ -1,11 +1,13 @@ // // WorkspaceStateKey.swift -// CodeEdit +// CodeEditCore // // Created by Khan Winter on 7/3/23. // -enum WorkspaceStateKey: String { +import Foundation + +public enum WorkspaceStateKey: String { case utilityAreaCollapsed case utilityAreaMaximized case utilityAreaHeight diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift similarity index 58% rename from CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift index 54063fc1c6..7f3497e742 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceStatePersisting.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift @@ -1,14 +1,13 @@ // // WorkspaceStatePersisting.swift -// CodeEdit +// CodeEditCore // // Created by Matthijs Eikelenboom on 06.04.26. // import Foundation -/// Protocol for workspace state persistence, enabling mock implementations for testing. -protocol WorkspaceStatePersisting: AnyObject { +public protocol WorkspaceStatePersisting: AnyObject { func get(_ key: WorkspaceStateKey) -> Any? func set(key: WorkspaceStateKey, value: Any?) } From 2e15148a2192bc2b2b6cd5569e968939a9f97f59 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 09:07:46 +0200 Subject: [PATCH 120/335] Refactor: Decouple CodeFileView from ThemeModel via environment-injected Theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace @ObservedObject ThemeModel.shared in CodeFileView with @Environment(\.currentTheme). Theme is now injected at WorkspaceView and CodeEditWindowController, breaking the Editor→ThemeModel coupling. --- .../CodeEditWindowController.swift | 2 ++ .../Features/Editor/Views/CodeFileView.swift | 4 ++-- CodeEdit/WorkspaceView.swift | 1 + .../Store/Environment+Theme.swift | 19 +++++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index e183bd543a..9a05f0cad3 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -7,6 +7,7 @@ import Cocoa import CodeEditDocument +import CodeEditSettings import SwiftUI import CodeEditUI import Factory @@ -187,6 +188,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.filePreview) { file in AnyView(FilePreviewView(item: file)) } + .environment(\.currentTheme, ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first!) panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) window?.addChildWindow(panel, ordered: .above) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 2af051c92a..1d9f92089a 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -70,7 +70,7 @@ struct CodeFileView: View { @EnvironmentObject var undoRegistration: UndoManagerRegistration - @ObservedObject private var themeModel: ThemeModel = .shared + @Environment(\.currentTheme) private var injectedTheme @State private var treeSitter = TreeSitterClient() @@ -112,7 +112,7 @@ struct CodeFileView: View { } private var currentTheme: Theme { - themeModel.selectedTheme ?? themeModel.themes.first! + injectedTheme! } @State private var font: NSFont = Settings[\.textEditing].font.current diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 10dd303726..d105a63679 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -165,6 +165,7 @@ struct WorkspaceView: View { } } .frame(minHeight: 170 + 29 + 29) + .environment(\.currentTheme, themeModel.selectedTheme ?? themeModel.themes.first!) .collapsable() .collapsed($utilityAreaViewModel.isMaximized) .holdingPriority(.init(1)) diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift new file mode 100644 index 0000000000..53c1c509c7 --- /dev/null +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift @@ -0,0 +1,19 @@ +// +// Environment+Theme.swift +// CodeEditSettings +// +// Created by Matthijs Eikelenboom on 10.07.26. +// + +import SwiftUI + +private struct CurrentThemeKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: Theme? = nil +} + +public extension EnvironmentValues { + var currentTheme: Theme? { + get { self[CurrentThemeKey.self] } + set { self[CurrentThemeKey.self] = newValue } + } +} From 2ade4bf529c0e2e46de85ec9e55edeb86eb49870 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 09:13:11 +0200 Subject: [PATCH 121/335] Refactor: Move generic SplitView widgets to CodeEditUI package Relocate 8 generic SplitView files (pure SwiftUI/AppKit, no Editor coupling) from Features/SplitView/ to CodeEditUI/Views/SplitView/. Public-ify all APIs and fix Swift 6 strict concurrency issues. The 2 editor-coupled SplitView files stay app-side for now. --- .../CodeEditSplitViewController.swift | 1 + .../SplitView}/CodeEditDividerStyle.swift | 13 ++---- .../Environment+ContentInsets.swift | 10 ++--- .../Views/SplitView}/SplitView.swift | 8 ++-- .../SplitView}/SplitViewControllerView.swift | 40 +++++++++---------- .../Views/SplitView}/SplitViewItem.swift | 17 ++++---- .../Views/SplitView}/SplitViewModifiers.swift | 25 ++++++------ .../Views/SplitView}/SplitViewReader.swift | 30 ++++++-------- .../Views/SplitView}/Variadic.swift | 10 ++--- 9 files changed, 69 insertions(+), 85 deletions(-) rename {CodeEdit/Features/SplitView/Model => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/CodeEditDividerStyle.swift (60%) rename {CodeEdit/Features/SplitView/Model => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/Environment+ContentInsets.swift (60%) rename {CodeEdit/Features/SplitView/Views => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/SplitView.swift (78%) rename {CodeEdit/Features/SplitView/Views => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/SplitViewControllerView.swift (77%) rename {CodeEdit/Features/SplitView/Model => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/SplitViewItem.swift (76%) rename {CodeEdit/Features/SplitView/Views => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/SplitViewModifiers.swift (51%) rename {CodeEdit/Features/SplitView/Views => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/SplitViewReader.swift (53%) rename {CodeEdit/Features/SplitView/Views => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView}/Variadic.swift (60%) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 827c742460..fdca589745 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -7,6 +7,7 @@ import Cocoa import CodeEditCore +import CodeEditUI import SwiftUI import Notifications diff --git a/CodeEdit/Features/SplitView/Model/CodeEditDividerStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift similarity index 60% rename from CodeEdit/Features/SplitView/Model/CodeEditDividerStyle.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift index 1f41340b6b..bab5b94c05 100644 --- a/CodeEdit/Features/SplitView/Model/CodeEditDividerStyle.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift @@ -1,22 +1,17 @@ // // CodeEditDividerStyle.swift -// CodeEdit +// CodeEditUI // // Created by Khan Winter on 5/30/25. // import AppKit -/// The style of divider used by ``SplitView``. -/// -/// To add a new style, add another case to this enum and fill in the ``customColor`` and ``customThickness`` -/// variables. When passed to ``SplitView``, the custom styles will be used instead of the default styles. Leave -/// values as `nil` to use default styles. -enum CodeEditDividerStyle: Equatable { +public enum CodeEditDividerStyle: Equatable, Sendable { case system(NSSplitView.DividerStyle) case editorDivider - var customColor: NSColor? { + public var customColor: NSColor? { switch self { case .system: return nil @@ -31,7 +26,7 @@ enum CodeEditDividerStyle: Equatable { } } - var customThickness: CGFloat? { + public var customThickness: CGFloat? { switch self { case .system: return nil diff --git a/CodeEdit/Features/SplitView/Model/Environment+ContentInsets.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift similarity index 60% rename from CodeEdit/Features/SplitView/Model/Environment+ContentInsets.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift index 0c8f577f64..bdf7cbadc8 100644 --- a/CodeEdit/Features/SplitView/Model/Environment+ContentInsets.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift @@ -1,24 +1,24 @@ // // Environment+ContentInsets.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 24/02/2023. // import SwiftUI -struct EdgeInsetsEnvironmentKey: EnvironmentKey { - static var defaultValue: EdgeInsets = EdgeInsets(top: 1, leading: 0, bottom: 0, trailing: 0) +public struct EdgeInsetsEnvironmentKey: EnvironmentKey { + nonisolated(unsafe) public static var defaultValue: EdgeInsets = EdgeInsets(top: 1, leading: 0, bottom: 0, trailing: 0) } -extension EnvironmentValues { +public extension EnvironmentValues { var edgeInsets: EdgeInsetsEnvironmentKey.Value { get { self[EdgeInsetsEnvironmentKey.self] } set { self[EdgeInsetsEnvironmentKey.self] = newValue } } } -extension EdgeInsets { +public extension EdgeInsets { var nsEdgeInsets: NSEdgeInsets { .init(top: top, left: leading, bottom: bottom, right: trailing) } diff --git a/CodeEdit/Features/SplitView/Views/SplitView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitView.swift similarity index 78% rename from CodeEdit/Features/SplitView/Views/SplitView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitView.swift index 9fe9463c98..e052dccbc0 100644 --- a/CodeEdit/Features/SplitView/Views/SplitView.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitView.swift @@ -1,18 +1,18 @@ // // SplitView.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 22/02/2023. // import SwiftUI -struct SplitView: View { +public struct SplitView: View { var axis: Axis var dividerStyle: CodeEditDividerStyle var content: Content - init(axis: Axis, dividerStyle: CodeEditDividerStyle = .system(.thin), @ViewBuilder content: () -> Content) { + public init(axis: Axis, dividerStyle: CodeEditDividerStyle = .system(.thin), @ViewBuilder content: () -> Content) { self.axis = axis self.dividerStyle = dividerStyle self.content = content() @@ -20,7 +20,7 @@ struct SplitView: View { @State private var viewController: () -> SplitViewController? = { nil } - var body: some View { + public var body: some View { VStack { content.variadic { children in SplitViewControllerView( diff --git a/CodeEdit/Features/SplitView/Views/SplitViewControllerView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift similarity index 77% rename from CodeEdit/Features/SplitView/Views/SplitViewControllerView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift index d0094d3aad..e4db9728ce 100644 --- a/CodeEdit/Features/SplitView/Views/SplitViewControllerView.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift @@ -1,27 +1,27 @@ // // SplitViewControllerView.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 20/02/2023. // import SwiftUI -struct SplitViewControllerView: NSViewControllerRepresentable { +public struct SplitViewControllerView: NSViewControllerRepresentable { var axis: Axis var dividerStyle: CodeEditDividerStyle var children: _VariadicView.Children @Binding var viewController: () -> SplitViewController? - func makeNSViewController(context: Context) -> SplitViewController { + public func makeNSViewController(context: Context) -> SplitViewController { let controller = SplitViewController(axis: axis, parentView: self) { controller in updateItems(controller: controller) } return controller } - func updateNSViewController(_ controller: SplitViewController, context: Context) { + public func updateNSViewController(_ controller: SplitViewController, context: Context) { updateItems(controller: controller) controller.setDividerStyle(dividerStyle) } @@ -48,11 +48,8 @@ struct SplitViewControllerView: NSViewControllerRepresentable { let numerator = splitView.isVertical ? splitView.frame.width : splitView.frame.height for idx in 0.. Void)? @@ -125,12 +122,12 @@ final class SplitViewController: NSSplitViewController { fatalError("init(coder:) has not been implemented") } - override func loadView() { + override public func loadView() { splitView = CustomSplitView() super.loadView() } - override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() splitView.isVertical = axis != .vertical setUpItems?(self) @@ -141,21 +138,22 @@ final class SplitViewController: NSSplitViewController { } } - override func splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool { - // For some reason, AppKit _really_ wants to hide dividers when there's only one item (and no dividers) - // so we do this check for them. + override public func splitView( + _ splitView: NSSplitView, + shouldHideDividerAt dividerIndex: Int + ) -> Bool { guard items.count > 1 else { return false } return super.splitView(splitView, shouldHideDividerAt: dividerIndex) } - func setDividerStyle(_ dividerStyle: CodeEditDividerStyle) { + public func setDividerStyle(_ dividerStyle: CodeEditDividerStyle) { guard let splitView = splitView as? CustomSplitView else { return } splitView.customDividerStyle = dividerStyle } - func collapse(for id: AnyHashable, enabled: Bool) { + public func collapse(for id: AnyHashable, enabled: Bool) { items.first { $0.id == id }?.item.animator().isCollapsed = enabled } } diff --git a/CodeEdit/Features/SplitView/Model/SplitViewItem.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift similarity index 76% rename from CodeEdit/Features/SplitView/Model/SplitViewItem.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift index 9f8521e808..62155924cd 100644 --- a/CodeEdit/Features/SplitView/Model/SplitViewItem.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift @@ -1,6 +1,6 @@ // // SplitViewItem.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 05/03/2023. // @@ -8,10 +8,11 @@ import SwiftUI import Combine -class SplitViewItem: ObservableObject { +@MainActor +public class SplitViewItem: ObservableObject { - var id: AnyHashable - var item: NSSplitViewItem + public var id: AnyHashable + public var item: NSSplitViewItem var collapsed: Binding @@ -19,14 +20,13 @@ class SplitViewItem: ObservableObject { var observers: [NSKeyValueObservation] = [] - init(child: _VariadicView.Children.Element) { + public init(child: _VariadicView.Children.Element) { self.id = child.id self.item = NSSplitViewItem(viewController: NSHostingController(rootView: child)) self.collapsed = child[SplitViewItemCollapsedViewTraitKey.self] self.item.canCollapse = child[SplitViewItemCanCollapseViewTraitKey.self] self.item.isCollapsed = self.collapsed.wrappedValue self.item.holdingPriority = child[SplitViewHoldingPriorityTraitKey.self] - // Skip the initial observation via a dispatch to avoid a "updating during view update" error DispatchQueue.main.async { self.observers = self.createObservers() } @@ -40,10 +40,7 @@ class SplitViewItem: ObservableObject { ] } - /// Updates a SplitViewItem. - /// This will fetch updated binding values and update them if needed. - /// - Parameter child: the view corresponding to the SplitViewItem. - func update(child: _VariadicView.Children.Element) { + public func update(child: _VariadicView.Children.Element) { self.item.canCollapse = child[SplitViewItemCanCollapseViewTraitKey.self] let canAnimate = child[SplitViewItemCanAnimateViewTraitKey.self] DispatchQueue.main.async { diff --git a/CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift similarity index 51% rename from CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift index 95f4e01bb1..e73c2d3c11 100644 --- a/CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift @@ -1,36 +1,35 @@ // // SplitViewModifiers.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 05/03/2023. // import SwiftUI -struct SplitViewControllerLayoutValueKey: _ViewTraitKey { - static var defaultValue: () -> SplitViewController? = { nil } +public struct SplitViewControllerLayoutValueKey: _ViewTraitKey { + nonisolated(unsafe) public static var defaultValue: () -> SplitViewController? = { nil } } -struct SplitViewItemCollapsedViewTraitKey: _ViewTraitKey { - static var defaultValue: Binding = .constant(false) +public struct SplitViewItemCollapsedViewTraitKey: _ViewTraitKey { + nonisolated(unsafe) public static var defaultValue: Binding = .constant(false) } -struct SplitViewItemCanCollapseViewTraitKey: _ViewTraitKey { - static var defaultValue: Bool = false +public struct SplitViewItemCanCollapseViewTraitKey: _ViewTraitKey { + nonisolated(unsafe) public static var defaultValue: Bool = false } -struct SplitViewHoldingPriorityTraitKey: _ViewTraitKey { - static var defaultValue: NSLayoutConstraint.Priority = .defaultLow +public struct SplitViewHoldingPriorityTraitKey: _ViewTraitKey { + nonisolated(unsafe) public static var defaultValue: NSLayoutConstraint.Priority = .defaultLow } -struct SplitViewItemCanAnimateViewTraitKey: _ViewTraitKey { - static var defaultValue: Bool { true } +public struct SplitViewItemCanAnimateViewTraitKey: _ViewTraitKey { + public static var defaultValue: Bool { true } } -extension View { +public extension View { func collapsed(_ value: Binding) -> some View { self - // Use get/set instead of binding directly, so a view update will be triggered if the binding changes. ._trait(SplitViewItemCollapsedViewTraitKey.self, .init { value.wrappedValue } set: { diff --git a/CodeEdit/Features/SplitView/Views/SplitViewReader.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift similarity index 53% rename from CodeEdit/Features/SplitView/Views/SplitViewReader.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift index e909edc176..196be47446 100644 --- a/CodeEdit/Features/SplitView/Views/SplitViewReader.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift @@ -1,23 +1,27 @@ // // SplitViewReader.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 05/03/2023. // import SwiftUI -struct SplitViewReader: View { +public struct SplitViewReader: View { @ViewBuilder var content: (SplitViewProxy) -> Content + public init(@ViewBuilder content: @escaping (SplitViewProxy) -> Content) { + self.content = content + } + @State private var viewController: () -> SplitViewController? = { nil } private var proxy: SplitViewProxy { .init(viewController: viewController) } - var body: some View { + public var body: some View { content(proxy) .variadic { children in ForEach(children, id: \.id) { child in @@ -30,28 +34,20 @@ struct SplitViewReader: View { } } -struct SplitViewProxy { +public struct SplitViewProxy { private var viewController: () -> SplitViewController? - fileprivate init(viewController: @escaping () -> SplitViewController?) { + public init(viewController: @escaping () -> SplitViewController?) { self.viewController = viewController } - /// Set the position of a divider in a splitview. - /// - Parameters: - /// - index: index of the divider. The mostleft / top divider has index 0. - /// - position: position to place the divider. This is a position inside the views width / height. - /// For example, if the splitview has a width of 500, setting the position to 250 - /// will put the divider in the middle of the splitview. - func setPosition(of index: Int, position: CGFloat) { + @MainActor + public func setPosition(of index: Int, position: CGFloat) { viewController()?.splitView.setPosition(position, ofDividerAt: index) } - /// Collapse a view of the splitview. - /// - Parameters: - /// - id: ID of the view - /// - enabled: true for collapse. - func collapseView(with id: AnyHashable, _ enabled: Bool) { + @MainActor + public func collapseView(with id: AnyHashable, _ enabled: Bool) { viewController()?.collapse(for: id, enabled: enabled) } } diff --git a/CodeEdit/Features/SplitView/Views/Variadic.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift similarity index 60% rename from CodeEdit/Features/SplitView/Views/Variadic.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift index b5ab5aecbe..65b1cd9298 100644 --- a/CodeEdit/Features/SplitView/Views/Variadic.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift @@ -1,23 +1,21 @@ // // Variadic.swift -// CodeEdit +// CodeEditUI // // Created by Wouter Hennen on 05/03/2023. // import SwiftUI -struct Helper: _VariadicView_UnaryViewRoot { +public struct Helper: _VariadicView_UnaryViewRoot { var _body: (_VariadicView.Children) -> Result - func body(children: _VariadicView.Children) -> some View { + public func body(children: _VariadicView.Children) -> some View { _body(children) } } -extension View { - - /// Exposes the children of a ViewBuilder so they can be accessed individually. +public extension View { func variadic(@ViewBuilder process: @escaping (_VariadicView.Children) -> R) -> some View { _VariadicView.Tree(Helper(_body: process), content: { self }) } From 36d91356588fd36f0c8514c0d0d020a1a39bfebf Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 11:47:35 +0200 Subject: [PATCH 122/335] Refactor: Extract Editor feature into dedicated Swift package Move ~50 Editor files into Packages/Features/Editor with Swift 5 language mode. Relocate shared utilities (Color+HEX, Theme+Color, View+if, SplitView environment keys) to Foundation packages. Wire Factory DI for cross-package access via EditorContainer with fatalError defaults. --- CodeEdit.xcodeproj/project.pbxproj | 7 + CodeEdit.xcworkspace/contents.xcworkspacedata | 3 + .../Notifications/TaskNotificationView.swift | 1 + .../Models/CEWorkspaceFile+Editor.swift | 13 -- .../Models/CEWorkspaceFile+Presentation.swift | 28 ++-- .../Views/ToolbarBranchPicker.swift | 1 + .../AppCodeFileDocumentDelegate.swift | 1 + .../CodeEditSplitViewController.swift | 1 + .../CodeEditWindowController.swift | 1 + .../Protocols/WorkspaceManaging.swift | 1 + .../Keybindings/ModifierKeysObserver.swift | 11 -- .../ProjectNavigatorOutlineView.swift | 1 + .../StatusBarCursorPositionLabel.swift | 1 + .../WindowCommands/EditorCommands.swift | 1 + .../WindowCommands/NavigateCommands.swift | 1 + .../WindowControllerPropertyWrapper.swift | 1 + .../Models/Environment+Workspace.swift | 9 -- .../Features/Workspace/Models/Workspace.swift | 1 + .../Services/AppWorkspaceNavigator.swift | 1 + .../Features/Workspace/WorkspaceFactory.swift | 1 + CodeEdit/Utils/Environment/Env+Window.swift | 24 ---- CodeEdit/Utils/Extensions/View/View+if.swift | 52 -------- CodeEdit/WindowObserver.swift | 1 + CodeEdit/WorkspaceView.swift | 1 + .../Editor/AppActiveCursorStateTests.swift | 1 + .../Editor/AppActiveEditorStateTests.swift | 1 + .../Editor/AppFileEditorOverridesTests.swift | 1 + .../Editor/DocumentRegistryTests.swift | 1 + .../Editor/EditorStateRestorationTests.swift | 1 + .../Editor/UndoManagerRegistrationTests.swift | 1 + .../AppWorkspaceNavigatorTests.swift | 1 + .../Utils/UnitTests_Extensions.swift | 1 + Packages/Features/Editor/Package.swift | 48 +++++++ .../Editor/CEWorkspaceFile+Editor.swift | 45 +++++++ .../Sources/Editor/EditorContainer.swift | 15 +++ .../Editor}/Environment+SplitEditor.swift | 6 +- .../Views/EditorJumpBarComponent.swift | 0 .../JumpBar/Views/EditorJumpBarMenu.swift | 4 +- .../JumpBar/Views/EditorJumpBarView.swift | 0 .../Editor/Models/AppActiveCursorState.swift | 10 +- .../Editor/Models/AppActiveEditorState.swift | 8 +- .../Models/AppFileEditorOverrides.swift | 14 +- .../Editor/Models/DocumentRegistry.swift | 10 +- .../Editor/Models/Editor/Editor+History.swift | 12 +- .../Models/Editor/Editor+TabSwitch.swift | 4 +- .../Editor/Models/Editor/Editor.swift | 52 ++++---- .../Editor/Models/EditorInstance.swift | 38 +++--- .../EditorLayout+StateRestoration.swift | 28 ++-- .../Models/EditorLayout/EditorLayout.swift | 18 +-- .../Editor/Models/EditorManager.swift | 48 +++---- .../Models/Environment+ActiveEditor.swift | 6 +- .../Sources/Editor/Models/FileIcon.swift | 125 ++++++++++++++++++ .../Restoration/EditorStateRestoration.swift | 31 +++-- .../Restoration/UndoManagerRegistration.swift | 14 +- .../Editor/Models/Theme+EditorTheme.swift | 35 +---- .../Sources/Editor}/SplitViewData.swift | 18 +-- .../Tabs/Tab/EditorFileTabCloseButton.swift | 0 .../TabBar/Tabs/Tab/EditorTabBackground.swift | 0 .../Tabs/Tab/EditorTabButtonStyle.swift | 0 .../Tabs/Tab/EditorTabCloseButton.swift | 0 .../TabBar/Tabs/Tab/EditorTabView.swift | 0 .../Tab/Models/EditorTabFileObserver.swift | 0 .../Tab/Models/EditorTabRepresentable.swift | 0 .../Tabs/Views/EditorTabOnDropDelegate.swift | 0 .../Tabs/Views/EditorTabs+DragGesture.swift | 0 .../Editor/TabBar/Tabs/Views/EditorTabs.swift | 0 .../Tabs/Views/EditorTabsOverflowShadow.swift | 0 .../TabBar/Views/EditorHistoryMenus.swift | 0 .../TabBar/Views/EditorTabBarAccessory.swift | 0 .../Views/EditorTabBarContextMenu.swift | 2 +- .../TabBar/Views/EditorTabBarDivider.swift | 0 .../EditorTabBarLeadingAccessories.swift | 0 .../EditorTabBarTrailingAccessories.swift | 0 .../TabBar/Views/EditorTabBarView.swift | 0 .../UseCases/RestoreEditorStateUseCase.swift | 6 +- .../Sources}/Editor/Views/AnyFileView.swift | 0 .../Sources}/Editor/Views/CodeFileView.swift | 0 .../Editor/Views/EditorAreaFileView.swift | 0 .../Editor/Views/EditorAreaView.swift | 0 .../Editor/Views/EditorLayoutView.swift | 17 ++- .../Editor/Views/FilePreviewView.swift | 6 +- .../Sources}/Editor/Views/ImageFileView.swift | 0 .../Editor/Views/LoadingFileView.swift | 0 .../Editor/Views/NonTextFileView.swift | 0 .../Sources}/Editor/Views/PDFFileView.swift | 0 .../Editor/Views/WindowCodeFileView.swift | 6 +- .../CodeEditDocument/URL+AbsolutePath.swift | 5 +- .../CodeEditSettings/Models/Theme+Color.swift | 28 ++++ .../CodeEditSettings/Store}/Color+HEX.swift | 41 +----- .../Environment+IsFullscreen.swift | 8 +- .../Environment+ModifierKeys.swift | 19 +++ .../EnvironmentKeys/Environment+Window.swift | 25 ++++ .../Sources/CodeEditUI/Views/View+if.swift | 42 ++++++ .../Environment+WorkspaceFileManager.swift | 19 +++ 94 files changed, 608 insertions(+), 376 deletions(-) delete mode 100644 CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift delete mode 100644 CodeEdit/Utils/Environment/Env+Window.swift delete mode 100644 CodeEdit/Utils/Extensions/View/View+if.swift create mode 100644 Packages/Features/Editor/Package.swift create mode 100644 Packages/Features/Editor/Sources/Editor/CEWorkspaceFile+Editor.swift create mode 100644 Packages/Features/Editor/Sources/Editor/EditorContainer.swift rename {CodeEdit/Features/SplitView/Model => Packages/Features/Editor/Sources/Editor}/Environment+SplitEditor.swift (61%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/JumpBar/Views/EditorJumpBarComponent.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/JumpBar/Views/EditorJumpBarMenu.swift (96%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/JumpBar/Views/EditorJumpBarView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/AppActiveCursorState.swift (84%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/AppActiveEditorState.swift (72%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/AppFileEditorOverrides.swift (71%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/DocumentRegistry.swift (85%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/Editor/Editor+History.swift (89%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/Editor/Editor+TabSwitch.swift (93%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/Editor/Editor.swift (89%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/EditorInstance.swift (82%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift (88%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/EditorLayout/EditorLayout.swift (87%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/EditorManager.swift (75%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/Environment+ActiveEditor.swift (63%) create mode 100644 Packages/Features/Editor/Sources/Editor/Models/FileIcon.swift rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/Restoration/EditorStateRestoration.swift (83%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Models/Restoration/UndoManagerRegistration.swift (84%) rename CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift => Packages/Features/Editor/Sources/Editor/Models/Theme+EditorTheme.swift (74%) rename {CodeEdit/Features/SplitView/Model => Packages/Features/Editor/Sources/Editor}/SplitViewData.swift (84%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/EditorTabView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Views/EditorTabs.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorHistoryMenus.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorTabBarAccessory.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorTabBarContextMenu.swift (98%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorTabBarDivider.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/TabBar/Views/EditorTabBarView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/UseCases/RestoreEditorStateUseCase.swift (97%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/AnyFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/CodeFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/EditorAreaFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/EditorAreaView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/EditorLayoutView.swift (87%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/FilePreviewView.swift (90%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/ImageFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/LoadingFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/NonTextFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/PDFFileView.swift (100%) rename {CodeEdit/Features => Packages/Features/Editor/Sources}/Editor/Views/WindowCodeFileView.swift (89%) create mode 100644 Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift rename {CodeEdit/Utils/Extensions/Color => Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store}/Color+HEX.swift (54%) rename CodeEdit/Utils/Environment/Env+IsFullscreen.swift => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift (68%) create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift create mode 100644 Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index d26a69ffbb..7ae9da447f 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; + 58ED10022FFB0001004BE116 /* Editor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* Editor */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; @@ -191,6 +192,7 @@ 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, 6CCF73D02E26DE3200B94F75 /* SwiftTerm in Frameworks */, 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */, + 58ED10022FFB0001004BE116 /* Editor in Frameworks */, 6C315FC82E05E33D0011BFC5 /* CodeEditSourceEditor in Frameworks */, 6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */, 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, @@ -363,6 +365,7 @@ 588950C42FFA5C05004BE116 /* Search */, 588957122FFA679E004BE116 /* CodeEditServices */, 5889639D2FFA9A87004BE116 /* Notifications */, + 58ED10012FFB0001004BE116 /* Editor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1940,6 +1943,10 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditCore; }; + 58ED10012FFB0001004BE116 /* Editor */ = { + isa = XCSwiftPackageProductDependency; + productName = Editor; + }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 9e4ef2ffd7..b29b46b48c 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -36,5 +36,8 @@ + + diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift index be10d084e0..198b99d04b 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift @@ -7,6 +7,7 @@ import SwiftUI import CodeEditCore +import CodeEditUI struct TaskNotificationView: View { @Environment(\.controlActiveState) diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift deleted file mode 100644 index 0ef3302e40..0000000000 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Editor.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// CEWorkspaceFile+Editor.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -import CodeEditCore - -extension CEWorkspaceFile: EditorTabRepresentable { - /// The `id` in `EditorTabID` form. - var tabID: EditorTabID { .codeEditor(id) } -} diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift index 7cbab34c96..fdbce35f46 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift @@ -6,11 +6,13 @@ // import SwiftUI -import CodeEditSettings import CodeEditCore +import CodeEditSettings +import CodeEditSymbols extension CEWorkspaceFile { - /// The file's icon as a SwiftUI `Image`. + // MARK: Icons + var icon: Image { if let customImage = NSImage.symbol(named: systemImage) { return Image(nsImage: customImage) @@ -19,7 +21,6 @@ extension CEWorkspaceFile { } } - /// The file's icon as an `NSImage`. var nsIcon: NSImage { if let customImage = NSImage.symbol(named: systemImage) { return customImage @@ -29,27 +30,20 @@ extension CEWorkspaceFile { } } - /// SF Symbol name for the file/folder. + var iconColor: Color { + FileIcon.iconColor(fileType: type) + } + var systemImage: String { if isFolder { - return folderIcon() + if self.parent == nil { return "folder.fill.badge.gearshape" } + if self.name == ".codeedit" { return "folder.fill.badge.gearshape" } + return isEmptyFolder ? "folder" : "folder.fill" } else { return FileIcon.fileIcon(fileType: type) } } - /// Icon tint color for the file type. - var iconColor: Color { - FileIcon.iconColor(fileType: type) - } - - /// SF Symbol name for folders (root / `.codeedit` / populated / empty). - private func folderIcon() -> String { - if self.parent == nil { return "folder.fill.badge.gearshape" } - if self.name == ".codeedit" { return "folder.fill.badge.gearshape" } - return isEmptyFolder ? "folder" : "folder.fill" - } - // MARK: Intents /// Reveal the file/folder in Finder. diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index 4979fd1673..d27e24416c 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -10,6 +10,7 @@ import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore import CodeEditSymbols +import CodeEditUI import Combine /// A view that pops up a branch picker. diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift index 7db85fc808..76ea377222 100644 --- a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -6,6 +6,7 @@ // import AppKit +import Editor import SwiftUI import Factory import CodeEditTextView diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index fdca589745..f9867d4c9b 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -8,6 +8,7 @@ import Cocoa import CodeEditCore import CodeEditUI +import Editor import SwiftUI import Notifications diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 9a05f0cad3..0fe5cdd9f2 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -8,6 +8,7 @@ import Cocoa import CodeEditDocument import CodeEditSettings +import Editor import SwiftUI import CodeEditUI import Factory diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index fd599175b5..b25d65c854 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -7,6 +7,7 @@ import Foundation import CEWorkspaceFileManager +import Editor import Notifications import Search diff --git a/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift b/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift index c0a1329c14..fc0d090a64 100644 --- a/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift +++ b/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift @@ -8,17 +8,6 @@ import SwiftUI import Combine -struct EventModifierEnvironmentKey: EnvironmentKey { - static var defaultValue: NSEvent.ModifierFlags = [] -} - -extension EnvironmentValues { - var modifierKeys: EventModifierEnvironmentKey.Value { - get { self[EventModifierEnvironmentKey.self] } - set { self[EventModifierEnvironmentKey.self] = newValue } - } -} - extension NSEvent { static func publisher(scope: Publisher.Scope, matching: EventTypeMask) -> Publisher { return Publisher(scope: scope, matching: matching) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 0209ccbe41..a9022ff526 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -9,6 +9,7 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore import CodeEditSettings +import Editor import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift index 42b9d7321e..ded23882a6 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift @@ -7,6 +7,7 @@ import SwiftUI import CodeEditCore +import CodeEditUI struct StatusBarCursorPositionLabel: View { @Environment(\.activeCursorState) diff --git a/CodeEdit/Features/WindowCommands/EditorCommands.swift b/CodeEdit/Features/WindowCommands/EditorCommands.swift index e99c8dfa36..1ea5fe6a1e 100644 --- a/CodeEdit/Features/WindowCommands/EditorCommands.swift +++ b/CodeEdit/Features/WindowCommands/EditorCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Editor import CodeEditKit struct EditorCommands: Commands { diff --git a/CodeEdit/Features/WindowCommands/NavigateCommands.swift b/CodeEdit/Features/WindowCommands/NavigateCommands.swift index 45b9cddf38..d129523b51 100644 --- a/CodeEdit/Features/WindowCommands/NavigateCommands.swift +++ b/CodeEdit/Features/WindowCommands/NavigateCommands.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Editor struct NavigateCommands: Commands { diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index 701cdc0e16..904d06a55c 100644 --- a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -6,6 +6,7 @@ // import AppKit +import Editor import SwiftUI import Combine diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index cbbd79343a..975f1a4faa 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -9,10 +9,6 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore -private struct WorkspaceFileManagerKey: EnvironmentKey { - static let defaultValue: CEWorkspaceFileManager? = nil -} - private struct ActiveEditorStateKey: EnvironmentKey { static let defaultValue: ActiveEditorState = NoOpActiveEditorState() } @@ -38,11 +34,6 @@ private struct FilePreviewFactoryKey: EnvironmentKey { } extension EnvironmentValues { - var workspaceFileManager: CEWorkspaceFileManager? { - get { self[WorkspaceFileManagerKey.self] } - set { self[WorkspaceFileManagerKey.self] = newValue } - } - var workspaceFileURL: URL? { get { self[WorkspaceFileURLKey.self] } set { self[WorkspaceFileURLKey.self] = newValue } diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index b00936204b..634ec0b61c 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -7,6 +7,7 @@ import AppKit import CEWorkspaceFileManager +import Editor import Notifications import Search import SwiftUI diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift index da8bf9179c..45e679e0cb 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -8,6 +8,7 @@ import Foundation import CodeEditCore import CEWorkspaceFileManager +import Editor import Factory /// App-shell binding of the `WorkspaceNavigator` command interface. diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 2f66aed53c..b4d251c6cb 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -7,6 +7,7 @@ import Foundation import CEWorkspaceFileManager +import Editor import Search import Factory diff --git a/CodeEdit/Utils/Environment/Env+Window.swift b/CodeEdit/Utils/Environment/Env+Window.swift deleted file mode 100644 index 15b0c414c6..0000000000 --- a/CodeEdit/Utils/Environment/Env+Window.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Env+Window.swift -// CodeEdit -// -// Created by Wouter Hennen on 14/01/2023. -// - -import SwiftUI - -struct WindowBox { - weak var value: NSWindow? -} - -struct NSWindowEnvironmentKey: EnvironmentKey { - typealias Value = WindowBox - static var defaultValue = WindowBox(value: nil) -} - -extension EnvironmentValues { - var window: WindowBox { - get { self[NSWindowEnvironmentKey.self] } - set { self[NSWindowEnvironmentKey.self] = newValue } - } -} diff --git a/CodeEdit/Utils/Extensions/View/View+if.swift b/CodeEdit/Utils/Extensions/View/View+if.swift deleted file mode 100644 index 1275187510..0000000000 --- a/CodeEdit/Utils/Extensions/View/View+if.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// View+if.swift -// CodeEdit -// -// Created by Khan Winter on 8/28/25. -// - -import SwiftUI - -extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder - func `if`(_ condition: Bool, @ViewBuilder transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } - - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder - func `if`( - _ condition: Bool, - @ViewBuilder transform: (Self) -> Content, - @ViewBuilder else elseTransform: (Self) -> ElseContent - ) -> some View { - if condition { - transform(self) - } else { - elseTransform(self) - } - } -} - -extension Bool { - static var tahoe: Bool { - if #available(macOS 26, *) { - return true - } else { - return false - } - } - } diff --git a/CodeEdit/WindowObserver.swift b/CodeEdit/WindowObserver.swift index 530505f4c9..b21c91b5bc 100644 --- a/CodeEdit/WindowObserver.swift +++ b/CodeEdit/WindowObserver.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct WindowObserver: View { diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index d105a63679..8a401a0485 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -9,6 +9,7 @@ import SwiftUI import CodeEditSettings import CodeEditCore import CodeEditUI +import Editor import Notifications import UniformTypeIdentifiers diff --git a/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift b/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift index 3006c87ba3..e88746113b 100644 --- a/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift +++ b/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift @@ -10,6 +10,7 @@ import Combine import Testing import CodeEditCore import CEWorkspaceFileManager +@testable import Editor import CodeEditSourceEditor @testable import CodeEdit diff --git a/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift b/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift index 6af3aef184..c0d033468b 100644 --- a/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift +++ b/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift @@ -10,6 +10,7 @@ import Combine import Testing import CodeEditCore import CEWorkspaceFileManager +@testable import Editor @testable import CodeEdit @Suite diff --git a/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift b/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift index dbb4a4697e..627d84c8ac 100644 --- a/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift +++ b/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift @@ -10,6 +10,7 @@ import Testing import CodeEditCore import CEWorkspaceFileManager import CodeEditDocument +@testable import Editor import CodeEditLanguages @testable import CodeEdit diff --git a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift index 28de22808a..fa64894f4e 100644 --- a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift +++ b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift @@ -10,6 +10,7 @@ import CodeEditDocument import Combine import CodeEditCore @testable import CodeEdit +@testable import Editor final class DocumentRegistryTests: XCTestCase { private func makeFile(_ path: String = "/tmp/reg-\(UUID().uuidString).swift") -> CEWorkspaceFile { diff --git a/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift b/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift index a97363fc68..a507d47a4a 100644 --- a/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift +++ b/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation @testable import CodeEdit +@testable import Editor @Suite struct EditorStateRestorationTests { diff --git a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift b/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift index ba23c8f387..9eaefce864 100644 --- a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift +++ b/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift @@ -6,6 +6,7 @@ // @testable import CodeEdit +@testable import Editor import Testing import CodeEditCore import Foundation diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index d2b09cd183..7208720a0c 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -9,6 +9,7 @@ import Foundation import Testing import CodeEditCore @testable import CodeEdit +@testable import Editor @Suite struct AppWorkspaceNavigatorTests { diff --git a/CodeEditTests/Utils/UnitTests_Extensions.swift b/CodeEditTests/Utils/UnitTests_Extensions.swift index cadcb7c40a..26fd7e01c3 100644 --- a/CodeEditTests/Utils/UnitTests_Extensions.swift +++ b/CodeEditTests/Utils/UnitTests_Extensions.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import XCTest import CodeEditCore +import CodeEditSettings @testable import CodeEdit final class CodeEditUtilsExtensionsUnitTests: XCTestCase { diff --git a/Packages/Features/Editor/Package.swift b/Packages/Features/Editor/Package.swift new file mode 100644 index 0000000000..ac1997467d --- /dev/null +++ b/Packages/Features/Editor/Package.swift @@ -0,0 +1,48 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Editor", + platforms: [.macOS(.v14)], + products: [ + .library(name: "Editor", targets: ["Editor"]) + ], + dependencies: [ + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditUI"), + .package(path: "../../Foundation/CodeEditDocument"), + .package(path: "../../Foundation/CodeEditSettings"), + .package(path: "../../Services/CodeEditServices"), + .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3"), + .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), + .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), + .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), + .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3"), + .package(url: "https://github.com/groue/GRDB.swift.git", from: "6.0.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0") + ], + targets: [ + .target( + name: "Editor", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditUI", package: "CodeEditUI"), + .product(name: "CodeEditDocument", package: "CodeEditDocument"), + .product(name: "CodeEditSettings", package: "CodeEditSettings"), + .product(name: "CodeEditServices", package: "CodeEditServices"), + .product(name: "Factory", package: "Factory"), + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "CodeEditTextView", package: "CodeEditTextView"), + .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), + .product(name: "CodeEditSymbols", package: "CodeEditSymbols"), + .product(name: "GRDB", package: "GRDB.swift"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] + ) + ] +) diff --git a/Packages/Features/Editor/Sources/Editor/CEWorkspaceFile+Editor.swift b/Packages/Features/Editor/Sources/Editor/CEWorkspaceFile+Editor.swift new file mode 100644 index 0000000000..1abca160d1 --- /dev/null +++ b/Packages/Features/Editor/Sources/Editor/CEWorkspaceFile+Editor.swift @@ -0,0 +1,45 @@ +// +// CEWorkspaceFile+Editor.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 05/07/2026. +// + +import SwiftUI +import CodeEditCore +import CodeEditSymbols + +extension CEWorkspaceFile: EditorTabRepresentable { + public var tabID: EditorTabID { .codeEditor(id) } + + var icon: Image { + if let customImage = NSImage.symbol(named: systemImage) { + return Image(nsImage: customImage) + } else { + return Image(systemName: systemImage) + } + } + + var nsIcon: NSImage { + if let customImage = NSImage.symbol(named: systemImage) { + return customImage + } else { + return NSImage(systemSymbolName: systemImage, accessibilityDescription: systemImage) + ?? NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")! + } + } + + var iconColor: Color { + FileIcon.iconColor(fileType: type) + } + + var systemImage: String { + if isFolder { + if self.parent == nil { return "folder.fill.badge.gearshape" } + if self.name == ".codeedit" { return "folder.fill.badge.gearshape" } + return isEmptyFolder ? "folder" : "folder.fill" + } else { + return FileIcon.fileIcon(fileType: type) + } + } +} diff --git a/Packages/Features/Editor/Sources/Editor/EditorContainer.swift b/Packages/Features/Editor/Sources/Editor/EditorContainer.swift new file mode 100644 index 0000000000..978cda4013 --- /dev/null +++ b/Packages/Features/Editor/Sources/Editor/EditorContainer.swift @@ -0,0 +1,15 @@ +// +// EditorContainer.swift +// Editor +// +// Created by Matthijs Eikelenboom on 10.07.26. +// + +import Factory +import CodeEditDocument + +extension Container { + var languageServicesProvider: Factory { + self { fatalError("languageServicesProvider not registered") } + } +} diff --git a/CodeEdit/Features/SplitView/Model/Environment+SplitEditor.swift b/Packages/Features/Editor/Sources/Editor/Environment+SplitEditor.swift similarity index 61% rename from CodeEdit/Features/SplitView/Model/Environment+SplitEditor.swift rename to Packages/Features/Editor/Sources/Editor/Environment+SplitEditor.swift index 1fd3eb14ac..81e56a0df1 100644 --- a/CodeEdit/Features/SplitView/Model/Environment+SplitEditor.swift +++ b/Packages/Features/Editor/Sources/Editor/Environment+SplitEditor.swift @@ -7,11 +7,11 @@ import SwiftUI -struct SplitEditorEnvironmentKey: EnvironmentKey { - static var defaultValue: (Edge, Editor) -> Void = { _, _ in } +public struct SplitEditorEnvironmentKey: EnvironmentKey { + nonisolated(unsafe) public static var defaultValue: (Edge, Editor) -> Void = { _, _ in } } -extension EnvironmentValues { +public extension EnvironmentValues { var splitEditor: SplitEditorEnvironmentKey.Value { get { self[SplitEditorEnvironmentKey.self] } set { self[SplitEditorEnvironmentKey.self] = newValue } diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift b/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarComponent.swift similarity index 100% rename from CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift rename to Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarComponent.swift diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift b/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarMenu.swift similarity index 96% rename from CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift rename to Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarMenu.swift index 4d0012ee8d..40fcefa986 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarMenu.swift +++ b/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarMenu.swift @@ -79,10 +79,10 @@ final class JumpBarMenuItem: NSMenuItem { if fileItem.isFolder { let subMenu = NSMenu() submenu = subMenu - color = NSColor.folderBlue + color = NSColor(named: "FolderBlue") ?? .systemBlue } if generalSettings.fileIconStyle == .monochrome { - color = NSColor.coolGray + color = NSColor(named: "CoolGray") ?? .systemGray } let image = fileItem.nsIcon.withSymbolConfiguration(.init(paletteColors: [color])) self.image = image diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift b/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarView.swift similarity index 100% rename from CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift rename to Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarView.swift diff --git a/CodeEdit/Features/Editor/Models/AppActiveCursorState.swift b/Packages/Features/Editor/Sources/Editor/Models/AppActiveCursorState.swift similarity index 84% rename from CodeEdit/Features/Editor/Models/AppActiveCursorState.swift rename to Packages/Features/Editor/Sources/Editor/Models/AppActiveCursorState.swift index eeae1fc134..56dea480ae 100644 --- a/CodeEdit/Features/Editor/Models/AppActiveCursorState.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/AppActiveCursorState.swift @@ -14,14 +14,14 @@ import CodeEditSourceEditor /// active editor's cursor positions across both active-editor switches and /// within-editor tab changes, and forwards `linesInRange(_:)` to the live /// `EditorInstance.rangeTranslator`. -final class AppActiveCursorState: ActiveCursorState { +public final class AppActiveCursorState: ActiveCursorState { private let subject: CurrentValueSubject<[EditorCursorPosition], Never> private weak var currentTab: EditorInstance? private var editorCancellable: AnyCancellable? private var cursorCancellable: AnyCancellable? @MainActor - init(editorManager: EditorManager) { + public init(editorManager: EditorManager) { let initialTab = editorManager.activeEditor.selectedTab currentTab = initialTab subject = CurrentValueSubject(Self.map(initialTab?.cursorPositions ?? [])) @@ -51,13 +51,13 @@ final class AppActiveCursorState: ActiveCursorState { positions.map { EditorCursorPosition(line: $0.start.line, column: $0.start.column, range: $0.range) } } - var cursorPositions: [EditorCursorPosition] { subject.value } + public var cursorPositions: [EditorCursorPosition] { subject.value } - var cursorPositionsPublisher: AnyPublisher<[EditorCursorPosition], Never> { + public var cursorPositionsPublisher: AnyPublisher<[EditorCursorPosition], Never> { subject.eraseToAnyPublisher() } - func linesInRange(_ range: NSRange) -> Int { + public func linesInRange(_ range: NSRange) -> Int { currentTab?.rangeTranslator.linesInRange(range) ?? 0 } } diff --git a/CodeEdit/Features/Editor/Models/AppActiveEditorState.swift b/Packages/Features/Editor/Sources/Editor/Models/AppActiveEditorState.swift similarity index 72% rename from CodeEdit/Features/Editor/Models/AppActiveEditorState.swift rename to Packages/Features/Editor/Sources/Editor/Models/AppActiveEditorState.swift index 893fad386d..355bb15ba3 100644 --- a/CodeEdit/Features/Editor/Models/AppActiveEditorState.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/AppActiveEditorState.swift @@ -12,12 +12,12 @@ import CEWorkspaceFileManager /// App-side `ActiveEditorState` over a window's `EditorManager`. Emits the active editor's /// selected file across both active-editor switches and within-editor tab changes. -final class AppActiveEditorState: ActiveEditorState { +public final class AppActiveEditorState: ActiveEditorState { private let subject: CurrentValueSubject private var cancellable: AnyCancellable? @MainActor - init(editorManager: EditorManager) { + public init(editorManager: EditorManager) { subject = CurrentValueSubject(editorManager.activeEditor.selectedTab?.file) cancellable = editorManager.$activeEditor .flatMap { $0.$selectedTab } @@ -25,6 +25,6 @@ final class AppActiveEditorState: ActiveEditorState { .sink { [weak subject] file in subject?.send(file) } } - var selectedFile: CEWorkspaceFile? { subject.value } - var selectedFilePublisher: AnyPublisher { subject.eraseToAnyPublisher() } + public var selectedFile: CEWorkspaceFile? { subject.value } + public var selectedFilePublisher: AnyPublisher { subject.eraseToAnyPublisher() } } diff --git a/CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift b/Packages/Features/Editor/Sources/Editor/Models/AppFileEditorOverrides.swift similarity index 71% rename from CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift rename to Packages/Features/Editor/Sources/Editor/Models/AppFileEditorOverrides.swift index 26eac9b8e6..53105fc864 100644 --- a/CodeEdit/Features/Editor/Models/AppFileEditorOverrides.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/AppFileEditorOverrides.swift @@ -13,16 +13,16 @@ import CodeEditDocument /// App-side `FileEditorOverrides` over a window's `EditorManager`. Reads and writes /// the per-file overrides on the file's open `CodeFileDocument`, translating the /// language override to/from `CodeLanguage.id.rawValue`. -final class AppFileEditorOverrides: FileEditorOverrides { +public final class AppFileEditorOverrides: FileEditorOverrides { private let editorManager: EditorManager @MainActor - init(editorManager: EditorManager) { + public init(editorManager: EditorManager) { self.editorManager = editorManager } @MainActor - func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues { + public func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues { let document = editorManager.document(for: file) return FileEditorOverrideValues( indentOption: document?.indentOption, @@ -33,22 +33,22 @@ final class AppFileEditorOverrides: FileEditorOverrides { } @MainActor - func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) { + public func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) { editorManager.document(for: file)?.indentOption = value } @MainActor - func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) { + public func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) { editorManager.document(for: file)?.defaultTabWidth = value } @MainActor - func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) { + public func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) { editorManager.document(for: file)?.wrapLines = value } @MainActor - func setLanguageId(_ value: String?, for file: CEWorkspaceFile) { + public func setLanguageId(_ value: String?, for file: CEWorkspaceFile) { editorManager.document(for: file)?.language = value.flatMap { id in CodeLanguage.allLanguages.first { $0.id.rawValue == id } } diff --git a/CodeEdit/Features/Editor/Models/DocumentRegistry.swift b/Packages/Features/Editor/Sources/Editor/Models/DocumentRegistry.swift similarity index 85% rename from CodeEdit/Features/Editor/Models/DocumentRegistry.swift rename to Packages/Features/Editor/Sources/Editor/Models/DocumentRegistry.swift index b88b9b7397..47b83dd7bf 100644 --- a/CodeEdit/Features/Editor/Models/DocumentRegistry.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/DocumentRegistry.swift @@ -21,7 +21,7 @@ import CodeEditCore /// **not** `@MainActor`: its owner (`EditorManager`) and callers are not yet actor-isolated, and /// marking only this type does not compile in the app's current Swift 5 mode. It should become /// `@MainActor` together with `EditorManager` during the eventual app-wide Swift 6 migration. -final class DocumentRegistry { +public final class DocumentRegistry { private final class Box { weak var document: CodeFileDocument? let subject = PassthroughSubject() @@ -37,12 +37,12 @@ final class DocumentRegistry { } /// The open document for the file, or `nil` if none is loaded. - func document(for file: CEWorkspaceFile) -> CodeFileDocument? { + public func document(for file: CEWorkspaceFile) -> CodeFileDocument? { boxes[file.id]?.document } /// Associates (or clears, with `nil`) a document for the file and notifies subscribers. - func setDocument(_ document: CodeFileDocument?, for file: CEWorkspaceFile) { + public func setDocument(_ document: CodeFileDocument?, for file: CEWorkspaceFile) { let box = box(for: file.id) box.document = document box.subject.send(document) @@ -50,7 +50,7 @@ final class DocumentRegistry { /// Loads a new `CodeFileDocument` for the file from disk, registers it, and returns it. @discardableResult - func loadDocument(for file: CEWorkspaceFile) throws -> CodeFileDocument { + public func loadDocument(for file: CEWorkspaceFile) throws -> CodeFileDocument { let document = try CodeFileDocument( contentsOf: file.resolvedURL, ofType: file.contentType?.identifier ?? "" @@ -61,7 +61,7 @@ final class DocumentRegistry { /// Emits whenever the document association for the file changes. Like the original, /// this does not replay the current value on subscription. - func documentPublisher(for file: CEWorkspaceFile) -> AnyPublisher { + public func documentPublisher(for file: CEWorkspaceFile) -> AnyPublisher { box(for: file.id).subject.eraseToAnyPublisher() } } diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor+History.swift b/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+History.swift similarity index 89% rename from CodeEdit/Features/Editor/Models/Editor/Editor+History.swift rename to Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+History.swift index 8dfcf228d5..b1b0debcaf 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor+History.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+History.swift @@ -11,7 +11,7 @@ import Foundation extension Editor { /// Add the tab to the history list. /// - Parameter tab: The tab to add to the history. - func addToHistory(_ tab: Tab) { + public func addToHistory(_ tab: Tab) { if history.first != tab.file { history.prepend(tab.file) } @@ -19,21 +19,21 @@ extension Editor { /// Clear any tabs in the "future" on the history list. Resets the history offset and removes any tabs that were /// available to navigate forwards to. - func clearFuture() { + public func clearFuture() { guard historyOffset > 0 else { return } // nothing to clear, avoid an out of bounds error history.removeFirst(historyOffset) historyOffset = 0 } /// Move backwards in the history list by one place. - func goBackInHistory() { + public func goBackInHistory() { if canGoBackInHistory { historyOffset += 1 } } /// Move forwards in the history list by one place. - func goForwardInHistory() { + public func goForwardInHistory() { if canGoForwardInHistory { historyOffset -= 1 } @@ -41,13 +41,13 @@ extension Editor { // TODO: move to @Observable so this works better /// Warning: NOT published! - var canGoBackInHistory: Bool { + public var canGoBackInHistory: Bool { historyOffset != history.count - 1 && !history.isEmpty } // TODO: move to @Observable so this works better /// Warning: NOT published! - var canGoForwardInHistory: Bool { + public var canGoForwardInHistory: Bool { historyOffset != 0 } diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor+TabSwitch.swift b/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+TabSwitch.swift similarity index 93% rename from CodeEdit/Features/Editor/Models/Editor/Editor+TabSwitch.swift rename to Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+TabSwitch.swift index 62e94a3150..ae6587b596 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor+TabSwitch.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+TabSwitch.swift @@ -8,7 +8,7 @@ import Foundation extension Editor { - func selectNextTab() { + public func selectNextTab() { guard let currentTab = selectedTab, let currentIndex = tabs.firstIndex(of: currentTab) else { return } let nextIndex = tabs.index(after: currentIndex) if nextIndex < tabs.endIndex { @@ -19,7 +19,7 @@ extension Editor { } } - func selectPreviousTab() { + public func selectPreviousTab() { guard let currentTab = selectedTab, let currentIndex = tabs.firstIndex(of: currentTab) else { return } let previousIndex = tabs.index(before: currentIndex) if previousIndex >= tabs.startIndex { diff --git a/CodeEdit/Features/Editor/Models/Editor/Editor.swift b/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor.swift similarity index 89% rename from CodeEdit/Features/Editor/Models/Editor/Editor.swift rename to Packages/Features/Editor/Sources/Editor/Models/Editor/Editor.swift index 011b8ac591..02fa6604b1 100644 --- a/CodeEdit/Features/Editor/Models/Editor/Editor.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor.swift @@ -12,15 +12,15 @@ import DequeModule import AppKit import OSLog -final class Editor: ObservableObject, Identifiable { - enum EditorError: Error { +public final class Editor: ObservableObject, Identifiable { + public enum EditorError: Error { case noWorkspaceAttached } - typealias Tab = EditorInstance + public typealias Tab = EditorInstance /// Set of open tabs. - @Published var tabs: OrderedSet = [] { + @Published public var tabs: OrderedSet = [] { didSet { let change = tabs.symmetricDifference(oldValue) @@ -43,7 +43,7 @@ final class Editor: ObservableObject, Identifiable { /// The current offset in the history list. /// When set, updates the ``selectedTab`` to the tab indicated by the offset. /// See the ``historyOffsetDidChange()`` method for more details. - @Published var historyOffset: Int = 0 { + @Published public var historyOffset: Int = 0 { didSet { historyOffsetDidChange() } @@ -51,32 +51,32 @@ final class Editor: ObservableObject, Identifiable { /// Maintains the list of tabs that have been switched to. /// - Warning: Use the ``addToHistory(_:)`` or ``clearFuture()`` methods to modify this. Do not modify directly. - @Published var history: Deque = [] + @Published public var history: Deque = [] /// Currently selected tab. - @Published private(set) var selectedTab: Tab? + @Published public private(set) var selectedTab: Tab? - @Published var temporaryTab: Tab? + @Published public var temporaryTab: Tab? - var id = UUID() + public var id = UUID() - weak var parent: SplitViewData? - weak var findReplaceQuery: FindReplaceQuery? - weak var editorManager: EditorManager? + public weak var parent: SplitViewData? + public weak var findReplaceQuery: FindReplaceQuery? + public weak var editorManager: EditorManager? /// Whether this editor is attached to a workspace. Used to guard file loading operations. - var isAttachedToWorkspace: Bool = false + public var isAttachedToWorkspace: Bool = false private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "Editor") - init() { + public init() { self.tabs = [] self.temporaryTab = nil self.parent = nil self.findReplaceQuery = nil } - init( + public init( files: OrderedSet = [], selectedTab: Tab? = nil, temporaryTab: Tab? = nil, @@ -97,7 +97,7 @@ final class Editor: ObservableObject, Identifiable { self.temporaryTab = temporaryTab } - init( + public init( files: OrderedSet = [], selectedTab: Tab? = nil, temporaryTab: Tab? = nil, @@ -113,18 +113,18 @@ final class Editor: ObservableObject, Identifiable { } /// Closes the editor. - func close() { + public func close() { parent?.closeEditor(with: id) } /// Gets the editor layout. - func getEditorLayout() -> EditorLayout? { + public func getEditorLayout() -> EditorLayout? { return parent?.getEditorLayout(with: id) } /// Set the selected tab. Loads the file's contents if it hasn't already been opened. /// - Parameter file: The file to set as the selected tab. - func setSelectedTab(_ file: CEWorkspaceFile?) { + public func setSelectedTab(_ file: CEWorkspaceFile?) { guard let file else { selectedTab = nil return @@ -149,7 +149,7 @@ final class Editor: ObservableObject, Identifiable { /// - fromHistory: If `true`, does not clear tabs ahead of the ``historyOffset`` /// Used when opening tabs from the history queue where tabs ahead of the ``historyOffset`` should /// not be removed. - func closeTab(file: CEWorkspaceFile, fromHistory: Bool = false) { + public func closeTab(file: CEWorkspaceFile, fromHistory: Bool = false) { guard canCloseTab(file: file) else { return } if temporaryTab?.file == file { @@ -175,7 +175,7 @@ final class Editor: ObservableObject, Identifiable { } /// Closes the currently opened tab in the tab group. - func closeSelectedTab() { + public func closeSelectedTab() { guard let file = selectedTab?.file else { return } @@ -188,7 +188,7 @@ final class Editor: ObservableObject, Identifiable { /// - Parameters: /// - file: the file to open. /// - asTemporary: indicates whether the tab should be opened as a temporary tab or a permanent tab. - func openTab(file: CEWorkspaceFile, asTemporary: Bool) { + public func openTab(file: CEWorkspaceFile, asTemporary: Bool) { let item = EditorInstance(findReplaceQuery: findReplaceQuery, file: file) // Item is already opened in a tab. guard !tabs.contains(item) || !asTemporary else { @@ -246,7 +246,7 @@ final class Editor: ObservableObject, Identifiable { /// - file: The tab to open. /// - index: Index where the tab needs to be added. If nil, it is added to the back. /// - fromHistory: Indicates whether the tab has been opened from going back in history. - func openTab(file: CEWorkspaceFile, at index: Int? = nil, fromHistory: Bool = false) { + public func openTab(file: CEWorkspaceFile, at index: Int? = nil, fromHistory: Bool = false) { let item = Tab(findReplaceQuery: findReplaceQuery, file: file) if let index { tabs.insert(item, at: index) @@ -329,7 +329,7 @@ final class Editor: ObservableObject, Identifiable { /// Remove the given file from tabs. /// - Parameter file: The file to remove. - func removeTab(_ file: CEWorkspaceFile) { + public func removeTab(_ file: CEWorkspaceFile) { tabs.removeAll(where: { tab in tab.file == file }) if temporaryTab?.file == file { temporaryTab = nil @@ -338,11 +338,11 @@ final class Editor: ObservableObject, Identifiable { } extension Editor: Equatable, Hashable { - static func == (lhs: Editor, rhs: Editor) -> Bool { + public static func == (lhs: Editor, rhs: Editor) -> Bool { lhs.id == rhs.id } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(id) } } diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/Packages/Features/Editor/Sources/Editor/Models/EditorInstance.swift similarity index 82% rename from CodeEdit/Features/Editor/Models/EditorInstance.swift rename to Packages/Features/Editor/Sources/Editor/Models/EditorInstance.swift index 72b49f8dad..ae094caa4b 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/EditorInstance.swift @@ -14,27 +14,27 @@ import CodeEditSourceEditor /// A single instance of an editor in a group with a published ``EditorInstance/cursorPositions`` variable to publish /// the user's current location in a file. -class EditorInstance: ObservableObject, Hashable { +public class EditorInstance: ObservableObject, Hashable { /// The file presented in this editor instance. - let file: CEWorkspaceFile + public let file: CEWorkspaceFile /// A publisher for the user's current location in a file. - @Published var cursorPositions: [CursorPosition] - @Published var scrollPosition: CGPoint? + @Published public var cursorPositions: [CursorPosition] + @Published public var scrollPosition: CGPoint? - @Published var findText: String? - var findTextSubject: PassthroughSubject + @Published public var findText: String? + public var findTextSubject: PassthroughSubject - @Published var replaceText: String? - var replaceTextSubject: PassthroughSubject + @Published public var replaceText: String? + public var replaceTextSubject: PassthroughSubject - var rangeTranslator: RangeTranslator = RangeTranslator() + public var rangeTranslator: RangeTranslator = RangeTranslator() private var cancellables: Set = [] // MARK: - Init - init(findReplaceQuery: FindReplaceQuery?, file: CEWorkspaceFile, cursorPositions: [CursorPosition]? = nil) { + public init(findReplaceQuery: FindReplaceQuery?, file: CEWorkspaceFile, cursorPositions: [CursorPosition]? = nil) { self.file = file let url = file.url let editorState = EditorStateRestoration.shared?.restorationState(for: url) @@ -113,33 +113,33 @@ class EditorInstance: ObservableObject, Hashable { // MARK: - Hashable, Equatable - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(file) } - static func == (lhs: EditorInstance, rhs: EditorInstance) -> Bool { + public static func == (lhs: EditorInstance, rhs: EditorInstance) -> Bool { lhs.file == rhs.file } // MARK: - RangeTranslator /// Translates ranges (eg: from a cursor position) to other information like the number of lines in a range. - class RangeTranslator: TextViewCoordinator { + public class RangeTranslator: TextViewCoordinator { private weak var textViewController: TextViewController? init() { } - func prepareCoordinator(controller: TextViewController) { + public func prepareCoordinator(controller: TextViewController) { self.textViewController = controller } - func controllerDidAppear(controller: TextViewController) { + public func controllerDidAppear(controller: TextViewController) { if controller.isEditable && controller.isSelectable { controller.view.window?.makeFirstResponder(controller.textView) } } - func destroy() { + public func destroy() { self.textViewController = nil } @@ -147,7 +147,7 @@ class EditorInstance: ObservableObject, Hashable { /// - Parameter range: The range to use. /// - Returns: The number of lines contained by the given range. Or `0` if the text view could not be found, /// or lines could not be found for the given range. - func linesInRange(_ range: NSRange) -> Int { + public func linesInRange(_ range: NSRange) -> Int { guard let controller = textViewController, let scrollView = controller.view as? NSScrollView, let textView = scrollView.documentView as? TextView, @@ -159,12 +159,12 @@ class EditorInstance: ObservableObject, Hashable { return (endTextLine.index - startTextLine.index) + 1 } - func moveLinesUp() { + public func moveLinesUp() { guard let controller = textViewController else { return } controller.moveLinesUp() } - func moveLinesDown() { + public func moveLinesDown() { guard let controller = textViewController else { return } controller.moveLinesDown() } diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift similarity index 88% rename from CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift rename to Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 993d38d592..53d2be8c09 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -17,7 +17,7 @@ extension EditorManager { /// - statePersistence: The persistence service to retrieve saved state from. /// - fileManager: The file manager to resolve file references. /// - findReplaceQuery: The shared find/replace query for editor instances. - func restoreFromState( + public func restoreFromState( statePersistence: any WorkspaceStatePersisting, fileManager: CEWorkspaceFileManager?, findReplaceQuery: FindReplaceQuery? @@ -49,7 +49,7 @@ extension EditorManager { } } - func saveRestorationState(_ statePersistence: any WorkspaceStatePersisting) { + public func saveRestorationState(_ statePersistence: any WorkspaceStatePersisting) { if let data = try? JSONEncoder().encode( EditorRestorationState(activeEditor: activeEditor.id, groups: editorLayout) ) { @@ -60,9 +60,9 @@ extension EditorManager { } } -struct EditorRestorationState: Codable { - var activeEditor: UUID - var groups: EditorLayout +public struct EditorRestorationState: Codable { + public var activeEditor: UUID + public var groups: EditorLayout } extension EditorLayout: Codable { @@ -72,12 +72,12 @@ extension EditorLayout: Codable { case horizontal } - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case type case tabs } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(EditorLayoutType.self, forKey: .type) switch type { @@ -93,7 +93,7 @@ extension EditorLayout: Codable { } } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .one(data): @@ -128,19 +128,19 @@ extension SplitViewData: Codable { } } - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case editorLayouts case axis } - convenience init(from decoder: Decoder) throws { + public convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let axis = try container.decode(SplitViewAxis.self, forKey: .axis).swiftUI let editorLayouts = try container.decode([EditorLayout].self, forKey: .editorLayouts) self.init(axis, editorLayouts: editorLayouts) } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(editorLayouts, forKey: .editorLayouts) try container.encode(SplitViewAxis(axis), forKey: .axis) @@ -148,13 +148,13 @@ extension SplitViewData: Codable { } extension Editor: Codable { - enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case tabs case selectedTab case id } - convenience init(from decoder: Decoder) throws { + public convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let fileURLs = try container.decode([URL].self, forKey: .tabs) let selectedTab = try? container.decode(URL.self, forKey: .selectedTab) @@ -171,7 +171,7 @@ extension Editor: Codable { self.id = id } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(tabs.map { $0.file.url }, forKey: .tabs) try container.encode(selectedTab?.file.url, forKey: .selectedTab) diff --git a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift b/Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout.swift similarity index 87% rename from CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift rename to Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout.swift index 9af83b6fbd..c07e0e18c6 100644 --- a/CodeEdit/Features/Editor/Models/EditorLayout/EditorLayout.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout.swift @@ -8,14 +8,14 @@ import Foundation import CodeEditCore -enum EditorLayout: Equatable { +public enum EditorLayout: Equatable { case one(Editor) case vertical(SplitViewData) case horizontal(SplitViewData) /// Closes all tabs which present the given file /// - Parameter file: a file. - func closeAllTabs(of file: CEWorkspaceFile) { + public func closeAllTabs(of file: CEWorkspaceFile) { switch self { case .one(let editor): editor.removeTab(file) @@ -29,7 +29,7 @@ enum EditorLayout: Equatable { /// Returns some editor, except the given editor. /// - Parameter except: the search will exclude this editor. /// - Returns: Some editor. - func findSomeEditor(except: Editor? = nil) -> Editor? { + public func findSomeEditor(except: Editor? = nil) -> Editor? { switch self { case .one(let editor) where editor != except: return editor @@ -45,7 +45,7 @@ enum EditorLayout: Equatable { } } - func find(editor id: UUID) -> Editor? { + public func find(editor id: UUID) -> Editor? { switch self { case .one(let editor): if editor.id == id { @@ -63,7 +63,7 @@ enum EditorLayout: Equatable { } /// Forms a set of all files currently represented by tabs. - func gatherOpenFiles() -> Set { + public func gatherOpenFiles() -> Set { switch self { case .one(let editor): return Set(editor.tabs.map { $0.file }) @@ -73,7 +73,7 @@ enum EditorLayout: Equatable { } /// Flattens the splitviews. - mutating func flatten(parent: SplitViewData) { + public mutating func flatten(parent: SplitViewData) { switch self { case .one: break @@ -91,7 +91,7 @@ enum EditorLayout: Equatable { } /// Gets flattened splitviews. - func getFlattened(parent: SplitViewData) -> [Editor] { + public func getFlattened(parent: SplitViewData) -> [Editor] { switch self { case .one(let editor): return [editor] @@ -108,7 +108,7 @@ enum EditorLayout: Equatable { } } - var isEmpty: Bool { + public var isEmpty: Bool { switch self { case .one: return false @@ -119,7 +119,7 @@ enum EditorLayout: Equatable { } } - static func == (lhs: EditorLayout, rhs: EditorLayout) -> Bool { + public static func == (lhs: EditorLayout, rhs: EditorLayout) -> Bool { switch (lhs, rhs) { case let (.one(lhs), .one(rhs)): return lhs == rhs diff --git a/CodeEdit/Features/Editor/Models/EditorManager.swift b/Packages/Features/Editor/Sources/Editor/Models/EditorManager.swift similarity index 75% rename from CodeEdit/Features/Editor/Models/EditorManager.swift rename to Packages/Features/Editor/Sources/Editor/Models/EditorManager.swift index cf51083948..522ccee199 100644 --- a/CodeEdit/Features/Editor/Models/EditorManager.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/EditorManager.swift @@ -12,19 +12,19 @@ import Foundation import DequeModule import os -class EditorManager: ObservableObject { +public class EditorManager: ObservableObject { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "EditorManager") /// Owns the file→document association for this workspace. - let documents = DocumentRegistry() + public let documents = DocumentRegistry() /// The complete editor layout. - @Published var editorLayout: EditorLayout + @Published public var editorLayout: EditorLayout - @Published var isFocusingActiveEditor: Bool + @Published public var isFocusingActiveEditor: Bool /// The Editor with active focus. - @Published var activeEditor: Editor { + @Published public var activeEditor: Editor { didSet { activeEditorHistory.prepend { [weak oldValue] in oldValue } switchToActiveEditor() @@ -32,16 +32,16 @@ class EditorManager: ObservableObject { } /// History of last-used editors. - var activeEditorHistory: Deque<() -> Editor?> = [] + public var activeEditorHistory: Deque<() -> Editor?> = [] /// notify listeners whenever tab selection changes on the active editor. - var tabBarTabIdSubject = PassthroughSubject() + public var tabBarTabIdSubject = PassthroughSubject() var cancellable: AnyCancellable? // This caching mechanism is a temporary solution and is not optimized - @Published var updateCachedFlattenedEditors: Bool = true - var cachedFlettenedEditors: [Editor] = [] - var flattenedEditors: [Editor] { + @Published public var updateCachedFlattenedEditors: Bool = true + public var cachedFlettenedEditors: [Editor] = [] + public var flattenedEditors: [Editor] { if updateCachedFlattenedEditors { cachedFlettenedEditors = self.getFlattened() updateCachedFlattenedEditors = false @@ -51,7 +51,7 @@ class EditorManager: ObservableObject { // MARK: - Init - init() { + public init() { let tab = Editor() self.activeEditor = tab self.activeEditorHistory.prepend { [weak tab] in tab } @@ -64,7 +64,7 @@ class EditorManager: ObservableObject { /// Initializes the editor manager's state to the "initial" state. /// /// Functionally identical to the initializer for this class. - func initCleanState() { + public func initCleanState() { let tab = Editor() self.activeEditor = tab self.activeEditorHistory.prepend { [weak tab] in tab } @@ -75,7 +75,7 @@ class EditorManager: ObservableObject { } /// Flattens the splitviews. - func flatten() { + public func flatten() { switch editorLayout { case .horizontal(let data), .vertical(let data): data.flatten() @@ -85,7 +85,7 @@ class EditorManager: ObservableObject { } /// Returns and array of flattened splitviews. - func getFlattened() -> [Editor] { + public func getFlattened() -> [Editor] { switch editorLayout { case .horizontal(let data), .vertical(let data): return data.getFlattened() @@ -99,13 +99,13 @@ class EditorManager: ObservableObject { /// - item: The tab to open. /// - editor: The editor to add the tab to. If nil, it is added to the active tab group. /// - asTemporary: Indicates whether the tab should be opened as a temporary tab or a permanent tab. - func openTab(item: CEWorkspaceFile, in editor: Editor? = nil, asTemporary: Bool = false) { + public func openTab(item: CEWorkspaceFile, in editor: Editor? = nil, asTemporary: Bool = false) { let editor = editor ?? activeEditor editor.openTab(file: item, asTemporary: asTemporary) } /// bind active tap group to listen to file selection changes. - func switchToActiveEditor() { + public func switchToActiveEditor() { cancellable?.cancel() cancellable = nil cancellable = activeEditor.$selectedTab @@ -118,7 +118,7 @@ class EditorManager: ObservableObject { /// Close an editor and fix editor manager state, updating active editor, etc. /// - Parameter editor: The editor to close - func closeEditor(_ editor: Editor) { + public func closeEditor(_ editor: Editor) { editor.close() if activeEditor == editor { setNewActiveEditor(excluding: editor) @@ -131,7 +131,7 @@ class EditorManager: ObservableObject { /// Set a new active editor. /// - Parameter editor: The editor to exclude. - func setNewActiveEditor(excluding editor: Editor) { + public func setNewActiveEditor(excluding editor: Editor) { activeEditorHistory.removeAll { $0() == nil || $0() == editor } if activeEditorHistory.isEmpty { activeEditor = findSomeEditor(excluding: editor) @@ -143,7 +143,7 @@ class EditorManager: ObservableObject { /// Find some editor, or if one cannot be found set up the editor manager with a clean state. /// - Parameter editor: The editor to exclude. /// - Returns: Some editor, order is not guaranteed. - func findSomeEditor(excluding editor: Editor) -> Editor { + public func findSomeEditor(excluding editor: Editor) -> Editor { guard let someEditor = editorLayout.findSomeEditor(except: editor) else { initCleanState() return activeEditor @@ -153,7 +153,7 @@ class EditorManager: ObservableObject { // MARK: - Focus - func toggleFocusingEditor(from editor: Editor) { + public func toggleFocusingEditor(from editor: Editor) { if !isFocusingActiveEditor { activeEditor = editor } @@ -162,20 +162,20 @@ class EditorManager: ObservableObject { // MARK: - Documents - func document(for file: CEWorkspaceFile) -> CodeFileDocument? { + public func document(for file: CEWorkspaceFile) -> CodeFileDocument? { documents.document(for: file) } - func setDocument(_ document: CodeFileDocument?, for file: CEWorkspaceFile) { + public func setDocument(_ document: CodeFileDocument?, for file: CEWorkspaceFile) { documents.setDocument(document, for: file) } @discardableResult - func loadDocument(for file: CEWorkspaceFile) throws -> CodeFileDocument { + public func loadDocument(for file: CEWorkspaceFile) throws -> CodeFileDocument { try documents.loadDocument(for: file) } - func documentPublisher(for file: CEWorkspaceFile) -> AnyPublisher { + public func documentPublisher(for file: CEWorkspaceFile) -> AnyPublisher { documents.documentPublisher(for: file) } } diff --git a/CodeEdit/Features/Editor/Models/Environment+ActiveEditor.swift b/Packages/Features/Editor/Sources/Editor/Models/Environment+ActiveEditor.swift similarity index 63% rename from CodeEdit/Features/Editor/Models/Environment+ActiveEditor.swift rename to Packages/Features/Editor/Sources/Editor/Models/Environment+ActiveEditor.swift index 6e400e3e90..f89c11e8c3 100644 --- a/CodeEdit/Features/Editor/Models/Environment+ActiveEditor.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Environment+ActiveEditor.swift @@ -7,11 +7,11 @@ import SwiftUI -struct ActiveEditorEnvironmentKey: EnvironmentKey { - static var defaultValue = false +public struct ActiveEditorEnvironmentKey: EnvironmentKey { + nonisolated(unsafe) public static var defaultValue = false } -extension EnvironmentValues { +public extension EnvironmentValues { var isActiveEditor: Bool { get { self[ActiveEditorEnvironmentKey.self] } set { self[ActiveEditorEnvironmentKey.self] = newValue } diff --git a/Packages/Features/Editor/Sources/Editor/Models/FileIcon.swift b/Packages/Features/Editor/Sources/Editor/Models/FileIcon.swift new file mode 100644 index 0000000000..2662acf0c6 --- /dev/null +++ b/Packages/Features/Editor/Sources/Editor/Models/FileIcon.swift @@ -0,0 +1,125 @@ +// +// FileIcon.swift +// Editor +// +// Created by Nanashi Li on 2022/05/20. +// + +import SwiftUI +import CodeEditCore + +enum FileIcon { + + static func fileIcon(fileType: FileType?) -> String { // swiftlint:disable:this cyclomatic_complexity function_body_length + switch fileType { + case .json, .yml, .resolved: + return "doc.json" + case .lock: + return "lock.doc" + case .css: + return "curlybraces" + case .js, .mjs: + return "doc.javascript" + case .jsx, .tsx: + return "atom" + case .swift: + return "swift" + case .env, .example: + return "gearshape.fill" + case .gitignore: + return "arrow.triangle.branch" + case .pdf, .png, .jpg, .jpeg, .ico: + return "photo" + case .svg: + return "square.fill.on.circle.fill" + case .entitlements: + return "checkmark.seal" + case .plist: + return "tablecells" + case .md, .txt: + return "doc.plaintext" + case .rtf: + return "doc.richtext" + case .html: + return "chevron.left.forwardslash.chevron.right" + case .LICENSE: + return "key.fill" + case .java: + return "cup.and.saucer" + case .py: + return "doc.python" + case .rb: + return "doc.ruby" + case .strings: + return "text.quote" + case .h: + return "h.square" + case .m: + return "m.square" + case .vue: + return "v.square" + case .go: + return "g.square" + case .sum: + return "s.square" + case .mod: + return "m.square" + case .bash, .sh, .Makefile, .zsh: + return "terminal" + case .rs: + return "r.square" + case .wav, .mp3, .aif, .mid: + return "speaker.wave.2" + case .avi, .mp4, .mov: + return "film" + case .scpt: + return "applescript" + case .xcconfig: + return "gearshape.2" + case .cetheme: + return "paintbrush" + case .adb, .clj, .cls, .cs, .d, .dart, .elm, .ex, .f95, .fs, .gs, .hs, + .jl, .kt, .l, .lsp, .lua, .mk, .pas, .pl, .scm, .ss: + return "doc.plaintext" + default: + return "doc" + } + } + + static func iconColor(fileType: FileType?) -> Color { // swiftlint:disable:this cyclomatic_complexity + switch fileType { + case .swift, .html: + return .orange + case .java, .jpg, .png, .svg, .ts: + return .blue + case .css: + return .teal + case .js, .mjs, .py, .entitlements, .LICENSE: + return Color("Amber", bundle: .main) + case .json, .resolved, .rb, .strings, .yml: + return Color("Scarlet", bundle: .main) + case .jsx, .tsx: + return .cyan + case .plist, .xcconfig, .sh: + return Color("Steel", bundle: .main) + case .c, .cetheme: + return .purple + case .vue: + return Color(red: 0.255, green: 0.722, blue: 0.514, opacity: 1.0) + case .h: + return Color(red: 0.667, green: 0.031, blue: 0.133, opacity: 1.0) + case .m: + return Color(red: 0.271, green: 0.106, blue: 0.525, opacity: 1.0) + case .go: + return Color(red: 0.02, green: 0.675, blue: 0.757, opacity: 1.0) + case .sum, .mod: + return Color(red: 0.925, green: 0.251, blue: 0.478, opacity: 1.0) + case .Makefile: + return Color(red: 0.937, green: 0.325, blue: 0.314, opacity: 1.0) + case .rs: + return .orange + default: + return Color("Steel", bundle: .main) + } + } +} diff --git a/CodeEdit/Features/Editor/Models/Restoration/EditorStateRestoration.swift b/Packages/Features/Editor/Sources/Editor/Models/Restoration/EditorStateRestoration.swift similarity index 83% rename from CodeEdit/Features/Editor/Models/Restoration/EditorStateRestoration.swift rename to Packages/Features/Editor/Sources/Editor/Models/Restoration/EditorStateRestoration.swift index 4b375ac887..519560ef06 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/EditorStateRestoration.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Restoration/EditorStateRestoration.swift @@ -22,37 +22,36 @@ import OSLog /// /// Use the database migrator in the initializer for this class, see GRDB's documentation for adding a migration /// version. **Do not ever** delete migration versions that have made it to a released version of CodeEdit. -final class EditorStateRestoration { +public final class EditorStateRestoration { /// Optional here so we can gracefully catch errors. /// The nice thing is this feature is optional in that if we don't have it available the user's experience is /// degraded but not catastrophic. - static let shared: EditorStateRestoration? = try? EditorStateRestoration() + nonisolated(unsafe) public static let shared: EditorStateRestoration? = try? EditorStateRestoration() private static let logger = Logger( subsystem: Bundle.main.bundleIdentifier ?? "", category: "EditorStateRestoration" ) - struct StateRestorationRecord: Codable, TableRecord, FetchableRecord, PersistableRecord { - let uri: String - let data: Data + public struct StateRestorationRecord: Codable, TableRecord, FetchableRecord, PersistableRecord { + public let uri: String + public let data: Data } - struct StateRestorationData: Codable, Equatable { - // Cursor positions as range values (not row/column!) - let cursorPositions: [Range] - let scrollPositionX: Double - let scrollPositionY: Double + public struct StateRestorationData: Codable, Equatable { + public let cursorPositions: [Range] + public let scrollPositionX: Double + public let scrollPositionY: Double - var scrollPosition: CGPoint { + public var scrollPosition: CGPoint { CGPoint(x: scrollPositionX, y: scrollPositionY) } - var editorCursorPositions: [CursorPosition] { + public var editorCursorPositions: [CursorPosition] { cursorPositions.map { CursorPosition(range: NSRange(start: $0.lowerBound, end: $0.upperBound)) } } - init(cursorPositions: [CursorPosition], scrollPosition: CGPoint) { + public init(cursorPositions: [CursorPosition], scrollPosition: CGPoint) { self.cursorPositions = cursorPositions .compactMap { $0.range } .map { $0.location..<($0.location + $0.length) } @@ -68,7 +67,7 @@ final class EditorStateRestoration { /// - Parameter databaseURL: The database URL to use. Must point to a file, not a directory. If left `nil`, will /// create a new database named `editor-restoration.db` in the application support /// directory. - init(_ databaseURL: URL? = nil) throws { + public init(_ databaseURL: URL? = nil) throws { self.databaseURL = databaseURL ?? FileManager.default .homeDirectoryForCurrentUser .appending(path: "Library/Application Support/CodeEdit", directoryHint: .isDirectory) @@ -108,7 +107,7 @@ final class EditorStateRestoration { /// - Parameters: /// - documentUrl: The URL of the document. /// - data: The data to store for the file, retrieved using ``restorationState(for:)``. - func updateRestorationState(for documentUrl: URL, data: StateRestorationData) { + public func updateRestorationState(for documentUrl: URL, data: StateRestorationData) { do { let serializedData = try JSONEncoder().encode(data) let dbRow = StateRestorationRecord(uri: documentUrl.absolutePath, data: serializedData) @@ -121,7 +120,7 @@ final class EditorStateRestoration { /// Find the restoration state for a document. /// - Parameter documentUrl: The URL of the document. /// - Returns: Any data saved for this file. - func restorationState(for documentUrl: URL) -> StateRestorationData? { + public func restorationState(for documentUrl: URL) -> StateRestorationData? { do { guard let row = try databaseQueue?.read({ try StateRestorationRecord.fetchOne($0, key: documentUrl.absolutePath) diff --git a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift b/Packages/Features/Editor/Sources/Editor/Models/Restoration/UndoManagerRegistration.swift similarity index 84% rename from CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift rename to Packages/Features/Editor/Sources/Editor/Models/Restoration/UndoManagerRegistration.swift index 988f3a4b9f..7723e5f5d4 100644 --- a/CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Restoration/UndoManagerRegistration.swift @@ -18,25 +18,25 @@ import CodeEditTextView /// - `CEWorkspaceFile` can be refreshed and reloaded at any point. /// - `CodeFileDocument` is released once there are no editors viewing it. /// Undo stacks need to be retained for the duration of a workspace session, enduring editor closes.. -final class UndoManagerRegistration: ObservableObject { +public final class UndoManagerRegistration: ObservableObject { private var managerMap: [String: CEUndoManager] = [:] /// Used to check whether a file still has an open document. Wired by `WorkspaceFactory`. - weak var editorManager: EditorManager? + public weak var editorManager: EditorManager? - init() { } + public init() { } /// Find or create a new undo manager. /// - Parameter file: The file to create for. /// - Returns: The undo manager for the given file. - func manager(forFile file: CEWorkspaceFile) -> CEUndoManager { + public func manager(forFile file: CEWorkspaceFile) -> CEUndoManager { manager(forFile: file.url) } /// Find or create a new undo manager. /// - Parameter path: The path of the file to create for. /// - Returns: The undo manager for the given file. - func manager(forFile path: URL) -> CEUndoManager { + public func manager(forFile path: URL) -> CEUndoManager { if let manager = managerMap[path.absolutePath] { return manager } else { @@ -49,7 +49,7 @@ final class UndoManagerRegistration: ObservableObject { /// Find or create a new undo manager. /// - Parameter path: The path of the file to create for. /// - Returns: The undo manager for the given file. - func managerIfExists(forFile path: URL) -> CEUndoManager? { + public func managerIfExists(forFile path: URL) -> CEUndoManager? { managerMap[path.absolutePath] } } @@ -61,7 +61,7 @@ extension UndoManagerRegistration: CEWorkspaceFileManagerObserver { /// /// To handle this? /// - When we receive a file update, if the file is not open in any editors we clear the undo stack - func fileManagerUpdated(updatedItems: Set) { + public func fileManagerUpdated(updatedItems: Set) { for file in updatedItems where editorManager?.document(for: file) == nil { managerMap.removeValue(forKey: file.url.absolutePath) } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift b/Packages/Features/Editor/Sources/Editor/Models/Theme+EditorTheme.swift similarity index 74% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift rename to Packages/Features/Editor/Sources/Editor/Models/Theme+EditorTheme.swift index f0c7b0a645..2abf5f12b7 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+Color.swift +++ b/Packages/Features/Editor/Sources/Editor/Models/Theme+EditorTheme.swift @@ -1,40 +1,15 @@ // -// Theme+Color.swift -// CodeEdit +// Theme+EditorTheme.swift +// Editor // -// Created by Lukas Pistrol on 31.03.22. +// Created by Matthijs Eikelenboom on 10.07.26. // -import SwiftUI import CodeEditSettings import CodeEditSourceEditor +import AppKit -/// Color conversion extensions for Theme types. -/// These bridge between the hex string storage format and SwiftUI/AppKit color types. - -extension Theme.Attributes { - /// The `SwiftUI` color of ``color`` - var swiftColor: Color { - get { - Color(hex: color) - } - set { - self.color = newValue.hexString - } - } - - /// The `NSColor` of ``color`` - var nsColor: NSColor { - get { - NSColor(hex: color) - } - set { - self.color = newValue.hexString - } - } -} - -extension Theme.EditorColors { +public extension Theme.EditorColors { var editorTheme: EditorTheme { get { .init( diff --git a/CodeEdit/Features/SplitView/Model/SplitViewData.swift b/Packages/Features/Editor/Sources/Editor/SplitViewData.swift similarity index 84% rename from CodeEdit/Features/SplitView/Model/SplitViewData.swift rename to Packages/Features/Editor/Sources/Editor/SplitViewData.swift index a874085764..987dc1ebd5 100644 --- a/CodeEdit/Features/SplitView/Model/SplitViewData.swift +++ b/Packages/Features/Editor/Sources/Editor/SplitViewData.swift @@ -7,12 +7,12 @@ import SwiftUI -final class SplitViewData: ObservableObject { - @Published var editorLayouts: [EditorLayout] +public final class SplitViewData: ObservableObject { + @Published public var editorLayouts: [EditorLayout] - var axis: Axis + public var axis: Axis - init(_ axis: Axis, editorLayouts: [EditorLayout] = []) { + public init(_ axis: Axis, editorLayouts: [EditorLayout] = []) { self.editorLayouts = editorLayouts self.axis = axis @@ -30,7 +30,7 @@ final class SplitViewData: ObservableObject { /// the editor is added to the ancestor instead of creating a new split container. /// - index: index where the divider will be added. /// - editor: new editor class that will be used for the editor. - func split(_ direction: Edge, at index: Int, new editor: Editor) { + public func split(_ direction: Edge, at index: Int, new editor: Editor) { editor.parent = self switch (axis, direction) { case (.horizontal, .trailing), (.vertical, .bottom): @@ -55,7 +55,7 @@ final class SplitViewData: ObservableObject { /// Closes an Editor. /// - Parameter id: ID of the Editor. - func closeEditor(with id: Editor.ID) { + public func closeEditor(with id: Editor.ID) { editorLayouts.removeAll { editorLayout in if case .one(let editor) = editorLayout { if editor.id == id { @@ -67,7 +67,7 @@ final class SplitViewData: ObservableObject { } } - func getEditorLayout(with id: Editor.ID) -> EditorLayout? { + public func getEditorLayout(with id: Editor.ID) -> EditorLayout? { for editorLayout in editorLayouts { if case .one(let editor) = editorLayout { if editor.id == id { @@ -80,14 +80,14 @@ final class SplitViewData: ObservableObject { } /// Flattens the splitviews. - func flatten() { + public func flatten() { for index in editorLayouts.indices { editorLayouts[index].flatten(parent: self) } } /// Gets flattened splitviews. - func getFlattened() -> [Editor] { + public func getFlattened() -> [Editor] { var arr: [Editor] = [] for index in editorLayouts.indices { arr += editorLayouts[index].getFlattened(parent: self) diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabView.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabView.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs.swift diff --git a/CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorHistoryMenus.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Views/EditorHistoryMenus.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorHistoryMenus.swift diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarAccessory.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarAccessory.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Views/EditorTabBarAccessory.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarAccessory.swift diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift similarity index 98% rename from CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift index f7d76781f9..982c7d25b4 100644 --- a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -103,7 +103,7 @@ struct EditorTabBarContextMenu: ViewModifier { Group { Button("Show in Finder") { - item.showInFinder() + NSWorkspace.shared.activateFileViewerSelecting([item.url]) } Button("Reveal in Project Navigator") { diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarDivider.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarDivider.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Views/EditorTabBarDivider.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarDivider.swift diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift diff --git a/CodeEdit/Features/Editor/TabBar/Views/EditorTabBarView.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarView.swift similarity index 100% rename from CodeEdit/Features/Editor/TabBar/Views/EditorTabBarView.swift rename to Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarView.swift diff --git a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift b/Packages/Features/Editor/Sources/Editor/UseCases/RestoreEditorStateUseCase.swift similarity index 97% rename from CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift rename to Packages/Features/Editor/Sources/Editor/UseCases/RestoreEditorStateUseCase.swift index 24909f982b..e4b4d9e9da 100644 --- a/CodeEdit/Features/Editor/UseCases/RestoreEditorStateUseCase.swift +++ b/Packages/Features/Editor/Sources/Editor/UseCases/RestoreEditorStateUseCase.swift @@ -12,9 +12,9 @@ import OSLog import OrderedCollections /// Restores an editor layout from persisted state, resolving file references against the current file manager. -final class RestoreEditorStateUseCase { +public final class RestoreEditorStateUseCase { - enum Outcome { + public enum Outcome { /// Persisted state was loaded and resolved successfully. case restored(layout: EditorLayout, activeEditor: Editor) /// Persisted state was found but is empty/invalid; caller should initialize a clean state. @@ -26,7 +26,7 @@ final class RestoreEditorStateUseCase { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RestoreEditorStateUseCase") /// Decodes persisted editor state, validates it, and resolves file references. - func execute( + public func execute( statePersistence: any WorkspaceStatePersisting, fileManager: CEWorkspaceFileManager?, findReplaceQuery: FindReplaceQuery?, diff --git a/CodeEdit/Features/Editor/Views/AnyFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/AnyFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/AnyFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/AnyFileView.swift diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/CodeFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/EditorAreaFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/Packages/Features/Editor/Sources/Editor/Views/EditorAreaView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/EditorAreaView.swift rename to Packages/Features/Editor/Sources/Editor/Views/EditorAreaView.swift diff --git a/CodeEdit/Features/Editor/Views/EditorLayoutView.swift b/Packages/Features/Editor/Sources/Editor/Views/EditorLayoutView.swift similarity index 87% rename from CodeEdit/Features/Editor/Views/EditorLayoutView.swift rename to Packages/Features/Editor/Sources/Editor/Views/EditorLayoutView.swift index 026da785bd..4196bff0ac 100644 --- a/CodeEdit/Features/Editor/Views/EditorLayoutView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/EditorLayoutView.swift @@ -8,8 +8,8 @@ import SwiftUI import CodeEditUI -struct EditorLayoutView: View { - var layout: EditorLayout +public struct EditorLayoutView: View { + public var layout: EditorLayout @FocusState.Binding var focus: Editor? @@ -23,7 +23,12 @@ struct EditorLayoutView: View { window?.contentView?.safeAreaInsets.top ?? .zero } - var body: some View { + public init(layout: EditorLayout, focus: FocusState.Binding) { + self.layout = layout + self._focus = focus + } + + public var body: some View { VStack { switch layout { case .one(let detailEditor): @@ -88,11 +93,11 @@ struct EditorLayoutView: View { } } -struct BelowToolbarEnvironmentKey: EnvironmentKey { - static var defaultValue: VerticalEdge.Set = .all +public struct BelowToolbarEnvironmentKey: EnvironmentKey { + nonisolated(unsafe) public static var defaultValue: VerticalEdge.Set = .all } -extension EnvironmentValues { +public extension EnvironmentValues { var isEditorLayoutAtEdge: BelowToolbarEnvironmentKey.Value { get { self[BelowToolbarEnvironmentKey.self] } set { self[BelowToolbarEnvironmentKey.self] = newValue } diff --git a/CodeEdit/Features/Editor/Views/FilePreviewView.swift b/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift similarity index 90% rename from CodeEdit/Features/Editor/Views/FilePreviewView.swift rename to Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift index 8b1daca878..c9045efcb9 100644 --- a/CodeEdit/Features/Editor/Views/FilePreviewView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift @@ -9,14 +9,14 @@ import SwiftUI import CodeEditDocument import CodeEditCore -struct FilePreviewView: View { +public struct FilePreviewView: View { private let item: CEWorkspaceFile @StateObject private var editorInstance: EditorInstance @StateObject private var document: CodeFileDocument @StateObject private var undoRegistration = UndoManagerRegistration() - init(item: CEWorkspaceFile) { + public init(item: CEWorkspaceFile) { self.item = item let doc = try? CodeFileDocument( for: item.url, @@ -27,7 +27,7 @@ struct FilePreviewView: View { self._document = .init(wrappedValue: doc ?? .init()) } - var body: some View { + public var body: some View { if let utType = document.utType, utType.conforms(to: .text) { CodeFileView(editorInstance: editorInstance, codeFile: document, isEditable: false) .environmentObject(undoRegistration) diff --git a/CodeEdit/Features/Editor/Views/ImageFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/ImageFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/ImageFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/ImageFileView.swift diff --git a/CodeEdit/Features/Editor/Views/LoadingFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/LoadingFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/LoadingFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/LoadingFileView.swift diff --git a/CodeEdit/Features/Editor/Views/NonTextFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/NonTextFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/NonTextFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/NonTextFileView.swift diff --git a/CodeEdit/Features/Editor/Views/PDFFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/PDFFileView.swift similarity index 100% rename from CodeEdit/Features/Editor/Views/PDFFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/PDFFileView.swift diff --git a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift similarity index 89% rename from CodeEdit/Features/Editor/Views/WindowCodeFileView.swift rename to Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift index a2ea975ec1..3f64a1eec3 100644 --- a/CodeEdit/Features/Editor/Views/WindowCodeFileView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift @@ -12,12 +12,12 @@ import SwiftUI /// View that fixes [#1158](https://github.com/CodeEditApp/CodeEdit/issues/1158) /// # Should **not** be used other than in a single file window. -struct WindowCodeFileView: View { +public struct WindowCodeFileView: View { @StateObject var editorInstance: EditorInstance @StateObject var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() var codeFile: CodeFileDocument - init(codeFile: CodeFileDocument) { + public init(codeFile: CodeFileDocument) { self._editorInstance = .init( wrappedValue: EditorInstance( findReplaceQuery: nil, @@ -27,7 +27,7 @@ struct WindowCodeFileView: View { self.codeFile = codeFile } - var body: some View { + public var body: some View { if let utType = codeFile.utType, utType.conforms(to: .text) { CodeFileView(editorInstance: editorInstance, codeFile: codeFile) .environmentObject(undoRegistration) diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift index 170a19b53e..6fa5b6473f 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift @@ -8,9 +8,8 @@ import Foundation extension URL { - /// The non-percent-encoded absolute path. Package-internal copy of the app's helper - /// (kept private to this module to avoid a cross-module import ripple). - var absolutePath: String { + /// The non-percent-encoded absolute path. + public var absolutePath: String { absoluteURL.path(percentEncoded: false) } } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift new file mode 100644 index 0000000000..c7a6144580 --- /dev/null +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift @@ -0,0 +1,28 @@ +// +// Theme+Color.swift +// CodeEditSettings +// +// Created by Lukas Pistrol on 31.03.22. +// + +import SwiftUI + +public extension Theme.Attributes { + var swiftColor: Color { + get { + Color(hex: color) + } + set { + self.color = newValue.hexString + } + } + + var nsColor: NSColor { + get { + NSColor(hex: color) + } + set { + self.color = newValue.hexString + } + } +} diff --git a/CodeEdit/Utils/Extensions/Color/Color+HEX.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift similarity index 54% rename from CodeEdit/Utils/Extensions/Color/Color+HEX.swift rename to Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift index 8a258d9dd6..1035008666 100644 --- a/CodeEdit/Utils/Extensions/Color/Color+HEX.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift @@ -1,18 +1,13 @@ // // Color+HEX.swift -// CodeEditModules/CodeEditUtils +// CodeEditSettings // // Created by Lukas Pistrol on 23.03.22. // import SwiftUI -extension Color { - - /// Initializes a `Color` from a HEX String (e.g.: `#1D2E3F`) and an optional alpha value. - /// - Parameters: - /// - hex: A String of a HEX representation of a color (format: `#1D2E3F`) - /// - alpha: A Double indicating the alpha value from `0.0` to `1.0` +public extension Color { init(hex: String, alpha: Double = 1.0) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 @@ -20,10 +15,6 @@ extension Color { self.init(hex: Int(int), alpha: alpha) } - /// Initializes a `Color` from an Int (e.g.: `0x1D2E3F`)and an optional alpha value. - /// - Parameters: - /// - hex: An Int of a HEX representation of a color (format: `0x1D2E3F`) - /// - alpha: A Double indicating the alpha value from `0.0` to `1.0` init(hex: Int, alpha: Double = 1.0) { let red = (hex >> 16) & 0xFF let green = (hex >> 8) & 0xFF @@ -31,36 +22,24 @@ extension Color { self.init(.sRGB, red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255, opacity: alpha) } - /// Returns an Int representing the `Color` in hex format (e.g.: 0x112233) var hex: Int { guard let components = cgColor?.components, components.count >= 3 else { return 0 } - let red = lround((Double(components[0]) * 255.0)) << 16 let green = lround((Double(components[1]) * 255.0)) << 8 let blue = lround((Double(components[2]) * 255.0)) - return red | green | blue } - /// Returns a HEX String representing the `Color` (e.g.: #112233) var hexString: String { - let color = self.hex - - return "#" + String(format: "%06x", color) + "#" + String(format: "%06x", hex) } - /// The alpha (opacity) component of the Color (0.0 - 1.0) var alphaComponent: Double { NSColor(self).alphaComponent } } -extension NSColor { - - /// Initializes a `NSColor` from a HEX String (e.g.: `#1D2E3F`) and an optional alpha value. - /// - Parameters: - /// - hex: A String of a HEX representation of a color (format: `#1D2E3F`) - /// - alpha: A Double indicating the alpha value from `0.0` to `1.0` +public extension NSColor { convenience init(hex: String, alpha: Double = 1.0) { let hex = hex.trimmingCharacters(in: .alphanumerics.inverted) var int: UInt64 = 0 @@ -68,10 +47,6 @@ extension NSColor { self.init(hex: Int(int), alpha: alpha) } - /// Initializes a `NSColor` from an Int (e.g.: `0x1D2E3F`)and an optional alpha value. - /// - Parameters: - /// - hex: An Int of a HEX representation of a color (format: `0x1D2E3F`) - /// - alpha: A Double indicating the alpha value from `0.0` to `1.0` convenience init(hex: Int, alpha: Double = 1.0) { let red = (hex >> 16) & 0xFF let green = (hex >> 8) & 0xFF @@ -79,21 +54,15 @@ extension NSColor { self.init(srgbRed: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255, alpha: alpha) } - /// Returns an Int representing the `NSColor` in hex format (e.g.: 0x112233) var hex: Int { guard let components = cgColor.components, components.count >= 3 else { return 0 } - let red = lround((Double(components[0]) * 255.0)) << 16 let green = lround((Double(components[1]) * 255.0)) << 8 let blue = lround((Double(components[2]) * 255.0)) - return red | green | blue } - /// Returns a HEX String representing the `NSColor` (e.g.: #112233) var hexString: String { - let color = self.hex - - return "#" + String(format: "%06x", color) + "#" + String(format: "%06x", hex) } } diff --git a/CodeEdit/Utils/Environment/Env+IsFullscreen.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift similarity index 68% rename from CodeEdit/Utils/Environment/Env+IsFullscreen.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift index eb1c93bf63..ebde99b341 100644 --- a/CodeEdit/Utils/Environment/Env+IsFullscreen.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift @@ -1,6 +1,6 @@ // -// Env+IsFullscreen.swift -// CodeEdit +// Environment+IsFullscreen.swift +// CodeEditUI // // Created by Wouter Hennen on 14/01/2023. // @@ -8,10 +8,10 @@ import SwiftUI private struct WorkspaceFullscreenStateEnvironmentKey: EnvironmentKey { - static let defaultValue: Bool = false + nonisolated(unsafe) static let defaultValue: Bool = false } -extension EnvironmentValues { +public extension EnvironmentValues { var isFullscreen: Bool { get { self[WorkspaceFullscreenStateEnvironmentKey.self] } set { self[WorkspaceFullscreenStateEnvironmentKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift new file mode 100644 index 0000000000..5aa9895442 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift @@ -0,0 +1,19 @@ +// +// Environment+ModifierKeys.swift +// CodeEditUI +// +// Created by Wouter Hennen on 04/03/2023. +// + +import SwiftUI + +public struct EventModifierEnvironmentKey: EnvironmentKey { + nonisolated(unsafe) public static var defaultValue: NSEvent.ModifierFlags = [] +} + +public extension EnvironmentValues { + var modifierKeys: EventModifierEnvironmentKey.Value { + get { self[EventModifierEnvironmentKey.self] } + set { self[EventModifierEnvironmentKey.self] = newValue } + } +} diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift new file mode 100644 index 0000000000..700d031ba2 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift @@ -0,0 +1,25 @@ +// +// Environment+Window.swift +// CodeEditUI +// +// Created by Wouter Hennen on 14/01/2023. +// + +import SwiftUI + +public struct WindowBox { + public weak var value: NSWindow? + public init(value: NSWindow? = nil) { self.value = value } +} + +public struct NSWindowEnvironmentKey: EnvironmentKey { + public typealias Value = WindowBox + nonisolated(unsafe) public static var defaultValue = WindowBox(value: nil) +} + +public extension EnvironmentValues { + var window: WindowBox { + get { self[NSWindowEnvironmentKey.self] } + set { self[NSWindowEnvironmentKey.self] = newValue } + } +} diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift new file mode 100644 index 0000000000..71d0c23ed8 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift @@ -0,0 +1,42 @@ +// +// View+if.swift +// CodeEditUI +// +// Created by Khan Winter on 8/28/25. +// + +import SwiftUI + +public extension View { + @ViewBuilder + func `if`(_ condition: Bool, @ViewBuilder transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + @ViewBuilder + func `if`( + _ condition: Bool, + @ViewBuilder transform: (Self) -> Content, + @ViewBuilder else elseTransform: (Self) -> ElseContent + ) -> some View { + if condition { + transform(self) + } else { + elseTransform(self) + } + } +} + +public extension Bool { + static var tahoe: Bool { + if #available(macOS 26, *) { + return true + } else { + return false + } + } +} diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift new file mode 100644 index 0000000000..d40d1a91fe --- /dev/null +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift @@ -0,0 +1,19 @@ +// +// Environment+WorkspaceFileManager.swift +// CEWorkspaceFileManager +// +// Created by Matthijs Eikelenboom on 03/07/2026. +// + +import SwiftUI + +private struct WorkspaceFileManagerKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: CEWorkspaceFileManager? = nil +} + +public extension EnvironmentValues { + var workspaceFileManager: CEWorkspaceFileManager? { + get { self[WorkspaceFileManagerKey.self] } + set { self[WorkspaceFileManagerKey.self] = newValue } + } +} From bdc5e9107f5e5763e3cfd601eaae9959cab48676 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 12:19:35 +0200 Subject: [PATCH 123/335] Fix: Move languageServicesProvider Factory registration to CodeEditDocument The Editor package declared its own Container extension with a fatalError default, but cross-module Factory properties are separate instances. Move the declaration to CodeEditDocument (where the protocol lives) with a NoOp default, matching the existing codeFileDocumentDelegate pattern. --- CodeEdit/CodeEditApp.swift | 1 + CodeEdit/CodeEditContainer.swift | 4 ---- .../Editor/Sources/Editor/EditorContainer.swift | 15 --------------- .../LanguageServicesProvider.swift | 7 +++++++ 4 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 Packages/Features/Editor/Sources/Editor/EditorContainer.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 81ec4ae5a5..c738e6215e 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -27,6 +27,7 @@ struct CodeEditApp: App { Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } Container.shared.fileRelocator.register { AppFileRelocator() } + Container.shared.languageServicesProvider.register { @MainActor in AppLanguageServicesProvider() } SettingsData.TextEditingSettings.registerCommands() SettingsData.reconcileDefaultKeybindings() } diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index c4ba0bf040..993866255d 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -15,10 +15,6 @@ extension Container { self { @MainActor in LSPService() }.singleton } - var languageServicesProvider: Factory { - self { @MainActor in AppLanguageServicesProvider() as LanguageServicesProvider }.singleton - } - var workspaceWindowManager: Factory { self { @MainActor in WorkspaceWindowManager() }.singleton } diff --git a/Packages/Features/Editor/Sources/Editor/EditorContainer.swift b/Packages/Features/Editor/Sources/Editor/EditorContainer.swift deleted file mode 100644 index 978cda4013..0000000000 --- a/Packages/Features/Editor/Sources/Editor/EditorContainer.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// EditorContainer.swift -// Editor -// -// Created by Matthijs Eikelenboom on 10.07.26. -// - -import Factory -import CodeEditDocument - -extension Container { - var languageServicesProvider: Factory { - self { fatalError("languageServicesProvider not registered") } - } -} diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift index c04e4f0e59..838e9b035d 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift @@ -9,6 +9,7 @@ import CodeEditTextView import CodeEditLanguages import AppKit +import Factory public struct LanguageServices { public let textCoordinator: TextViewCoordinator @@ -20,6 +21,12 @@ public struct LanguageServices { } } +extension Container { + public var languageServicesProvider: Factory { + self { @MainActor in NoOpLanguageServicesProvider() }.singleton + } +} + @MainActor public protocol LanguageServicesProvider: AnyObject { func languageServices(for document: CodeFileDocument) -> LanguageServices From 9855240ab953354b2b810c3c061b84ca89e2bfb2 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 20:25:34 +0200 Subject: [PATCH 124/335] Refactor: Introduce AppDependencies composition root bridging Container.shared --- CodeEdit/AppDelegate.swift | 12 ++++-------- CodeEdit/AppDependencies.swift | 34 ++++++++++++++++++++++++++++++++++ CodeEdit/CodeEditApp.swift | 4 ++-- 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 CodeEdit/AppDependencies.swift diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 2f75ad0070..eb00cb7090 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -9,7 +9,6 @@ import Combine import CodeEditSettings import CodeEditDocument import SwiftUI -import Factory import CodeEditCore import CodeEditSymbols import CodeEditSourceEditor @@ -23,14 +22,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @Environment(\.openWindow) var openWindow - @LazyInjected(\.lspService) - var lspService + let dependencies = AppDependencies() - @LazyInjected(\.workspaceWindowManager) - var windowManager - - @LazyInjected(\.eventBus) - var eventBus + var lspService: LSPService { dependencies.lspService } + var windowManager: WorkspaceWindowManager { dependencies.workspaceWindowManager } + var eventBus: EventBus { dependencies.eventBus } private let shutdownUseCase = ShutdownApplicationUseCase() diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift new file mode 100644 index 0000000000..7846120d10 --- /dev/null +++ b/CodeEdit/AppDependencies.swift @@ -0,0 +1,34 @@ +// +// AppDependencies.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 10/07/2026. +// + +import CodeEditCore +import CodeEditDocument +import Factory +import Notifications +import ShellClient + +/// The app-scope composition root. Owns every process-lifetime service. +/// +/// Transitional note: during the strangler-bridge migration, properties are +/// initialized FROM `Container.shared` so both worlds share instances. The +/// final migration task replaces these reads with direct construction and +/// deletes the container. +/// +/// Properties are `lazy` to preserve the resolution timing of the +/// `@LazyInjected` sites they replace (e.g. `RegistryManager` performs I/O on +/// first touch) and to allow adapters to reference sibling properties. +@MainActor +final class AppDependencies { + private(set) lazy var eventBus: EventBus = Container.shared.eventBus() + private(set) lazy var shellClient: ShellClientProtocol = Container.shared.shellClient() + private(set) lazy var commandManager: CommandManaging = Container.shared.commandManager() + private(set) lazy var keybindingManager: KeybindingManaging = Container.shared.keybindingManager() + private(set) lazy var notificationManager: NotificationManaging = Container.shared.notificationManager() + private(set) lazy var lspService: LSPService = Container.shared.lspService() + private(set) lazy var registryManager: RegistryManager = Container.shared.registryManager() + private(set) lazy var workspaceWindowManager: WorkspaceWindowManager = Container.shared.workspaceWindowManager() +} diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index c738e6215e..979ee00308 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -42,7 +42,7 @@ struct CodeEditApp: App { OpenFileOrFolderButton(dismissWindow: dismissWindow) }, onDrop: { url, dismissWindow in - let windowManager = Container.shared.workspaceWindowManager() + let windowManager = appdelegate.dependencies.workspaceWindowManager Task { do { try windowManager.openWorkspace(at: url) @@ -53,7 +53,7 @@ struct CodeEditApp: App { } }, openHandler: { urls, dismissWindow in - let windowManager = Container.shared.workspaceWindowManager() + let windowManager = appdelegate.dependencies.workspaceWindowManager for url in urls { windowManager.openDocument(at: url, onCompletion: {}) } From 5b13a221f4939a4d99c15226876202fd6c3879d0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 20:29:54 +0200 Subject: [PATCH 125/335] Refactor: Constructor-inject the workspace construction trunk from AppDependencies --- CodeEdit/AppDelegate.swift | 11 +++++++++-- CodeEdit/AppDependencies.swift | 5 +++++ CodeEdit/CodeEditContainer.swift | 2 +- .../Controllers/CodeEditWindowController.swift | 10 ++++++---- .../CodeEditWindowControllerExtensions.swift | 3 +-- CodeEdit/Features/Workspace/Models/Workspace.swift | 4 ++-- .../Services/WorkspaceWindowManager.swift | 14 ++++++++++---- .../Workspace/UseCases/CloseWorkspaceUseCase.swift | 8 +++++--- .../Workspace/UseCases/OpenWorkspaceUseCase.swift | 10 ++++++++-- .../UseCases/ShutdownApplicationUseCase.swift | 11 ++++++----- CodeEdit/Features/Workspace/WorkspaceFactory.swift | 7 +++---- .../Utils/Extensions/URL/URL+FindWorkspace.swift | 7 ++++--- 12 files changed, 60 insertions(+), 32 deletions(-) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index eb00cb7090..854a9a7684 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -22,13 +22,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @Environment(\.openWindow) var openWindow - let dependencies = AppDependencies() + let dependencies: AppDependencies = { + let dependencies = AppDependencies() + AppDependencies.bridgeShared = dependencies + return dependencies + }() var lspService: LSPService { dependencies.lspService } var windowManager: WorkspaceWindowManager { dependencies.workspaceWindowManager } var eventBus: EventBus { dependencies.eventBus } - private let shutdownUseCase = ShutdownApplicationUseCase() + private lazy var shutdownUseCase = ShutdownApplicationUseCase( + windowManager: dependencies.workspaceWindowManager, + eventBus: dependencies.eventBus + ) private var cancellables = Set() diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 7846120d10..38bf335b84 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -23,6 +23,11 @@ import ShellClient /// first touch) and to allow adapters to reference sibling properties. @MainActor final class AppDependencies { + /// Bridge-phase backdoor so the container registration can construct the + /// window manager without creating a second dependency graph. AppDelegate + /// assigns this before anything resolves the key. Deleted with the container. + nonisolated(unsafe) static var bridgeShared: AppDependencies! + private(set) lazy var eventBus: EventBus = Container.shared.eventBus() private(set) lazy var shellClient: ShellClientProtocol = Container.shared.shellClient() private(set) lazy var commandManager: CommandManaging = Container.shared.commandManager() diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 993866255d..b7ea424040 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -16,7 +16,7 @@ extension Container { } var workspaceWindowManager: Factory { - self { @MainActor in WorkspaceWindowManager() }.singleton + self { @MainActor in WorkspaceWindowManager(dependencies: AppDependencies.bridgeShared) }.singleton } var shellClient: Factory { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 0fe5cdd9f2..ee06b79e11 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -11,7 +11,6 @@ import CodeEditSettings import Editor import SwiftUI import CodeEditUI -import Factory import Combine final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject, NSWindowDelegate { @@ -29,6 +28,8 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs var observers: [NSKeyValueObservation] = [] + let dependencies: AppDependencies + var workspace: Workspace? var workspaceSettingsWindow: NSWindow? var quickOpenPanel: SearchPanel? @@ -43,8 +44,10 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs init( window: NSWindow?, - workspace: Workspace? + workspace: Workspace?, + dependencies: AppDependencies ) { + self.dependencies = dependencies super.init(window: window) window?.delegate = self guard let workspace else { return } @@ -243,8 +246,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs // Notify the window manager to clean up workspace state if let workspace { - let windowManager = Container.shared.workspaceWindowManager() - windowManager.closeWorkspace(workspace) + dependencies.workspaceWindowManager.closeWorkspace(workspace) } workspace = nil return true diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index be7b4e8f49..2cd5c091cc 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -6,13 +6,12 @@ // import SwiftUI -import Factory import Combine extension CodeEditWindowController { /// These are example items that added as commands to command palette func registerCommands() { - let commandManager = Container.shared.commandManager() + let commandManager = dependencies.commandManager commandManager.addCommand( name: "Quick Open", title: "Quick Open", diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 634ec0b61c..6b583dac33 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -50,8 +50,8 @@ final class Workspace: ObservableObject, WorkspaceManaging { // MARK: - Initialization - init(url: URL) { - WorkspaceFactory.populate(self, url: url) + init(url: URL, dependencies: AppDependencies) { + WorkspaceFactory.populate(self, url: url, dependencies: dependencies) } /// Minimal initializer for testing. Does not set up workspace state. diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 81e038d516..1ef1e88d82 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -9,7 +9,6 @@ import AppKit import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore -import Factory import Notifications import SwiftUI import WelcomeWindow @@ -18,12 +17,19 @@ import WelcomeWindow @MainActor final class WorkspaceWindowManager: WorkspaceWindowManaging { - @LazyInjected(\.eventBus) private var eventBus + private let dependencies: AppDependencies + private var eventBus: EventBus { dependencies.eventBus } - private let openWorkspaceUseCase = OpenWorkspaceUseCase() - private let closeWorkspaceUseCase = CloseWorkspaceUseCase() + private let openWorkspaceUseCase: OpenWorkspaceUseCase + private let closeWorkspaceUseCase: CloseWorkspaceUseCase private lazy var openDocumentUseCase = OpenDocumentUseCase(windowManager: self) + init(dependencies: AppDependencies) { + self.dependencies = dependencies + self.openWorkspaceUseCase = OpenWorkspaceUseCase(dependencies: dependencies) + self.closeWorkspaceUseCase = CloseWorkspaceUseCase(lspService: dependencies.lspService) + } + /// All currently open workspaces. private(set) var openWorkspaces: [Workspace] = [] diff --git a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift index 453af77be8..77b1b9a425 100644 --- a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift @@ -6,14 +6,16 @@ // import Foundation -import Factory /// Coordinates cleanup when a workspace is closed (LSP shutdown + workspace teardown). @MainActor final class CloseWorkspaceUseCase { - @LazyInjected(\.lspService) - private var lspService + private let lspService: LSPService + + init(lspService: LSPService) { + self.lspService = lspService + } func execute(workspace: Workspace) { if let path = workspace.fileURL?.absoluteURL.path() { diff --git a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift index 2c30517878..a6f4baeea7 100644 --- a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift @@ -10,6 +10,11 @@ import AppKit /// Creates and configures a workspace, window, and window controller for a given URL. @MainActor final class OpenWorkspaceUseCase { + private let dependencies: AppDependencies + + init(dependencies: AppDependencies) { + self.dependencies = dependencies + } struct Result { let workspace: Workspace @@ -18,7 +23,7 @@ final class OpenWorkspaceUseCase { } func execute(url: URL) -> Result { - let workspace = Workspace(url: url) + let workspace = Workspace(url: url, dependencies: dependencies) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), @@ -29,7 +34,8 @@ final class OpenWorkspaceUseCase { let windowController = CodeEditWindowController( window: window, - workspace: workspace + workspace: workspace, + dependencies: dependencies ) // Restore saved window geometry, or use default centered frame diff --git a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift index 3bcbc27912..4e860bc1b6 100644 --- a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift @@ -6,7 +6,6 @@ // import Foundation -import Factory import CodeEditCore /// Orchestrates application shutdown: saves workspace paths, checks for unsaved changes, @@ -18,11 +17,13 @@ import CodeEditCore @MainActor final class ShutdownApplicationUseCase { - @LazyInjected(\.workspaceWindowManager) - private var windowManager + private let windowManager: WorkspaceWindowManaging + private let eventBus: EventBus - @LazyInjected(\.eventBus) - private var eventBus + init(windowManager: WorkspaceWindowManaging, eventBus: EventBus) { + self.windowManager = windowManager + self.eventBus = eventBus + } /// - Returns: `true` if the app should proceed with termination, `false` if the user cancelled. func execute() -> Bool { diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index b4d251c6cb..0dd83e2a71 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -9,7 +9,6 @@ import Foundation import CEWorkspaceFileManager import Editor import Search -import Factory /// Constructs and wires the manager/service object graph for a ``Workspace``. /// @@ -28,7 +27,7 @@ enum WorkspaceFactory { /// must already be initialized (they are set at declaration time). /// - url: The root URL of the workspace folder. @MainActor - static func populate(_ workspace: Workspace, url: URL) { + static func populate(_ workspace: Workspace, url: URL, dependencies: AppDependencies) { // Begin security-scoped access on the original (possibly bookmark-derived) URL so a // sandboxed build can read a workspace opened from recents. `startAccessingSecurityScopedResource` // returns `false` for non-scoped URLs (e.g. from the open panel / Powerbox), which access @@ -53,8 +52,8 @@ enum WorkspaceFactory { return } - let shellClient = Container.shared.shellClient() - let eventBus = Container.shared.eventBus() + let shellClient = dependencies.shellClient + let eventBus = dependencies.eventBus let sourceControlManager = SourceControlManager( workspaceURL: url, shellClient: shellClient, diff --git a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift index a834fcdd3c..a15cd30e30 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift @@ -6,13 +6,14 @@ // import Foundation -import Factory extension URL { /// Finds a workspace that contains the url. + /// + /// Bridge-phase shim: remaining callers (LSP cluster) receive an injected + /// window manager in a later migration task, after which this file is deleted. @MainActor func findWorkspace() -> Workspace? { - let windowManager = Container.shared.workspaceWindowManager() - return windowManager.workspace(containing: self) + AppDependencies.bridgeShared.workspaceWindowManager.workspace(containing: self) } } From 3aeddc41b33354bb1304b633ccddf9d99ade31e8 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 20:36:45 +0200 Subject: [PATCH 126/335] Refactor: Constructor-inject Search package; drop its Factory dependency --- CodeEdit/AppDependencies.swift | 5 +++++ CodeEdit/CodeEditApp.swift | 1 - .../CodeEditSplitViewController.swift | 5 +++++ .../CodeEditWindowController.swift | 3 ++- .../Services/AppWorkspaceFileOpener.swift | 8 ++++--- .../Features/Workspace/WorkspaceFactory.swift | 2 +- .../Documents/DocumentsUnitTests.swift | 4 +++- ...ment+SearchState+FindAndReplaceTests.swift | 2 +- ...nt+SearchState+FindReplaceQueryTests.swift | 2 +- ...kspaceDocument+SearchState+FindTests.swift | 2 +- ...spaceDocument+SearchState+IndexTests.swift | 2 +- Packages/Features/Search/Package.swift | 6 ++--- .../Environment+WorkspaceFileOpener.swift | 22 +++++++++++++++++++ .../FindNavigatorListViewController.swift | 7 +++--- .../FindNavigatorResultList.swift | 3 ++- .../Search/SearchState/SearchState.swift | 7 +++--- 16 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 Packages/Features/Search/Sources/Search/Environment+WorkspaceFileOpener.swift diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 38bf335b84..7f691c4d13 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -36,4 +36,9 @@ final class AppDependencies { private(set) lazy var lspService: LSPService = Container.shared.lspService() private(set) lazy var registryManager: RegistryManager = Container.shared.registryManager() private(set) lazy var workspaceWindowManager: WorkspaceWindowManager = Container.shared.workspaceWindowManager() + + // MARK: - Command-interface adapters (stateless routers over the window manager) + + private(set) lazy var workspaceFileOpener: WorkspaceFileOpener = + AppWorkspaceFileOpener(windowManager: workspaceWindowManager) } diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 979ee00308..a5da291d0f 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -23,7 +23,6 @@ struct CodeEditApp: App { init() { NSMenuItem.swizzle() NSSplitViewItem.swizzle() - Container.shared.workspaceFileOpener.register { AppWorkspaceFileOpener() } Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } Container.shared.fileRelocator.register { AppFileRelocator() } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index f9867d4c9b..70074af26d 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -18,6 +18,8 @@ final class CodeEditSplitViewController: NSSplitViewController { static let snapWidth: CGFloat = 272 static let minSnapWidth: CGFloat = snapWidth - 10 + private let dependencies: AppDependencies + private weak var workspace: Workspace? private weak var navigatorViewModel: NavigatorAreaViewModel? private weak var windowRef: NSWindow? @@ -39,8 +41,10 @@ final class CodeEditSplitViewController: NSSplitViewController { workspace: Workspace, navigatorViewModel: NavigatorAreaViewModel, windowRef: NSWindow, + dependencies: AppDependencies, hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer ) { + self.dependencies = dependencies self.workspace = workspace self.navigatorViewModel = navigatorViewModel self.windowRef = windowRef @@ -100,6 +104,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.activeEditorState, activeEditorState) + .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) }) addSplitViewItem(navigator) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index ee06b79e11..d6dda6ca5b 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -108,7 +108,8 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs return CodeEditSplitViewController( workspace: workspace, navigatorViewModel: navigatorModel, - windowRef: window + windowRef: window, + dependencies: dependencies ) } diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift index f958095206..4c2e25b6cb 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift @@ -7,14 +7,16 @@ import Foundation import CodeEditCore -import Factory /// App-shell binding of the `WorkspaceFileOpener` command interface. /// Delegates to `WorkspaceWindowManager.openFileInWorkspace(url:)`, which maps the /// URL to the workspace that owns it, opens the tab, and focuses that workspace. final class AppWorkspaceFileOpener: WorkspaceFileOpener { - @LazyInjected(\.workspaceWindowManager) - private var windowManager + private let windowManager: WorkspaceWindowManaging + + init(windowManager: WorkspaceWindowManaging) { + self.windowManager = windowManager + } @MainActor func openFile(at url: URL) { diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 0dd83e2a71..944e80ca08 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -71,7 +71,7 @@ enum WorkspaceFactory { workspace.workspaceFileManager = workspaceFileManager // --- Phase 2: Independent managers --- - workspace.searchState = SearchState(workspaceURL: url) + workspace.searchState = SearchState(workspaceURL: url, eventBus: eventBus) workspace.openQuicklyViewModel = OpenQuicklyViewModel(fileURL: url) workspace.commandsPaletteState = QuickActionsViewModel() workspace.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index f0e0b6c3ff..e337267e80 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -7,6 +7,7 @@ import XCTest import Factory +import CodeEditCore import Search @testable import CodeEdit @@ -35,12 +36,13 @@ final class DocumentsUnitTests: XCTestCase { eventBus: Container.shared.eventBus() ) workspace.sourceControlViewModel = SourceControlViewModel() - workspace.searchState = SearchState(workspaceURL: URL(filePath: "/tmp")) + workspace.searchState = SearchState(workspaceURL: URL(filePath: "/tmp"), eventBus: EventBus()) window = NSWindow() splitViewController = .init( workspace: workspace, navigatorViewModel: navigatorViewModel, windowRef: window, + dependencies: AppDependencies(), hapticPerformer: hapticFeedbackPerformerMock ) splitViewController.viewDidLoad() diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 0e67cbbfca..17ea8b8cc7 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -63,7 +63,7 @@ final class FindAndReplaceTests: XCTestCase { // swiftlint:disable:this type_bod files[2].parent = folder2File // SearchState indexes the workspace as part of its initializer. - searchState = SearchState(workspaceURL: directory) + searchState = SearchState(workspaceURL: directory, eventBus: EventBus()) // NOTE: This is a temporary solution. In the future, a file watcher should track file updates // and trigger an index update. diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift index 52cf9da6b5..f7b231173c 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift @@ -25,7 +25,7 @@ final class FindReplaceQueryBridgeTests: XCTestCase { try? FileManager.default.removeItem(at: directory) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - searchState = SearchState(workspaceURL: directory) + searchState = SearchState(workspaceURL: directory, eventBus: EventBus()) // Wait for indexing to settle before the test starts mutating state, to avoid racing // background indexing work against the next test's setUp/tearDown on the same directory. diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 9d94cddd9e..624a7bf5b3 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -59,7 +59,7 @@ final class FindTests: XCTestCase { files[2].parent = parent2 // SearchState indexes the workspace as part of its initializer. - searchState = SearchState(workspaceURL: directory) + searchState = SearchState(workspaceURL: directory, eventBus: EventBus()) // The following code also tests whether the workspace is indexed correctly // Wait until the index is up to date and flushed diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index 80a6fa9b6f..b07cdfb781 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -62,7 +62,7 @@ final class WorkspaceIndexTests: XCTestCase { files[2].parent = folder2File // SearchState indexes the workspace as part of its initializer. - searchState = SearchState(workspaceURL: directory) + searchState = SearchState(workspaceURL: directory, eventBus: EventBus()) // The following code also tests whether the workspace is indexed correctly // Wait until the index is up to date and flushed diff --git a/Packages/Features/Search/Package.swift b/Packages/Features/Search/Package.swift index 92f5022caa..115d56e288 100644 --- a/Packages/Features/Search/Package.swift +++ b/Packages/Features/Search/Package.swift @@ -10,16 +10,14 @@ let package = Package( ], dependencies: [ .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditUI"), - .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") + .package(path: "../../Foundation/CodeEditUI") ], targets: [ .target( name: "Search", dependencies: [ .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditUI", package: "CodeEditUI"), - .product(name: "Factory", package: "Factory") + .product(name: "CodeEditUI", package: "CodeEditUI") ] ) ] diff --git a/Packages/Features/Search/Sources/Search/Environment+WorkspaceFileOpener.swift b/Packages/Features/Search/Sources/Search/Environment+WorkspaceFileOpener.swift new file mode 100644 index 0000000000..21ca987696 --- /dev/null +++ b/Packages/Features/Search/Sources/Search/Environment+WorkspaceFileOpener.swift @@ -0,0 +1,22 @@ +// +// Environment+WorkspaceFileOpener.swift +// Search +// +// Created by Matthijs Eikelenboom on 10/07/2026. +// + +import SwiftUI +import CodeEditCore + +private struct WorkspaceFileOpenerKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: WorkspaceFileOpener = NoOpWorkspaceFileOpener() +} + +extension EnvironmentValues { + /// The command used to open a file in the owning workspace. + /// No-op by default (previews, tests); injected by the app shell. + public var workspaceFileOpener: WorkspaceFileOpener { + get { self[WorkspaceFileOpenerKey.self] } + set { self[WorkspaceFileOpenerKey.self] = newValue } + } +} diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift index 7a7e18e11d..7f05dd11f5 100644 --- a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift +++ b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift @@ -7,12 +7,10 @@ import SwiftUI import CodeEditCore -import Factory final class FindNavigatorListViewController: NSViewController { - @LazyInjected(\.workspaceFileOpener) - private var fileOpener + private let fileOpener: WorkspaceFileOpener var configuration: FindNavigatorConfiguration @@ -49,8 +47,9 @@ final class FindNavigatorListViewController: NSViewController { self.scrollView.contentView.contentInsets = .init(top: 0, left: 0, bottom: 0, right: 0) } - init(configuration: FindNavigatorConfiguration) { + init(configuration: FindNavigatorConfiguration, fileOpener: WorkspaceFileOpener) { self.configuration = configuration + self.fileOpener = fileOpener super.init(nibName: nil, bundle: nil) } diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift index f2d4502693..e6d1a9d2e3 100644 --- a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift +++ b/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift @@ -11,13 +11,14 @@ import Combine struct FindNavigatorResultList: NSViewControllerRepresentable { @EnvironmentObject var state: SearchState + @Environment(\.workspaceFileOpener) private var fileOpener let configuration: FindNavigatorConfiguration typealias NSViewControllerType = FindNavigatorListViewController func makeNSViewController(context: Context) -> FindNavigatorListViewController { - let controller = FindNavigatorListViewController(configuration: configuration) + let controller = FindNavigatorListViewController(configuration: configuration, fileOpener: fileOpener) controller.setSearchResults(state.searchResult) controller.rowHeight = configuration.rowHeight context.coordinator.controller = controller diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift b/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift index 91dfbb0399..2015cba4ae 100644 --- a/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift +++ b/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift @@ -8,7 +8,6 @@ import Foundation import CodeEditCore import Combine -import Factory /// Manages the search/find state for a workspace, including indexing, search results, /// and find-and-replace operations. Extracted from Workspace to be independently @@ -50,8 +49,7 @@ public final class SearchState: ObservableObject { public let workspaceURL: URL - @LazyInjected(\.eventBus) - var eventBus + let eventBus: EventBus var tempSearchResults = [SearchResultModel]() public var caseSensitive: Bool = false @@ -62,8 +60,9 @@ public final class SearchState: ObservableObject { .Containing ] - public init(workspaceURL: URL) { + public init(workspaceURL: URL, eventBus: EventBus) { self.workspaceURL = workspaceURL + self.eventBus = eventBus self.indexer = SearchIndexer.Memory.create() addProjectToIndex() bridgeFindReplaceQuery() From 860c336ee289107755d4e94ca7323913cec3aae5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 20:44:40 +0200 Subject: [PATCH 127/335] Refactor: Constructor-inject Notifications and all notificationManager resolvers; drop package Factory --- CodeEdit/AppDependencies.swift | 2 +- CodeEdit/CodeEditApp.swift | 1 + CodeEdit/CodeEditContainer.swift | 11 ++++++++-- .../CodeEditSplitViewController.swift | 1 + .../NotificationPanelViewModel+Toolbar.swift | 3 +-- ...InternalDevelopmentNotificationsView.swift | 11 +++++----- .../LSP/Registry/RegistryManager.swift | 11 +++++----- .../Features/LSP/Service/LSPService.swift | 10 +++++---- .../Features/Workspace/Models/Workspace.swift | 15 +++++++++++-- .../LSP/LSPServiceDocumentObjectsTests.swift | 6 ++++- CodeEditTests/Features/LSP/Registry.swift | 6 ++++- .../NotificationPanelViewModelTests.swift | 13 ++++++----- Packages/Features/Notifications/Package.swift | 6 ++--- .../Environment+NotificationManager.swift | 22 +++++++++++++++++++ .../NotificationManager+Delegate.swift | 4 ++-- .../Notifications/NotificationManager.swift | 9 ++++---- .../NotificationsContainer.swift | 17 -------------- .../NotificationPanelViewModel.swift | 13 ++++++----- 18 files changed, 98 insertions(+), 63 deletions(-) create mode 100644 Packages/Features/Notifications/Sources/Notifications/Environment+NotificationManager.swift delete mode 100644 Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 7f691c4d13..6db6562ccc 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -32,7 +32,7 @@ final class AppDependencies { private(set) lazy var shellClient: ShellClientProtocol = Container.shared.shellClient() private(set) lazy var commandManager: CommandManaging = Container.shared.commandManager() private(set) lazy var keybindingManager: KeybindingManaging = Container.shared.keybindingManager() - private(set) lazy var notificationManager: NotificationManaging = Container.shared.notificationManager() + private(set) lazy var notificationManager: NotificationManaging = NotificationManager(eventBus: eventBus) private(set) lazy var lspService: LSPService = Container.shared.lspService() private(set) lazy var registryManager: RegistryManager = Container.shared.registryManager() private(set) lazy var workspaceWindowManager: WorkspaceWindowManager = Container.shared.workspaceWindowManager() diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index a5da291d0f..e6a21f5bb9 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -81,5 +81,6 @@ struct CodeEditApp: App { } } .environment(\.settings, settings.preferences) // Add settings to each window environment + .environment(\.notificationManager, appdelegate.dependencies.notificationManager) } } diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index b7ea424040..066ffc4432 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -12,7 +12,9 @@ import Factory extension Container { var lspService: Factory { - self { @MainActor in LSPService() }.singleton + self { @MainActor in + LSPService(notificationManager: AppDependencies.bridgeShared.notificationManager) + }.singleton } var workspaceWindowManager: Factory { @@ -32,6 +34,11 @@ extension Container { } var registryManager: Factory { - self { @MainActor in RegistryManager() }.singleton + self { @MainActor in + RegistryManager( + eventBus: AppDependencies.bridgeShared.eventBus, + notificationManager: AppDependencies.bridgeShared.notificationManager + ) + }.singleton } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 70074af26d..fee9374876 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -142,6 +142,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.activeEditorState, activeEditorState) .environment(\.fileEditorOverrides, fileEditorOverrides) + .environment(\.notificationManager, dependencies.notificationManager) }) addSplitViewItem(inspector) diff --git a/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift index 91945c11b4..721390ddf0 100644 --- a/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift @@ -6,7 +6,6 @@ // import AppKit -import Factory import Notifications /// App-shell integration for the notification toolbar badge. @@ -23,7 +22,7 @@ extension NotificationPanelViewModel { } let shouldShow = !visibleNotifications.isEmpty - || Container.shared.notificationManager().unreadCount > 0 + || notificationManager.unreadCount > 0 if shouldShow && toolbar.items.filter({ $0.itemIdentifier == .notificationItem }).first == nil { guard let activityItemIdx = toolbar.items .firstIndex(where: { $0.itemIdentifier == .activityViewer }) else { diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift index 8524cb4a3b..fc6b8c4dd7 100644 --- a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift @@ -6,10 +6,11 @@ // import SwiftUI -import Factory import Notifications struct InternalDevelopmentNotificationsView: View { + @Environment(\.notificationManager) private var notificationManager + enum IconType: String, CaseIterable { case symbol = "Symbol" case image = "Image" @@ -131,7 +132,7 @@ struct InternalDevelopmentNotificationsView: View { let iconSymbol = selectedSymbol ?? availableSymbols.randomElement() ?? "bell.fill" let iconColor = selectedColor ?? availableColors.randomElement()?.1 ?? .blue - Container.shared.notificationManager().post( + notificationManager?.post( iconSymbol: iconSymbol, iconColor: iconColor, title: notificationTitle, @@ -145,7 +146,7 @@ struct InternalDevelopmentNotificationsView: View { case .image: let imageName = selectedImage ?? availableImages.randomElement() ?? "GitHubIcon" - Container.shared.notificationManager().post( + notificationManager?.post( iconImage: Image(imageName), title: notificationTitle, description: notificationDescription, @@ -159,7 +160,7 @@ struct InternalDevelopmentNotificationsView: View { let text = selectedText ?? randomLetter() let iconColor = selectedColor ?? availableColors.randomElement()?.1 ?? .blue - Container.shared.notificationManager().post( + notificationManager?.post( iconText: text, iconTextColor: .white, iconColor: iconColor, @@ -175,7 +176,7 @@ struct InternalDevelopmentNotificationsView: View { let emoji = selectedEmoji ?? availableEmojis.randomElement() ?? "🔔" let iconColor = selectedColor ?? availableColors.randomElement()?.1 ?? .blue - Container.shared.notificationManager().post( + notificationManager?.post( iconText: emoji, iconTextColor: .white, iconColor: iconColor, diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index 95c21cfac8..f58ef9a00d 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -10,7 +10,6 @@ import CodeEditSettings import Foundation import ZIPFoundation import Combine -import Factory import CodeEditCore import Notifications @@ -51,10 +50,12 @@ final class RegistryManager: ObservableObject, RegistryManaging { @AppSettings(\.languageServers.installedLanguageServers) var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] - @LazyInjected(\.eventBus) - private var eventBus + private let eventBus: EventBus + private let notificationManager: NotificationManaging - init() { + init(eventBus: EventBus, notificationManager: NotificationManaging) { + self.eventBus = eventBus + self.notificationManager = notificationManager // Load the registry items from disk again after cache expires if let items = loadItemsFromDisk() { setRegistryItems(items) @@ -179,7 +180,7 @@ final class RegistryManager: ObservableObject, RegistryManaging { fail failed: Bool ) { if failed { - Container.shared.notificationManager().post( + notificationManager.post( iconSymbol: "xmark.circle", iconColor: .clear, title: "Could not install \(activityName)", diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 1c9862345f..5f4bb6faa8 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -11,7 +11,6 @@ import CodeEditDocument import JSONRPC import SwiftUI import Foundation -import Factory import LanguageClient import LanguageServerProtocol import CodeEditLanguages @@ -155,7 +154,10 @@ final class LSPService: ObservableObject, LSPServiceProtocol { documentObjects[uri] = nil } - init() { + private let notificationManager: NotificationManaging + + init(notificationManager: NotificationManaging) { + self.notificationManager = notificationManager // Load the LSP binaries from the developer menu for binary in lspBinaries { if let language = LanguageIdentifier(rawValue: binary.key) { @@ -353,11 +355,11 @@ extension LSPService { let lspLanguageTitle = lspLanguage.rawValue.capitalized let notificationTitle = "Install \(lspLanguageTitle) Language Server" // Make sure the user doesn't have the same existing notification - guard !Container.shared.notificationManager().notifications.contains(where: { $0.title == notificationTitle }) else { + guard !notificationManager.notifications.contains(where: { $0.title == notificationTitle }) else { return } - Container.shared.notificationManager().post( + notificationManager.post( iconSymbol: "arrow.down.circle", iconColor: .clear, title: notificationTitle, diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 6b583dac33..6482036635 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -7,6 +7,7 @@ import AppKit import CEWorkspaceFileManager +import CodeEditCore import Editor import Notifications import Search @@ -41,7 +42,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() - var notificationPanel = NotificationPanelViewModel() + var notificationPanel: NotificationPanelViewModel /// The original (possibly bookmark-derived) security-scoped URL whose access is held for this /// workspace's lifetime. Set by `WorkspaceFactory` when the URL is security-scoped (e.g. opened @@ -51,11 +52,21 @@ final class Workspace: ObservableObject, WorkspaceManaging { // MARK: - Initialization init(url: URL, dependencies: AppDependencies) { + self.notificationPanel = NotificationPanelViewModel( + notificationManager: dependencies.notificationManager, + eventBus: dependencies.eventBus + ) WorkspaceFactory.populate(self, url: url, dependencies: dependencies) } /// Minimal initializer for testing. Does not set up workspace state. - internal init() {} + internal init() { + let eventBus = EventBus() + self.notificationPanel = NotificationPanelViewModel( + notificationManager: NotificationManager(eventBus: eventBus), + eventBus: eventBus + ) + } // MARK: - Tear Down diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift index e77065fb34..5b38f547bd 100644 --- a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -6,12 +6,16 @@ // import XCTest +import CodeEditCore import CodeEditDocument +import Notifications @testable import CodeEdit @MainActor final class LSPServiceDocumentObjectsTests: XCTestCase { - private func makeService() -> LSPService { LSPService() } + private func makeService() -> LSPService { + LSPService(notificationManager: NotificationManager(eventBus: EventBus())) + } private func makeDocument(path: String) throws -> CodeFileDocument { let url = FileManager.default.temporaryDirectory diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index 6b22843144..a34e48a667 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -8,12 +8,16 @@ import Testing import Foundation import CodeEditCore +import Notifications @testable import CodeEdit @MainActor @Suite() struct RegistryTests { - var registry: RegistryManager = RegistryManager() + var registry: RegistryManager = RegistryManager( + eventBus: EventBus(), + notificationManager: NotificationManager(eventBus: EventBus()) + ) // MARK: - Download Tests diff --git a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift index 670364e160..a2f25047d1 100644 --- a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift +++ b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift @@ -7,27 +7,28 @@ import XCTest import CodeEditCore -import Factory @testable import Notifications @testable import CodeEdit @MainActor final class NotificationPanelViewModelTests: XCTestCase { + var eventBus: EventBus! var notificationManager: (any NotificationManaging)! var viewModel: NotificationPanelViewModel! override func setUp() { super.setUp() - // Fresh manager so the view model doesn't preload notifications from earlier tests. - Container.shared.notificationManager.reset() - notificationManager = Container.shared.notificationManager() - viewModel = NotificationPanelViewModel() + // Fresh manager and bus so the view model doesn't preload notifications from earlier tests. + // Manager and view model must share one bus: posts flow manager → bus → view model. + eventBus = EventBus() + notificationManager = NotificationManager(eventBus: eventBus) + viewModel = NotificationPanelViewModel(notificationManager: notificationManager, eventBus: eventBus) } override func tearDown() { viewModel = nil notificationManager = nil - Container.shared.notificationManager.reset() + eventBus = nil super.tearDown() } diff --git a/Packages/Features/Notifications/Package.swift b/Packages/Features/Notifications/Package.swift index 1e8210d6b9..cfe3a602ac 100644 --- a/Packages/Features/Notifications/Package.swift +++ b/Packages/Features/Notifications/Package.swift @@ -10,16 +10,14 @@ let package = Package( ], dependencies: [ .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditUI"), - .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") + .package(path: "../../Foundation/CodeEditUI") ], targets: [ .target( name: "Notifications", dependencies: [ .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditUI", package: "CodeEditUI"), - .product(name: "Factory", package: "Factory") + .product(name: "CodeEditUI", package: "CodeEditUI") ] ) ] diff --git a/Packages/Features/Notifications/Sources/Notifications/Environment+NotificationManager.swift b/Packages/Features/Notifications/Sources/Notifications/Environment+NotificationManager.swift new file mode 100644 index 0000000000..566eec1933 --- /dev/null +++ b/Packages/Features/Notifications/Sources/Notifications/Environment+NotificationManager.swift @@ -0,0 +1,22 @@ +// +// Environment+NotificationManager.swift +// Notifications +// +// Created by Matthijs Eikelenboom on 10/07/2026. +// + +import SwiftUI + +private struct NotificationManagerKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: NotificationManaging? = nil +} + +extension EnvironmentValues { + /// The app-wide notification manager. Optional because notification UI rendered + /// outside a configured app context (previews, tests) has no manager; consumers + /// no-op via optional chaining. Injected by the app shell. + public var notificationManager: NotificationManaging? { + get { self[NotificationManagerKey.self] } + set { self[NotificationManagerKey.self] = newValue } + } +} diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift index 5e7dbc6a0f..087da22489 100644 --- a/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift +++ b/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift @@ -10,7 +10,7 @@ import UserNotifications extension NotificationManager: UNUserNotificationCenterDelegate { // System-invoked (not guaranteed main); `nonisolated` + hop to the main actor for state. - nonisolated func userNotificationCenter( + nonisolated public func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void @@ -42,7 +42,7 @@ extension NotificationManager: UNUserNotificationCenterDelegate { completionHandler() } - nonisolated func userNotificationCenter( + nonisolated public func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift index 139a35d9d4..5e0f964d2e 100644 --- a/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift +++ b/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift @@ -8,7 +8,6 @@ import SwiftUI import Combine import UserNotifications -import Factory import CodeEditCore /// Manages the application's notification system, handling both in-app notifications and system notifications. @@ -17,7 +16,7 @@ import CodeEditCore /// - Tracking notification read status /// - Broadcasting notifications to workspaces @MainActor -final class NotificationManager: NSObject, NotificationManaging { +public final class NotificationManager: NSObject, NotificationManaging { /// Collection of all notifications, both read and unread @Published public private(set) var notifications: [CENotification] = [] @@ -27,8 +26,7 @@ final class NotificationManager: NSObject, NotificationManaging { $notifications.eraseToAnyPublisher() } - @LazyInjected(\.eventBus) - private var eventBus + private let eventBus: EventBus private var isAppActive: Bool = true @@ -51,7 +49,8 @@ final class NotificationManager: NSObject, NotificationManaging { } } - override init() { + public init(eventBus: EventBus) { + self.eventBus = eventBus super.init() setupNotificationDelegate() diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift b/Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift deleted file mode 100644 index 325fd4c335..0000000000 --- a/Packages/Features/Notifications/Sources/Notifications/NotificationsContainer.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// NotificationsContainer.swift -// Notifications -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -import Factory - -public extension Container { - /// The app-wide notification manager. Owned by the Notifications package so the - /// package's own view models can `@LazyInjected` it; the default is the real - /// `NotificationManager` singleton. - var notificationManager: Factory { - self { @MainActor in NotificationManager() as NotificationManaging }.singleton - } -} diff --git a/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift index 7fd719dafb..a115522081 100644 --- a/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift +++ b/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift @@ -7,7 +7,6 @@ import SwiftUI import Combine -import Factory import CodeEditCore /// Coordinates notification display, auto-hide timers, panel visibility, and toolbar integration. @@ -42,11 +41,10 @@ public final class NotificationPanelViewModel: ObservableObject { /// Whether notifications are paused var isPaused: Bool = false - @LazyInjected(\.notificationManager) - var notificationManager + /// Non-private so the app shell's toolbar extension can read `unreadCount` through it. + public let notificationManager: NotificationManaging - @LazyInjected(\.eventBus) - var eventBus + let eventBus: EventBus private var cancellables = Set() @@ -62,7 +60,10 @@ public final class NotificationPanelViewModel: ObservableObject { /// app-defined `NSToolbarItem.Identifier`s), so the package only signals; the app acts. public var onToolbarUpdateRequested: (() -> Void)? - public init() { + public init(notificationManager: NotificationManaging, eventBus: EventBus) { + self.notificationManager = notificationManager + self.eventBus = eventBus + // Observe notification additions and dismissals eventBus.subscribe(CENotificationEvent.self) .receive(on: RunLoop.main) From 6099f15a2b96f207bc5234dfd19a8fae0351a13e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 21:12:30 +0200 Subject: [PATCH 128/335] Refactor: Static delegate provider and env keys for Document/Editor; drop Factory from Document, Editor, Settings --- CodeEdit/AppDependencies.swift | 16 ++++++++ CodeEdit/CodeEditApp.swift | 7 ++-- .../AppCodeFileDocumentDelegate.swift | 20 +++++++--- .../CodeEditSplitViewController.swift | 4 ++ .../CodeEditWindowController.swift | 1 + .../FileInspector/FileInspectorView.swift | 6 +-- .../Service/AppLanguageServicesProvider.swift | 7 +++- .../ProjectNavigatorMenuActions.swift | 11 +++-- .../ProjectNavigatorOutlineView.swift | 2 + ...ViewController+NSOutlineViewDelegate.swift | 3 +- .../ProjectNavigatorViewController.swift | 7 +++- .../ProjectNavigatorToolbarBottom.swift | 4 +- .../SourceControlNavigatorChangesList.swift | 4 +- .../Models/Environment+AppCommands.swift | 22 ++++++++++ .../Workspace/Services/AppFileRelocator.swift | 3 +- .../Services/AppWorkspaceNavigator.swift | 3 +- .../CodeFile/CodeFileDocumentTests.swift | 40 ++++++++++++++----- .../LSP/LanguageServer+CodeFileDocument.swift | 19 +++++---- Packages/Features/Editor/Package.swift | 2 - .../Environment+WorkspaceNavigator.swift | 22 ++++++++++ .../Views/EditorTabBarContextMenu.swift | 6 ++- .../Sources/Editor/Views/CodeFileView.swift | 4 +- .../Editor/Views/EditorAreaFileView.swift | 6 ++- .../Views/Environment+LanguageServices.swift | 22 ++++++++++ .../Editor/Views/FilePreviewView.swift | 4 +- .../Editor/Views/WindowCodeFileView.swift | 4 +- .../Foundation/CodeEditDocument/Package.swift | 2 - .../CodeEditDocument/CodeFileDocument.swift | 17 +++++--- .../CodeFileDocumentDelegate.swift | 16 +++----- .../LanguageServicesProvider.swift | 9 +---- .../Foundation/CodeEditSettings/Package.swift | 6 +-- 31 files changed, 207 insertions(+), 92 deletions(-) create mode 100644 CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift create mode 100644 Packages/Features/Editor/Sources/Editor/Models/Environment+WorkspaceNavigator.swift create mode 100644 Packages/Features/Editor/Sources/Editor/Views/Environment+LanguageServices.swift diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 6db6562ccc..b10d307154 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -41,4 +41,20 @@ final class AppDependencies { private(set) lazy var workspaceFileOpener: WorkspaceFileOpener = AppWorkspaceFileOpener(windowManager: workspaceWindowManager) + + private(set) lazy var workspaceNavigator: WorkspaceNavigator = + AppWorkspaceNavigator(windowManager: workspaceWindowManager) + + private(set) lazy var fileRelocator: FileRelocator = + AppFileRelocator(windowManager: workspaceWindowManager) + + private(set) lazy var languageServicesProvider: LanguageServicesProvider = + AppLanguageServicesProvider(lspService: lspService) + + private(set) lazy var codeFileDocumentDelegate: CodeFileDocumentDelegate = + AppCodeFileDocumentDelegate( + lspService: lspService, + windowManager: workspaceWindowManager, + languageServices: languageServicesProvider + ) } diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index e6a21f5bb9..005e953526 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -23,10 +23,9 @@ struct CodeEditApp: App { init() { NSMenuItem.swizzle() NSSplitViewItem.swizzle() - Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } - Container.shared.workspaceNavigator.register { AppWorkspaceNavigator() } - Container.shared.fileRelocator.register { AppFileRelocator() } - Container.shared.languageServicesProvider.register { @MainActor in AppLanguageServicesProvider() } + CodeFileDocument.delegateProvider = { [dependencies = appdelegate.dependencies] in + dependencies.codeFileDocumentDelegate + } SettingsData.TextEditingSettings.registerCommands() SettingsData.reconcileDefaultKeybindings() } diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift index 76ea377222..e6f11de747 100644 --- a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -8,7 +8,6 @@ import AppKit import Editor import SwiftUI -import Factory import CodeEditTextView import CodeEditDocument @@ -17,19 +16,28 @@ import CodeEditDocument /// standalone-window view, and `LSPService` lifecycle notifications. @MainActor final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { - @LazyInjected(\.lspService) private var lspService + private let lspService: LSPService + private let windowManager: WorkspaceWindowManaging + private let languageServices: LanguageServicesProvider - /// `nonisolated` so the Factory registration closure can construct it from any context - /// (e.g. a non-isolated test `setUp`); the init touches no main-actor state. - nonisolated init() {} + init( + lspService: LSPService, + windowManager: WorkspaceWindowManaging, + languageServices: LanguageServicesProvider + ) { + self.lspService = lspService + self.windowManager = windowManager + self.languageServices = languageServices + } func undoManager(forFile url: URL) -> CEUndoManager? { - url.findWorkspace()?.undoRegistration.managerIfExists(forFile: url) + windowManager.workspace(containing: url)?.undoRegistration.managerIfExists(forFile: url) } func makeWindowContentView(for document: CodeFileDocument) -> NSView { NSHostingView(rootView: SettingsInjector { WindowCodeFileView(codeFile: document) + .environment(\.languageServices, languageServices) }) } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index fee9374876..e39f5401a8 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -105,6 +105,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.activeEditorState, activeEditorState) .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) + .environment(\.workspaceNavigator, dependencies.workspaceNavigator) }) addSplitViewItem(navigator) @@ -126,6 +127,8 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceStatePersistence, workspace.statePersistence) .environment(\.activeEditorState, activeEditorState) .environment(\.activeCursorState, activeCursorState) + .environment(\.workspaceNavigator, dependencies.workspaceNavigator) + .environment(\.languageServices, dependencies.languageServicesProvider) } } @@ -143,6 +146,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.activeEditorState, activeEditorState) .environment(\.fileEditorOverrides, fileEditorOverrides) .environment(\.notificationManager, dependencies.notificationManager) + .environment(\.fileRelocator, dependencies.fileRelocator) }) addSplitViewItem(inspector) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index d6dda6ca5b..df4830b115 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -193,6 +193,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.filePreview) { file in AnyView(FilePreviewView(item: file)) } + .environment(\.languageServices, dependencies.languageServicesProvider) .environment(\.currentTheme, ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first!) panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index 168937ce73..d8e6dee0e5 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -8,12 +8,12 @@ import SwiftUI import CodeEditSettings import CodeEditCore import CodeEditLanguages -import Factory struct FileInspectorView: View { @Environment(\.activeEditorState) private var activeEditorState @Environment(\.fileEditorOverrides) private var fileEditorOverrides + @Environment(\.fileRelocator) private var fileRelocator @AppSettings(\.textEditing) private var textEditing @@ -92,7 +92,7 @@ struct FileInspectorView: View { .appending(path: fileName) DispatchQueue.main.async { do { - _ = try Container.shared.fileRelocator().relocate(file: file, to: destinationURL) + _ = try fileRelocator.relocate(file: file, to: destinationURL) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") @@ -136,7 +136,7 @@ struct FileInspectorView: View { // And if the files are re-built at the same time as the tab is opened, it causes a memory error DispatchQueue.main.async { do { - _ = try Container.shared.fileRelocator().relocate(file: file, to: newURL) + _ = try fileRelocator.relocate(file: file, to: newURL) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") diff --git a/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift b/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift index 543f1019a8..3a25a41a1d 100644 --- a/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift +++ b/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift @@ -6,11 +6,14 @@ // import CodeEditDocument -import Factory @MainActor final class AppLanguageServicesProvider: LanguageServicesProvider { - @LazyInjected(\.lspService) private var lspService + private let lspService: LSPService + + init(lspService: LSPService) { + self.lspService = lspService + } func languageServices(for document: CodeFileDocument) -> LanguageServices { let objects = lspService.languageServerObjects(for: document) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 65bac51161..6a5c92535c 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -9,7 +9,6 @@ import AppKit import CEWorkspaceFileManager import CodeEditCore import SwiftUI -import Factory extension ProjectNavigatorMenu { /// - Returns: the currently selected `CEWorkspaceFile` items in the outline view. @@ -70,7 +69,7 @@ extension ProjectNavigatorMenu { /// Open the items in order. sortedItems.forEach { item in - Container.shared.workspaceNavigator().open(file: item, asTemporary: false) + sender.workspaceNavigator.open(file: item, asTemporary: false) } } @@ -92,7 +91,7 @@ extension ProjectNavigatorMenu { do { if let newFile = try workspace?.workspaceFileManager?.addFile(fileName: "untitled", toFile: item) { workspace?.listenerModel.highlightedFileItem = newFile - Container.shared.workspaceNavigator().open(file: newFile, asTemporary: false) + sender.workspaceNavigator.open(file: newFile, asTemporary: false) } } catch { let alert = NSAlert(error: error) @@ -132,7 +131,7 @@ extension ProjectNavigatorMenu { contents: clipBoardContent ) { workspace?.listenerModel.highlightedFileItem = newFile - Container.shared.workspaceNavigator().open(file: newFile, asTemporary: false) + sender.workspaceNavigator.open(file: newFile, asTemporary: false) renameFile() } } catch { @@ -193,7 +192,7 @@ extension ProjectNavigatorMenu { do { try selectedItems().forEach { item in withAnimation { - Container.shared.workspaceNavigator().closeTab(file: item) + sender.workspaceNavigator.closeTab(file: item) } guard FileManager.default.fileExists(atPath: item.url.path) else { // Was likely already trashed (eg selecting files in a folder and deleting the folder and files) @@ -242,7 +241,7 @@ extension ProjectNavigatorMenu { withAnimation { selectedItems.forEach { item in - Container.shared.workspaceNavigator().closeTab(file: item) + sender.workspaceNavigator.closeTab(file: item) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index a9022ff526..77b5456b1d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -19,6 +19,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @EnvironmentObject var editorManager: EditorManager @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.workspaceNavigator) private var workspaceNavigator @StateObject var prefs: CodeEditSettings.Settings = .shared @@ -29,6 +30,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { controller.workspace = workspace controller.iconColor = prefs.preferences.general.fileIconStyle controller.activeEditorState = activeEditorState + controller.workspaceNavigator = workspaceNavigator workspace.workspaceFileManager?.addObserver(context.coordinator) context.coordinator.controller = controller diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 4b9c13c05b..ba1d99196c 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -9,7 +9,6 @@ import AppKit import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore -import Factory extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView( @@ -51,7 +50,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { if !item.isFolder && shouldSendSelectionUpdate { shouldSendSelectionUpdate = false if activeEditorState?.selectedFile != item { - Container.shared.workspaceNavigator().open(file: item, asTemporary: true) + workspaceNavigator.open(file: item, asTemporary: true) } shouldSendSelectionUpdate = true } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 46b51302a0..40c111977a 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -11,7 +11,6 @@ import CEWorkspaceFileManager import SwiftUI import OSLog import CodeEditCore -import Factory /// A `NSViewController` that handles the **ProjectNavigatorView** in the **NavigatorArea**. /// @@ -40,6 +39,10 @@ final class ProjectNavigatorViewController: NSViewController { var expandedItems: Set = [] weak var workspace: Workspace? + + /// The navigation command interface; assigned by `ProjectNavigatorOutlineView` from the + /// environment. No-op until set so the controller stays constructible in isolation. + var workspaceNavigator: WorkspaceNavigator = NoOpWorkspaceNavigator() weak var activeEditorState: (any ActiveEditorState)? var iconColor: SettingsData.FileIconStyle = .color { @@ -182,7 +185,7 @@ final class ProjectNavigatorViewController: NSViewController { outlineView.expandItem(item) } } else if Settings[\.navigation].navigationStyle == .openInTabs { - Container.shared.workspaceNavigator().open(file: item, asTemporary: false) + workspaceNavigator.open(file: item, asTemporary: false) } } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 6d6a78c57c..50058a5943 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -9,7 +9,6 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditUI import CodeEditCore -import Factory struct ProjectNavigatorToolbarBottom: View { @Environment(\.controlActiveState) @@ -19,6 +18,7 @@ struct ProjectNavigatorToolbarBottom: View { private var colorScheme @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.workspaceNavigator) private var workspaceNavigator @EnvironmentObject var listenerModel: WorkspaceNotificationModel @EnvironmentObject var projectNavigatorViewModel: ProjectNavigatorViewModel @@ -117,7 +117,7 @@ struct ProjectNavigatorToolbarBottom: View { toFile: rootFile ) { listenerModel.highlightedFileItem = newFile - Container.shared.workspaceNavigator().open(file: newFile, asTemporary: false) + workspaceNavigator.open(file: newFile, asTemporary: false) } } catch { let alert = NSAlert(error: error) diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 3ed92e87b1..20880018d6 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -9,10 +9,10 @@ import AppKit import CEWorkspaceFileManager import SwiftUI import CodeEditCore -import Factory struct SourceControlNavigatorChangesList: View { @EnvironmentObject var sourceControlManager: SourceControlManager + @Environment(\.workspaceNavigator) private var workspaceNavigator @Environment(\.workspaceFileManager) private var workspaceFileManager @@ -81,7 +81,7 @@ struct SourceControlNavigatorChangesList: View { return } DispatchQueue.main.async { - Container.shared.workspaceNavigator().open(file: ceFile, asTemporary: true) + workspaceNavigator.open(file: ceFile, asTemporary: true) } } } diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift new file mode 100644 index 0000000000..4a3a4a05c3 --- /dev/null +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -0,0 +1,22 @@ +// +// Environment+AppCommands.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 10/07/2026. +// + +import SwiftUI +import CodeEditCore + +private struct FileRelocatorKey: EnvironmentKey { + static let defaultValue: FileRelocator = NoOpFileRelocator() +} + +extension EnvironmentValues { + /// The command used to move/rename a file within its owning workspace. + /// No-op by default (previews, tests); injected at the workspace window's root. + var fileRelocator: FileRelocator { + get { self[FileRelocatorKey.self] } + set { self[FileRelocatorKey.self] = newValue } + } +} diff --git a/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift b/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift index 6721820621..a683e167f9 100644 --- a/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift +++ b/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift @@ -8,7 +8,6 @@ import Foundation import CodeEditCore import CEWorkspaceFileManager -import Factory /// App-shell binding of the `FileRelocator` command. Resolves the workspace that /// owns the file and delegates to `MoveFileUseCase`, which moves the file and @@ -16,7 +15,7 @@ import Factory final class AppFileRelocator: FileRelocator { private let windowManager: WorkspaceWindowManaging - init(windowManager: WorkspaceWindowManaging = Container.shared.workspaceWindowManager()) { + init(windowManager: WorkspaceWindowManaging) { self.windowManager = windowManager } diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift index 45e679e0cb..cdbf969134 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -9,7 +9,6 @@ import Foundation import CodeEditCore import CEWorkspaceFileManager import Editor -import Factory /// App-shell binding of the `WorkspaceNavigator` command interface. /// Delegates to `WorkspaceWindowManager.openFileInWorkspace(url:asTemporary:)`, which maps the @@ -17,7 +16,7 @@ import Factory final class AppWorkspaceNavigator: WorkspaceNavigator { private let windowManager: WorkspaceWindowManaging - init(windowManager: WorkspaceWindowManaging = Container.shared.workspaceWindowManager()) { + init(windowManager: WorkspaceWindowManaging) { self.windowManager = windowManager } diff --git a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift index 4f7e202cd4..929f4f767d 100644 --- a/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift +++ b/CodeEditTests/Features/CodeFile/CodeFileDocumentTests.swift @@ -11,7 +11,6 @@ import AppKit import Testing import CodeEditCore import CodeEditDocument -import Factory import CodeEditTextView @testable import CodeEdit @@ -37,8 +36,9 @@ struct CodeFileDocumentTests { @Test func delegateConsultedForUndoOnReread() throws { let mock = MockDelegate() - Container.shared.codeFileDocumentDelegate.register { mock } - defer { Container.shared.codeFileDocumentDelegate.reset() } + let previousProvider = CodeFileDocument.delegateProvider + CodeFileDocument.delegateProvider = { mock } + defer { CodeFileDocument.delegateProvider = previousProvider } try withCodeFile { codeFile in // First read happened in `withCodeFile` (content now loaded). A second read @@ -53,8 +53,9 @@ struct CodeFileDocumentTests { @Test func delegateReceivesOpenAndCloseNotifications() throws { let mock = MockDelegate() - Container.shared.codeFileDocumentDelegate.register { mock } - defer { Container.shared.codeFileDocumentDelegate.reset() } + let previousProvider = CodeFileDocument.delegateProvider + CodeFileDocument.delegateProvider = { mock } + defer { CodeFileDocument.delegateProvider = previousProvider } try withCodeFile { codeFile in #expect(mock.openedDocuments.contains { $0 === codeFile }) @@ -71,6 +72,7 @@ struct CodeFileDocumentTests { } } + @MainActor private func withCodeFile(_ operation: (CodeFileDocument) throws -> Void) throws { try withFile { fileURL in try defaultString.write(to: fileURL, atomically: true, encoding: .utf8) @@ -79,6 +81,7 @@ struct CodeFileDocumentTests { } } + @MainActor @Test func autosavesInPlaceReflectsProvider() { let original = CodeFileDocument.isAutoSaveOnProvider @@ -91,6 +94,7 @@ struct CodeFileDocumentTests { #expect(CodeFileDocument.autosavesInPlace == false) } + @MainActor @Test func indentOptionOverrideUsesCoreType() { let codeFile = CodeFileDocument() @@ -99,6 +103,7 @@ struct CodeFileDocumentTests { #expect(codeFile.indentOption?.spaceCount == 2) } + @MainActor @Test func testLoadUTF8Encoding() throws { try withFile { fileURL in @@ -113,6 +118,7 @@ struct CodeFileDocumentTests { } } + @MainActor @Test func testWriteUTF8Encoding() throws { try withFile { fileURL in @@ -138,6 +144,7 @@ struct CodeFileDocumentTests { } } + @MainActor @Test func ignoresExternalUpdatesWithOutstandingChanges() throws { try withCodeFile { codeFile in @@ -156,18 +163,29 @@ struct CodeFileDocumentTests { } } + // Deliberately NOT @MainActor: `presentedItemDidChange` is a file-presenter callback that + // performs `DispatchQueue.main.sync` internally, so it must be invoked off the main thread + // (as NSFileCoordinator does in production). The document itself is created on main. @Test - func loadsExternalUpdatesWithNoOutstandingChanges() throws { - try withCodeFile { codeFile in + func loadsExternalUpdatesWithNoOutstandingChanges() async throws { + try await withTempDir { dir in + let fileURL = dir.appending(path: "file.swift") + try defaultString.write(to: fileURL, atomically: true, encoding: .utf8) + let codeFile = try await MainActor.run { + try CodeFileDocument(contentsOf: fileURL, ofType: "public.source-code") + } + // Update the modification date - try "different contents".write(to: codeFile.fileURL!, atomically: true, encoding: .utf8) + try "different contents".write(to: fileURL, atomically: true, encoding: .utf8) - // Tell the file the disk representation changed + // Tell the file the disk representation changed (off-main, like a real presenter callback) codeFile.presentedItemDidChange() // The file should have reloaded (it was clean) - #expect(codeFile.content?.string == "different contents") - #expect(codeFile.isDocumentEdited == false) + await MainActor.run { + #expect(codeFile.content?.string == "different contents") + #expect(codeFile.isDocumentEdited == false) + } } } } diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 1ad4fe6c3a..638f6324c9 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -13,7 +13,6 @@ import CodeEditTextView import CodeEditSourceEditor import LanguageClient import LanguageServerProtocol -import Factory @testable import CodeEdit @@ -29,8 +28,12 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { var tempTestDir: URL! + /// The host app's live dependency graph (the test bundle runs inside CodeEdit). + @MainActor var appDependencies: AppDependencies { + (NSApplication.shared.delegate as! AppDelegate).dependencies // swiftlint:disable:this force_cast + } + override func setUp() { - Container.shared.codeFileDocumentDelegate.register { AppCodeFileDocumentDelegate() } continueAfterFailure = false do { let tempDir = FileManager.default.temporaryDirectory.appending( @@ -78,8 +81,8 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { serverCapabilities: capabilities, rootPath: tempTestDir, logContainer: LanguageServerLogContainer(language: .swift), - provideObjects: { Container.shared.lspService().languageServerObjects(for: $0) }, - clearObjects: { Container.shared.lspService().removeLanguageServerObjects(for: $0) } + provideObjects: { self.appDependencies.lspService.languageServerObjects(for: $0) }, + clearObjects: { self.appDependencies.lspService.removeLanguageServerObjects(for: $0) } ) _ = try await server.lspInstance.initializeIfNeeded() return (connection: bufferingConnection, server: server) @@ -87,7 +90,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { @MainActor func makeTestWorkspace() throws -> (Workspace, CEWorkspaceFileManager) { - let windowManager = Container.shared.workspaceWindowManager() + let windowManager = appDependencies.workspaceWindowManager try windowManager.openWorkspace(at: tempTestDir) guard let workspace = windowManager.openWorkspaces.first(where: { $0.fileURL?.standardizedFileURL.path() == tempTestDir.standardizedFileURL.path() @@ -160,7 +163,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { let (connection, server) = try await makeTestServer() // This service should receive the didOpen/didClose notifications - let lspService = Container.shared.lspService() + let lspService = appDependencies.lspService lspService.languageClients[.init(.swift, tempTestDir.path() + "/")] = server // Set up workspace. Registers it with the workspace window manager. @@ -245,7 +248,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { let (connection, server) = try await makeTestServer() // Create a CodeFileDocument to test with, attach it to the workspace and file let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) - let lspObjects = Container.shared.lspService().languageServerObjects(for: codeFile) + let lspObjects = appDependencies.lspService.languageServerObjects(for: codeFile) XCTAssertNotNil(lspObjects.textCoordinator.languageServer) lspObjects.textCoordinator.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) @@ -303,7 +306,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { // Set up test server let (connection, server) = try await makeTestServer() let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) - let lspObjects = Container.shared.lspService().languageServerObjects(for: codeFile) + let lspObjects = appDependencies.lspService.languageServerObjects(for: codeFile) XCTAssertNotNil(lspObjects.textCoordinator.languageServer) lspObjects.textCoordinator.setUpUpdatesTask() diff --git a/Packages/Features/Editor/Package.swift b/Packages/Features/Editor/Package.swift index ac1997467d..a989f1af57 100644 --- a/Packages/Features/Editor/Package.swift +++ b/Packages/Features/Editor/Package.swift @@ -14,7 +14,6 @@ let package = Package( .package(path: "../../Foundation/CodeEditDocument"), .package(path: "../../Foundation/CodeEditSettings"), .package(path: "../../Services/CodeEditServices"), - .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3"), .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), @@ -31,7 +30,6 @@ let package = Package( .product(name: "CodeEditDocument", package: "CodeEditDocument"), .product(name: "CodeEditSettings", package: "CodeEditSettings"), .product(name: "CodeEditServices", package: "CodeEditServices"), - .product(name: "Factory", package: "Factory"), .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), .product(name: "CodeEditTextView", package: "CodeEditTextView"), .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), diff --git a/Packages/Features/Editor/Sources/Editor/Models/Environment+WorkspaceNavigator.swift b/Packages/Features/Editor/Sources/Editor/Models/Environment+WorkspaceNavigator.swift new file mode 100644 index 0000000000..3cf2410f7d --- /dev/null +++ b/Packages/Features/Editor/Sources/Editor/Models/Environment+WorkspaceNavigator.swift @@ -0,0 +1,22 @@ +// +// Environment+WorkspaceNavigator.swift +// Editor +// +// Created by Matthijs Eikelenboom on 10/07/2026. +// + +import SwiftUI +import CodeEditCore + +private struct WorkspaceNavigatorKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: WorkspaceNavigator = NoOpWorkspaceNavigator() +} + +extension EnvironmentValues { + /// The command interface for opening, revealing, and closing files in the owning + /// workspace. No-op by default (previews, tests); injected by the app shell. + public var workspaceNavigator: WorkspaceNavigator { + get { self[WorkspaceNavigatorKey.self] } + set { self[WorkspaceNavigatorKey.self] = newValue } + } +} diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift index 982c7d25b4..e9ee64c5ec 100644 --- a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift @@ -8,7 +8,6 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore -import Factory import Foundation extension View { @@ -28,6 +27,9 @@ struct EditorTabBarContextMenu: ViewModifier { @EnvironmentObject var editorManager: EditorManager + @Environment(\.workspaceNavigator) + private var workspaceNavigator + @Environment(\.workspaceFileManager) private var workspaceFileManager @@ -107,7 +109,7 @@ struct EditorTabBarContextMenu: ViewModifier { } Button("Reveal in Project Navigator") { - Container.shared.workspaceNavigator().reveal(file: item) + workspaceNavigator.reveal(file: item) } Button("Open in New Window") { diff --git a/Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift index 1d9f92089a..4bdcf6d5ec 100644 --- a/Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift @@ -15,7 +15,6 @@ import CodeEditTextView import CodeEditLanguages import CodeEditCore import Combine -import Factory /// CodeFileView is just a wrapper of the `CodeEditor` dependency struct CodeFileView: View { @@ -81,13 +80,14 @@ struct CodeFileView: View { init( editorInstance: EditorInstance, codeFile: CodeFileDocument, + languageServices languageServicesProvider: LanguageServicesProvider, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true ) { self._editorInstance = .init(wrappedValue: editorInstance) self._codeFile = .init(wrappedValue: codeFile) - let languageServices = Container.shared.languageServicesProvider().languageServices(for: codeFile) + let languageServices = languageServicesProvider.languageServices(for: codeFile) self.textViewCoordinators = textViewCoordinators + [editorInstance.rangeTranslator] diff --git a/Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift index 30cf6dcaae..42d2be98b7 100644 --- a/Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift @@ -20,6 +20,9 @@ struct EditorAreaFileView: View { @Environment(\.edgeInsets) private var edgeInsets + @Environment(\.languageServices) + private var languageServices + var editorInstance: EditorInstance var codeFile: CodeFileDocument @@ -27,7 +30,8 @@ struct EditorAreaFileView: View { if let utType = codeFile.utType, utType.conforms(to: .text) { CodeFileView( editorInstance: editorInstance, - codeFile: codeFile + codeFile: codeFile, + languageServices: languageServices ) } else { NonTextFileView(fileDocument: codeFile) diff --git a/Packages/Features/Editor/Sources/Editor/Views/Environment+LanguageServices.swift b/Packages/Features/Editor/Sources/Editor/Views/Environment+LanguageServices.swift new file mode 100644 index 0000000000..12def92888 --- /dev/null +++ b/Packages/Features/Editor/Sources/Editor/Views/Environment+LanguageServices.swift @@ -0,0 +1,22 @@ +// +// Environment+LanguageServices.swift +// Editor +// +// Created by Matthijs Eikelenboom on 10/07/2026. +// + +import SwiftUI +import CodeEditDocument + +private struct LanguageServicesKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: LanguageServicesProvider = NoOpLanguageServicesProvider() +} + +extension EnvironmentValues { + /// Vends per-document language services (text coordinator + highlight provider). + /// No-op by default (previews render without LSP); injected by the app shell. + public var languageServices: LanguageServicesProvider { + get { self[LanguageServicesKey.self] } + set { self[LanguageServicesKey.self] = newValue } + } +} diff --git a/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift b/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift index c9045efcb9..39f2f2a852 100644 --- a/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift @@ -27,9 +27,11 @@ public struct FilePreviewView: View { self._document = .init(wrappedValue: doc ?? .init()) } + @Environment(\.languageServices) private var languageServices + public var body: some View { if let utType = document.utType, utType.conforms(to: .text) { - CodeFileView(editorInstance: editorInstance, codeFile: document, isEditable: false) + CodeFileView(editorInstance: editorInstance, codeFile: document, languageServices: languageServices, isEditable: false) .environmentObject(undoRegistration) } else { NonTextFileView(fileDocument: document) diff --git a/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift b/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift index 3f64a1eec3..3f027ae601 100644 --- a/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift +++ b/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift @@ -27,9 +27,11 @@ public struct WindowCodeFileView: View { self.codeFile = codeFile } + @Environment(\.languageServices) private var languageServices + public var body: some View { if let utType = codeFile.utType, utType.conforms(to: .text) { - CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + CodeFileView(editorInstance: editorInstance, codeFile: codeFile, languageServices: languageServices) .environmentObject(undoRegistration) } else { NonTextFileView(fileDocument: codeFile) diff --git a/Packages/Foundation/CodeEditDocument/Package.swift b/Packages/Foundation/CodeEditDocument/Package.swift index 181e5a795a..ab5520545e 100644 --- a/Packages/Foundation/CodeEditDocument/Package.swift +++ b/Packages/Foundation/CodeEditDocument/Package.swift @@ -11,7 +11,6 @@ let package = Package( dependencies: [ // Pins match the app's Package.resolved to avoid a second resolved copy. .package(path: "../CodeEditCore"), - .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3"), .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), @@ -22,7 +21,6 @@ let package = Package( name: "CodeEditDocument", dependencies: [ .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "Factory", package: "Factory"), .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), .product(name: "CodeEditTextView", package: "CodeEditTextView"), .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift index a5c69826a5..986d570464 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift @@ -16,7 +16,6 @@ import CodeEditCore import Combine import OSLog import TextStory -import Factory enum CodeFileError: Error { case failedToDecode @@ -36,11 +35,17 @@ public final class CodeFileDocument: NSDocument, ObservableObject { static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") - /// The app-registered delegate (see ``CodeFileDocumentDelegate``). Provides LSP lifecycle - /// notifications, the standalone-window content view, and undo-manager lookup, keeping this - /// document free of app-tier types. `nil` when unhosted (e.g. tests that don't register one). - @LazyInjected(\.codeFileDocumentDelegate) - private var delegate + /// Vends the app-registered delegate (see ``CodeFileDocumentDelegate``). A static provider — + /// not a per-instance property — because framework-created documents fire `documentDidOpen` + /// from `read()` during `init(contentsOf:)`, before any caller could set an instance property. + /// Wired by the app at launch (like ``isAutoSaveOnProvider``); defaults to `nil` so tests and + /// previews are safe. Call sites handle main-actor hops themselves. + nonisolated(unsafe) public static var delegateProvider: () -> CodeFileDocumentDelegate? = { nil } + + /// The app-registered delegate. Provides LSP lifecycle notifications, the standalone-window + /// content view, and undo-manager lookup, keeping this document free of app-tier types. + /// `nil` when unhosted (e.g. tests that don't wire a provider). + private var delegate: CodeFileDocumentDelegate? { Self.delegateProvider() } /// The text content of the document, stored as a text storage /// diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift index 054051be4d..5f1976f2ca 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift @@ -6,17 +6,17 @@ // import AppKit -import Factory import CodeEditTextView /// The app-provided capabilities a `CodeFileDocument` needs from its host application /// (undo registry, standalone-window content, and language-server lifecycle). Keeps the /// document free of `Workspace`, Settings-view, and `LSPService` references. /// -/// A single shared delegate is registered by the app in `CodeEditApp.init`; it is delivered -/// via Factory rather than a per-instance `weak var delegate` so that framework-created -/// documents (`init()` → `read()` fires `documentDidOpen` before any completion handler could -/// set a per-instance delegate) get correct timing without a custom `NSDocumentController`. +/// A single shared delegate is wired by the app at launch via +/// ``CodeFileDocument/delegateProvider`` rather than a per-instance `weak var delegate`, +/// so that framework-created documents (`init()` → `read()` fires `documentDidOpen` before +/// any completion handler could set a per-instance delegate) get correct timing without a +/// custom `NSDocumentController`. @MainActor public protocol CodeFileDocumentDelegate: AnyObject { /// The undo manager already registered for a file, if any (nil if none exists yet). @@ -28,9 +28,3 @@ public protocol CodeFileDocumentDelegate: AnyObject { /// The document at `url` closed. func documentDidClose(at url: URL) } - -extension Container { - /// App shell registers the real delegate in `CodeEditApp.init`; defaults to `nil` - /// (no-op) so tests and pre-launch contexts are safe. - public var codeFileDocumentDelegate: Factory { self { nil } } -} diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift index 838e9b035d..968bc027a2 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift @@ -9,7 +9,6 @@ import CodeEditTextView import CodeEditLanguages import AppKit -import Factory public struct LanguageServices { public let textCoordinator: TextViewCoordinator @@ -21,19 +20,13 @@ public struct LanguageServices { } } -extension Container { - public var languageServicesProvider: Factory { - self { @MainActor in NoOpLanguageServicesProvider() }.singleton - } -} - @MainActor public protocol LanguageServicesProvider: AnyObject { func languageServices(for document: CodeFileDocument) -> LanguageServices } public final class NoOpLanguageServicesProvider: LanguageServicesProvider { - public init() {} + public nonisolated init() {} @MainActor public func languageServices(for document: CodeFileDocument) -> LanguageServices { diff --git a/Packages/Foundation/CodeEditSettings/Package.swift b/Packages/Foundation/CodeEditSettings/Package.swift index dd3067b9e7..2608ae4e35 100644 --- a/Packages/Foundation/CodeEditSettings/Package.swift +++ b/Packages/Foundation/CodeEditSettings/Package.swift @@ -10,15 +10,13 @@ let package = Package( ], dependencies: [ // Pins match the app's Package.resolved to avoid a second resolved copy. - .package(path: "../CodeEditCore"), - .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") + .package(path: "../CodeEditCore") ], targets: [ .target( name: "CodeEditSettings", dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "Factory", package: "Factory") + .product(name: "CodeEditCore", package: "CodeEditCore") ] ) ] From cc1624ed1ae16f3159754d0a4a39b9d362e7142c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 21:15:27 +0200 Subject: [PATCH 129/335] Refactor: Constructor-inject LSP registry package managers and shell tools --- CodeEdit/CodeEditContainer.swift | 9 +++++++-- .../InstallationMethod+PackageManager.swift | 12 ++++++------ .../PackageManagerInstallOperation.swift | 6 +++--- .../Install/PackageManagerProgressModel.swift | 1 - .../Sources/CargoPackageManager.swift | 5 ++--- .../Sources/GithubPackageManager.swift | 5 ++--- .../Sources/GolangPackageManager.swift | 5 ++--- .../Sources/NPMPackageManager.swift | 5 ++--- .../Sources/PipPackageManager.swift | 5 ++--- .../LSP/Registry/RegistryManager.swift | 8 +++++--- .../Features/LSP/Service/LSPService.swift | 7 ++++++- .../Extensions/URL/URL+FindWorkspace.swift | 19 ------------------- CodeEditTests/Features/LSP/Registry.swift | 4 +++- 13 files changed, 40 insertions(+), 51 deletions(-) delete mode 100644 CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift index 066ffc4432..45706a72eb 100644 --- a/CodeEdit/CodeEditContainer.swift +++ b/CodeEdit/CodeEditContainer.swift @@ -13,7 +13,11 @@ import Factory extension Container { var lspService: Factory { self { @MainActor in - LSPService(notificationManager: AppDependencies.bridgeShared.notificationManager) + let service = LSPService(notificationManager: AppDependencies.bridgeShared.notificationManager) + service.workspaceFinder = { url in + AppDependencies.bridgeShared.workspaceWindowManager.workspace(containing: url) + } + return service }.singleton } @@ -37,7 +41,8 @@ extension Container { self { @MainActor in RegistryManager( eventBus: AppDependencies.bridgeShared.eventBus, - notificationManager: AppDependencies.bridgeShared.notificationManager + notificationManager: AppDependencies.bridgeShared.notificationManager, + shellClient: AppDependencies.bridgeShared.shellClient ) }.singleton } diff --git a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift b/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift index e52c28ba41..b130a6f42d 100644 --- a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift @@ -9,18 +9,18 @@ import Foundation import CodeEditCore extension InstallationMethod { - func packageManager(installPath: URL) -> PackageManagerProtocol? { + func packageManager(installPath: URL, shellClient: ShellClientProtocol) -> PackageManagerProtocol? { switch packageManagerType { case .npm: - return NPMPackageManager(installationDirectory: installPath) + return NPMPackageManager(installationDirectory: installPath, shellClient: shellClient) case .cargo: - return CargoPackageManager(installationDirectory: installPath) + return CargoPackageManager(installationDirectory: installPath, shellClient: shellClient) case .pip: - return PipPackageManager(installationDirectory: installPath) + return PipPackageManager(installationDirectory: installPath, shellClient: shellClient) case .golang: - return GolangPackageManager(installationDirectory: installPath) + return GolangPackageManager(installationDirectory: installPath, shellClient: shellClient) case .github, .sourceBuild: - return GithubPackageManager(installationDirectory: installPath) + return GithubPackageManager(installationDirectory: installPath, shellClient: shellClient) case .nuget, .opam, .gem, .composer: // TODO: IMPLEMENT OTHER PACKAGE MANAGERS return nil diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift index 3ac7d2652a..d3f656bd03 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift @@ -6,7 +6,6 @@ // import Foundation -import Factory import Combine import CodeEditCore @@ -66,7 +65,7 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { /// If non-nil, indicates that this operation has halted and requires confirmation. @Published public private(set) var waitingForConfirmation: String? - private let shellClient: ShellClientProtocol = Container.shared.shellClient() + private let shellClient: ShellClientProtocol private var operationTask: Task? private var confirmationContinuation: CheckedContinuation? private var outputIdx = 0 @@ -76,7 +75,8 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { /// - Parameters: /// - package: The package to install. /// - steps: The steps that make up the operation. - init(package: RegistryItem, steps: [PackageManagerInstallStep]) { + init(package: RegistryItem, steps: [PackageManagerInstallStep], shellClient: ShellClientProtocol) { + self.shellClient = shellClient self.package = package self.steps = steps self.progress = Progress(totalUnitCount: Int64(steps.count)) diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift index e0e74bd908..787b107a0e 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift @@ -6,7 +6,6 @@ // import Combine -import Factory import Foundation import CodeEditCore diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift index beba3b1331..2f5a3086a0 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift @@ -5,7 +5,6 @@ // Created by Abe Malla on 2/3/25. // -import Factory import Foundation import CodeEditCore @@ -14,9 +13,9 @@ final class CargoPackageManager: PackageManagerProtocol { let shellClient: ShellClientProtocol - init(installationDirectory: URL) { + init(installationDirectory: URL, shellClient: ShellClientProtocol) { self.installationDirectory = installationDirectory - self.shellClient = Container.shared.shellClient() + self.shellClient = shellClient } func install(method installationMethod: InstallationMethod) throws -> [PackageManagerInstallStep] { diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift index 91bc71131e..30cd2a93df 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift @@ -5,7 +5,6 @@ // Created by Abe Malla on 3/10/25. // -import Factory import Foundation import CodeEditCore @@ -14,9 +13,9 @@ final class GithubPackageManager: PackageManagerProtocol { let shellClient: ShellClientProtocol - init(installationDirectory: URL) { + init(installationDirectory: URL, shellClient: ShellClientProtocol) { self.installationDirectory = installationDirectory - self.shellClient = Container.shared.shellClient() + self.shellClient = shellClient } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift index cf07c56a9e..2259e789ad 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift @@ -5,7 +5,6 @@ // Created by Abe Malla on 2/3/25. // -import Factory import Foundation import CodeEditCore @@ -14,9 +13,9 @@ final class GolangPackageManager: PackageManagerProtocol { let shellClient: ShellClientProtocol - init(installationDirectory: URL) { + init(installationDirectory: URL, shellClient: ShellClientProtocol) { self.installationDirectory = installationDirectory - self.shellClient = Container.shared.shellClient() + self.shellClient = shellClient } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift index 4287518247..f299f18cc1 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift @@ -5,7 +5,6 @@ // Created by Abe Malla on 2/2/25. // -import Factory import Foundation import CodeEditCore @@ -14,9 +13,9 @@ final class NPMPackageManager: PackageManagerProtocol { let shellClient: ShellClientProtocol - init(installationDirectory: URL) { + init(installationDirectory: URL, shellClient: ShellClientProtocol) { self.installationDirectory = installationDirectory - self.shellClient = Container.shared.shellClient() + self.shellClient = shellClient } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift index a1acaeba98..63b42dde79 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift +++ b/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift @@ -5,7 +5,6 @@ // Created by Abe Malla on 2/3/25. // -import Factory import Foundation import CodeEditCore @@ -14,9 +13,9 @@ final class PipPackageManager: PackageManagerProtocol { let shellClient: ShellClientProtocol - init(installationDirectory: URL) { + init(installationDirectory: URL, shellClient: ShellClientProtocol) { self.installationDirectory = installationDirectory - self.shellClient = Container.shared.shellClient() + self.shellClient = shellClient } // MARK: - PackageManagerProtocol diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index f58ef9a00d..3456fcf353 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -52,10 +52,12 @@ final class RegistryManager: ObservableObject, RegistryManaging { private let eventBus: EventBus private let notificationManager: NotificationManaging + private let shellClient: ShellClientProtocol - init(eventBus: EventBus, notificationManager: NotificationManaging) { + init(eventBus: EventBus, notificationManager: NotificationManaging, shellClient: ShellClientProtocol) { self.eventBus = eventBus self.notificationManager = notificationManager + self.shellClient = shellClient // Load the registry items from disk again after cache expires if let items = loadItemsFromDisk() { setRegistryItems(items) @@ -110,11 +112,11 @@ final class RegistryManager: ObservableObject, RegistryManaging { throw RegistryManagerError.installationRunning } guard let method = package.installMethod, - let manager = method.packageManager(installPath: installPath) else { + let manager = method.packageManager(installPath: installPath, shellClient: shellClient) else { throw PackageManagerError.invalidConfiguration } let installSteps = try manager.install(method: method) - return PackageManagerInstallOperation(package: package, steps: installSteps) + return PackageManagerInstallOperation(package: package, steps: installSteps, shellClient: shellClient) } /// Starts the actual installation process for a package diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 5f4bb6faa8..5f39726119 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -156,6 +156,11 @@ final class LSPService: ObservableObject, LSPServiceProtocol { private let notificationManager: NotificationManaging + /// Resolves the workspace that owns a file URL. Property-injected (not init-injected) by the + /// composition root because the window manager's own construction consumes `LSPService` — + /// init injection in both directions would recurse. Assigned before any document opens. + var workspaceFinder: (URL) -> Workspace? = { _ in nil } + init(notificationManager: NotificationManaging) { self.notificationManager = notificationManager // Load the LSP binaries from the developer menu @@ -225,7 +230,7 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// - Note: Must be invoked after the contents of the file are available. /// - Parameter document: The code document that was opened. func openDocument(_ document: CodeFileDocument) { - guard let workspace = document.fileURL?.findWorkspace(), + guard let workspace = document.fileURL.flatMap({ workspaceFinder($0) }), let workspacePath = workspace.fileURL?.absolutePath, let lspLanguage = document.getLanguage().lspLanguage else { return diff --git a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift deleted file mode 100644 index a15cd30e30..0000000000 --- a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// URL+FindWorkspace.swift -// CodeEdit -// -// Created by Khan Winter on 12/19/24. -// - -import Foundation - -extension URL { - /// Finds a workspace that contains the url. - /// - /// Bridge-phase shim: remaining callers (LSP cluster) receive an injected - /// window manager in a later migration task, after which this file is deleted. - @MainActor - func findWorkspace() -> Workspace? { - AppDependencies.bridgeShared.workspaceWindowManager.workspace(containing: self) - } -} diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index a34e48a667..f3345c1898 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -9,6 +9,7 @@ import Testing import Foundation import CodeEditCore import Notifications +import ShellClient @testable import CodeEdit @MainActor @@ -16,7 +17,8 @@ import Notifications struct RegistryTests { var registry: RegistryManager = RegistryManager( eventBus: EventBus(), - notificationManager: NotificationManager(eventBus: EventBus()) + notificationManager: NotificationManager(eventBus: EventBus()), + shellClient: ShellClient() ) // MARK: - Download Tests From 4a0b82d456ca8da14ad606c3edc2368b3f173da0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 21:29:40 +0200 Subject: [PATCH 130/335] Refactor: Env keys and init params for remaining app-side Factory consumers --- CodeEdit/CodeEditApp.swift | 24 ++-- .../TaskNotificationHandler.swift | 9 +- .../Notifications/TaskNotificationView.swift | 2 +- .../TaskNotificationsDetailView.swift | 3 +- .../ViewModels/QuickActionsViewModel.swift | 8 +- .../CodeEditSplitViewController.swift | 9 +- .../CodeEditWindowController+Toolbar.swift | 2 +- .../Features/Keybindings/CommandManager.swift | 4 +- .../ChangedFile/GitChangedFileLabel.swift | 6 +- .../SettingsData+CommandRegistration.swift | 5 +- .../SettingsData+KeybindingReconcile.swift | 8 +- .../Extensions/LanguageServersView.swift | 3 +- .../Models/IgnorePatternModel.swift | 8 +- .../SourceControlGeneralView.swift | 7 +- .../SourceControlGitView.swift | 14 ++- CodeEdit/Features/Settings/SettingsView.swift | 6 +- .../Clone/GitCheckoutBranchView.swift | 4 +- .../SourceControl/Clone/GitCloneView.swift | 9 +- .../GitCheckoutBranchViewModel.swift | 6 +- .../Clone/ViewModels/GitCloneViewModel.swift | 10 +- .../UseCases/CloneRepositoryUseCase.swift | 11 +- .../Views/SourceControlPullView.swift | 7 +- .../StatusBarToggleUtilityAreaButton.swift | 8 +- .../Features/Tasks/Models/CEActiveTask.swift | 7 +- CodeEdit/Features/Tasks/TaskManager.swift | 7 +- .../ToolbarItems/StartTaskToolbarItem.swift | 7 +- .../Tasks/Views/StartTaskToolbarButton.swift | 6 +- .../View/UtilityAreaOutputSourcePicker.swift | 10 +- .../Features/Welcome/GitCloneButton.swift | 10 +- CodeEdit/Features/Welcome/NewFileButton.swift | 4 +- .../Welcome/OpenFileOrFolderButton.swift | 4 +- .../WindowCommands/CodeEditCommands.swift | 4 +- .../WindowCommands/FileCommands.swift | 5 +- .../Utils/RecentProjectsMenu.swift | 7 +- .../WindowCommands/ViewCommands.swift | 3 +- .../Models/Environment+AppCommands.swift | 104 ++++++++++++++++++ .../Features/Workspace/Models/Workspace.swift | 4 +- .../Features/Workspace/WorkspaceFactory.swift | 5 +- .../TaskNotificationHandlerTests.swift | 5 +- .../Commands/QuickActionsViewModelTests.swift | 5 +- .../Documents/DocumentsUnitTests.swift | 9 +- .../Features/Tasks/CEActiveTaskTests.swift | 4 +- .../Features/Tasks/TaskManagerTests.swift | 2 +- 43 files changed, 272 insertions(+), 113 deletions(-) diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 005e953526..da12f23c74 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -26,8 +26,8 @@ struct CodeEditApp: App { CodeFileDocument.delegateProvider = { [dependencies = appdelegate.dependencies] in dependencies.codeFileDocumentDelegate } - SettingsData.TextEditingSettings.registerCommands() - SettingsData.reconcileDefaultKeybindings() + SettingsData.TextEditingSettings.registerCommands(in: appdelegate.dependencies.commandManager) + SettingsData.reconcileDefaultKeybindings(keybindingManager: appdelegate.dependencies.keybindingManager) } var body: some Scene { @@ -35,9 +35,19 @@ struct CodeEditApp: App { WelcomeWindow( subtitleView: { WelcomeSubtitleView() }, actions: { dismissWindow in - NewFileButton(dismissWindow: dismissWindow) - GitCloneButton(dismissWindow: dismissWindow) - OpenFileOrFolderButton(dismissWindow: dismissWindow) + NewFileButton( + windowManager: appdelegate.dependencies.workspaceWindowManager, + dismissWindow: dismissWindow + ) + GitCloneButton( + windowManager: appdelegate.dependencies.workspaceWindowManager, + shellClient: appdelegate.dependencies.shellClient, + dismissWindow: dismissWindow + ) + OpenFileOrFolderButton( + windowManager: appdelegate.dependencies.workspaceWindowManager, + dismissWindow: dismissWindow + ) }, onDrop: { url, dismissWindow in let windowManager = appdelegate.dependencies.workspaceWindowManager @@ -76,10 +86,10 @@ struct CodeEditApp: App { SettingsWindow() .commands { - CodeEditCommands() + CodeEditCommands(dependencies: appdelegate.dependencies) } } .environment(\.settings, settings.preferences) // Add settings to each window environment - .environment(\.notificationManager, appdelegate.dependencies.notificationManager) + .appServices(appdelegate.dependencies) } } diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift index 59b9b005f0..abdfdafd49 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift @@ -8,7 +8,6 @@ import Foundation import Combine import CodeEditCore -import Factory /// Maintains the list of task notifications shown in the activity viewer. /// @@ -28,7 +27,7 @@ import Factory /// /// ## Example /// ```swift -/// @LazyInjected(\.eventBus) private var eventBus +/// let eventBus: EventBus // injected via the initializer /// /// eventBus.publish(TaskNotificationEvent( /// .create(TaskNotificationModel(id: UUID().uuidString, title: "Indexing")) @@ -39,12 +38,12 @@ final class TaskNotificationHandler: ObservableObject { var workspaceURL: URL? var cancellables: Set = [] - @LazyInjected(\.eventBus) - private var eventBus + private let eventBus: EventBus /// Initialises a new `TaskNotificationHandler` and starts observing for task notification events. - init(workspaceURL: URL? = nil) { + init(workspaceURL: URL? = nil, eventBus: EventBus) { self.workspaceURL = workspaceURL + self.eventBus = eventBus eventBus.subscribe(TaskNotificationEvent.self) .receive(on: DispatchQueue.main) diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift index 198b99d04b..473ede59cc 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift @@ -88,5 +88,5 @@ struct TaskNotificationView: View { } #Preview { - TaskNotificationView(taskNotificationHandler: TaskNotificationHandler()) + TaskNotificationView(taskNotificationHandler: TaskNotificationHandler(eventBus: EventBus())) } diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift index df417c0d25..bbc97de62e 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift @@ -5,6 +5,7 @@ // Created by Tommy Ludwig on 21.06.24. // +import CodeEditCore import SwiftUI struct TaskNotificationsDetailView: View { @@ -44,5 +45,5 @@ struct TaskNotificationsDetailView: View { } #Preview { - TaskNotificationsDetailView(taskNotificationHandler: TaskNotificationHandler()) + TaskNotificationsDetailView(taskNotificationHandler: TaskNotificationHandler(eventBus: EventBus())) } diff --git a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift index f2e12bdf88..6e010dc5fb 100644 --- a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift +++ b/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift @@ -6,15 +6,13 @@ // import SwiftUI -import Factory import CodeEditCore /// Simple state class for command palette view. Contains currently selected command, /// query text and list of filtered commands final class QuickActionsViewModel: ObservableObject { - @LazyInjected(\.commandManager) - private var commandManager + private let commandManager: CommandManaging @Published var commandQuery: String = "" @@ -24,7 +22,9 @@ final class QuickActionsViewModel: ObservableObject { @Published var filteredCommands: [Command] = [] - init() {} + init(commandManager: CommandManaging) { + self.commandManager = commandManager + } func reset() { commandQuery = "" diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index e39f5401a8..e86432f3b5 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -104,8 +104,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.activeEditorState, activeEditorState) - .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) - .environment(\.workspaceNavigator, dependencies.workspaceNavigator) + .appServices(dependencies) }) addSplitViewItem(navigator) @@ -127,8 +126,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceStatePersistence, workspace.statePersistence) .environment(\.activeEditorState, activeEditorState) .environment(\.activeCursorState, activeCursorState) - .environment(\.workspaceNavigator, dependencies.workspaceNavigator) - .environment(\.languageServices, dependencies.languageServicesProvider) + .appServices(dependencies) } } @@ -145,8 +143,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.activeEditorState, activeEditorState) .environment(\.fileEditorOverrides, fileEditorOverrides) - .environment(\.notificationManager, dependencies.notificationManager) - .environment(\.fileRelocator, dependencies.fileRelocator) + .appServices(dependencies) }) addSplitViewItem(inspector) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index 2832e0ec96..fe574785d0 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -186,7 +186,7 @@ extension CodeEditWindowController { let stop = StopTaskToolbarItem(workspace: workspace) else { return nil } - let start = StartTaskToolbarItem(workspace: workspace) + let start = StartTaskToolbarItem(workspace: workspace, commandManager: dependencies.commandManager) let group = NSToolbarItemGroup(itemIdentifier: .taskSidebarItem) group.isBordered = true diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/Features/Keybindings/CommandManager.swift index 2af2762600..1a81402ab8 100644 --- a/CodeEdit/Features/Keybindings/CommandManager.swift +++ b/CodeEdit/Features/Keybindings/CommandManager.swift @@ -7,8 +7,8 @@ import Foundation import CodeEditCore -/// Registry backing the command palette. Registered as a singleton in the Factory container -/// (`Container.shared.commandManager`); inject via `@LazyInjected(\.commandManager)`. +/// Registry backing the command palette. Owned by `AppDependencies`; objects receive it +/// through their initializer, views through the `\.commandManager` environment key. final class CommandManager: CommandManaging { private var commandsList: [String: Command] diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index 875d38cc09..e7ecc1ec27 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -6,8 +6,8 @@ // import SwiftUI +import ShellClient import CEWorkspaceFileManager -import Factory import CodeEditCore struct GitChangedFileLabel: View { @@ -43,7 +43,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: nil )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient(), eventBus: Container.shared.eventBus())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), eventBus: EventBus())) .environmentObject(Workspace()) GitChangedFileLabel(file: GitChangedFile( @@ -52,7 +52,7 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: "app2.jsx" )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: Container.shared.shellClient(), eventBus: Container.shared.eventBus())) + .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), eventBus: EventBus())) .environmentObject(Workspace()) }.padding() } diff --git a/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift b/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift index 0125c6395a..f22c21d19d 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift @@ -6,14 +6,13 @@ // import Foundation +import CodeEditCore import CodeEditSettings -import Factory extension SettingsData.TextEditingSettings { /// Registers toggle-able text-editing preferences with the command palette. /// Invoked once at app startup (previously ran as a side effect of decoding). - static func registerCommands() { - let mgr = Container.shared.commandManager() + static func registerCommands(in mgr: CommandManaging) { mgr.addCommand( name: "Toggle Type-Over Completion", diff --git a/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift b/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift index 6857e67167..eacbd66956 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift @@ -7,18 +7,18 @@ import Foundation import CodeEditSettings -import Factory +import CodeEditCore extension SettingsData { /// Merges bundled-default keybindings (from `default_keybindings.json`, owned by /// `KeybindingManager`) into the persisted settings, adding only keys the user does /// not already have. Preserves user overrides. Invoked once at app startup — /// previously ran as a side effect of decoding `KeybindingsSettings`. - static func reconcileDefaultKeybindings() { - let defaults = Container.shared.keybindingManager().keyboardShortcuts + static func reconcileDefaultKeybindings(keybindingManager: KeybindingManaging) { + let defaults = keybindingManager.keyboardShortcuts var current = Settings.shared.preferences.keybindings.keybindings for (key, _) in defaults where current[key] == nil { - current[key] = Container.shared.keybindingManager().named(with: key) + current[key] = keybindingManager.named(with: key) } Settings.shared.preferences.keybindings.keybindings = current } diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 6fa1d5f787..56d2fd6ba9 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -6,12 +6,11 @@ // import SwiftUI -import Factory import CodeEditCore /// Displays a searchable list of packages from the ``RegistryManager``. struct LanguageServersView: View { - @ObservedObject var registryManager: RegistryManager = Container.shared.registryManager() + @ObservedObject var registryManager: RegistryManager @StateObject private var searchModel = FuzzySearchUIModel() @State private var searchText: String = "" @State private var selectedInstall: PackageManagerInstallOperation? diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift index 7abf6c1554..b15ca7ff29 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift @@ -5,9 +5,10 @@ // Created by Austin Condiff on 11/1/24. // +import CodeEditCore import Foundation +import ShellClient import CodeEditSettings -import Factory /// A model to manage Git ignore patterns for a file, including loading, saving, and monitoring changes. @MainActor @@ -30,7 +31,7 @@ class IgnorePatternModel: ObservableObject { @Published var selection: Set = [] /// A client for interacting with the Git configuration. - private let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) + private let gitConfig: GitConfigClient /// A file system monitor for detecting changes to the Git ignore file. private var fileMonitor: DispatchSourceFileSystemObject? @@ -38,7 +39,8 @@ class IgnorePatternModel: ObservableObject { /// Task tracking the current save operation private var savingTask: Task? - init() { + init(shellClient: ShellClientProtocol = ShellClient()) { + self.gitConfig = GitConfigClient(shellClient: shellClient) Task { try? await startFileMonitor() await loadPatterns() diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index 336572aab5..433f7a1678 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -5,15 +5,18 @@ // Created by Raymond Vleeshouwer on 02/04/23. // +import CodeEditCore import SwiftUI +import ShellClient import CodeEditSettings -import Factory struct SourceControlGeneralView: View { @AppSettings(\.sourceControl.general) var settings - let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) + @Environment(\.shellClient) private var shellClient + + private var gitConfig: GitConfigClient { GitConfigClient(shellClient: shellClient ?? ShellClient()) } var body: some View { Group { diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index d6dac8c257..0c9ec98554 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -5,15 +5,19 @@ // Created by Raymond Vleeshouwer on 02/04/23. // +import CodeEditCore import SwiftUI +import ShellClient import CodeEditSettings -import Factory struct SourceControlGitView: View { @AppSettings(\.sourceControl.git) var git - let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) + @Environment(\.shellClient) private var shellClient + @Environment(\.workspaceWindowManager) private var windowManager + + private var gitConfig: GitConfigClient { GitConfigClient(shellClient: shellClient ?? ShellClient()) } @State private var authorName: String = "" @State private var authorEmail: String = "" @@ -204,8 +208,7 @@ private extension SourceControlGitView { FileManager.default.createFile(atPath: fileURL.path, contents: nil) } - let windowManager = Container.shared.workspaceWindowManager() - windowManager.openDocument(at: fileURL, onCompletion: {}) + windowManager?.openDocument(at: fileURL, onCompletion: {}) } private func openGitIgnoreFile() { @@ -219,8 +222,7 @@ private extension SourceControlGitView { } // Open the file in the editor - let windowManager = Container.shared.workspaceWindowManager() - windowManager.openDocument(at: fileURL, onCompletion: {}) + windowManager?.openDocument(at: fileURL, onCompletion: {}) } catch { print("Failed to open document: \(error.localizedDescription)") } diff --git a/CodeEdit/Features/Settings/SettingsView.swift b/CodeEdit/Features/Settings/SettingsView.swift index 712d5620f6..dd1d442267 100644 --- a/CodeEdit/Features/Settings/SettingsView.swift +++ b/CodeEdit/Features/Settings/SettingsView.swift @@ -10,6 +10,8 @@ import CodeEditSettings /// A struct for settings struct SettingsView: View { + @Environment(\.registryManager) private var registryManager + @StateObject var model = SettingsViewModel() @Environment(\.colorScheme) private var colorScheme @@ -200,7 +202,9 @@ struct SettingsView: View { case .location: LocationsSettingsView() case .languageServers: - LanguageServersView() + if let registryManager { + LanguageServersView(registryManager: registryManager) + } case .developer: DeveloperSettingsView() default: diff --git a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift b/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift index a98bfec591..c880d9b4d8 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift +++ b/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import CodeEditCore +import ShellClient struct GitCheckoutBranchView: View { @Environment(\.dismiss) @@ -18,9 +19,10 @@ struct GitCheckoutBranchView: View { init( repoLocalPath: URL, + shellClient: ShellClientProtocol, openDocument: @escaping (URL) -> Void ) { - _viewModel = .init(wrappedValue: GitCheckoutBranchViewModel(repoPath: repoLocalPath)) + _viewModel = .init(wrappedValue: GitCheckoutBranchViewModel(repoPath: repoLocalPath, shellClient: shellClient)) self.openDocument = openDocument } var body: some View { diff --git a/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift b/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift index 82ddf741c4..5875668cae 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift +++ b/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift @@ -5,24 +5,27 @@ // Created by Aleksi Puttonen on 23.3.2022. // +import CodeEditCore import SwiftUI import Foundation -import Factory import Combine +import ShellClient struct GitCloneView: View { @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel: GitCloneViewModel = .init() + @StateObject private var viewModel: GitCloneViewModel private let openBranchView: (URL) -> Void private let openDocument: (URL) -> Void init( + shellClient: ShellClientProtocol, openBranchView: @escaping (URL) -> Void, openDocument: @escaping (URL) -> Void ) { + _viewModel = .init(wrappedValue: GitCloneViewModel(shellClient: shellClient)) self.openBranchView = openBranchView self.openDocument = openDocument } @@ -100,7 +103,7 @@ struct GitCloneView: View { viewModel.cloneRepository { localPath in dismiss() - let gitClient = GitClient(directoryURL: localPath, shellClient: Container.shared.shellClient()) + let gitClient = GitClient(directoryURL: localPath, shellClient: viewModel.shellClient) Task { let branches = ((try? await gitClient.getBranches()) ?? []) diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift index e1ea205e74..af69013bc4 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift @@ -6,8 +6,8 @@ // import Foundation -import Factory import CodeEditCore +import ShellClient class GitCheckoutBranchViewModel: ObservableObject { @Published var selectedBranch: GitBranch? @@ -16,9 +16,9 @@ class GitCheckoutBranchViewModel: ObservableObject { let repoPath: URL private let gitClient: GitClient - init(repoPath: URL) { + init(repoPath: URL, shellClient: ShellClientProtocol) { self.repoPath = repoPath - gitClient = .init(directoryURL: repoPath, shellClient: Container.shared.shellClient()) + gitClient = .init(directoryURL: repoPath, shellClient: shellClient) } func loadBranches() async { diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift index 7809cc4037..96e39502ab 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift +++ b/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift @@ -5,7 +5,9 @@ // Created by Albert Vinizhanau on 10/17/23. // +import CodeEditCore import Foundation +import ShellClient import AppKit class GitCloneViewModel: ObservableObject { @@ -15,7 +17,13 @@ class GitCloneViewModel: ObservableObject { var cloningTask: Task? - private let useCase = CloneRepositoryUseCase() + let shellClient: ShellClientProtocol + private let useCase: CloneRepositoryUseCase + + init(shellClient: ShellClientProtocol) { + self.shellClient = shellClient + self.useCase = CloneRepositoryUseCase(shellClient: shellClient) + } /// Check if url is valid /// - Parameter url: Url to check diff --git a/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift b/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift index 30f1937278..379bb93d74 100644 --- a/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift +++ b/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift @@ -5,11 +5,18 @@ // Created by Matthijs Eikelenboom on 15/04/26. // +import CodeEditCore import Foundation -import Factory +import ShellClient /// Validates and orchestrates a `git clone` operation, streaming progress to the caller. final class CloneRepositoryUseCase { + private let shellClient: ShellClientProtocol + + init(shellClient: ShellClientProtocol) { + self.shellClient = shellClient + } + enum Failure: Error, LocalizedError { case gitNotInstalled @@ -86,7 +93,7 @@ final class CloneRepositoryUseCase { throw Failure.directoryCreationFailed(error) } - let gitClient = GitClient(directoryURL: localPath, shellClient: Container.shared.shellClient()) + let gitClient = GitClient(directoryURL: localPath, shellClient: shellClient) return gitClient.cloneRepository(remoteUrl: remoteUrl, localPath: localPath) } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift index 95791f54bc..094a0027c5 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift @@ -5,8 +5,9 @@ // Created by Austin Condiff on 6/28/24. // +import CodeEditCore import SwiftUI -import Factory +import ShellClient struct SourceControlPullView: View { @Environment(\.dismiss) @@ -15,7 +16,9 @@ struct SourceControlPullView: View { @EnvironmentObject var sourceControlManager: SourceControlManager @EnvironmentObject var sourceControlViewModel: SourceControlViewModel - let gitConfig = GitConfigClient(shellClient: Container.shared.shellClient()) + @Environment(\.shellClient) private var shellClient + + private var gitConfig: GitConfigClient { GitConfigClient(shellClient: shellClient ?? ShellClient()) } @State var loading: Bool = false diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift index 08252569bb..3e100d33a8 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift @@ -7,12 +7,14 @@ import SwiftUI import CodeEditUI -import Factory internal struct StatusBarToggleUtilityAreaButton: View { @Environment(\.controlActiveState) var controlActiveState + @Environment(\.commandManager) + private var commandManager + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel internal var body: some View { @@ -27,7 +29,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { .onHover { isHovering($0) } .onChange(of: controlActiveState) { _, newValue in if newValue == .key { - Container.shared.commandManager().addCommand( + commandManager?.addCommand( name: "Toggle Utility Area", title: "Toggle Utility Area", id: "open.drawer", @@ -36,7 +38,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { } } .onAppear { - Container.shared.commandManager().addCommand( + commandManager?.addCommand( name: "Toggle Utility Area", title: "Toggle Utility Area", id: "open.drawer", diff --git a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift b/CodeEdit/Features/Tasks/Models/CEActiveTask.swift index 825176cacc..0604f59231 100644 --- a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift +++ b/CodeEdit/Features/Tasks/Models/CEActiveTask.swift @@ -9,7 +9,6 @@ import SwiftUI import Combine import SwiftTerm import CodeEditCore -import Factory /// Stores the state of a task once it's executed class CEActiveTask: ObservableObject, Identifiable, Hashable { @@ -35,11 +34,11 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { var workspaceURL: URL? - @LazyInjected(\.eventBus) - private var eventBus + private let eventBus: EventBus - init(task: CETask) { + init(task: CETask, eventBus: EventBus) { self.task = task + self.eventBus = eventBus } @MainActor diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/CodeEdit/Features/Tasks/TaskManager.swift index 3369703560..e2233902d9 100644 --- a/CodeEdit/Features/Tasks/TaskManager.swift +++ b/CodeEdit/Features/Tasks/TaskManager.swift @@ -21,7 +21,10 @@ class TaskManager: ObservableObject { private var workspaceURL: URL? private var settingsListener: AnyCancellable? - init(settingsStore: CEWorkspaceSettings, workspaceURL: URL?) { + private let eventBus: EventBus + + init(settingsStore: CEWorkspaceSettings, workspaceURL: URL?, eventBus: EventBus) { + self.eventBus = eventBus self.workspaceURL = workspaceURL self.settingsStore = settingsStore @@ -82,7 +85,7 @@ class TaskManager: ObservableObject { } activeTask.run(workspaceURL: workspaceURL) } else { - let runningTask = CEActiveTask(task: task) + let runningTask = CEActiveTask(task: task, eventBus: eventBus) runningTask.run(workspaceURL: workspaceURL) await MainActor.run { activeTasks[task.id] = runningTask diff --git a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift b/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift index 6d532ff3fc..c1a0f2c8cd 100644 --- a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift +++ b/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift @@ -6,18 +6,19 @@ // import AppKit -import Factory @available(macOS 26, *) final class StartTaskToolbarItem: NSToolbarItem { private weak var workspace: Workspace? + private let commandManager: CommandManaging private var utilityAreaCollapsed: Bool { workspace?.utilityAreaModel?.isCollapsed ?? true } - init(workspace: Workspace) { + init(workspace: Workspace, commandManager: CommandManaging) { self.workspace = workspace + self.commandManager = commandManager super.init(itemIdentifier: NSToolbarItem.Identifier("StartTaskToolbarItem")) image = NSImage(systemSymbolName: "play.fill", accessibilityDescription: nil) @@ -37,7 +38,7 @@ final class StartTaskToolbarItem: NSToolbarItem { taskManager.executeActiveTask() if utilityAreaCollapsed { - Container.shared.commandManager().executeCommand("open.drawer") + commandManager.executeCommand("open.drawer") } workspace?.utilityAreaModel?.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID diff --git a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift index 41ab0a2f91..ace008a717 100644 --- a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift +++ b/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift @@ -6,12 +6,14 @@ // import SwiftUI -import Factory struct StartTaskToolbarButton: View { @Environment(\.controlActiveState) private var activeState + @Environment(\.commandManager) + private var commandManager + @ObservedObject var taskManager: TaskManager @EnvironmentObject var utilityAreaModel: UtilityAreaViewModel @@ -23,7 +25,7 @@ struct StartTaskToolbarButton: View { Button { taskManager.executeActiveTask() if utilityAreaCollapsed { - Container.shared.commandManager().executeCommand("open.drawer") + commandManager?.executeCommand("open.drawer") } utilityAreaModel.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index ed6d16514b..01b93f1a4c 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -6,8 +6,8 @@ // import SwiftUI +import Combine import CodeEditSettings -import Factory struct UtilityAreaOutputSourcePicker: View { typealias Sources = UtilityAreaOutputView.Sources @@ -22,7 +22,7 @@ struct UtilityAreaOutputSourcePicker: View { @ObservedObject var extensionManager = ExtensionManager.shared - @Injected(\.lspService) var lspService + @Environment(\.lspService) var lspService @State private var updater: UUID = UUID() @State private var languageServerClients: [LSPService.LanguageServerType] = [] @@ -66,9 +66,11 @@ struct UtilityAreaOutputSourcePicker: View { .labelsHidden() .controlSize(.small) .onAppear { - updateLanguageServers(lspService.languageClients) + updateLanguageServers(lspService?.languageClients ?? [:]) } - .onReceive(lspService.$languageClients) { clients in + .onReceive( + lspService?.$languageClients.eraseToAnyPublisher() ?? Just([:]).eraseToAnyPublisher() + ) { clients in updateLanguageServers(clients) } .onReceive(extensionManager.$extensions) { _ in diff --git a/CodeEdit/Features/Welcome/GitCloneButton.swift b/CodeEdit/Features/Welcome/GitCloneButton.swift index 721ab9fe97..06963483e1 100644 --- a/CodeEdit/Features/Welcome/GitCloneButton.swift +++ b/CodeEdit/Features/Welcome/GitCloneButton.swift @@ -5,8 +5,9 @@ // Created by Giorgi Tchelidze on 07.06.25. // +import CodeEditCore import SwiftUI -import Factory +import ShellClient import WelcomeWindow struct GitCloneButton: View { @@ -14,6 +15,9 @@ struct GitCloneButton: View { @State private var showGitClone = false @State private var showCheckoutBranchItem: URL? + let windowManager: WorkspaceWindowManager + let shellClient: ShellClientProtocol + var dismissWindow: () -> Void var body: some View { @@ -26,11 +30,11 @@ struct GitCloneButton: View { ) .sheet(isPresented: $showGitClone) { GitCloneView( + shellClient: shellClient, openBranchView: { url in showCheckoutBranchItem = url }, openDocument: { url in - let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: url, onCompletion: { dismissWindow() }) } ) @@ -38,8 +42,8 @@ struct GitCloneButton: View { .sheet(item: $showCheckoutBranchItem) { url in GitCheckoutBranchView( repoLocalPath: url, + shellClient: shellClient, openDocument: { url in - let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocument(at: url, onCompletion: { dismissWindow() }) } ) diff --git a/CodeEdit/Features/Welcome/NewFileButton.swift b/CodeEdit/Features/Welcome/NewFileButton.swift index a5cd1dfe0e..b9aa183e13 100644 --- a/CodeEdit/Features/Welcome/NewFileButton.swift +++ b/CodeEdit/Features/Welcome/NewFileButton.swift @@ -6,11 +6,12 @@ // import SwiftUI -import Factory import WelcomeWindow struct NewFileButton: View { + let windowManager: WorkspaceWindowManager + var dismissWindow: () -> Void var body: some View { @@ -18,7 +19,6 @@ struct NewFileButton: View { iconName: "plus.square", title: "Create New File...", action: { - let windowManager = Container.shared.workspaceWindowManager() windowManager.newDocumentFromPanel() dismissWindow() } diff --git a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift index 9537328b96..f0da10648a 100644 --- a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift +++ b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Factory import WelcomeWindow struct OpenFileOrFolderButton: View { @@ -14,6 +13,8 @@ struct OpenFileOrFolderButton: View { @Environment(\.openWindow) private var openWindow + let windowManager: WorkspaceWindowManager + var dismissWindow: () -> Void var body: some View { @@ -21,7 +22,6 @@ struct OpenFileOrFolderButton: View { iconName: "folder", title: "Open File or Folder...", action: { - let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocumentWithDialog( canChooseFiles: true, canChooseDirectories: true, diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift index e1107077ce..fad981919b 100644 --- a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift +++ b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift @@ -9,13 +9,15 @@ import SwiftUI import CodeEditSettings struct CodeEditCommands: Commands { + let dependencies: AppDependencies + @AppSettings(\.sourceControl.general.sourceControlIsEnabled) private var sourceControlIsEnabled var body: some Commands { Group { // SwiftUI limits to 9 items in an initializer, so we have to group every 9 items. MainCommands() - FileCommands() + FileCommands(windowManager: dependencies.workspaceWindowManager) ViewCommands() FindCommands() NavigateCommands() diff --git a/CodeEdit/Features/WindowCommands/FileCommands.swift b/CodeEdit/Features/WindowCommands/FileCommands.swift index b1b923d95e..665243220e 100644 --- a/CodeEdit/Features/WindowCommands/FileCommands.swift +++ b/CodeEdit/Features/WindowCommands/FileCommands.swift @@ -6,11 +6,12 @@ // import SwiftUI -import Factory struct FileCommands: Commands { static let recentProjectsMenu = RecentProjectsMenu() + let windowManager: WorkspaceWindowManager + @Environment(\.openWindow) private var openWindow @@ -22,13 +23,11 @@ struct FileCommands: Commands { CommandGroup(replacing: .newItem) { Group { Button("New") { - let windowManager = Container.shared.workspaceWindowManager() windowManager.newDocumentFromPanel() } .keyboardShortcut("n") Button("Open...") { - let windowManager = Container.shared.workspaceWindowManager() windowManager.openDocumentFromPanel() } .keyboardShortcut("o") diff --git a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift index 5cc0cc1d76..d9623779c0 100644 --- a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift +++ b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift @@ -6,7 +6,6 @@ // import AppKit -import Factory import WelcomeWindow @MainActor @@ -127,7 +126,11 @@ final class RecentProjectsMenu: NSObject, NSMenuDelegate { @objc private func recentProjectItemClicked(_ sender: NSMenuItem) { guard let projectURL = sender.representedObject as? URL else { return } - let windowManager = Container.shared.workspaceWindowManager() + // This menu is installed via a hidden AppKit API (see CommandsFixes.swift), outside any + // injectable construction flow, so it reaches the composition root through the app delegate. + guard let windowManager = (NSApp.delegate as? AppDelegate)?.dependencies.workspaceWindowManager else { + return + } windowManager.openDocument(at: projectURL, onCompletion: {}) } diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 23d599a4c1..9165714e22 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -7,7 +7,6 @@ import SwiftUI import CodeEditSettings -import Factory import Combine struct ViewCommands: Commands { @@ -136,7 +135,7 @@ extension ViewCommands { .keyboardShortcut("i", modifiers: [.control, .command]) Button("\(utilityAreaCollapsed ? "Show" : "Hide") Utility Area") { - Container.shared.commandManager().executeCommand("open.drawer") + windowController?.dependencies.commandManager.executeCommand("open.drawer") } .disabled(windowController == nil) .keyboardShortcut("y", modifiers: [.shift, .command]) diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 4a3a4a05c3..14cd31fef3 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -7,6 +7,10 @@ import SwiftUI import CodeEditCore +import Editor +import Notifications +import Search +import ShellClient private struct FileRelocatorKey: EnvironmentKey { static let defaultValue: FileRelocator = NoOpFileRelocator() @@ -20,3 +24,103 @@ extension EnvironmentValues { set { self[FileRelocatorKey.self] = newValue } } } + +private struct CommandManagerKey: EnvironmentKey { + static let defaultValue: CommandManaging? = nil +} + +private struct ShellClientKey: EnvironmentKey { + static let defaultValue: ShellClientProtocol? = nil +} + +private struct LSPServiceKey: EnvironmentKey { + static let defaultValue: LSPService? = nil +} + +private struct RegistryManagerKey: EnvironmentKey { + static let defaultValue: RegistryManager? = nil +} + +private struct WorkspaceWindowManagerKey: EnvironmentKey { + static let defaultValue: WorkspaceWindowManager? = nil +} + +private struct EventBusKey: EnvironmentKey { + /// A fresh, isolated bus: previews and tests publish into the void, which is legitimate. + static let defaultValue: EventBus = EventBus() +} + +extension EnvironmentValues { + /// The command palette registry. Optional: command UI no-ops in previews. Injected by the app shell. + var commandManager: CommandManaging? { + get { self[CommandManagerKey.self] } + set { self[CommandManagerKey.self] = newValue } + } + + /// The shell client for git and other subprocess work. Optional: git UI without a shell + /// only occurs in previews. Injected by the app shell. + var shellClient: ShellClientProtocol? { + get { self[ShellClientKey.self] } + set { self[ShellClientKey.self] = newValue } + } + + /// The LSP service. Optional: language-server UI is empty in previews. Injected by the app shell. + var lspService: LSPService? { + get { self[LSPServiceKey.self] } + set { self[LSPServiceKey.self] = newValue } + } + + /// The language-server registry. Optional: registry UI is empty in previews. Injected by the app shell. + var registryManager: RegistryManager? { + get { self[RegistryManagerKey.self] } + set { self[RegistryManagerKey.self] = newValue } + } + + /// The workspace window manager, for flows that open arbitrary files or workspaces + /// (e.g. Settings pages opening ~/.gitconfig). Optional: nil in previews. + var workspaceWindowManager: WorkspaceWindowManager? { + get { self[WorkspaceWindowManagerKey.self] } + set { self[WorkspaceWindowManagerKey.self] = newValue } + } + + /// The app-wide event bus. Defaults to an isolated instance so previews publish harmlessly. + var eventBus: EventBus { + get { self[EventBusKey.self] } + set { self[EventBusKey.self] = newValue } + } +} + +extension View { + /// Injects the app-scope services into this view subtree. Applied at every SwiftUI root: + /// the app's scenes and each workspace window's split-view content. + func appServices(_ dependencies: AppDependencies) -> some View { + environment(\.commandManager, dependencies.commandManager) + .environment(\.shellClient, dependencies.shellClient) + .environment(\.lspService, dependencies.lspService) + .environment(\.registryManager, dependencies.registryManager) + .environment(\.eventBus, dependencies.eventBus) + .environment(\.workspaceWindowManager, dependencies.workspaceWindowManager) + .environment(\.notificationManager, dependencies.notificationManager) + .environment(\.fileRelocator, dependencies.fileRelocator) + .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) + .environment(\.workspaceNavigator, dependencies.workspaceNavigator) + .environment(\.languageServices, dependencies.languageServicesProvider) + } +} + +extension Scene { + /// Scene-level counterpart of `View.appServices(_:)` for the app's scene group. + func appServices(_ dependencies: AppDependencies) -> some Scene { + environment(\.commandManager, dependencies.commandManager) + .environment(\.shellClient, dependencies.shellClient) + .environment(\.lspService, dependencies.lspService) + .environment(\.registryManager, dependencies.registryManager) + .environment(\.eventBus, dependencies.eventBus) + .environment(\.workspaceWindowManager, dependencies.workspaceWindowManager) + .environment(\.notificationManager, dependencies.notificationManager) + .environment(\.fileRelocator, dependencies.fileRelocator) + .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) + .environment(\.workspaceNavigator, dependencies.workspaceNavigator) + .environment(\.languageServices, dependencies.languageServicesProvider) + } +} diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 6482036635..ea15c364b4 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -36,7 +36,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { var taskManager: TaskManager? var workspaceSettingsManager: CEWorkspaceSettings? - var taskNotificationHandler: TaskNotificationHandler = TaskNotificationHandler() + var taskNotificationHandler: TaskNotificationHandler var statePersistence: WorkspaceStatePersistence? @@ -52,6 +52,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { // MARK: - Initialization init(url: URL, dependencies: AppDependencies) { + self.taskNotificationHandler = TaskNotificationHandler(eventBus: dependencies.eventBus) self.notificationPanel = NotificationPanelViewModel( notificationManager: dependencies.notificationManager, eventBus: dependencies.eventBus @@ -62,6 +63,7 @@ final class Workspace: ObservableObject, WorkspaceManaging { /// Minimal initializer for testing. Does not set up workspace state. internal init() { let eventBus = EventBus() + self.taskNotificationHandler = TaskNotificationHandler(eventBus: eventBus) self.notificationPanel = NotificationPanelViewModel( notificationManager: NotificationManager(eventBus: eventBus), eventBus: eventBus diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 944e80ca08..f5256f9b7c 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -73,12 +73,13 @@ enum WorkspaceFactory { // --- Phase 2: Independent managers --- workspace.searchState = SearchState(workspaceURL: url, eventBus: eventBus) workspace.openQuicklyViewModel = OpenQuicklyViewModel(fileURL: url) - workspace.commandsPaletteState = QuickActionsViewModel() + workspace.commandsPaletteState = QuickActionsViewModel(commandManager: dependencies.commandManager) workspace.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) if let workspaceSettingsManager = workspace.workspaceSettingsManager { workspace.taskManager = TaskManager( settingsStore: workspaceSettingsManager, - workspaceURL: url + workspaceURL: url, + eventBus: eventBus ) } workspace.taskNotificationHandler.workspaceURL = url diff --git a/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift b/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift index 7c2f1309a9..24e7a775bc 100644 --- a/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift +++ b/CodeEditTests/Features/ActivityViewer/TaskNotificationHandlerTests.swift @@ -7,7 +7,6 @@ import XCTest import CodeEditCore -import Factory @testable import CodeEdit final class TaskNotificationHandlerTests: XCTestCase { @@ -16,8 +15,8 @@ final class TaskNotificationHandlerTests: XCTestCase { override func setUp() { super.setUp() - eventBus = Container.shared.eventBus() - taskNotificationHandler = TaskNotificationHandler() + eventBus = EventBus() + taskNotificationHandler = TaskNotificationHandler(eventBus: eventBus) } override func tearDown() { diff --git a/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift b/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift index 3b23dfdd8f..d26d9fd140 100644 --- a/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift +++ b/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift @@ -7,7 +7,6 @@ import XCTest import CodeEditCore -import Factory @testable import CodeEdit private final class MockCommandManager: CommandManaging { @@ -31,13 +30,11 @@ final class QuickActionsViewModelTests: XCTestCase { override func setUp() { super.setUp() - Container.shared.commandManager.register { MockCommandManager() } - viewModel = QuickActionsViewModel() + viewModel = QuickActionsViewModel(commandManager: MockCommandManager()) } override func tearDown() { viewModel = nil - Container.shared.commandManager.reset() super.tearDown() } diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index e337267e80..19b8643125 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -6,8 +6,8 @@ // import XCTest -import Factory import CodeEditCore +import ShellClient import Search @testable import CodeEdit @@ -28,12 +28,13 @@ final class DocumentsUnitTests: XCTestCase { navigatorViewModel = .init() workspace.taskManager = TaskManager( settingsStore: CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())), - workspaceURL: nil + workspaceURL: nil, + eventBus: EventBus() ) workspace.sourceControlManager = SourceControlManager( workspaceURL: URL(filePath: "/tmp"), - shellClient: Container.shared.shellClient(), - eventBus: Container.shared.eventBus() + shellClient: ShellClient(), + eventBus: EventBus() ) workspace.sourceControlViewModel = SourceControlViewModel() workspace.searchState = SearchState(workspaceURL: URL(filePath: "/tmp"), eventBus: EventBus()) diff --git a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift index 40b430e591..4f787510b1 100644 --- a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift +++ b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift @@ -21,7 +21,7 @@ class CEActiveTaskTests { command: "echo $STATE", environmentVariables: [CETask.EnvironmentVariable(key: "STATE", value: "Testing")] ) - activeTask = CEActiveTask(task: task) + activeTask = CEActiveTask(task: task, eventBus: EventBus()) } @Test @@ -53,7 +53,7 @@ class CEActiveTaskTests { func testHandleProcessFinished(_ shell: Shell) async throws { // CETask is a value type, so build a fresh active task around the failing command // rather than mutating `task` after `activeTask` already copied it. - let activeTask = CEActiveTask(task: CETask(name: "Test Task", command: "aNon-existentCommand")) + let activeTask = CEActiveTask(task: CETask(name: "Test Task", command: "aNon-existentCommand"), eventBus: EventBus()) activeTask.run(workspaceURL: nil, shell: shell) activeTask.waitForExit() diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index 729fd08cc7..dd38586f8f 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -20,7 +20,7 @@ class TaskManagerTests { init() throws { settingsStore = CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())) settingsStore.settings = try JSONDecoder().decode(CEWorkspaceSettingsData.self, from: Data("{}".utf8)) - taskManager = TaskManager(settingsStore: settingsStore, workspaceURL: nil) + taskManager = TaskManager(settingsStore: settingsStore, workspaceURL: nil, eventBus: EventBus()) } func testInitialization() { From 438685c87b2fd8a29279b8e940e2421bfa0ae66c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 10 Jul 2026 21:33:44 +0200 Subject: [PATCH 131/335] Refactor: Delete Factory container; AppDependencies constructs the full graph --- CodeEdit.xcodeproj/project.pbxproj | 25 -------- .../xcshareddata/swiftpm/Package.resolved | 11 +--- CodeEdit/AppDelegate.swift | 6 +- CodeEdit/AppDependencies.swift | 58 ++++++++++++------- CodeEdit/CodeEditApp.swift | 1 - CodeEdit/CodeEditContainer.swift | 49 ---------------- .../Foundation/CodeEditCore/Package.swift | 7 +-- .../Infrastructure/CoreContainer.swift | 29 ---------- 8 files changed, 41 insertions(+), 145 deletions(-) delete mode 100644 CodeEdit/CodeEditContainer.swift delete mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 7ae9da447f..3c046b3938 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -19,8 +19,6 @@ 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* Notifications */; }; - 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F392F86D64F009F4AA7 /* Factory */; }; - 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 58CF9F412F86D981009F4AA7 /* FactoryTesting */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58ED10022FFB0001004BE116 /* Editor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* Editor */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; @@ -209,7 +207,6 @@ 6C73A6D32D4F1E550012D95C /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, - 58CF9F3A2F86D64F009F4AA7 /* Factory in Frameworks */, 5EACE6222DF4BF08005E08B8 /* WelcomeWindow in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, @@ -224,7 +221,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 58CF9F422F86D981009F4AA7 /* FactoryTesting in Frameworks */, 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -357,7 +353,6 @@ 6C76D6D32E15B91E00EF52C3 /* CodeEditSourceEditor */, 6CCF6DD22E26D48F00B94F75 /* SwiftTerm */, 6CCF73CF2E26DE3200B94F75 /* SwiftTerm */, - 58CF9F392F86D64F009F4AA7 /* Factory */, 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */, 5800E2F72FF843390085ECF1 /* CodeEditUI */, 5AD0C0DE2D00000000000002 /* CodeEditDocument */, @@ -390,7 +385,6 @@ name = CodeEditTests; packageProductDependencies = ( 583E529B29361BAB001AB554 /* SnapshotTesting */, - 58CF9F412F86D981009F4AA7 /* FactoryTesting */, ); productName = CodeEditTests; productReference = B658FB3D27DA9E1000EA4DBD /* CodeEditTests.xctest */; @@ -473,7 +467,6 @@ 5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */, 6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6CCF73CE2E26DE3200B94F75 /* XCRemoteSwiftPackageReference "SwiftTerm" */, - 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */, ); preferredProjectObjectVersion = 55; productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; @@ -1773,14 +1766,6 @@ minimumVersion = 1.14.2; }; }; - 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/hmlongco/Factory"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.5.3; - }; - }; 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; @@ -1929,16 +1914,6 @@ isa = XCSwiftPackageProductDependency; productName = Notifications; }; - 58CF9F392F86D64F009F4AA7 /* Factory */ = { - isa = XCSwiftPackageProductDependency; - package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; - productName = Factory; - }; - 58CF9F412F86D981009F4AA7 /* FactoryTesting */ = { - isa = XCSwiftPackageProductDependency; - package = 58CF9F382F86D64F009F4AA7 /* XCRemoteSwiftPackageReference "Factory" */; - productName = FactoryTesting; - }; 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */ = { isa = XCSwiftPackageProductDependency; productName = CodeEditCore; diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved index 63b20a4ac0..beda0fa474 100644 --- a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c4368adf5cf7353593e131deab35e74464b98a4f7755aea8cdd711a421976b22", + "originHash" : "284326793962ba3e88f2e400e6ddd71d0f84fc0dffae3cc190272c17b6b8d095", "pins" : [ { "identity" : "aboutwindow", @@ -82,15 +82,6 @@ "version" : "0.4.2" } }, - { - "identity" : "factory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hmlongco/Factory", - "state" : { - "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", - "version" : "2.5.3" - } - }, { "identity" : "fseventswrapper", "kind" : "remoteSourceControl", diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 854a9a7684..3fd37e3908 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -22,11 +22,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @Environment(\.openWindow) var openWindow - let dependencies: AppDependencies = { - let dependencies = AppDependencies() - AppDependencies.bridgeShared = dependencies - return dependencies - }() + let dependencies = AppDependencies() var lspService: LSPService { dependencies.lspService } var windowManager: WorkspaceWindowManager { dependencies.workspaceWindowManager } diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index b10d307154..9353b37680 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -7,35 +7,51 @@ import CodeEditCore import CodeEditDocument -import Factory import Notifications import ShellClient -/// The app-scope composition root. Owns every process-lifetime service. +/// The app-scope composition root. Owns every process-lifetime service and is the only +/// place where interfaces are bound to implementations. /// -/// Transitional note: during the strangler-bridge migration, properties are -/// initialized FROM `Container.shared` so both worlds share instances. The -/// final migration task replaces these reads with direct construction and -/// deletes the container. +/// Ownership: `AppDelegate` creates the single instance; `CodeEditApp` reads it through the +/// delegate adaptor. Only composition roots (`AppDelegate`, `CodeEditApp`, +/// `WorkspaceWindowManager` and its use cases, `WorkspaceFactory`, the window controllers) +/// may hold this whole object — everything else declares the specific dependencies it needs, +/// via initializer parameters (objects) or environment keys (views, see `appServices(_:)`). /// -/// Properties are `lazy` to preserve the resolution timing of the -/// `@LazyInjected` sites they replace (e.g. `RegistryManager` performs I/O on -/// first touch) and to allow adapters to reference sibling properties. +/// Properties are `lazy` to preserve on-first-use construction timing (`RegistryManager` +/// performs I/O when created) and to let `workspaceWindowManager`-adjacent adapters +/// reference sibling properties without initialization-order cycles. @MainActor final class AppDependencies { - /// Bridge-phase backdoor so the container registration can construct the - /// window manager without creating a second dependency graph. AppDelegate - /// assigns this before anything resolves the key. Deleted with the container. - nonisolated(unsafe) static var bridgeShared: AppDependencies! - - private(set) lazy var eventBus: EventBus = Container.shared.eventBus() - private(set) lazy var shellClient: ShellClientProtocol = Container.shared.shellClient() - private(set) lazy var commandManager: CommandManaging = Container.shared.commandManager() - private(set) lazy var keybindingManager: KeybindingManaging = Container.shared.keybindingManager() + private(set) lazy var eventBus = EventBus() + + private(set) lazy var shellClient: ShellClientProtocol = ShellClient() + + private(set) lazy var commandManager: CommandManaging = CommandManager() + + private(set) lazy var keybindingManager: KeybindingManaging = KeybindingManager() + private(set) lazy var notificationManager: NotificationManaging = NotificationManager(eventBus: eventBus) - private(set) lazy var lspService: LSPService = Container.shared.lspService() - private(set) lazy var registryManager: RegistryManager = Container.shared.registryManager() - private(set) lazy var workspaceWindowManager: WorkspaceWindowManager = Container.shared.workspaceWindowManager() + + private(set) lazy var lspService: LSPService = { + let service = LSPService(notificationManager: notificationManager) + // Property-injected (not init-injected): the window manager's construction consumes + // `lspService`, so init injection in both directions would recurse. Resolved at call + // time, long after both objects exist. + service.workspaceFinder = { [weak self] url in + self?.workspaceWindowManager.workspace(containing: url) + } + return service + }() + + private(set) lazy var registryManager = RegistryManager( + eventBus: eventBus, + notificationManager: notificationManager, + shellClient: shellClient + ) + + private(set) lazy var workspaceWindowManager = WorkspaceWindowManager(dependencies: self) // MARK: - Command-interface adapters (stateless routers over the window manager) diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index da12f23c74..31c886d183 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -9,7 +9,6 @@ import SwiftUI import CodeEditSettings import CodeEditDocument import CodeEditCore -import Factory import WelcomeWindow import AboutWindow diff --git a/CodeEdit/CodeEditContainer.swift b/CodeEdit/CodeEditContainer.swift deleted file mode 100644 index 45706a72eb..0000000000 --- a/CodeEdit/CodeEditContainer.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CodeEditContainer.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 08.04.26. -// - -import CodeEditCore -import CodeEditDocument -import ShellClient -import Factory - -extension Container { - var lspService: Factory { - self { @MainActor in - let service = LSPService(notificationManager: AppDependencies.bridgeShared.notificationManager) - service.workspaceFinder = { url in - AppDependencies.bridgeShared.workspaceWindowManager.workspace(containing: url) - } - return service - }.singleton - } - - var workspaceWindowManager: Factory { - self { @MainActor in WorkspaceWindowManager(dependencies: AppDependencies.bridgeShared) }.singleton - } - - var shellClient: Factory { - self { ShellClient() as ShellClientProtocol }.singleton - } - - var commandManager: Factory { - self { CommandManager() as CommandManaging }.singleton - } - - var keybindingManager: Factory { - self { KeybindingManager() as KeybindingManaging }.singleton - } - - var registryManager: Factory { - self { @MainActor in - RegistryManager( - eventBus: AppDependencies.bridgeShared.eventBus, - notificationManager: AppDependencies.bridgeShared.notificationManager, - shellClient: AppDependencies.bridgeShared.shellClient - ) - }.singleton - } -} diff --git a/Packages/Foundation/CodeEditCore/Package.swift b/Packages/Foundation/CodeEditCore/Package.swift index 7d2ded171d..28735ccec3 100644 --- a/Packages/Foundation/CodeEditCore/Package.swift +++ b/Packages/Foundation/CodeEditCore/Package.swift @@ -8,14 +8,11 @@ let package = Package( products: [ .library(name: "CodeEditCore", targets: ["CodeEditCore"]) ], - dependencies: [ - // Pin matches the app's Package.resolved. - .package(url: "https://github.com/hmlongco/Factory", exact: "2.5.3") - ], + dependencies: [], targets: [ .target( name: "CodeEditCore", - dependencies: [.product(name: "Factory", package: "Factory")] + dependencies: [] ) ] ) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift deleted file mode 100644 index b80a89d1a8..0000000000 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/CoreContainer.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CoreContainer.swift -// CodeEditCore -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -import Factory - -/// Factory keys for Core-owned cross-cutting dependencies. -/// Keys live beside the types they vend; the app shell registers real -/// implementations where a default is not sufficient. -extension Container { - public var eventBus: Factory { - self { EventBus() }.singleton - } - - public var workspaceFileOpener: Factory { - self { NoOpWorkspaceFileOpener() }.singleton - } - - public var workspaceNavigator: Factory { - self { NoOpWorkspaceNavigator() }.singleton - } - - public var fileRelocator: Factory { - self { NoOpFileRelocator() }.singleton - } -} From 884b36bbb292dc78537d021b310c6f943960be6c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 12:33:03 +0200 Subject: [PATCH 132/335] Refactor: Rename Search feature package to CESearch --- CodeEdit.xcodeproj/project.pbxproj | 10 ++++---- CodeEdit.xcworkspace/contents.xcworkspacedata | 2 +- .../Protocols/WorkspaceManaging.swift | 2 +- .../FindNavigator/FindNavigatorTab.swift | 2 +- .../Models/Environment+AppCommands.swift | 2 +- .../Features/Workspace/Models/Workspace.swift | 2 +- .../Features/Workspace/WorkspaceFactory.swift | 2 +- .../Documents/DocumentsUnitTests.swift | 2 +- .../Indexer/AsyncIndexingTests.swift | 2 +- .../Indexer/MemoryIndexingTests.swift | 2 +- .../Documents/Indexer/MemorySearchTests.swift | 2 +- ...ment+SearchState+FindAndReplaceTests.swift | 2 +- ...nt+SearchState+FindReplaceQueryTests.swift | 2 +- ...kspaceDocument+SearchState+FindTests.swift | 2 +- ...spaceDocument+SearchState+IndexTests.swift | 2 +- Packages/Features/CESearch/Package.resolved | 15 ++++++++++++ .../{Search => CESearch}/Package.swift | 6 ++--- .../Environment+WorkspaceFileOpener.swift | 0 .../CESearch}/Extensions/Array+Index.swift | 0 .../FindNavigator/FindModePicker.swift | 0 .../FindNavigatorConfiguration.swift | 0 .../FindNavigator/FindNavigatorForm.swift | 0 .../FindNavigator/FindNavigatorIndexBar.swift | 0 .../FindNavigatorListViewController.swift | 0 .../FindNavigatorMatchListCell.swift | 0 .../FindNavigatorResultList.swift | 0 .../SearchResultFileCell.swift | 0 .../FindNavigatorToolbarBottom.swift | 0 .../FindNavigator/FindNavigatorView.swift | 0 .../CESearch}/Indexer/AsyncFileIterator.swift | 0 .../CESearch}/Indexer/FileHelper.swift | 0 .../CESearch}/Indexer/SearchIndexer+Add.swift | 0 .../SearchIndexer+AsyncController.swift | 0 .../Indexer/SearchIndexer+File.swift | 0 .../SearchIndexer+InternalMethods.swift | 0 .../Indexer/SearchIndexer+Memory.swift | 0 .../SearchIndexer+ProgressiveSearch.swift | 0 .../Indexer/SearchIndexer+Search.swift | 0 .../Indexer/SearchIndexer+Terms.swift | 0 .../CESearch}/Indexer/SearchIndexer.swift | 0 .../Model/SearchResultMatchModel.swift | 0 .../CESearch}/Model/SearchResultModel.swift | 0 .../SearchState/SearchState+Find.swift | 0 .../SearchState+FindAndReplace.swift | 0 .../SearchState/SearchState+Index.swift | 0 .../SearchState+MatchExtraction.swift | 0 .../SearchState+QueryProcessing.swift | 0 .../CESearch}/SearchState/SearchState.swift | 0 Packages/Features/Search/Package.resolved | 24 ------------------- 49 files changed, 37 insertions(+), 46 deletions(-) create mode 100644 Packages/Features/CESearch/Package.resolved rename Packages/Features/{Search => CESearch}/Package.swift (82%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Environment+WorkspaceFileOpener.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Extensions/Array+Index.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindModePicker.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorConfiguration.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorForm.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorIndexBar.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorToolbarBottom.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/FindNavigator/FindNavigatorView.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/AsyncFileIterator.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/FileHelper.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+Add.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+AsyncController.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+File.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+InternalMethods.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+Memory.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+ProgressiveSearch.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+Search.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer+Terms.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Indexer/SearchIndexer.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Model/SearchResultMatchModel.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/Model/SearchResultModel.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/SearchState/SearchState+Find.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/SearchState/SearchState+FindAndReplace.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/SearchState/SearchState+Index.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/SearchState/SearchState+MatchExtraction.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/SearchState/SearchState+QueryProcessing.swift (100%) rename Packages/Features/{Search/Sources/Search => CESearch/Sources/CESearch}/SearchState/SearchState.swift (100%) delete mode 100644 Packages/Features/Search/Package.resolved diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 3c046b3938..051507ad84 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -16,7 +16,7 @@ 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; - 588950C52FFA5C05004BE116 /* Search in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* Search */; }; + 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* Notifications */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; @@ -210,7 +210,7 @@ 5EACE6222DF4BF08005E08B8 /* WelcomeWindow in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, - 588950C52FFA5C05004BE116 /* Search in Frameworks */, + 588950C52FFA5C05004BE116 /* CESearch in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, 6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */, @@ -357,7 +357,7 @@ 5800E2F72FF843390085ECF1 /* CodeEditUI */, 5AD0C0DE2D00000000000002 /* CodeEditDocument */, 5AD0C0DE2D00000000000012 /* CodeEditSettings */, - 588950C42FFA5C05004BE116 /* Search */, + 588950C42FFA5C05004BE116 /* CESearch */, 588957122FFA679E004BE116 /* CodeEditServices */, 5889639D2FFA9A87004BE116 /* Notifications */, 58ED10012FFB0001004BE116 /* Editor */, @@ -1902,9 +1902,9 @@ package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; productName = SnapshotTesting; }; - 588950C42FFA5C05004BE116 /* Search */ = { + 588950C42FFA5C05004BE116 /* CESearch */ = { isa = XCSwiftPackageProductDependency; - productName = Search; + productName = CESearch; }; 588957122FFA679E004BE116 /* CodeEditServices */ = { isa = XCSwiftPackageProductDependency; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index b29b46b48c..ba0be28442 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -31,7 +31,7 @@ location = "group:Packages/Features" name = "Features"> + location = "group:CESearch"> diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index b25d65c854..ab0ff1d9a4 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -9,7 +9,7 @@ import Foundation import CEWorkspaceFileManager import Editor import Notifications -import Search +import CESearch /// Protocol defining the interface that workspace consumers depend on. /// Enables testability via mock implementations and decouples views from the concrete Workspace type. diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift index 7c576799d0..a909b05882 100644 --- a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift +++ b/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift @@ -7,7 +7,7 @@ import SwiftUI import CodeEditSettings -import Search +import CESearch /// App-side wrapper for the Search package's find navigator: reads Settings /// (which the package cannot import) and passes them down as configuration. diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 14cd31fef3..80730a695c 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -9,7 +9,7 @@ import SwiftUI import CodeEditCore import Editor import Notifications -import Search +import CESearch import ShellClient private struct FileRelocatorKey: EnvironmentKey { diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index ea15c364b4..aef714de73 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -10,7 +10,7 @@ import CEWorkspaceFileManager import CodeEditCore import Editor import Notifications -import Search +import CESearch import SwiftUI import Foundation diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index f5256f9b7c..9a7bea0021 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -8,7 +8,7 @@ import Foundation import CEWorkspaceFileManager import Editor -import Search +import CESearch /// Constructs and wires the manager/service object graph for a ``Workspace``. /// diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 19b8643125..1fc7b84525 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -8,7 +8,7 @@ import XCTest import CodeEditCore import ShellClient -import Search +import CESearch @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift b/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift index cee9260821..2e8fdc5c99 100644 --- a/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift +++ b/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift @@ -6,7 +6,7 @@ // import XCTest -import Search +import CESearch @testable import CodeEdit final class AsyncIndexingTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift b/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift index 97c7dc18e7..b1cc513363 100644 --- a/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift +++ b/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift @@ -6,7 +6,7 @@ // import XCTest -import Search +import CESearch @testable import CodeEdit final class MemoryIndexingTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift b/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift index ff019a2e92..35a446ae7d 100644 --- a/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift +++ b/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift @@ -6,7 +6,7 @@ // import XCTest -import Search +import CESearch @testable import CodeEdit final class MemoryIndexSearchTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 17ea8b8cc7..7529eec5e2 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -7,7 +7,7 @@ import XCTest import CodeEditCore -@testable import Search +@testable import CESearch @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift index f7b231173c..c6eaaf1148 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift @@ -7,7 +7,7 @@ import XCTest import CodeEditCore -import Search +import CESearch final class FindReplaceQueryBridgeTests: XCTestCase { private var directory: URL! diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 624a7bf5b3..38a2051dd4 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -7,7 +7,7 @@ import XCTest import CodeEditCore -@testable import Search +@testable import CESearch @testable import CodeEdit final class FindTests: XCTestCase { diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index b07cdfb781..b18ddd5469 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -7,7 +7,7 @@ import XCTest import CodeEditCore -@testable import Search +@testable import CESearch @testable import CodeEdit final class WorkspaceIndexTests: XCTestCase { diff --git a/Packages/Features/CESearch/Package.resolved b/Packages/Features/CESearch/Package.resolved new file mode 100644 index 0000000000..36b9b98a1a --- /dev/null +++ b/Packages/Features/CESearch/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "8ea09dcc49375fb167b233dc483c2613886ed8cdda256195fc8b29196333173a", + "pins" : [ + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + } + ], + "version" : 3 +} diff --git a/Packages/Features/Search/Package.swift b/Packages/Features/CESearch/Package.swift similarity index 82% rename from Packages/Features/Search/Package.swift rename to Packages/Features/CESearch/Package.swift index 115d56e288..4ebfa41286 100644 --- a/Packages/Features/Search/Package.swift +++ b/Packages/Features/CESearch/Package.swift @@ -3,10 +3,10 @@ import PackageDescription let package = Package( - name: "Search", + name: "CESearch", platforms: [.macOS(.v14)], products: [ - .library(name: "Search", targets: ["Search"]) + .library(name: "CESearch", targets: ["CESearch"]) ], dependencies: [ .package(path: "../../Foundation/CodeEditCore"), @@ -14,7 +14,7 @@ let package = Package( ], targets: [ .target( - name: "Search", + name: "CESearch", dependencies: [ .product(name: "CodeEditCore", package: "CodeEditCore"), .product(name: "CodeEditUI", package: "CodeEditUI") diff --git a/Packages/Features/Search/Sources/Search/Environment+WorkspaceFileOpener.swift b/Packages/Features/CESearch/Sources/CESearch/Environment+WorkspaceFileOpener.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Environment+WorkspaceFileOpener.swift rename to Packages/Features/CESearch/Sources/CESearch/Environment+WorkspaceFileOpener.swift diff --git a/Packages/Features/Search/Sources/Search/Extensions/Array+Index.swift b/Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Extensions/Array+Index.swift rename to Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindModePicker.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindModePicker.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindModePicker.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindModePicker.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorConfiguration.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorForm.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorForm.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorForm.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorIndexBar.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorToolbarBottom.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift diff --git a/Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorView.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorView.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/FindNavigator/FindNavigatorView.swift rename to Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorView.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/AsyncFileIterator.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/AsyncFileIterator.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/AsyncFileIterator.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/AsyncFileIterator.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/FileHelper.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/FileHelper.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/FileHelper.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/FileHelper.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Add.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Add.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Add.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Add.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+AsyncController.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+File.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+File.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+File.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+File.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+InternalMethods.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Memory.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Memory.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Memory.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+ProgressiveSearch.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Search.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Search.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Search.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Search.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Terms.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer+Terms.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Terms.swift diff --git a/Packages/Features/Search/Sources/Search/Indexer/SearchIndexer.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Indexer/SearchIndexer.swift rename to Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer.swift diff --git a/Packages/Features/Search/Sources/Search/Model/SearchResultMatchModel.swift b/Packages/Features/CESearch/Sources/CESearch/Model/SearchResultMatchModel.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Model/SearchResultMatchModel.swift rename to Packages/Features/CESearch/Sources/CESearch/Model/SearchResultMatchModel.swift diff --git a/Packages/Features/Search/Sources/Search/Model/SearchResultModel.swift b/Packages/Features/CESearch/Sources/CESearch/Model/SearchResultModel.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/Model/SearchResultModel.swift rename to Packages/Features/CESearch/Sources/CESearch/Model/SearchResultModel.swift diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState+Find.swift b/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Find.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/SearchState/SearchState+Find.swift rename to Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Find.swift diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift b/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/SearchState/SearchState+FindAndReplace.swift rename to Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState+Index.swift b/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Index.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/SearchState/SearchState+Index.swift rename to Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Index.swift diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift b/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/SearchState/SearchState+MatchExtraction.swift rename to Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift b/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/SearchState/SearchState+QueryProcessing.swift rename to Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift diff --git a/Packages/Features/Search/Sources/Search/SearchState/SearchState.swift b/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState.swift similarity index 100% rename from Packages/Features/Search/Sources/Search/SearchState/SearchState.swift rename to Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState.swift diff --git a/Packages/Features/Search/Package.resolved b/Packages/Features/Search/Package.resolved deleted file mode 100644 index 2498af37f1..0000000000 --- a/Packages/Features/Search/Package.resolved +++ /dev/null @@ -1,24 +0,0 @@ -{ - "originHash" : "861c101de336c2f73d77f6b01de83702d01ca8143fc22eeec15fff8cbf5e3fa4", - "pins" : [ - { - "identity" : "codeeditsymbols", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", - "state" : { - "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", - "version" : "0.2.3" - } - }, - { - "identity" : "factory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hmlongco/Factory", - "state" : { - "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", - "version" : "2.5.3" - } - } - ], - "version" : 3 -} From 06f9266a8e5500c90b335107bce80485a2377088 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 12:34:28 +0200 Subject: [PATCH 133/335] Refactor: Rename Notifications feature package to CENotifications --- CodeEdit.xcodeproj/project.pbxproj | 10 +++++----- CodeEdit.xcworkspace/contents.xcworkspacedata | 2 +- CodeEdit/AppDependencies.swift | 2 +- .../Controllers/CodeEditSplitViewController.swift | 2 +- .../Controllers/CodeEditWindowController+Toolbar.swift | 2 +- .../NotificationPanelViewModel+Toolbar.swift | 2 +- .../Documents/Protocols/WorkspaceManaging.swift | 2 +- .../InternalDevelopmentNotificationsView.swift | 2 +- CodeEdit/Features/LSP/Registry/RegistryManager.swift | 2 +- CodeEdit/Features/LSP/Service/LSPService.swift | 2 +- .../Workspace/Models/Environment+AppCommands.swift | 2 +- CodeEdit/Features/Workspace/Models/Workspace.swift | 2 +- .../Workspace/Services/WorkspaceWindowManager.swift | 2 +- CodeEdit/WorkspaceView.swift | 2 +- .../Features/LSP/LSPServiceDocumentObjectsTests.swift | 2 +- CodeEditTests/Features/LSP/Registry.swift | 2 +- .../NotificationPanelViewModelTests.swift | 2 +- .../{Notifications => CENotifications}/Package.swift | 6 +++--- .../Environment+NotificationManager.swift | 0 .../CENotifications}/Models/CENotification.swift | 0 .../NotificationManager+Delegate.swift | 0 .../CENotifications}/NotificationManager+System.swift | 0 .../Sources/CENotifications}/NotificationManager.swift | 0 .../Protocols/NotificationManaging.swift | 0 ...tificationPanelViewModel+NotificationHandling.swift | 0 .../NotificationPanelViewModel+TimerManagement.swift | 0 .../NotificationPanelViewModel+Visibility.swift | 0 .../ViewModels/NotificationPanelViewModel.swift | 0 .../Views/NotificationBannerView.swift | 0 .../CENotifications}/Views/NotificationPanelView.swift | 0 .../Views/NotificationToolbarItem.swift | 0 31 files changed, 24 insertions(+), 24 deletions(-) rename Packages/Features/{Notifications => CENotifications}/Package.swift (78%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/Environment+NotificationManager.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/Models/CENotification.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/NotificationManager+Delegate.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/NotificationManager+System.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/NotificationManager.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/Protocols/NotificationManaging.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/ViewModels/NotificationPanelViewModel+NotificationHandling.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/ViewModels/NotificationPanelViewModel+TimerManagement.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/ViewModels/NotificationPanelViewModel+Visibility.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/ViewModels/NotificationPanelViewModel.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/Views/NotificationBannerView.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/Views/NotificationPanelView.swift (100%) rename Packages/Features/{Notifications/Sources/Notifications => CENotifications/Sources/CENotifications}/Views/NotificationToolbarItem.swift (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 051507ad84..a8eb627a72 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -18,7 +18,7 @@ 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; - 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* Notifications */; }; + 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58ED10022FFB0001004BE116 /* Editor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* Editor */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; @@ -189,7 +189,7 @@ 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, 6CCF73D02E26DE3200B94F75 /* SwiftTerm in Frameworks */, - 5889639E2FFA9A87004BE116 /* Notifications in Frameworks */, + 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */, 58ED10022FFB0001004BE116 /* Editor in Frameworks */, 6C315FC82E05E33D0011BFC5 /* CodeEditSourceEditor in Frameworks */, 6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */, @@ -359,7 +359,7 @@ 5AD0C0DE2D00000000000012 /* CodeEditSettings */, 588950C42FFA5C05004BE116 /* CESearch */, 588957122FFA679E004BE116 /* CodeEditServices */, - 5889639D2FFA9A87004BE116 /* Notifications */, + 5889639D2FFA9A87004BE116 /* CENotifications */, 58ED10012FFB0001004BE116 /* Editor */, ); productName = CodeEdit; @@ -1910,9 +1910,9 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditServices; }; - 5889639D2FFA9A87004BE116 /* Notifications */ = { + 5889639D2FFA9A87004BE116 /* CENotifications */ = { isa = XCSwiftPackageProductDependency; - productName = Notifications; + productName = CENotifications; }; 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */ = { isa = XCSwiftPackageProductDependency; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index ba0be28442..06a9f7a4ad 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -34,7 +34,7 @@ location = "group:CESearch"> + location = "group:CENotifications"> diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 9353b37680..d2eaba5c53 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -7,7 +7,7 @@ import CodeEditCore import CodeEditDocument -import Notifications +import CENotifications import ShellClient /// The app-scope composition root. Owns every process-lifetime service and is the only diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index e86432f3b5..df7d1bed77 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -10,7 +10,7 @@ import CodeEditCore import CodeEditUI import Editor import SwiftUI -import Notifications +import CENotifications final class CodeEditSplitViewController: NSSplitViewController { static let minSidebarWidth: CGFloat = 242 diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index fe574785d0..9af47c04f8 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -9,7 +9,7 @@ import AppKit import CEWorkspaceFileManager import SwiftUI import Combine -import Notifications +import CENotifications extension CodeEditWindowController { internal func setupToolbar() { diff --git a/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift index 721390ddf0..03f00975b7 100644 --- a/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift @@ -6,7 +6,7 @@ // import AppKit -import Notifications +import CENotifications /// App-shell integration for the notification toolbar badge. /// diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index ab0ff1d9a4..0fdcc568c8 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -8,7 +8,7 @@ import Foundation import CEWorkspaceFileManager import Editor -import Notifications +import CENotifications import CESearch /// Protocol defining the interface that workspace consumers depend on. diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift index fc6b8c4dd7..5834f0a64d 100644 --- a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Notifications +import CENotifications struct InternalDevelopmentNotificationsView: View { @Environment(\.notificationManager) private var notificationManager diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index 3456fcf353..6f58fc9b3e 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -11,7 +11,7 @@ import Foundation import ZIPFoundation import Combine import CodeEditCore -import Notifications +import CENotifications @MainActor final class RegistryManager: ObservableObject, RegistryManaging { diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 5f39726119..b9029b570f 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -14,7 +14,7 @@ import Foundation import LanguageClient import LanguageServerProtocol import CodeEditLanguages -import Notifications +import CENotifications /// `LSPService` is a service class responsible for managing the lifecycle and event handling /// of Language Server Protocol (LSP) clients within the CodeEdit application. It handles the initialization, diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 80730a695c..7ae8d6ec22 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -8,7 +8,7 @@ import SwiftUI import CodeEditCore import Editor -import Notifications +import CENotifications import CESearch import ShellClient diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index aef714de73..5dde211aac 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -9,7 +9,7 @@ import AppKit import CEWorkspaceFileManager import CodeEditCore import Editor -import Notifications +import CENotifications import CESearch import SwiftUI import Foundation diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 1ef1e88d82..f21a980fa9 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -9,7 +9,7 @@ import AppKit import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore -import Notifications +import CENotifications import SwiftUI import WelcomeWindow diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 8a401a0485..502a4eda27 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -10,7 +10,7 @@ import CodeEditSettings import CodeEditCore import CodeEditUI import Editor -import Notifications +import CENotifications import UniformTypeIdentifiers struct WorkspaceView: View { diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift index 5b38f547bd..e4e4583e67 100644 --- a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -8,7 +8,7 @@ import XCTest import CodeEditCore import CodeEditDocument -import Notifications +import CENotifications @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index f3345c1898..f8630d480e 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -8,7 +8,7 @@ import Testing import Foundation import CodeEditCore -import Notifications +import CENotifications import ShellClient @testable import CodeEdit diff --git a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift index a2f25047d1..e791b18ed2 100644 --- a/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift +++ b/CodeEditTests/Features/Notifications/NotificationPanelViewModelTests.swift @@ -7,7 +7,7 @@ import XCTest import CodeEditCore -@testable import Notifications +@testable import CENotifications @testable import CodeEdit @MainActor diff --git a/Packages/Features/Notifications/Package.swift b/Packages/Features/CENotifications/Package.swift similarity index 78% rename from Packages/Features/Notifications/Package.swift rename to Packages/Features/CENotifications/Package.swift index cfe3a602ac..af65c0b562 100644 --- a/Packages/Features/Notifications/Package.swift +++ b/Packages/Features/CENotifications/Package.swift @@ -3,10 +3,10 @@ import PackageDescription let package = Package( - name: "Notifications", + name: "CENotifications", platforms: [.macOS(.v14)], products: [ - .library(name: "Notifications", targets: ["Notifications"]) + .library(name: "CENotifications", targets: ["CENotifications"]) ], dependencies: [ .package(path: "../../Foundation/CodeEditCore"), @@ -14,7 +14,7 @@ let package = Package( ], targets: [ .target( - name: "Notifications", + name: "CENotifications", dependencies: [ .product(name: "CodeEditCore", package: "CodeEditCore"), .product(name: "CodeEditUI", package: "CodeEditUI") diff --git a/Packages/Features/Notifications/Sources/Notifications/Environment+NotificationManager.swift b/Packages/Features/CENotifications/Sources/CENotifications/Environment+NotificationManager.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/Environment+NotificationManager.swift rename to Packages/Features/CENotifications/Sources/CENotifications/Environment+NotificationManager.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/Models/CENotification.swift b/Packages/Features/CENotifications/Sources/CENotifications/Models/CENotification.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/Models/CENotification.swift rename to Packages/Features/CENotifications/Sources/CENotifications/Models/CENotification.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift b/Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+Delegate.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/NotificationManager+Delegate.swift rename to Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+Delegate.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationManager+System.swift b/Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+System.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/NotificationManager+System.swift rename to Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+System.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift b/Packages/Features/CENotifications/Sources/CENotifications/NotificationManager.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/NotificationManager.swift rename to Packages/Features/CENotifications/Sources/CENotifications/NotificationManager.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/Protocols/NotificationManaging.swift b/Packages/Features/CENotifications/Sources/CENotifications/Protocols/NotificationManaging.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/Protocols/NotificationManaging.swift rename to Packages/Features/CENotifications/Sources/CENotifications/Protocols/NotificationManaging.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift rename to Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift b/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift rename to Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift b/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel+Visibility.swift rename to Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift b/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/ViewModels/NotificationPanelViewModel.swift rename to Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/Views/NotificationBannerView.swift b/Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationBannerView.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/Views/NotificationBannerView.swift rename to Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationBannerView.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/Views/NotificationPanelView.swift b/Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationPanelView.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/Views/NotificationPanelView.swift rename to Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationPanelView.swift diff --git a/Packages/Features/Notifications/Sources/Notifications/Views/NotificationToolbarItem.swift b/Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationToolbarItem.swift similarity index 100% rename from Packages/Features/Notifications/Sources/Notifications/Views/NotificationToolbarItem.swift rename to Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationToolbarItem.swift From 54fe51425c93c5c9426a6c9dfdfaf201c5e339db Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 12:35:54 +0200 Subject: [PATCH 134/335] Refactor: Rename Editor feature package to CEEditor --- CodeEdit.xcodeproj/project.pbxproj | 10 +++++----- CodeEdit.xcworkspace/contents.xcworkspacedata | 2 +- .../Documents/AppCodeFileDocumentDelegate.swift | 2 +- .../Controllers/CodeEditSplitViewController.swift | 2 +- .../Controllers/CodeEditWindowController.swift | 2 +- .../Documents/Protocols/WorkspaceManaging.swift | 2 +- .../OutlineView/ProjectNavigatorOutlineView.swift | 2 +- CodeEdit/Features/WindowCommands/EditorCommands.swift | 2 +- .../Features/WindowCommands/NavigateCommands.swift | 2 +- .../Utils/WindowControllerPropertyWrapper.swift | 2 +- .../Workspace/Models/Environment+AppCommands.swift | 2 +- CodeEdit/Features/Workspace/Models/Workspace.swift | 2 +- .../Workspace/Services/AppWorkspaceNavigator.swift | 2 +- CodeEdit/Features/Workspace/WorkspaceFactory.swift | 2 +- CodeEdit/WorkspaceView.swift | 2 +- .../Features/Editor/AppActiveCursorStateTests.swift | 2 +- .../Features/Editor/AppActiveEditorStateTests.swift | 2 +- .../Features/Editor/AppFileEditorOverridesTests.swift | 2 +- .../Features/Editor/DocumentRegistryTests.swift | 2 +- .../Features/Editor/EditorStateRestorationTests.swift | 2 +- .../Features/Editor/UndoManagerRegistrationTests.swift | 2 +- .../Workspace/AppWorkspaceNavigatorTests.swift | 2 +- Packages/Features/{Editor => CEEditor}/Package.swift | 6 +++--- .../Sources/CEEditor}/CEWorkspaceFile+Editor.swift | 0 .../Sources/CEEditor}/Environment+SplitEditor.swift | 0 .../JumpBar/Views/EditorJumpBarComponent.swift | 0 .../CEEditor}/JumpBar/Views/EditorJumpBarMenu.swift | 0 .../CEEditor}/JumpBar/Views/EditorJumpBarView.swift | 0 .../CEEditor}/Models/AppActiveCursorState.swift | 0 .../CEEditor}/Models/AppActiveEditorState.swift | 0 .../CEEditor}/Models/AppFileEditorOverrides.swift | 0 .../Sources/CEEditor}/Models/DocumentRegistry.swift | 0 .../CEEditor}/Models/Editor/Editor+History.swift | 0 .../CEEditor}/Models/Editor/Editor+TabSwitch.swift | 0 .../Sources/CEEditor}/Models/Editor/Editor.swift | 0 .../Sources/CEEditor}/Models/EditorInstance.swift | 0 .../EditorLayout/EditorLayout+StateRestoration.swift | 0 .../CEEditor}/Models/EditorLayout/EditorLayout.swift | 0 .../Sources/CEEditor}/Models/EditorManager.swift | 0 .../CEEditor}/Models/Environment+ActiveEditor.swift | 0 .../Models/Environment+WorkspaceNavigator.swift | 0 .../Sources/CEEditor}/Models/FileIcon.swift | 0 .../Models/Restoration/EditorStateRestoration.swift | 0 .../Models/Restoration/UndoManagerRegistration.swift | 0 .../Sources/CEEditor}/Models/Theme+EditorTheme.swift | 0 .../Sources/CEEditor}/SplitViewData.swift | 0 .../TabBar/Tabs/Tab/EditorFileTabCloseButton.swift | 0 .../TabBar/Tabs/Tab/EditorTabBackground.swift | 0 .../TabBar/Tabs/Tab/EditorTabButtonStyle.swift | 0 .../TabBar/Tabs/Tab/EditorTabCloseButton.swift | 0 .../CEEditor}/TabBar/Tabs/Tab/EditorTabView.swift | 0 .../TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift | 0 .../Tabs/Tab/Models/EditorTabRepresentable.swift | 0 .../TabBar/Tabs/Views/EditorTabOnDropDelegate.swift | 0 .../TabBar/Tabs/Views/EditorTabs+DragGesture.swift | 0 .../CEEditor}/TabBar/Tabs/Views/EditorTabs.swift | 0 .../TabBar/Tabs/Views/EditorTabsOverflowShadow.swift | 0 .../CEEditor}/TabBar/Views/EditorHistoryMenus.swift | 0 .../CEEditor}/TabBar/Views/EditorTabBarAccessory.swift | 0 .../TabBar/Views/EditorTabBarContextMenu.swift | 0 .../CEEditor}/TabBar/Views/EditorTabBarDivider.swift | 0 .../TabBar/Views/EditorTabBarLeadingAccessories.swift | 0 .../TabBar/Views/EditorTabBarTrailingAccessories.swift | 0 .../CEEditor}/TabBar/Views/EditorTabBarView.swift | 0 .../CEEditor}/UseCases/RestoreEditorStateUseCase.swift | 0 .../Sources/CEEditor}/Views/AnyFileView.swift | 0 .../Sources/CEEditor}/Views/CodeFileView.swift | 0 .../Sources/CEEditor}/Views/EditorAreaFileView.swift | 0 .../Sources/CEEditor}/Views/EditorAreaView.swift | 0 .../Sources/CEEditor}/Views/EditorLayoutView.swift | 0 .../CEEditor}/Views/Environment+LanguageServices.swift | 0 .../Sources/CEEditor}/Views/FilePreviewView.swift | 0 .../Sources/CEEditor}/Views/ImageFileView.swift | 0 .../Sources/CEEditor}/Views/LoadingFileView.swift | 0 .../Sources/CEEditor}/Views/NonTextFileView.swift | 0 .../Sources/CEEditor}/Views/PDFFileView.swift | 0 .../Sources/CEEditor}/Views/WindowCodeFileView.swift | 0 77 files changed, 29 insertions(+), 29 deletions(-) rename Packages/Features/{Editor => CEEditor}/Package.swift (95%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/CEWorkspaceFile+Editor.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Environment+SplitEditor.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/JumpBar/Views/EditorJumpBarComponent.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/JumpBar/Views/EditorJumpBarMenu.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/JumpBar/Views/EditorJumpBarView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/AppActiveCursorState.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/AppActiveEditorState.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/AppFileEditorOverrides.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/DocumentRegistry.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Editor/Editor+History.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Editor/Editor+TabSwitch.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Editor/Editor.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/EditorInstance.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/EditorLayout/EditorLayout+StateRestoration.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/EditorLayout/EditorLayout.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/EditorManager.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Environment+ActiveEditor.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Environment+WorkspaceNavigator.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/FileIcon.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Restoration/EditorStateRestoration.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Restoration/UndoManagerRegistration.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Models/Theme+EditorTheme.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/SplitViewData.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/EditorTabBackground.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/EditorTabButtonStyle.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/EditorTabCloseButton.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/EditorTabView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Views/EditorTabs+DragGesture.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Views/EditorTabs.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorHistoryMenus.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorTabBarAccessory.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorTabBarContextMenu.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorTabBarDivider.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorTabBarLeadingAccessories.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorTabBarTrailingAccessories.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/TabBar/Views/EditorTabBarView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/UseCases/RestoreEditorStateUseCase.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/AnyFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/CodeFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/EditorAreaFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/EditorAreaView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/EditorLayoutView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/Environment+LanguageServices.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/FilePreviewView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/ImageFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/LoadingFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/NonTextFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/PDFFileView.swift (100%) rename Packages/Features/{Editor/Sources/Editor => CEEditor/Sources/CEEditor}/Views/WindowCodeFileView.swift (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index a8eb627a72..0563361a03 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -20,7 +20,7 @@ 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; - 58ED10022FFB0001004BE116 /* Editor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* Editor */; }; + 58ED10022FFB0001004BE116 /* CEEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* CEEditor */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; @@ -190,7 +190,7 @@ 6C76D6D42E15B91E00EF52C3 /* CodeEditSourceEditor in Frameworks */, 6CCF73D02E26DE3200B94F75 /* SwiftTerm in Frameworks */, 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */, - 58ED10022FFB0001004BE116 /* Editor in Frameworks */, + 58ED10022FFB0001004BE116 /* CEEditor in Frameworks */, 6C315FC82E05E33D0011BFC5 /* CodeEditSourceEditor in Frameworks */, 6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */, 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, @@ -360,7 +360,7 @@ 588950C42FFA5C05004BE116 /* CESearch */, 588957122FFA679E004BE116 /* CodeEditServices */, 5889639D2FFA9A87004BE116 /* CENotifications */, - 58ED10012FFB0001004BE116 /* Editor */, + 58ED10012FFB0001004BE116 /* CEEditor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -1918,9 +1918,9 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditCore; }; - 58ED10012FFB0001004BE116 /* Editor */ = { + 58ED10012FFB0001004BE116 /* CEEditor */ = { isa = XCSwiftPackageProductDependency; - productName = Editor; + productName = CEEditor; }; 58F2EB1D292FB954004A9BDE /* Sparkle */ = { isa = XCSwiftPackageProductDependency; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 06a9f7a4ad..753c12700e 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -37,7 +37,7 @@ location = "group:CENotifications"> + location = "group:CEEditor"> diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift index e6f11de747..8831f3390f 100644 --- a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -6,7 +6,7 @@ // import AppKit -import Editor +import CEEditor import SwiftUI import CodeEditTextView import CodeEditDocument diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index df7d1bed77..d934fbe951 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -8,7 +8,7 @@ import Cocoa import CodeEditCore import CodeEditUI -import Editor +import CEEditor import SwiftUI import CENotifications diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index df4830b115..8fe5eefe76 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -8,7 +8,7 @@ import Cocoa import CodeEditDocument import CodeEditSettings -import Editor +import CEEditor import SwiftUI import CodeEditUI import Combine diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index 0fdcc568c8..bc7fc99d39 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -7,7 +7,7 @@ import Foundation import CEWorkspaceFileManager -import Editor +import CEEditor import CENotifications import CESearch diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 77b5456b1d..bc9f82f1ab 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -9,7 +9,7 @@ import SwiftUI import CEWorkspaceFileManager import CodeEditCore import CodeEditSettings -import Editor +import CEEditor import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` diff --git a/CodeEdit/Features/WindowCommands/EditorCommands.swift b/CodeEdit/Features/WindowCommands/EditorCommands.swift index 1ea5fe6a1e..d37ff60d10 100644 --- a/CodeEdit/Features/WindowCommands/EditorCommands.swift +++ b/CodeEdit/Features/WindowCommands/EditorCommands.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Editor +import CEEditor import CodeEditKit struct EditorCommands: Commands { diff --git a/CodeEdit/Features/WindowCommands/NavigateCommands.swift b/CodeEdit/Features/WindowCommands/NavigateCommands.swift index d129523b51..b7b51ae3f3 100644 --- a/CodeEdit/Features/WindowCommands/NavigateCommands.swift +++ b/CodeEdit/Features/WindowCommands/NavigateCommands.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Editor +import CEEditor struct NavigateCommands: Commands { diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index 904d06a55c..00f2f8acd4 100644 --- a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -6,7 +6,7 @@ // import AppKit -import Editor +import CEEditor import SwiftUI import Combine diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 7ae8d6ec22..f956c1fded 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -7,7 +7,7 @@ import SwiftUI import CodeEditCore -import Editor +import CEEditor import CENotifications import CESearch import ShellClient diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 5dde211aac..8eab2812df 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -8,7 +8,7 @@ import AppKit import CEWorkspaceFileManager import CodeEditCore -import Editor +import CEEditor import CENotifications import CESearch import SwiftUI diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift index cdbf969134..3a70233f5d 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -8,7 +8,7 @@ import Foundation import CodeEditCore import CEWorkspaceFileManager -import Editor +import CEEditor /// App-shell binding of the `WorkspaceNavigator` command interface. /// Delegates to `WorkspaceWindowManager.openFileInWorkspace(url:asTemporary:)`, which maps the diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 9a7bea0021..48ebf47924 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -7,7 +7,7 @@ import Foundation import CEWorkspaceFileManager -import Editor +import CEEditor import CESearch /// Constructs and wires the manager/service object graph for a ``Workspace``. diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 502a4eda27..9e4b5fc9ca 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -9,7 +9,7 @@ import SwiftUI import CodeEditSettings import CodeEditCore import CodeEditUI -import Editor +import CEEditor import CENotifications import UniformTypeIdentifiers diff --git a/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift b/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift index e88746113b..0440f6226c 100644 --- a/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift +++ b/CodeEditTests/Features/Editor/AppActiveCursorStateTests.swift @@ -10,7 +10,7 @@ import Combine import Testing import CodeEditCore import CEWorkspaceFileManager -@testable import Editor +@testable import CEEditor import CodeEditSourceEditor @testable import CodeEdit diff --git a/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift b/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift index c0d033468b..159c7436c6 100644 --- a/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift +++ b/CodeEditTests/Features/Editor/AppActiveEditorStateTests.swift @@ -10,7 +10,7 @@ import Combine import Testing import CodeEditCore import CEWorkspaceFileManager -@testable import Editor +@testable import CEEditor @testable import CodeEdit @Suite diff --git a/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift b/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift index 627d84c8ac..f27d7c4471 100644 --- a/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift +++ b/CodeEditTests/Features/Editor/AppFileEditorOverridesTests.swift @@ -10,7 +10,7 @@ import Testing import CodeEditCore import CEWorkspaceFileManager import CodeEditDocument -@testable import Editor +@testable import CEEditor import CodeEditLanguages @testable import CodeEdit diff --git a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift index fa64894f4e..330b1c7aac 100644 --- a/CodeEditTests/Features/Editor/DocumentRegistryTests.swift +++ b/CodeEditTests/Features/Editor/DocumentRegistryTests.swift @@ -10,7 +10,7 @@ import CodeEditDocument import Combine import CodeEditCore @testable import CodeEdit -@testable import Editor +@testable import CEEditor final class DocumentRegistryTests: XCTestCase { private func makeFile(_ path: String = "/tmp/reg-\(UUID().uuidString).swift") -> CEWorkspaceFile { diff --git a/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift b/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift index a507d47a4a..25c476f0a0 100644 --- a/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift +++ b/CodeEditTests/Features/Editor/EditorStateRestorationTests.swift @@ -8,7 +8,7 @@ import Testing import Foundation @testable import CodeEdit -@testable import Editor +@testable import CEEditor @Suite struct EditorStateRestorationTests { diff --git a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift b/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift index 9eaefce864..7da1965e19 100644 --- a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift +++ b/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift @@ -6,7 +6,7 @@ // @testable import CodeEdit -@testable import Editor +@testable import CEEditor import Testing import CodeEditCore import Foundation diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index 7208720a0c..e30368dbee 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -9,7 +9,7 @@ import Foundation import Testing import CodeEditCore @testable import CodeEdit -@testable import Editor +@testable import CEEditor @Suite struct AppWorkspaceNavigatorTests { diff --git a/Packages/Features/Editor/Package.swift b/Packages/Features/CEEditor/Package.swift similarity index 95% rename from Packages/Features/Editor/Package.swift rename to Packages/Features/CEEditor/Package.swift index a989f1af57..e8b15ab720 100644 --- a/Packages/Features/Editor/Package.swift +++ b/Packages/Features/CEEditor/Package.swift @@ -3,10 +3,10 @@ import PackageDescription let package = Package( - name: "Editor", + name: "CEEditor", platforms: [.macOS(.v14)], products: [ - .library(name: "Editor", targets: ["Editor"]) + .library(name: "CEEditor", targets: ["CEEditor"]) ], dependencies: [ .package(path: "../../Foundation/CodeEditCore"), @@ -23,7 +23,7 @@ let package = Package( ], targets: [ .target( - name: "Editor", + name: "CEEditor", dependencies: [ .product(name: "CodeEditCore", package: "CodeEditCore"), .product(name: "CodeEditUI", package: "CodeEditUI"), diff --git a/Packages/Features/Editor/Sources/Editor/CEWorkspaceFile+Editor.swift b/Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/CEWorkspaceFile+Editor.swift rename to Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift diff --git a/Packages/Features/Editor/Sources/Editor/Environment+SplitEditor.swift b/Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Environment+SplitEditor.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift diff --git a/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarComponent.swift b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarComponent.swift rename to Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift diff --git a/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarMenu.swift b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarMenu.swift rename to Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift diff --git a/Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarView.swift b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/JumpBar/Views/EditorJumpBarView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/AppActiveCursorState.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveCursorState.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/AppActiveCursorState.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveCursorState.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/AppActiveEditorState.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/AppActiveEditorState.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/AppFileEditorOverrides.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/AppFileEditorOverrides.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/AppFileEditorOverrides.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/AppFileEditorOverrides.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/DocumentRegistry.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/DocumentRegistry.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/DocumentRegistry.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/DocumentRegistry.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+History.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+History.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+History.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+History.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+TabSwitch.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Editor/Editor+TabSwitch.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Editor/Editor.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Editor/Editor.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/EditorInstance.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorInstance.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/EditorInstance.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/EditorInstance.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout+StateRestoration.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/EditorLayout/EditorLayout.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/EditorManager.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorManager.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/EditorManager.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/EditorManager.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Environment+ActiveEditor.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Environment+ActiveEditor.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Environment+WorkspaceNavigator.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Environment+WorkspaceNavigator.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/FileIcon.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/FileIcon.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Restoration/EditorStateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Restoration/EditorStateRestoration.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Restoration/UndoManagerRegistration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Restoration/UndoManagerRegistration.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift diff --git a/Packages/Features/Editor/Sources/Editor/Models/Theme+EditorTheme.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Models/Theme+EditorTheme.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift diff --git a/Packages/Features/Editor/Sources/Editor/SplitViewData.swift b/Packages/Features/CEEditor/Sources/CEEditor/SplitViewData.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/SplitViewData.swift rename to Packages/Features/CEEditor/Sources/CEEditor/SplitViewData.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabView.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/EditorTabView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabs.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorHistoryMenus.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorHistoryMenus.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarAccessory.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarAccessory.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarContextMenu.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarDivider.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarDivider.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarTrailingAccessories.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift diff --git a/Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarView.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/TabBar/Views/EditorTabBarView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift diff --git a/Packages/Features/Editor/Sources/Editor/UseCases/RestoreEditorStateUseCase.swift b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/UseCases/RestoreEditorStateUseCase.swift rename to Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/AnyFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/AnyFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/AnyFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/AnyFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/CodeFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/EditorAreaFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/EditorAreaView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/EditorAreaView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/EditorLayoutView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/EditorLayoutView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/Environment+LanguageServices.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/Environment+LanguageServices.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/Environment+LanguageServices.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/Environment+LanguageServices.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/FilePreviewView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/ImageFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/ImageFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/ImageFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/ImageFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/LoadingFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/LoadingFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/LoadingFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/LoadingFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/NonTextFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/NonTextFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/NonTextFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/NonTextFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/PDFFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/PDFFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift diff --git a/Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift similarity index 100% rename from Packages/Features/Editor/Sources/Editor/Views/WindowCodeFileView.swift rename to Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift From 7c1d36a7cee217b8329362c0a39a713a01bd01ac Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 12:42:29 +0200 Subject: [PATCH 135/335] Refactor: Seam CEEditor behind WorkspaceFileProviding; move observer protocol and env key out of the service target --- .../CodeEditSplitViewController.swift | 3 ++ .../CodeEditWindowController.swift | 1 + .../ProjectNavigatorOutlineView.swift | 2 +- .../Models/Environment+Workspace.swift | 11 ++++++ .../CEWorkspaceFileManagerTests.swift | 2 +- Packages/Features/CEEditor/Package.swift | 2 - .../Views/EditorJumpBarComponent.swift | 8 ++-- .../JumpBar/Views/EditorJumpBarMenu.swift | 5 +-- .../Models/AppActiveEditorState.swift | 1 - .../EditorLayout+StateRestoration.swift | 3 +- .../Environment+WorkspaceFileProvider.swift | 22 +++++++++++ .../Restoration/UndoManagerRegistration.swift | 3 +- .../TabBar/Tabs/Tab/EditorTabView.swift | 9 ++--- .../Tab/Models/EditorTabFileObserver.swift | 3 +- .../Views/EditorTabBarContextMenu.swift | 7 ++-- .../UseCases/RestoreEditorStateUseCase.swift | 7 ++-- .../Workspace/WorkspaceFileProviding.swift | 38 +++++++++++++++++++ ...WorkspaceFileManager+DirectoryEvents.swift | 6 +-- .../CEWorkspaceFileManager.swift | 8 ++-- .../Environment+WorkspaceFileManager.swift | 19 ---------- 20 files changed, 104 insertions(+), 56 deletions(-) create mode 100644 Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift delete mode 100644 Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index d934fbe951..1d6ce77b9c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -102,6 +102,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(sourceControlViewModel) .environmentObject(searchState) .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.activeEditorState, activeEditorState) .appServices(dependencies) @@ -122,6 +123,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(workspace.undoRegistration) .environmentObject(workspace.notificationPanel) .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.workspaceStatePersistence, workspace.statePersistence) .environment(\.activeEditorState, activeEditorState) @@ -141,6 +143,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(editorManager) .environmentObject(sourceControlManager) .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.activeEditorState, activeEditorState) .environment(\.fileEditorOverrides, fileEditorOverrides) .appServices(dependencies) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 8fe5eefe76..3984adf97b 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -192,6 +192,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs workspace.editorManager?.openTab(item: file) } .environment(\.workspaceFileManager, workspace.workspaceFileManager) + .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.filePreview) { file in AnyView(FilePreviewView(item: file)) } .environment(\.languageServices, dependencies.languageServicesProvider) .environment(\.currentTheme, ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first!) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index bc9f82f1ab..d9827bdf2b 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -55,7 +55,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } @MainActor - class Coordinator: NSObject, CEWorkspaceFileManagerObserver { + class Coordinator: NSObject, WorkspaceFileObserver { init(_ workspace: Workspace) { self.workspace = workspace self.fileManager = workspace.workspaceFileManager diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift index 975f1a4faa..eefc414af5 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift @@ -21,6 +21,10 @@ private struct FileEditorOverridesKey: EnvironmentKey { static let defaultValue: FileEditorOverrides = NoOpFileEditorOverrides() } +private struct WorkspaceFileManagerKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: CEWorkspaceFileManager? = nil +} + private struct WorkspaceFileURLKey: EnvironmentKey { static let defaultValue: URL? = nil } @@ -34,6 +38,13 @@ private struct FilePreviewFactoryKey: EnvironmentKey { } extension EnvironmentValues { + /// The concrete workspace file manager, for app-shell views that need the mutating + /// API (create/rename). Feature packages use `\.workspaceFileProvider` (CEEditor) instead. + var workspaceFileManager: CEWorkspaceFileManager? { + get { self[WorkspaceFileManagerKey.self] } + set { self[WorkspaceFileManagerKey.self] = newValue } + } + var workspaceFileURL: URL? { get { self[WorkspaceFileURLKey.self] } set { self[WorkspaceFileURLKey.self] = newValue } diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index 7467e7a392..1990da4856 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -15,7 +15,7 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { let typeOfExtensions = ["json", "txt", "swift", "js", "py", "md"] var directory: URL! - class DummyObserver: CEWorkspaceFileManagerObserver { + class DummyObserver: WorkspaceFileObserver { var completion: (() -> Void)? init(completion: @escaping () -> Void) { diff --git a/Packages/Features/CEEditor/Package.swift b/Packages/Features/CEEditor/Package.swift index e8b15ab720..9657d9d53c 100644 --- a/Packages/Features/CEEditor/Package.swift +++ b/Packages/Features/CEEditor/Package.swift @@ -13,7 +13,6 @@ let package = Package( .package(path: "../../Foundation/CodeEditUI"), .package(path: "../../Foundation/CodeEditDocument"), .package(path: "../../Foundation/CodeEditSettings"), - .package(path: "../../Services/CodeEditServices"), .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), @@ -29,7 +28,6 @@ let package = Package( .product(name: "CodeEditUI", package: "CodeEditUI"), .product(name: "CodeEditDocument", package: "CodeEditDocument"), .product(name: "CodeEditSettings", package: "CodeEditSettings"), - .product(name: "CodeEditServices", package: "CodeEditServices"), .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), .product(name: "CodeEditTextView", package: "CodeEditTextView"), .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), diff --git a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift index c74fc8033e..c35a793660 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift @@ -21,8 +21,8 @@ struct EditorJumpBarComponent: View { @Environment(\.controlActiveState) private var activeState - @Environment(\.workspaceFileManager) - private var workspaceFileManager + @Environment(\.workspaceFileProvider) + private var workspaceFileProvider @State var position: NSPoint? @State var selection: CEWorkspaceFile @@ -44,7 +44,7 @@ struct EditorJumpBarComponent: View { } var siblings: [CEWorkspaceFile] { - guard let fileManager = workspaceFileManager, + guard let fileManager = workspaceFileProvider, let parent = fileItem.parent else { return [fileItem] } @@ -57,7 +57,7 @@ struct EditorJumpBarComponent: View { var body: some View { NSPopUpButtonView(selection: $selection) { - guard let fileManager = workspaceFileManager else { return NSPopUpButton() } + guard let fileManager = workspaceFileProvider else { return NSPopUpButton() } button.menu = EditorJumpBarMenu( fileItems: siblings, diff --git a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift index 40fcefa986..996cecb652 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift @@ -7,17 +7,16 @@ import AppKit import CodeEditSettings -import CEWorkspaceFileManager import CodeEditCore final class EditorJumpBarMenu: NSMenu, NSMenuDelegate { private let fileItems: [CEWorkspaceFile] - private weak var fileManager: CEWorkspaceFileManager? + private weak var fileManager: (any WorkspaceFileProviding)? private let tappedOpenFile: (CEWorkspaceFile) -> Void init( fileItems: [CEWorkspaceFile], - fileManager: CEWorkspaceFileManager, + fileManager: any WorkspaceFileProviding, tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.fileItems = fileItems diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift index 355bb15ba3..1b973f6f64 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift @@ -8,7 +8,6 @@ import Foundation import Combine import CodeEditCore -import CEWorkspaceFileManager /// App-side `ActiveEditorState` over a window's `EditorManager`. Emits the active editor's /// selected file across both active-editor switches and within-editor tab changes. diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 53d2be8c09..b21a0e556d 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -6,7 +6,6 @@ // import Foundation -import CEWorkspaceFileManager import CodeEditCore import SwiftUI import OrderedCollections @@ -19,7 +18,7 @@ extension EditorManager { /// - findReplaceQuery: The shared find/replace query for editor instances. public func restoreFromState( statePersistence: any WorkspaceStatePersisting, - fileManager: CEWorkspaceFileManager?, + fileManager: (any WorkspaceFileProviding)?, findReplaceQuery: FindReplaceQuery? ) { defer { diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift new file mode 100644 index 0000000000..eb20879c0f --- /dev/null +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift @@ -0,0 +1,22 @@ +// +// Environment+WorkspaceFileProvider.swift +// CEEditor +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +import SwiftUI +import CodeEditCore + +private struct WorkspaceFileProviderKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: (any WorkspaceFileProviding)? = nil +} + +extension EnvironmentValues { + /// Read-mostly access to the workspace file tree (see `WorkspaceFileProviding`). + /// Optional: nil in previews and tests; injected by the app shell. + public var workspaceFileProvider: (any WorkspaceFileProviding)? { + get { self[WorkspaceFileProviderKey.self] } + set { self[WorkspaceFileProviderKey.self] = newValue } + } +} diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift index 7723e5f5d4..7723434d5d 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift @@ -7,7 +7,6 @@ import SwiftUI import CodeEditDocument -import CEWorkspaceFileManager import CodeEditCore import CodeEditTextView @@ -54,7 +53,7 @@ public final class UndoManagerRegistration: ObservableObject { } } -extension UndoManagerRegistration: CEWorkspaceFileManagerObserver { +extension UndoManagerRegistration: WorkspaceFileObserver { /// Managers need to be cleared when the following is true: /// - The file is not open in any editors /// - The file is updated externally diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift index e90dd79bbd..5947a58dd5 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift @@ -7,7 +7,6 @@ import SwiftUI import CodeEditSettings -import CEWorkspaceFileManager import CodeEditCore struct EditorTabView: View { @@ -26,8 +25,8 @@ struct EditorTabView: View { @EnvironmentObject private var editorManager: EditorManager - @Environment(\.workspaceFileManager) - private var workspaceFileManager + @Environment(\.workspaceFileProvider) + private var workspaceFileProvider @StateObject private var fileObserver: EditorTabFileObserver @@ -270,10 +269,10 @@ struct EditorTabView: View { .tabBarContextMenu(item: tabFile, isTemporary: isTemporary) .accessibilityElement(children: .contain) .onAppear { - workspaceFileManager?.addObserver(fileObserver) + workspaceFileProvider?.addObserver(fileObserver) } .onDisappear { - workspaceFileManager?.removeObserver(fileObserver) + workspaceFileProvider?.removeObserver(fileObserver) } } } diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift index 6b37822d45..8d5def90d6 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift @@ -6,14 +6,13 @@ // import Foundation -import CEWorkspaceFileManager import CodeEditCore import SwiftUI /// Observer ViewModel for tracking file deletion @MainActor final class EditorTabFileObserver: ObservableObject, - CEWorkspaceFileManagerObserver { + WorkspaceFileObserver { @Published private(set) var isDeleted = false private let tabFile: CEWorkspaceFile diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift index e9ee64c5ec..31cc8dda26 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift @@ -6,7 +6,6 @@ // import SwiftUI -import CEWorkspaceFileManager import CodeEditCore import Foundation @@ -30,8 +29,8 @@ struct EditorTabBarContextMenu: ViewModifier { @Environment(\.workspaceNavigator) private var workspaceNavigator - @Environment(\.workspaceFileManager) - private var workspaceFileManager + @Environment(\.workspaceFileProvider) + private var workspaceFileProvider @EnvironmentObject var tabs: Editor @@ -155,7 +154,7 @@ struct EditorTabBarContextMenu: ViewModifier { /// Copies the relative path from the workspace folder to the given file item to the pasteboard. /// - Parameter item: The `FileItem` to use. private func copyRelativePath(item: CEWorkspaceFile) { - guard let rootPath = workspaceFileManager?.folderUrl else { + guard let rootPath = workspaceFileProvider?.folderUrl else { return } let destinationComponents = item.url.standardizedFileURL.pathComponents diff --git a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift index e4b4d9e9da..d09a4d623c 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift @@ -6,7 +6,6 @@ // import Foundation -import CEWorkspaceFileManager import CodeEditCore import OSLog import OrderedCollections @@ -28,7 +27,7 @@ public final class RestoreEditorStateUseCase { /// Decodes persisted editor state, validates it, and resolves file references. public func execute( statePersistence: any WorkspaceStatePersisting, - fileManager: CEWorkspaceFileManager?, + fileManager: (any WorkspaceFileProviding)?, findReplaceQuery: FindReplaceQuery?, editorManager: EditorManager ) -> Outcome { @@ -70,7 +69,7 @@ public final class RestoreEditorStateUseCase { /// Recursively maps decoded `CEWorkspaceFile` references to their shared file-manager-owned representations. private func fixRestoredEditorLayout( _ group: EditorLayout, - fileManager: CEWorkspaceFileManager?, + fileManager: (any WorkspaceFileProviding)?, findReplaceQuery: FindReplaceQuery?, editorManager: EditorManager ) throws { @@ -98,7 +97,7 @@ public final class RestoreEditorStateUseCase { /// and loads each tab's underlying code file. private func fixEditor( _ editor: Editor, - fileManager: CEWorkspaceFileManager?, + fileManager: (any WorkspaceFileProviding)?, findReplaceQuery: FindReplaceQuery?, editorManager: EditorManager ) throws { diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift new file mode 100644 index 0000000000..e55434a606 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift @@ -0,0 +1,38 @@ +// +// WorkspaceFileProviding.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +import Foundation + +/// Read-mostly access to a workspace's file tree, plus change observation. +/// +/// The interface features program against; the implementation is the +/// `CEWorkspaceFileManager` service. Deliberately narrow — it carries only what +/// feature packages actually consume (path resolution, child listing, change +/// observation), not the mutating file-management API, which stays on the +/// concrete service for app-shell use. +public protocol WorkspaceFileProviding: AnyObject { + /// The root folder of the workspace. + var folderUrl: URL { get } + + /// Resolves a path to its file item, optionally indexing intermediate + /// directories to find it. + func getFile(_ path: String, createIfNotFound: Bool) -> CEWorkspaceFile? + + /// The cached children of a directory item, if loaded. + func childrenOfFile(_ file: CEWorkspaceFile) -> [CEWorkspaceFile]? + + /// Registers an observer for file-tree changes. Observers are held weakly. + func addObserver(_ observer: WorkspaceFileObserver) + + /// Removes a previously registered observer. + func removeObserver(_ observer: WorkspaceFileObserver) +} + +/// Receives file-tree change notifications from a ``WorkspaceFileProviding``. +public protocol WorkspaceFileObserver: AnyObject { + func fileManagerUpdated(updatedItems: Set) +} diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift index 90e229aa93..2b5aa24795 100644 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift @@ -110,7 +110,7 @@ extension CEWorkspaceFileManager { /// Notify observers that an update occurred in the watched files. func notifyObservers(updatedItems: Set) { observers.allObjects.reversed().forEach { delegate in - guard let delegate = delegate as? CEWorkspaceFileManagerObserver else { + guard let delegate = delegate as? WorkspaceFileObserver else { observers.remove(delegate) return } @@ -120,13 +120,13 @@ extension CEWorkspaceFileManager { /// Add an observer for file system events. /// - Parameter observer: The observer to add. - public func addObserver(_ observer: CEWorkspaceFileManagerObserver) { + public func addObserver(_ observer: WorkspaceFileObserver) { observers.add(observer as AnyObject) } /// Remove an observer for file system events. /// - Parameter observer: The observer to remove. - public func removeObserver(_ observer: CEWorkspaceFileManagerObserver) { + public func removeObserver(_ observer: WorkspaceFileObserver) { observers.remove(observer as AnyObject) } } diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift index 258514292b..c3a84adced 100644 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift +++ b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift @@ -10,9 +10,9 @@ import CodeEditCore import Foundation import OSLog -public protocol CEWorkspaceFileManagerObserver: AnyObject { - func fileManagerUpdated(updatedItems: Set) -} +/// The observer protocol moved to `CodeEditCore` as ``WorkspaceFileObserver`` so +/// feature packages can observe without depending on this service target. +public typealias CEWorkspaceFileManagerObserver = WorkspaceFileObserver /// This class is used to load, modify, and listen to files on a user's machine. /// @@ -259,3 +259,5 @@ public final class CEWorkspaceFileManager: @unchecked Sendable { observers.removeAllObjects() } } + +extension CEWorkspaceFileManager: WorkspaceFileProviding {} diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift b/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift deleted file mode 100644 index d40d1a91fe..0000000000 --- a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Environment+WorkspaceFileManager.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Environment+WorkspaceFileManager.swift -// CEWorkspaceFileManager -// -// Created by Matthijs Eikelenboom on 03/07/2026. -// - -import SwiftUI - -private struct WorkspaceFileManagerKey: EnvironmentKey { - nonisolated(unsafe) static let defaultValue: CEWorkspaceFileManager? = nil -} - -public extension EnvironmentValues { - var workspaceFileManager: CEWorkspaceFileManager? { - get { self[WorkspaceFileManagerKey.self] } - set { self[WorkspaceFileManagerKey.self] = newValue } - } -} From e8c6473c0601fddfaa47a1b5f91bb8fb6214ac57 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 12:44:09 +0200 Subject: [PATCH 136/335] Refactor: Split CodeEditServices umbrella into per-service products --- CodeEdit.xcodeproj/project.pbxproj | 17 ++++++++++++----- .../Services/CodeEditServices/Package.swift | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 0563361a03..cc32480ecb 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -17,7 +17,8 @@ 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; - 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* CodeEditServices */; }; + 588957132FFA679E004BE116 /* ShellClient in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* ShellClient */; }; + 5889571530CE12AB004BE116 /* CEWorkspaceFileManager in Frameworks */ = {isa = PBXBuildFile; productRef = 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */; }; 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58ED10022FFB0001004BE116 /* CEEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* CEEditor */; }; @@ -198,7 +199,8 @@ 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */, 6CCF6DD32E26D48F00B94F75 /* SwiftTerm in Frameworks */, - 588957132FFA679E004BE116 /* CodeEditServices in Frameworks */, + 588957132FFA679E004BE116 /* ShellClient in Frameworks */, + 5889571530CE12AB004BE116 /* CEWorkspaceFileManager in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, 5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */, 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, @@ -358,7 +360,8 @@ 5AD0C0DE2D00000000000002 /* CodeEditDocument */, 5AD0C0DE2D00000000000012 /* CodeEditSettings */, 588950C42FFA5C05004BE116 /* CESearch */, - 588957122FFA679E004BE116 /* CodeEditServices */, + 588957122FFA679E004BE116 /* ShellClient */, + 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */, 5889639D2FFA9A87004BE116 /* CENotifications */, 58ED10012FFB0001004BE116 /* CEEditor */, ); @@ -1906,9 +1909,13 @@ isa = XCSwiftPackageProductDependency; productName = CESearch; }; - 588957122FFA679E004BE116 /* CodeEditServices */ = { + 588957122FFA679E004BE116 /* ShellClient */ = { isa = XCSwiftPackageProductDependency; - productName = CodeEditServices; + productName = ShellClient; + }; + 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */ = { + isa = XCSwiftPackageProductDependency; + productName = CEWorkspaceFileManager; }; 5889639D2FFA9A87004BE116 /* CENotifications */ = { isa = XCSwiftPackageProductDependency; diff --git a/Packages/Services/CodeEditServices/Package.swift b/Packages/Services/CodeEditServices/Package.swift index 544f894f53..d624688371 100644 --- a/Packages/Services/CodeEditServices/Package.swift +++ b/Packages/Services/CodeEditServices/Package.swift @@ -6,10 +6,10 @@ let package = Package( name: "CodeEditServices", platforms: [.macOS(.v14)], products: [ - // Umbrella product: the app links this once; each service target is - // its own module (`import ShellClient`, `import CEWorkspaceFileManager`). - // Adding a service later is a manifest-only change. - .library(name: "CodeEditServices", targets: ["ShellClient", "CEWorkspaceFileManager"]) + // One product per service: every consumer's manifest names exactly the + // services it links (decided 2026-07-12; replaced the single umbrella). + .library(name: "ShellClient", targets: ["ShellClient"]), + .library(name: "CEWorkspaceFileManager", targets: ["CEWorkspaceFileManager"]) ], dependencies: [ .package(path: "../../Foundation/CodeEditCore") From a7e537d810a5d53e0b735060dde184fb1f1f48fc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 16:46:47 +0200 Subject: [PATCH 137/335] Refactor: Retype LSPService.workspaceFinder to return the workspace root URL --- CodeEdit/AppDependencies.swift | 2 +- CodeEdit/Features/LSP/Service/LSPService.swift | 13 +++++++------ .../Infrastructure/WorkspaceFileOpener.swift | 6 ++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index d2eaba5c53..808d89dc04 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -40,7 +40,7 @@ final class AppDependencies { // `lspService`, so init injection in both directions would recurse. Resolved at call // time, long after both objects exist. service.workspaceFinder = { [weak self] url in - self?.workspaceWindowManager.workspace(containing: url) + self?.workspaceWindowManager.workspace(containing: url)?.fileURL } return service }() diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index b9029b570f..caf00b50db 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -156,10 +156,11 @@ final class LSPService: ObservableObject, LSPServiceProtocol { private let notificationManager: NotificationManaging - /// Resolves the workspace that owns a file URL. Property-injected (not init-injected) by the - /// composition root because the window manager's own construction consumes `LSPService` — - /// init injection in both directions would recurse. Assigned before any document opens. - var workspaceFinder: (URL) -> Workspace? = { _ in nil } + /// Resolves the root URL of the workspace that owns a file URL. Property-injected (not + /// init-injected) by the composition root because the window manager's own construction + /// consumes `LSPService` — init injection in both directions would recurse. Assigned + /// before any document opens. + var workspaceFinder: (URL) -> URL? = { _ in nil } init(notificationManager: NotificationManaging) { self.notificationManager = notificationManager @@ -230,11 +231,11 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// - Note: Must be invoked after the contents of the file are available. /// - Parameter document: The code document that was opened. func openDocument(_ document: CodeFileDocument) { - guard let workspace = document.fileURL.flatMap({ workspaceFinder($0) }), - let workspacePath = workspace.fileURL?.absolutePath, + guard let workspaceURL = document.fileURL.flatMap({ workspaceFinder($0) }), let lspLanguage = document.getLanguage().lspLanguage else { return } + let workspacePath = workspaceURL.absolutePath Task { let languageServer: LanguageServerType do { diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift index 9ac072fe7c..38f928335f 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift @@ -10,11 +10,13 @@ import Foundation /// Command interface for opening a file in the workspace that owns it. /// One rightful handler (the app shell binds it); features request, never resolve. public protocol WorkspaceFileOpener: AnyObject { - @MainActor func openFile(at url: URL) + @MainActor + func openFile(at url: URL) } /// Default no-op used until the app registers a real implementation. public final class NoOpWorkspaceFileOpener: WorkspaceFileOpener { public init() {} - @MainActor public func openFile(at url: URL) {} + @MainActor + public func openFile(at url: URL) {} } From bccb43e76095437854a2a94030f6abb91e41d086 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 16:50:16 +0200 Subject: [PATCH 138/335] Fix: Give LSP integration tests an isolated AppDependencies graph (NSApp.delegate is SwiftUI's adaptor wrapper) --- .../LSP/LanguageServer+CodeFileDocument.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 638f6324c9..7837d58f5d 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -28,13 +28,25 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { var tempTestDir: URL! - /// The host app's live dependency graph (the test bundle runs inside CodeEdit). - @MainActor var appDependencies: AppDependencies { - (NSApplication.shared.delegate as! AppDelegate).dependencies // swiftlint:disable:this force_cast - } + /// A dedicated dependency graph for this test class. `NSApp.delegate` is SwiftUI's + /// adaptor wrapper (not our `AppDelegate`), so the host graph isn't reachable from + /// tests; an isolated graph is cleaner anyway. The static + /// `CodeFileDocument.delegateProvider` is repointed at it in `setUp` so document + /// lifecycle notifications reach THIS graph's `LSPService`, and restored in `tearDown`. + private var testDependencies: AppDependencies! + private var previousDelegateProvider: (() -> CodeFileDocumentDelegate?)! + + @MainActor var appDependencies: AppDependencies { testDependencies } override func setUp() { continueAfterFailure = false + // XCTest invokes setUp on the main thread; AppDependencies is main-actor isolated. + MainActor.assumeIsolated { + let dependencies = AppDependencies() + testDependencies = dependencies + previousDelegateProvider = CodeFileDocument.delegateProvider + CodeFileDocument.delegateProvider = { dependencies.codeFileDocumentDelegate } + } do { let tempDir = FileManager.default.temporaryDirectory.appending( path: "codeedit-lsp-tests" @@ -51,6 +63,10 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { } override func tearDown() { + MainActor.assumeIsolated { + CodeFileDocument.delegateProvider = previousDelegateProvider + testDependencies = nil + } do { try FileManager.default.removeItem(at: tempTestDir) } catch { From 9c1a48e25efc6e5263d97faef5a6fec0e01702b5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 16:51:42 +0200 Subject: [PATCH 139/335] Refactor: Delete LSPService's dead install-notification path and its SwiftUI/notification dependencies --- CodeEdit/AppDependencies.swift | 2 +- .../Features/LSP/Service/LSPService.swift | 40 +------------------ .../LSP/LSPServiceDocumentObjectsTests.swift | 6 +-- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 808d89dc04..314d8d5add 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -35,7 +35,7 @@ final class AppDependencies { private(set) lazy var notificationManager: NotificationManaging = NotificationManager(eventBus: eventBus) private(set) lazy var lspService: LSPService = { - let service = LSPService(notificationManager: notificationManager) + let service = LSPService() // Property-injected (not init-injected): the window manager's construction consumes // `lspService`, so init injection in both directions would recurse. Resolved at call // time, long after both objects exist. diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index caf00b50db..b243e95e9c 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -9,12 +9,10 @@ import os.log import CodeEditSettings import CodeEditDocument import JSONRPC -import SwiftUI import Foundation import LanguageClient import LanguageServerProtocol import CodeEditLanguages -import CENotifications /// `LSPService` is a service class responsible for managing the lifecycle and event handling /// of Language Server Protocol (LSP) clients within the CodeEdit application. It handles the initialization, @@ -132,9 +130,6 @@ final class LSPService: ObservableObject, LSPServiceProtocol { @AppSettings(\.developerSettings.lspBinaries) var lspBinaries - @Environment(\.openWindow) - private var openWindow - /// Returns the language-server objects for a document, creating and storing them on first use. /// A document without a URI (e.g. untitled) gets a fresh, unstored instance — it has no server. func languageServerObjects(for document: CodeFileDocument) -> LanguageServerDocumentObjects { @@ -154,16 +149,13 @@ final class LSPService: ObservableObject, LSPServiceProtocol { documentObjects[uri] = nil } - private let notificationManager: NotificationManaging - /// Resolves the root URL of the workspace that owns a file URL. Property-injected (not /// init-injected) by the composition root because the window manager's own construction /// consumes `LSPService` — init injection in both directions would recurse. Assigned /// before any document opens. var workspaceFinder: (URL) -> URL? = { _ in nil } - init(notificationManager: NotificationManaging) { - self.notificationManager = notificationManager + init() { // Load the LSP binaries from the developer menu for binary in lspBinaries { if let language = LanguageIdentifier(rawValue: binary.key) { @@ -245,7 +237,6 @@ final class LSPService: ObservableObject, LSPServiceProtocol { languageServer = try await self.startServer(for: lspLanguage, workspacePath: workspacePath) } } catch { - notifyToInstallLanguageServer(language: lspLanguage) // swiftlint:disable:next line_length self.logger.error("Failed to find/start server for language: \(lspLanguage.rawValue), workspace: \(workspacePath, privacy: .private)") return @@ -352,35 +343,6 @@ final class LSPService: ObservableObject, LSPServiceProtocol { } } -extension LSPService { - private func notifyToInstallLanguageServer(language lspLanguage: LanguageIdentifier) { - // TODO: Re-Enable when this is more fleshed out (don't send duplicate notifications in a session) - return - // FIXME: Unreachable code - remove or re-enable when ready - /* - let lspLanguageTitle = lspLanguage.rawValue.capitalized - let notificationTitle = "Install \(lspLanguageTitle) Language Server" - // Make sure the user doesn't have the same existing notification - guard !notificationManager.notifications.contains(where: { $0.title == notificationTitle }) else { - return - } - - notificationManager.post( - iconSymbol: "arrow.down.circle", - iconColor: .clear, - title: notificationTitle, - description: "Install the \(lspLanguageTitle) language server to enable code intelligence features.", - actionButtonTitle: "Install" - ) { [weak self] in - // TODO: Warning: - // Accessing Environment's value outside of being installed on a View. - // This will always read the default value and will not update - self?.openWindow(sceneID: .settings) - } - */ - } -} - // MARK: - Errors enum ServerManagerError: Error { diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift index e4e4583e67..e77065fb34 100644 --- a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -6,16 +6,12 @@ // import XCTest -import CodeEditCore import CodeEditDocument -import CENotifications @testable import CodeEdit @MainActor final class LSPServiceDocumentObjectsTests: XCTestCase { - private func makeService() -> LSPService { - LSPService(notificationManager: NotificationManager(eventBus: EventBus())) - } + private func makeService() -> LSPService { LSPService() } private func makeDocument(path: String) throws -> CodeFileDocument { let url = FileManager.default.temporaryDirectory From ecd5f6b564cd972326f34a0d9cc584d059314e85 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 16:53:24 +0200 Subject: [PATCH 140/335] Refactor: Route RegistryManager error notifications through the ErrorNotifying Core seam --- CodeEdit/AppDependencies.swift | 4 ++- .../LSP/Registry/RegistryManager.swift | 15 ++++------ .../Workspace/Services/AppErrorNotifier.swift | 30 +++++++++++++++++++ CodeEditTests/Features/LSP/Registry.swift | 3 +- .../Infrastructure/ErrorNotifying.swift | 19 ++++++++++++ 5 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 CodeEdit/Features/Workspace/Services/AppErrorNotifier.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 314d8d5add..752f4b1af0 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -45,9 +45,11 @@ final class AppDependencies { return service }() + private(set) lazy var errorNotifier: ErrorNotifying = AppErrorNotifier(notificationManager: notificationManager) + private(set) lazy var registryManager = RegistryManager( eventBus: eventBus, - notificationManager: notificationManager, + errorNotifier: errorNotifier, shellClient: shellClient ) diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/CodeEdit/Features/LSP/Registry/RegistryManager.swift index 6f58fc9b3e..7f1328a729 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/CodeEdit/Features/LSP/Registry/RegistryManager.swift @@ -11,7 +11,6 @@ import Foundation import ZIPFoundation import Combine import CodeEditCore -import CENotifications @MainActor final class RegistryManager: ObservableObject, RegistryManaging { @@ -51,12 +50,12 @@ final class RegistryManager: ObservableObject, RegistryManaging { var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] private let eventBus: EventBus - private let notificationManager: NotificationManaging + private let errorNotifier: ErrorNotifying private let shellClient: ShellClientProtocol - init(eventBus: EventBus, notificationManager: NotificationManaging, shellClient: ShellClientProtocol) { + init(eventBus: EventBus, errorNotifier: ErrorNotifying, shellClient: ShellClientProtocol) { self.eventBus = eventBus - self.notificationManager = notificationManager + self.errorNotifier = errorNotifier self.shellClient = shellClient // Load the registry items from disk again after cache expires if let items = loadItemsFromDisk() { @@ -182,13 +181,9 @@ final class RegistryManager: ObservableObject, RegistryManaging { fail failed: Bool ) { if failed { - notificationManager.post( - iconSymbol: "xmark.circle", - iconColor: .clear, + errorNotifier.postError( title: "Could not install \(activityName)", - description: "There was a problem during installation.", - actionButtonTitle: "Done", - action: {}, + description: "There was a problem during installation." ) } else { eventBus.publish(TaskNotificationEvent( diff --git a/CodeEdit/Features/Workspace/Services/AppErrorNotifier.swift b/CodeEdit/Features/Workspace/Services/AppErrorNotifier.swift new file mode 100644 index 0000000000..1a530e1534 --- /dev/null +++ b/CodeEdit/Features/Workspace/Services/AppErrorNotifier.swift @@ -0,0 +1,30 @@ +// +// AppErrorNotifier.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +import CodeEditCore +import CENotifications + +/// App-shell binding of the `ErrorNotifying` seam onto the notification system. +final class AppErrorNotifier: ErrorNotifying { + private let notificationManager: NotificationManaging + + init(notificationManager: NotificationManaging) { + self.notificationManager = notificationManager + } + + @MainActor + func postError(title: String, description: String) { + notificationManager.post( + iconSymbol: "xmark.circle", + iconColor: .clear, + title: title, + description: description, + actionButtonTitle: "Done", + action: {} + ) + } +} diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index f8630d480e..6ea20e8fa2 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -8,7 +8,6 @@ import Testing import Foundation import CodeEditCore -import CENotifications import ShellClient @testable import CodeEdit @@ -17,7 +16,7 @@ import ShellClient struct RegistryTests { var registry: RegistryManager = RegistryManager( eventBus: EventBus(), - notificationManager: NotificationManager(eventBus: EventBus()), + errorNotifier: NoOpErrorNotifier(), shellClient: ShellClient() ) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift new file mode 100644 index 0000000000..0b2b97be25 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift @@ -0,0 +1,19 @@ +// +// ErrorNotifying.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +import Foundation + +/// Posts a user-visible error notification. A one-method seam so packages can +/// surface errors without depending on the CENotifications feature package. +public protocol ErrorNotifying: AnyObject { + @MainActor func postError(title: String, description: String) +} + +public final class NoOpErrorNotifier: ErrorNotifying { + public init() {} + @MainActor public func postError(title: String, description: String) {} +} From 2bbdba3ad2a0a19764231a6d6b12d10ffd643aee Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 16:54:16 +0200 Subject: [PATCH 141/335] Refactor: Move LSP-support extensions from Utils into the LSP feature --- .../LSP/Utils}/LanguageIdentifier+CodeLanguage.swift | 0 .../LSP/Utils}/SemanticToken+Position.swift | 0 .../TextView => Features/LSP/Utils}/TextView+LSPRange.swift | 0 .../LSP/Utils}/TextView+SemanticTokenRangeProvider.swift | 0 .../{Utils/Extensions/URL => Features/LSP/Utils}/URL+LSPURI.swift | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/{Utils/Extensions/LanguageIdentifier => Features/LSP/Utils}/LanguageIdentifier+CodeLanguage.swift (100%) rename CodeEdit/{Utils/Extensions/SemanticToken => Features/LSP/Utils}/SemanticToken+Position.swift (100%) rename CodeEdit/{Utils/Extensions/TextView => Features/LSP/Utils}/TextView+LSPRange.swift (100%) rename CodeEdit/{Utils/Extensions/TextView => Features/LSP/Utils}/TextView+SemanticTokenRangeProvider.swift (100%) rename CodeEdit/{Utils/Extensions/URL => Features/LSP/Utils}/URL+LSPURI.swift (100%) diff --git a/CodeEdit/Utils/Extensions/LanguageIdentifier/LanguageIdentifier+CodeLanguage.swift b/CodeEdit/Features/LSP/Utils/LanguageIdentifier+CodeLanguage.swift similarity index 100% rename from CodeEdit/Utils/Extensions/LanguageIdentifier/LanguageIdentifier+CodeLanguage.swift rename to CodeEdit/Features/LSP/Utils/LanguageIdentifier+CodeLanguage.swift diff --git a/CodeEdit/Utils/Extensions/SemanticToken/SemanticToken+Position.swift b/CodeEdit/Features/LSP/Utils/SemanticToken+Position.swift similarity index 100% rename from CodeEdit/Utils/Extensions/SemanticToken/SemanticToken+Position.swift rename to CodeEdit/Features/LSP/Utils/SemanticToken+Position.swift diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift b/CodeEdit/Features/LSP/Utils/TextView+LSPRange.swift similarity index 100% rename from CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift rename to CodeEdit/Features/LSP/Utils/TextView+LSPRange.swift diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift b/CodeEdit/Features/LSP/Utils/TextView+SemanticTokenRangeProvider.swift similarity index 100% rename from CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift rename to CodeEdit/Features/LSP/Utils/TextView+SemanticTokenRangeProvider.swift diff --git a/CodeEdit/Utils/Extensions/URL/URL+LSPURI.swift b/CodeEdit/Features/LSP/Utils/URL+LSPURI.swift similarity index 100% rename from CodeEdit/Utils/Extensions/URL/URL+LSPURI.swift rename to CodeEdit/Features/LSP/Utils/URL+LSPURI.swift From e67f35d2042d04220b6072ec61e8b4e35e5a3834 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 16:55:30 +0200 Subject: [PATCH 142/335] Refactor: Scaffold CELSP feature package with LSPUtil seed --- CodeEdit.xcodeproj/project.pbxproj | 7 ++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 ++ Packages/Features/CELSP/Package.swift | 42 +++++++++++++++++++ .../CELSP/Sources/CELSP}/LSPUtil.swift | 0 4 files changed, 52 insertions(+) create mode 100644 Packages/Features/CELSP/Package.swift rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LSPUtil.swift (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index cc32480ecb..fd95848ac1 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; + 58CE15A100000001004BE201 /* CELSP in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE15A000000001004BE200 /* CELSP */; }; 588957132FFA679E004BE116 /* ShellClient in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* ShellClient */; }; 5889571530CE12AB004BE116 /* CEWorkspaceFileManager in Frameworks */ = {isa = PBXBuildFile; productRef = 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */; }; 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; @@ -213,6 +214,7 @@ 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, 588950C52FFA5C05004BE116 /* CESearch in Frameworks */, + 58CE15A100000001004BE201 /* CELSP in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, 6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */, @@ -360,6 +362,7 @@ 5AD0C0DE2D00000000000002 /* CodeEditDocument */, 5AD0C0DE2D00000000000012 /* CodeEditSettings */, 588950C42FFA5C05004BE116 /* CESearch */, + 58CE15A000000001004BE200 /* CELSP */, 588957122FFA679E004BE116 /* ShellClient */, 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */, 5889639D2FFA9A87004BE116 /* CENotifications */, @@ -1909,6 +1912,10 @@ isa = XCSwiftPackageProductDependency; productName = CESearch; }; + 58CE15A000000001004BE200 /* CELSP */ = { + isa = XCSwiftPackageProductDependency; + productName = CELSP; + }; 588957122FFA679E004BE116 /* ShellClient */ = { isa = XCSwiftPackageProductDependency; productName = ShellClient; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 753c12700e..7c8186d8a6 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -39,5 +39,8 @@ + + diff --git a/Packages/Features/CELSP/Package.swift b/Packages/Features/CELSP/Package.swift new file mode 100644 index 0000000000..c4a8994009 --- /dev/null +++ b/Packages/Features/CELSP/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CELSP", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CELSP", targets: ["CELSP"]) + ], + dependencies: [ + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditDocument"), + .package(path: "../../Foundation/CodeEditSettings"), + .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), + .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), + .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), + .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.8.2"), + .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.14.0"), + .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.9.0"), + .package(url: "https://github.com/weichsel/ZIPFoundation", exact: "0.9.19"), + .package(url: "https://github.com/apple/swift-async-algorithms.git", exact: "1.0.1") + ], + targets: [ + .target( + name: "CELSP", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditDocument", package: "CodeEditDocument"), + .product(name: "CodeEditSettings", package: "CodeEditSettings"), + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "CodeEditTextView", package: "CodeEditTextView"), + .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), + .product(name: "LanguageClient", package: "LanguageClient"), + .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), + .product(name: "JSONRPC", package: "JSONRPC"), + .product(name: "ZIPFoundation", package: "ZIPFoundation"), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") + ] + ) + ] +) diff --git a/CodeEdit/Features/LSP/LSPUtil.swift b/Packages/Features/CELSP/Sources/CELSP/LSPUtil.swift similarity index 100% rename from CodeEdit/Features/LSP/LSPUtil.swift rename to Packages/Features/CELSP/Sources/CELSP/LSPUtil.swift From 95f106aaf92282b56a9cf9176843c3240847efa8 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 17:18:37 +0200 Subject: [PATCH 143/335] Refactor: Move the LSP feature into the CELSP package --- CodeEdit/AppDelegate.swift | 1 + CodeEdit/AppDependencies.swift | 1 + .../AppCodeFileDocumentDelegate.swift | 1 + .../Settings/Models/SettingsData+Search.swift | 1 + .../DeveloperSettingsView.swift | 1 + .../LanguageServerInstallView.swift | 1 + .../Extensions/LanguageServerRowView.swift | 1 + .../Extensions/LanguageServersView.swift | 1 + .../RegistryItem+FuzzySearchable.swift | 13 +++ ...nguageServerLogContainer+UtilityArea.swift | 28 ++++++ .../Sources/LanguageServerLogContainer.swift | 64 -------------- .../View/UtilityAreaOutputSourcePicker.swift | 1 + .../View/UtilityAreaOutputView.swift | 1 + .../Models/Environment+AppCommands.swift | 1 + .../UseCases/CloseWorkspaceUseCase.swift | 1 + .../LSP/LSPServiceDocumentObjectsTests.swift | 1 + .../LSP/LanguageServer+CodeFileDocument.swift | 3 + .../LSP/LanguageServer+DocumentObjects.swift | 5 +- CodeEditTests/Features/LSP/Registry.swift | 1 + .../SemanticTokenMapTests.swift | 1 + .../SemanticTokenStorageTests.swift | 1 + ...eFileDocument+LanguageServerDocument.swift | 4 +- .../DocumentSync/LSPContentCoordinator.swift | 14 +-- .../SemanticTokenHighlightProvider.swift | 5 +- .../SemanticTokens/SemanticTokenMap.swift | 0 .../SemanticTokenMapRangeProvider.swift | 0 .../GenericSemanticTokenStorage.swift | 0 .../SemanticTokenRange.swift | 0 .../SemanticTokenStorage.swift | 0 .../LanguageServer+CallHierarchy.swift | 0 .../LanguageServer+ColorPresentation.swift | 0 .../LanguageServer+Completion.swift | 0 .../LanguageServer+Declaration.swift | 0 .../LanguageServer+Definition.swift | 0 .../LanguageServer+Diagnostics.swift | 0 .../LanguageServer+DocumentColor.swift | 0 .../LanguageServer+DocumentHighlight.swift | 0 .../LanguageServer+DocumentLink.swift | 0 .../LanguageServer+DocumentSymbol.swift | 0 .../LanguageServer+DocumentSync.swift | 0 .../LanguageServer+FoldingRange.swift | 0 .../LanguageServer+Formatting.swift | 0 .../Capabilities/LanguageServer+Hover.swift | 0 .../LanguageServer+Implementation.swift | 0 .../LanguageServer+InlayHint.swift | 0 .../LanguageServer+References.swift | 0 .../Capabilities/LanguageServer+Rename.swift | 0 .../LanguageServer+SelectionRange.swift | 0 .../LanguageServer+SemanticTokens.swift | 0 .../LanguageServer+SignatureHelp.swift | 0 .../LanguageServer+TypeDefinition.swift | 0 .../CELSP}/LanguageServer/LSPCache+Data.swift | 0 .../CELSP}/LanguageServer/LSPCache.swift | 0 .../LanguageServer/LanguageServer.swift | 16 ++-- .../LanguageServerFileMap.swift | 0 .../CELSP}/LanguageServerDocument.swift | 6 +- .../Registry/Errors/PackageManagerError.swift | 0 .../InstallationMethod+PackageManager.swift | 0 .../Model/RegistryItem+InstallMethod.swift | 8 +- .../Registry/PackageManagerProtocol.swift | 0 .../FileManager+MakeExecutable.swift | 0 .../Install/InstallStepConfirmation.swift | 2 +- .../PackageManagerInstallOperation.swift | 86 +++++++++---------- .../Install/PackageManagerInstallStep.swift | 8 +- .../Install/PackageManagerProgressModel.swift | 0 .../Sources/CargoPackageManager.swift | 0 .../Sources/GithubPackageManager.swift | 0 .../Sources/GolangPackageManager.swift | 0 .../Sources/NPMPackageManager.swift | 0 .../Sources/PipPackageManager.swift | 0 .../PackageSourceParser+Cargo.swift | 0 .../PackageSourceParser+Gem.swift | 0 .../PackageSourceParser+Golang.swift | 0 .../PackageSourceParser+NPM.swift | 0 .../PackageSourceParser+PYPI.swift | 0 .../PackageSourceParser.swift | 0 .../Registry/Protocols/RegistryManaging.swift | 2 +- .../Registry/RegistryItemTemplateParser.swift | 0 .../RegistryManager+HandleRegistryFile.swift | 0 .../CELSP}/Registry/RegistryManager.swift | 26 +++--- .../Service/AppLanguageServicesProvider.swift | 6 +- .../CELSP}/Service/LSPService+Events.swift | 7 +- .../Sources/CELSP}/Service/LSPService.swift | 46 +++++----- .../CELSP}/Service/LSPServiceError.swift | 0 .../CELSP}/Service/LSPServiceProtocol.swift | 2 +- .../Service/LanguageServerLogContainer.swift | 61 +++++++++++++ .../LanguageIdentifier+CodeLanguage.swift | 0 .../CELSP}/Utils/SemanticToken+Position.swift | 0 .../CELSP}/Utils/TextView+LSPRange.swift | 0 .../TextView+SemanticTokenRangeProvider.swift | 0 .../Sources/CELSP}/Utils/URL+LSPURI.swift | 0 .../Domain/Registry/RegistryItem+Source.swift | 16 ++-- .../Domain/Registry/RegistryItem.swift | 2 +- .../Collection+subscript_safe.swift | 2 +- 94 files changed, 256 insertions(+), 192 deletions(-) create mode 100644 CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift create mode 100644 CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer+UtilityArea.swift delete mode 100644 CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/CodeFileDocument+LanguageServerDocument.swift (76%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/DocumentSync/LSPContentCoordinator.swift (85%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/SemanticTokens/SemanticTokenHighlightProvider.swift (98%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/SemanticTokens/SemanticTokenMap.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Completion.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Declaration.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Definition.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Formatting.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Hover.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Implementation.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+InlayHint.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+References.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+Rename.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/LSPCache+Data.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/LSPCache.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/LanguageServer.swift (94%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServer/LanguageServerFileMap.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/LanguageServerDocument.swift (84%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/Errors/PackageManagerError.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/Model/InstallationMethod+PackageManager.swift (100%) rename CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift => Packages/Features/CELSP/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift (85%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagerProtocol.swift (100%) rename {CodeEdit/Utils/Extensions/FileManager => Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers}/FileManager+MakeExecutable.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Install/InstallStepConfirmation.swift (79%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift (63%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Install/PackageManagerInstallStep.swift (58%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Install/PackageManagerProgressModel.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Sources/CargoPackageManager.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Sources/GithubPackageManager.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Sources/GolangPackageManager.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Sources/NPMPackageManager.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageManagers/Sources/PipPackageManager.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageSourceParser/PackageSourceParser+Gem.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageSourceParser/PackageSourceParser+Golang.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageSourceParser/PackageSourceParser+NPM.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/PackageSourceParser/PackageSourceParser.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/Protocols/RegistryManaging.swift (92%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/RegistryItemTemplateParser.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/RegistryManager+HandleRegistryFile.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Registry/RegistryManager.swift (89%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Service/AppLanguageServicesProvider.swift (69%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Service/LSPService+Events.swift (94%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Service/LSPService.swift (91%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Service/LSPServiceError.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Service/LSPServiceProtocol.swift (92%) create mode 100644 Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Utils/LanguageIdentifier+CodeLanguage.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Utils/SemanticToken+Position.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Utils/TextView+LSPRange.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Utils/TextView+SemanticTokenRangeProvider.swift (100%) rename {CodeEdit/Features/LSP => Packages/Features/CELSP/Sources/CELSP}/Utils/URL+LSPURI.swift (100%) rename {CodeEdit/Utils/Extensions/Collection => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions}/Collection+subscript_safe.swift (84%) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 3fd37e3908..d94944a879 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -5,6 +5,7 @@ // Created by Pavel Kasila on 12.03.22. // +import CELSP import Combine import CodeEditSettings import CodeEditDocument diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 752f4b1af0..24aa1bd344 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 10/07/2026. // +import CELSP import CodeEditCore import CodeEditDocument import CENotifications diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift index 8831f3390f..b9fb909967 100644 --- a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom. // +import CELSP import AppKit import CEEditor import SwiftUI diff --git a/CodeEdit/Features/Settings/Models/SettingsData+Search.swift b/CodeEdit/Features/Settings/Models/SettingsData+Search.swift index 665cc11a9a..d80fa02578 100644 --- a/CodeEdit/Features/Settings/Models/SettingsData+Search.swift +++ b/CodeEdit/Features/Settings/Models/SettingsData+Search.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom. // +import CELSP import Foundation import CodeEditSettings diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift index efa77644fb..7585e385d0 100644 --- a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 5/16/24. // +import CELSP import SwiftUI import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift index cc629a92d1..3c39a6ccfd 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 8/14/25. // +import CELSP import SwiftUI import CodeEditUI diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift index d7857b79c7..7b26162a30 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 2/2/25. // +import CELSP import SwiftUI import CodeEditCore diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 56d2fd6ba9..324a9ab7c2 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 2/2/25. // +import CELSP import SwiftUI import CodeEditCore diff --git a/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift b/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift new file mode 100644 index 0000000000..5713cc52a2 --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift @@ -0,0 +1,13 @@ +// +// RegistryItem+FuzzySearchable.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +import CELSP +import CodeEditCore + +extension RegistryItem: FuzzySearchable { + var searchableString: String { name } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer+UtilityArea.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer+UtilityArea.swift new file mode 100644 index 0000000000..4c09568ca9 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer+UtilityArea.swift @@ -0,0 +1,28 @@ +// +// LanguageServerLogContainer+UtilityArea.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +import Foundation +import CELSP + +/// Adapts CELSP's log container to the UtilityArea output protocols. Lives app-side so +/// the package stays free of UtilityArea types. +extension LanguageServerLogContainer: UtilityAreaOutputSource {} + +extension LanguageServerLogContainer.LanguageServerMessage: UtilityAreaOutputMessage { + var level: UtilityAreaLogLevel { + switch log.type { + case .error: + .error + case .warning: + .warning + case .info: + .info + case .log: + .debug + } + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift deleted file mode 100644 index 60e29c0d89..0000000000 --- a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// LanguageServerLogContainer.swift -// CodeEdit -// -// Created by Khan Winter on 7/18/25. -// - -import OSLog -import LanguageServerProtocol - -class LanguageServerLogContainer: UtilityAreaOutputSource { - struct LanguageServerMessage: UtilityAreaOutputMessage { - let log: LogMessageParams - var id: UUID = UUID() - - var message: String { - log.message - } - - var level: UtilityAreaLogLevel { - switch log.type { - case .error: - .error - case .warning: - .warning - case .info: - .info - case .log: - .debug - } - } - - var date: Date = Date() - var subsystem: String? - var category: String? - } - - let id: String - - private var streamContinuation: AsyncStream.Continuation - private var stream: AsyncStream - private(set) var logs: [LanguageServerMessage] = [] - - init(language: LanguageIdentifier) { - id = language.rawValue - (stream, streamContinuation) = AsyncStream.makeStream( - bufferingPolicy: .bufferingNewest(0) - ) - } - - func appendLog(_ log: LogMessageParams) { - let message = LanguageServerMessage(log: log) - logs.append(message) - streamContinuation.yield(message) - } - - func cachedMessages() -> [LanguageServerMessage] { - logs - } - - func streamMessages() -> AsyncStream { - stream - } -} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index 01b93f1a4c..91e894834c 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 7/18/25. // +import CELSP import SwiftUI import Combine import CodeEditSettings diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift index 94bd5a5a7c..f191b20093 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 5/25/23. // +import CELSP import SwiftUI import LogStream diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index f956c1fded..261b0391ae 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 10/07/2026. // +import CELSP import SwiftUI import CodeEditCore import CEEditor diff --git a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift index 77b1b9a425..496bc80886 100644 --- a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 12/04/26. // +import CELSP import Foundation /// Coordinates cleanup when a workspace is closed (LSP shutdown + workspace teardown). diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift index e77065fb34..f7757aeca7 100644 --- a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 06/07/2026. // +@testable import CELSP import XCTest import CodeEditDocument @testable import CodeEdit diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 7837d58f5d..974fc74f02 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 9/9/24. // +@testable import CELSP import CEWorkspaceFileManager import CodeEditDocument import XCTest @@ -74,6 +75,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { } } + @MainActor func makeTestServer() async throws -> (connection: BufferingServerConnection, server: LanguageServerType) { let bufferingConnection = BufferingServerConnection() var capabilities = ServerCapabilities() @@ -121,6 +123,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { return (workspace, fileManager) } + @MainActor func openCodeFile( for server: LanguageServerType, connection: BufferingServerConnection, diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift index bcb5502639..1dfdab8fad 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 2/12/25. // +@testable import CELSP import XCTest import CodeEditTextView import CodeEditSourceEditor @@ -14,8 +15,10 @@ import LanguageServerProtocol @testable import CodeEdit +@MainActor final class LanguageServerDocumentObjectsTests: XCTestCase { - final class MockDocumentType: LanguageServerDocument { + @MainActor + final class MockDocumentType: @preconcurrency LanguageServerDocument { var content: NSTextStorage? var languageServerURI: String? /// Test-local store (the protocol no longer requires it; `LSPService` owns it in production). diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index 6ea20e8fa2..282a5e9bcf 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 2/2/25. // +@testable import CELSP import Testing import Foundation import CodeEditCore diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift index a9ec5c5a3b..b2aaf755c2 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 12/14/24. // +@testable import CELSP import XCTest import CodeEditSourceEditor import LanguageServerProtocol diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift index f2d0179caf..e5c459cea1 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 12/26/24. // +@testable import CELSP import Foundation import Testing import CodeEditSourceEditor diff --git a/CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift b/Packages/Features/CELSP/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift similarity index 76% rename from CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift rename to Packages/Features/CELSP/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift index 819fa48a2f..baef27c4ac 100644 --- a/CodeEdit/Features/LSP/CodeFileDocument+LanguageServerDocument.swift +++ b/Packages/Features/CELSP/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift @@ -8,10 +8,10 @@ import AppKit import CodeEditDocument -extension CodeFileDocument: LanguageServerDocument { +extension CodeFileDocument: @preconcurrency LanguageServerDocument { /// A stable string to use when identifying documents with language servers. /// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI. - var languageServerURI: String? { + public var languageServerURI: String? { fileURL?.lspURI } } diff --git a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift b/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift similarity index 85% rename from CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift index 8411d4e4ce..691bd7bb18 100644 --- a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift @@ -20,7 +20,8 @@ import LanguageServerProtocol /// Language servers expect edits to be sent in chunks (and it helps reduce processing overhead). To do this, this class /// keeps an async stream around for the duration of its lifetime. The stream is sent edit notifications, which are then /// chunked into 250ms timed groups before being sent to the ``LanguageServer``. -class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { +@MainActor +class LSPContentCoordinator: @preconcurrency TextViewCoordinator, @preconcurrency TextViewDelegate { // Required to avoid a large_tuple lint error private struct SequenceElement: Sendable { let uri: String @@ -29,10 +30,13 @@ class LSPContentCoordinator: TextViewCoord } private var editedRange: LSPRange? - private var sequenceContinuation: AsyncStream.Continuation? - private var task: Task? + // nonisolated(unsafe): assigned on the main actor during setup; read from the + // detached debounce task (`languageServer`, `sequenceContinuation`) and from + // `deinit` (`task`, `sequenceContinuation`), which cannot be actor-isolated. + private nonisolated(unsafe) var sequenceContinuation: AsyncStream.Continuation? + private nonisolated(unsafe) var task: Task? - weak var languageServer: LanguageServer? + nonisolated(unsafe) weak var languageServer: LanguageServer? var documentURI: String? /// Initializes a content coordinator, and begins an async stream of updates @@ -89,7 +93,7 @@ class LSPContentCoordinator: TextViewCoord self.sequenceContinuation?.yield(SequenceElement(uri: documentURI, range: lspRange, string: string)) } - func destroy() { + nonisolated func destroy() { task?.cancel() task = nil sequenceContinuation?.finish() diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift similarity index 98% rename from CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift index 2fbfb8ea8a..81f666721f 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift @@ -7,7 +7,7 @@ import Foundation import LanguageServerProtocol -import CodeEditSourceEditor +@preconcurrency import CodeEditSourceEditor import CodeEditTextView import CodeEditLanguages @@ -21,10 +21,11 @@ import CodeEditLanguages /// ``SemanticTokenHighlightProvider/applyEdit(textView:range:delta:completion:)`` method. One might expect this class /// to respond to that method immediately, but it does not. It instead stores the completion passed in that method until /// it can respond to the edit with invalidated indices. +@MainActor final class SemanticTokenHighlightProvider< Storage: GenericSemanticTokenStorage, DocumentType: LanguageServerDocument ->: HighlightProviding { +>: @preconcurrency HighlightProviding { enum HighlightError: Error { case lspRangeFailure } diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift similarity index 100% rename from CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift similarity index 100% rename from CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift similarity index 100% rename from CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift similarity index 100% rename from CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift b/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift similarity index 100% rename from CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift rename to Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Completion.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Completion.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Definition.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Definition.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Hover.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Hover.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+References.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+References.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Rename.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Rename.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/LSPCache+Data.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache+Data.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/LSPCache+Data.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache+Data.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/LSPCache.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/LSPCache.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache.swift diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift similarity index 94% rename from CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift index 8db31d832b..f422631436 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift @@ -12,14 +12,18 @@ import LanguageServerProtocol import OSLog /// A client for language servers. -class LanguageServer { - static var logger: Logger { // types with associated types cannot have constant static properties +/// Main-actor isolated: per-document work touches main-actor documents and editor +/// objects; network calls hop to the connection internally and pass only Sendable +/// LSP payloads. +@MainActor +public class LanguageServer { + nonisolated static var logger: Logger { // types with associated types cannot have constant static properties Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer") } let logger: Logger /// Identifies which language the server belongs to - let languageId: LanguageIdentifier + public let languageId: LanguageIdentifier /// Holds information about the language server binary let binary: LanguageServerBinary /// A cache to hold responses from the server, to minimize duplicate server requests @@ -42,7 +46,7 @@ class LanguageServer { /// The configuration options this server supports. var serverCapabilities: ServerCapabilities - var logContainer: LanguageServerLogContainer + public var logContainer: LanguageServerLogContainer /// An instance of a language server, that may or may not be initialized private(set) var lspInstance: InitializingServer @@ -136,7 +140,7 @@ class LanguageServer { /// - languageId: The ID of the language to create the channel for. /// - executionParams: The parameters for executing the local process. /// - Returns: A new connection to the language server. - static func makeLocalServerConnection( + nonisolated static func makeLocalServerConnection( languageId: LanguageIdentifier, executionParams: Process.ExecutionParameters, logContainer: LanguageServerLogContainer @@ -161,7 +165,7 @@ class LanguageServer { // MARK: - Get Init Params // swiftlint:disable function_body_length - static func getInitParams(workspacePath: String) -> InitializingServer.InitializeParamsProvider { + nonisolated static func getInitParams(workspacePath: String) -> InitializingServer.InitializeParamsProvider { let provider: InitializingServer.InitializeParamsProvider = { // Text Document Capabilities let textDocumentCapabilities = TextDocumentClientCapabilities( diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift diff --git a/CodeEdit/Features/LSP/LanguageServerDocument.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServerDocument.swift similarity index 84% rename from CodeEdit/Features/LSP/LanguageServerDocument.swift rename to Packages/Features/CELSP/Sources/CELSP/LanguageServerDocument.swift index 652afc17f9..2554fcb15e 100644 --- a/CodeEdit/Features/LSP/LanguageServerDocument.swift +++ b/Packages/Features/CELSP/Sources/CELSP/LanguageServerDocument.swift @@ -10,12 +10,14 @@ import CodeEditDocument import CodeEditLanguages /// A set of properties a language server sets when a document is registered. +/// Main-actor isolated: its members are editor-facing (text coordinator, highlight +/// provider) and are created/used from the main actor by `LSPService`. +@MainActor struct LanguageServerDocumentObjects { var textCoordinator: LSPContentCoordinator = LSPContentCoordinator() // swiftlint:disable:next line_length var highlightProvider: SemanticTokenHighlightProvider = SemanticTokenHighlightProvider() - @MainActor func setUp(server: LanguageServer, document: DocumentType) { textCoordinator.setUp(server: server, document: document) highlightProvider.setUp(server: server, document: document) @@ -27,7 +29,7 @@ struct LanguageServerDocumentObjects { /// Deliberately holds no LSP-typed state: the per-document objects /// (``LanguageServerDocumentObjects``) are owned by `LSPService`, keyed by URI, so conformers /// (e.g. `CodeFileDocument`) need not depend on LSP types. -protocol LanguageServerDocument: AnyObject { +public protocol LanguageServerDocument: AnyObject { var content: NSTextStorage? { get } var languageServerURI: String? { get } func getLanguage() -> CodeLanguage diff --git a/CodeEdit/Features/LSP/Registry/Errors/PackageManagerError.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Errors/PackageManagerError.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/Errors/PackageManagerError.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Errors/PackageManagerError.swift diff --git a/CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/Model/InstallationMethod+PackageManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift diff --git a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift similarity index 85% rename from CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift index ceedf03446..d7ba2fe5a5 100644 --- a/CodeEdit/Features/LSP/Registry/Model/RegistryItem+AppExtensions.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift @@ -1,5 +1,5 @@ // -// RegistryItem+AppExtensions.swift +// RegistryItem+InstallMethod.swift // CodeEdit // // Created by Matthijs Eikelenboom on 12/04/26. @@ -8,13 +8,9 @@ import Foundation import CodeEditCore -extension RegistryItem: FuzzySearchable { - var searchableString: String { name } -} - extension RegistryItem { /// The method for installation, parsed from this item's ``source`` parameter. - var installMethod: InstallationMethod? { + public var installMethod: InstallationMethod? { let sourceId = source.id if sourceId.hasPrefix("pkg:cargo/") { return PackageSourceParser.parseCargoPackage(self) diff --git a/CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagerProtocol.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagerProtocol.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagerProtocol.swift diff --git a/CodeEdit/Utils/Extensions/FileManager/FileManager+MakeExecutable.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift similarity index 100% rename from CodeEdit/Utils/Extensions/FileManager/FileManager+MakeExecutable.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift similarity index 79% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift index 5f9eccfe09..c2f82b08b1 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift @@ -5,7 +5,7 @@ // Created by Khan Winter on 8/8/25. // -enum InstallStepConfirmation { +public enum InstallStepConfirmation { case none case required(message: String) } diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift similarity index 63% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift index d3f656bd03..39a3221e6e 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift @@ -16,38 +16,38 @@ import CodeEditCore /// /// If a step requires confirmation, the ``waitingForConfirmation`` value will be filled. @MainActor -final class PackageManagerInstallOperation: ObservableObject, Identifiable { - enum RunningState { +public final class PackageManagerInstallOperation: ObservableObject, Identifiable { + public enum RunningState { case none case running case complete } - struct OutputItem: Identifiable, Equatable { - let id: UUID = UUID() - let isStepDivider: Bool - let outputIdx: Int? - let contents: String + public struct OutputItem: Identifiable, Equatable { + public let id: UUID = UUID() + public let isStepDivider: Bool + public let outputIdx: Int? + public let contents: String - init(outputIdx: Int? = nil, isStepDivider: Bool = false, contents: String) { + public init(outputIdx: Int? = nil, isStepDivider: Bool = false, contents: String) { self.isStepDivider = isStepDivider self.outputIdx = outputIdx self.contents = contents } } - nonisolated var id: String { package.name } + public nonisolated var id: String { package.name } - let package: RegistryItem - let steps: [PackageManagerInstallStep] + public let package: RegistryItem + public let steps: [PackageManagerInstallStep] /// The step the operation is currently executing or stopped at. - var currentStep: PackageManagerInstallStep? { + public var currentStep: PackageManagerInstallStep? { steps[safe: currentStepIdx] } /// The current state of the operation. - var runningState: RunningState { + public var runningState: RunningState { if operationTask != nil { return .running } else if error != nil || currentStepIdx == steps.count { @@ -57,10 +57,10 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { } } - @Published var accumulatedOutput: [OutputItem] = [] - @Published var currentStepIdx: Int = 0 - @Published var error: Error? - @Published var progress: Progress + @Published public var accumulatedOutput: [OutputItem] = [] + @Published public var currentStepIdx: Int = 0 + @Published public var error: Error? + @Published public var progress: Progress /// If non-nil, indicates that this operation has halted and requires confirmation. @Published public private(set) var waitingForConfirmation: String? @@ -75,14 +75,14 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { /// - Parameters: /// - package: The package to install. /// - steps: The steps that make up the operation. - init(package: RegistryItem, steps: [PackageManagerInstallStep], shellClient: ShellClientProtocol) { + public init(package: RegistryItem, steps: [PackageManagerInstallStep], shellClient: ShellClientProtocol) { self.shellClient = shellClient self.package = package self.steps = steps self.progress = Progress(totalUnitCount: Int64(steps.count)) } - func run() async throws { + public func run() async throws { guard operationTask == nil else { return } operationTask = Task { defer { operationTask = nil } @@ -91,13 +91,13 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { try await operationTask?.value } - func cancel() { + public func cancel() { operationTask?.cancel() operationTask = nil } /// Called by UI to confirm continuing to the next step - func confirmCurrentStep() { + public func confirmCurrentStep() { waitingForConfirmation = nil confirmationContinuation?.resume() confirmationContinuation = nil @@ -130,34 +130,30 @@ final class PackageManagerInstallOperation: ObservableObject, Identifiable { try Task.checkCancellation() accumulatedOutput.append(OutputItem(isStepDivider: true, contents: "Step \(currentStepIdx + 1): \(task.name)")) - await withTaskGroup(of: Void.self) { group in - group.addTask { - for await outputItem in model.outputStream { - await MainActor.run { - switch outputItem { - case .status(let string): - self.outputIdx += 1 - self.accumulatedOutput.append(OutputItem(outputIdx: self.outputIdx, contents: string)) - case .output(let string): - self.accumulatedOutput.append(OutputItem(contents: string)) - } - } - } - } - group.addTask { - do { - try await task.handler(model) - } catch { - await MainActor.run { - self.error = error - } - } - await MainActor.run { - model.finish() + // `Task {}` inherits this method's main-actor isolation, so the capture of `model` + // and `self` stays in-region (a task group's `addTask` requires `sending` closures, + // which the non-Sendable progress model can't satisfy). + let outputForwarding = Task { + for await outputItem in model.outputStream { + switch outputItem { + case .status(let string): + self.outputIdx += 1 + self.accumulatedOutput.append(OutputItem(outputIdx: self.outputIdx, contents: string)) + case .output(let string): + self.accumulatedOutput.append(OutputItem(contents: string)) } } } + do { + try await task.handler(model) + } catch { + self.error = error + } + model.finish() + // Drain the stream so all output lands before the next step begins. + await outputForwarding.value + self.currentStepIdx += 1 try Task.checkCancellation() diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift similarity index 58% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift index 7c57106462..ce0c9a6c15 100644 --- a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift @@ -6,9 +6,9 @@ // /// Represents a single executable step in a package install. -struct PackageManagerInstallStep: Identifiable { - var id: String { name } - let name: String - let confirmation: InstallStepConfirmation +public struct PackageManagerInstallStep: Identifiable { + public var id: String { name } + public let name: String + public let confirmation: InstallStepConfirmation let handler: (_ model: PackageManagerProgressModel) async throws -> Void } diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Sources/CargoPackageManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GithubPackageManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Sources/GolangPackageManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Sources/NPMPackageManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift diff --git a/CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/PackageSourceParser/PackageSourceParser.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift diff --git a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift similarity index 92% rename from CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift index 69b6cb3d35..594730ea66 100644 --- a/CodeEdit/Features/LSP/Registry/Protocols/RegistryManaging.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift @@ -14,7 +14,7 @@ import CodeEditCore /// Note: `@Published` properties are not included because consumers /// need the concrete type for SwiftUI observation. Use `RegistryManager` directly in views. @MainActor -protocol RegistryManaging: AnyObject, ObservableObject { +public protocol RegistryManaging: AnyObject, ObservableObject { var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] { get } var isInstalling: Bool { get } diff --git a/CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryItemTemplateParser.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/RegistryItemTemplateParser.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/RegistryItemTemplateParser.swift diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift similarity index 100% rename from CodeEdit/Features/LSP/Registry/RegistryManager+HandleRegistryFile.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift diff --git a/CodeEdit/Features/LSP/Registry/RegistryManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift similarity index 89% rename from CodeEdit/Features/LSP/Registry/RegistryManager.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift index 7f1328a729..7105e65a65 100644 --- a/CodeEdit/Features/LSP/Registry/RegistryManager.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift @@ -13,7 +13,7 @@ import Combine import CodeEditCore @MainActor -final class RegistryManager: ObservableObject, RegistryManaging { +public final class RegistryManager: ObservableObject, RegistryManaging { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RegistryManager") let installPath = Settings.shared.baseURL.appending(path: "Language Servers") @@ -27,33 +27,35 @@ final class RegistryManager: ObservableObject, RegistryManaging { string: "https://github.com/mason-org/mason-registry/releases/latest/download/checksums.txt" )! - @Published var isDownloadingRegistry: Bool = false + @Published public var isDownloadingRegistry: Bool = false /// Holds an errors found while downloading the registry file. Needs a UI to dismiss, is logged. - @Published var downloadError: Error? + @Published public var downloadError: Error? /// Any currently running installation operation. - @Published var runningInstall: PackageManagerInstallOperation? + @Published public var runningInstall: PackageManagerInstallOperation? private var installTask: Task? /// Indicates if the manager is currently installing a package. - var isInstalling: Bool { + public var isInstalling: Bool { installTask != nil } /// Reference to cached registry data. Will be removed from memory after a certain amount of time. private var cachedRegistry: CachedRegistry? - /// Timer to clear expired cache - private var cleanupTimer: Timer? + /// Timer to clear expired cache. + /// nonisolated(unsafe): scheduled and invalidated on the main actor; also + /// invalidated from `deinit`, which cannot be actor-isolated. + private nonisolated(unsafe) var cleanupTimer: Timer? /// Public access to registry items with cache management @Published public private(set) var registryItems: [RegistryItem] = [] @AppSettings(\.languageServers.installedLanguageServers) - var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] + public var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] private let eventBus: EventBus private let errorNotifier: ErrorNotifying private let shellClient: ShellClientProtocol - init(eventBus: EventBus, errorNotifier: ErrorNotifying, shellClient: ShellClientProtocol) { + public init(eventBus: EventBus, errorNotifier: ErrorNotifying, shellClient: ShellClientProtocol) { self.eventBus = eventBus self.errorNotifier = errorNotifier self.shellClient = shellClient @@ -73,14 +75,14 @@ final class RegistryManager: ObservableObject, RegistryManaging { // MARK: - Enable/Disable - func setPackageEnabled(packageName: String, enabled: Bool) { + public func setPackageEnabled(packageName: String, enabled: Bool) { installedLanguageServers[packageName]?.isEnabled = enabled } // MARK: - Uninstall @MainActor - func removeLanguageServer(packageName: String) async throws { + public func removeLanguageServer(packageName: String) async throws { let packageName = packageName.removingPercentEncoding ?? packageName let packageDirectory = installPath.appending(path: packageName) @@ -197,7 +199,7 @@ final class RegistryManager: ObservableObject, RegistryManaging { // MARK: - Cache - func setRegistryItems(_ items: [RegistryItem]) { + public func setRegistryItems(_ items: [RegistryItem]) { cachedRegistry = CachedRegistry(items: items) // Set up timer to clear the cache after expiration diff --git a/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift b/Packages/Features/CELSP/Sources/CELSP/Service/AppLanguageServicesProvider.swift similarity index 69% rename from CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift rename to Packages/Features/CELSP/Sources/CELSP/Service/AppLanguageServicesProvider.swift index 3a25a41a1d..9e92754b3a 100644 --- a/CodeEdit/Features/LSP/Service/AppLanguageServicesProvider.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/AppLanguageServicesProvider.swift @@ -8,14 +8,14 @@ import CodeEditDocument @MainActor -final class AppLanguageServicesProvider: LanguageServicesProvider { +public final class AppLanguageServicesProvider: LanguageServicesProvider { private let lspService: LSPService - init(lspService: LSPService) { + public init(lspService: LSPService) { self.lspService = lspService } - func languageServices(for document: CodeFileDocument) -> LanguageServices { + public func languageServices(for document: CodeFileDocument) -> LanguageServices { let objects = lspService.languageServerObjects(for: document) return LanguageServices( textCoordinator: objects.textCoordinator, diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService+Events.swift similarity index 94% rename from CodeEdit/Features/LSP/Service/LSPService+Events.swift rename to Packages/Features/CELSP/Sources/CELSP/Service/LSPService+Events.swift index 41d61f72ea..95812036ef 100644 --- a/CodeEdit/Features/LSP/Service/LSPService+Events.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService+Events.swift @@ -6,7 +6,7 @@ // import Foundation -import LanguageClient +@preconcurrency import LanguageClient import LanguageServerProtocol extension LSPService { @@ -16,9 +16,10 @@ extension LSPService { return } - // Create a new Task to listen to the events + // Capture the connection on the main actor; the detached task only awaits its events. + let lspInstance = languageClient.lspInstance let task = Task.detached { [weak self] in - for await event in languageClient.lspInstance.eventSequence { + for await event in lspInstance.eventSequence { await self?.handleEvent(event, for: key) } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift similarity index 91% rename from CodeEdit/Features/LSP/Service/LSPService.swift rename to Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift index b243e95e9c..e07f926610 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift @@ -100,23 +100,23 @@ import CodeEditLanguages /// } /// ``` @MainActor -final class LSPService: ObservableObject, LSPServiceProtocol { - typealias LanguageServerType = LanguageServer +public final class LSPService: ObservableObject, LSPServiceProtocol { + public typealias LanguageServerType = LanguageServer let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") - struct ClientKey: Hashable, Equatable { - let languageId: LanguageIdentifier - let workspacePath: String + public struct ClientKey: Hashable, Equatable, Sendable { + public let languageId: LanguageIdentifier + public let workspacePath: String - init(_ languageId: LanguageIdentifier, _ workspacePath: String) { + public init(_ languageId: LanguageIdentifier, _ workspacePath: String) { self.languageId = languageId self.workspacePath = workspacePath } } /// Holds the active language clients - @Published var languageClients: [ClientKey: LanguageServerType] = [:] + @Published public var languageClients: [ClientKey: LanguageServerType] = [:] /// Holds the language server configurations for all the installed language servers var languageConfigs: [LanguageIdentifier: LanguageServerBinary] = [:] /// Holds all the event listeners for each active language client @@ -153,9 +153,9 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// init-injected) by the composition root because the window manager's own construction /// consumes `LSPService` — init injection in both directions would recurse. Assigned /// before any document opens. - var workspaceFinder: (URL) -> URL? = { _ in nil } + public var workspaceFinder: (URL) -> URL? = { _ in nil } - init() { + public init() { // Load the LSP binaries from the developer menu for binary in lspBinaries { if let language = LanguageIdentifier(rawValue: binary.key) { @@ -222,7 +222,7 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// Notify all relevant language clients that a document was opened. /// - Note: Must be invoked after the contents of the file are available. /// - Parameter document: The code document that was opened. - func openDocument(_ document: CodeFileDocument) { + public func openDocument(_ document: CodeFileDocument) { guard let workspaceURL = document.fileURL.flatMap({ workspaceFinder($0) }), let lspLanguage = document.getLanguage().lspLanguage else { return @@ -253,7 +253,7 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// Notify all relevant language clients that a document was closed. /// - Parameter url: The url of the document that was closed - func closeDocument(_ url: URL) { + public func closeDocument(_ url: URL) { removeLanguageServerObjects(for: url.lspURI) guard let languageClient = languageClient(forDocument: url) else { return } Task { @@ -270,12 +270,12 @@ final class LSPService: ObservableObject, LSPServiceProtocol { /// Close all language clients for a workspace. /// - /// This is intentionally synchronous so we can exit from the workspace document's ``Workspace/close()`` + /// This is intentionally synchronous so the app's workspace-close path can call it on the way out /// method ASAP. /// /// Errors thrown in this method are logged and otherwise not handled. /// - Parameter workspacePath: The path of the workspace. - func closeWorkspace(_ workspacePath: String) { + public func closeWorkspace(_ workspacePath: String) { Task { let clientKeys = self.languageClients.filter({ $0.key.workspacePath == workspacePath }) for (key, languageClient) in clientKeys { @@ -316,16 +316,14 @@ final class LSPService: ObservableObject, LSPServiceProtocol { } /// Goes through all active language servers and attempts to shut them down. - func stopAllServers() async { - await withTaskGroup(of: Void.self) { group in - for (key, server) in languageClients { - group.addTask { - do { - try await server.shutdown() - } catch { - self.logger.warning("Shutting down \(key.languageId.rawValue): Error \(error)") - } - } + /// Sequential: `LanguageServer` is main-actor isolated, and the app's quit path + /// bounds this with a timeout + SIGKILL fallback. + public func stopAllServers() async { + for (key, server) in languageClients { + do { + try await server.shutdown() + } catch { + self.logger.warning("Shutting down \(key.languageId.rawValue): Error \(error)") } } languageClients.removeAll() @@ -336,7 +334,7 @@ final class LSPService: ObservableObject, LSPServiceProtocol { } /// Call this when a server is refusing to terminate itself. Sends the `SIGKILL` signal to all lsp processes. - func killAllServers() { + public func killAllServers() { for (_, server) in languageClients { kill(server.pid, SIGKILL) } diff --git a/CodeEdit/Features/LSP/Service/LSPServiceError.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceError.swift similarity index 100% rename from CodeEdit/Features/LSP/Service/LSPServiceError.swift rename to Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceError.swift diff --git a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceProtocol.swift similarity index 92% rename from CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift rename to Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceProtocol.swift index 006e458fb6..5a8d8260c3 100644 --- a/CodeEdit/Features/LSP/Service/LSPServiceProtocol.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceProtocol.swift @@ -14,7 +14,7 @@ import CodeEditDocument /// reactive observation of `@Published` properties require the concrete type. /// Use `LSPService` directly in those cases. @MainActor -protocol LSPServiceProtocol: AnyObject { +public protocol LSPServiceProtocol: AnyObject { func openDocument(_ document: CodeFileDocument) func closeDocument(_ url: URL) func closeWorkspace(_ workspacePath: String) diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift new file mode 100644 index 0000000000..65167a81c0 --- /dev/null +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift @@ -0,0 +1,61 @@ +// +// LanguageServerLogContainer.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import Foundation +import LanguageServerProtocol + +/// Collects a language server's log messages. UtilityArea adapts this to its output +/// protocols app-side (`LanguageServerLogContainer+UtilityArea.swift`). +/// +/// `@unchecked Sendable`: logs arrive from server event streams and process +/// termination handlers on arbitrary threads; all access to `logs` is guarded +/// by `logLock`. +public final class LanguageServerLogContainer: @unchecked Sendable { + public struct LanguageServerMessage: Identifiable { + public let log: LogMessageParams + public var id: UUID = UUID() + + public var message: String { + log.message + } + + + public var date: Date = Date() + public var subsystem: String? + public var category: String? + } + + public let id: String + + private let streamContinuation: AsyncStream.Continuation + private let stream: AsyncStream + private let logLock = NSLock() + private var logs: [LanguageServerMessage] = [] + + public init(language: LanguageIdentifier) { + id = language.rawValue + (stream, streamContinuation) = AsyncStream.makeStream( + bufferingPolicy: .bufferingNewest(0) + ) + } + + public func appendLog(_ log: LogMessageParams) { + let message = LanguageServerMessage(log: log) + logLock.withLock { + logs.append(message) + } + streamContinuation.yield(message) + } + + public func cachedMessages() -> [LanguageServerMessage] { + logLock.withLock { logs } + } + + public func streamMessages() -> AsyncStream { + stream + } +} diff --git a/CodeEdit/Features/LSP/Utils/LanguageIdentifier+CodeLanguage.swift b/Packages/Features/CELSP/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift similarity index 100% rename from CodeEdit/Features/LSP/Utils/LanguageIdentifier+CodeLanguage.swift rename to Packages/Features/CELSP/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift diff --git a/CodeEdit/Features/LSP/Utils/SemanticToken+Position.swift b/Packages/Features/CELSP/Sources/CELSP/Utils/SemanticToken+Position.swift similarity index 100% rename from CodeEdit/Features/LSP/Utils/SemanticToken+Position.swift rename to Packages/Features/CELSP/Sources/CELSP/Utils/SemanticToken+Position.swift diff --git a/CodeEdit/Features/LSP/Utils/TextView+LSPRange.swift b/Packages/Features/CELSP/Sources/CELSP/Utils/TextView+LSPRange.swift similarity index 100% rename from CodeEdit/Features/LSP/Utils/TextView+LSPRange.swift rename to Packages/Features/CELSP/Sources/CELSP/Utils/TextView+LSPRange.swift diff --git a/CodeEdit/Features/LSP/Utils/TextView+SemanticTokenRangeProvider.swift b/Packages/Features/CELSP/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift similarity index 100% rename from CodeEdit/Features/LSP/Utils/TextView+SemanticTokenRangeProvider.swift rename to Packages/Features/CELSP/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift diff --git a/CodeEdit/Features/LSP/Utils/URL+LSPURI.swift b/Packages/Features/CELSP/Sources/CELSP/Utils/URL+LSPURI.swift similarity index 100% rename from CodeEdit/Features/LSP/Utils/URL+LSPURI.swift rename to Packages/Features/CELSP/Sources/CELSP/Utils/URL+LSPURI.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift index 3f2de9290a..c463498898 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift @@ -6,7 +6,7 @@ // extension RegistryItem { - public struct Source: Codable { + public struct Source: Codable, Sendable { public let id: String public let asset: AssetContainer? public let build: BuildContainer? @@ -24,7 +24,7 @@ extension RegistryItem { self.versionOverrides = versionOverrides } - public enum AssetContainer: Codable { + public enum AssetContainer: Codable, Sendable { case single(Asset) case multiple([Asset]) case simpleFile(String) @@ -85,7 +85,7 @@ extension RegistryItem { } } - public enum BuildContainer: Codable { + public enum BuildContainer: Codable, Sendable { case single(Build) case multiple([Build]) case none @@ -133,7 +133,7 @@ extension RegistryItem { } } - public struct Build: Codable { + public struct Build: Codable, Sendable { public let target: Target? public let run: String public let env: [String: String]? @@ -152,7 +152,7 @@ extension RegistryItem { } } - public struct Asset: Codable { + public struct Asset: Codable, Sendable { public let target: Target public let file: String? public let bin: BinContainer? @@ -175,7 +175,7 @@ extension RegistryItem { } } - public enum Target: Codable { + public enum Target: Codable, Sendable { case single(String) case multiple([String]) @@ -228,7 +228,7 @@ extension RegistryItem { } } - public enum BinContainer: Codable { + public enum BinContainer: Codable, Sendable { case single(String) case multiple([String: String]) @@ -260,7 +260,7 @@ extension RegistryItem { } } - public struct VersionOverride: Codable { + public struct VersionOverride: Codable, Sendable { public let constraint: String public let id: String public let asset: AssetContainer? diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift index 554c3a5239..fe86678809 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift @@ -8,7 +8,7 @@ import Foundation /// A `RegistryItem` represents an entry in the Registry that saves language servers, DAPs, linters and formatters. -public struct RegistryItem: Codable { +public struct RegistryItem: Codable, Sendable { public let name: String public let description: String public let homepage: String diff --git a/CodeEdit/Utils/Extensions/Collection/Collection+subscript_safe.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift similarity index 84% rename from CodeEdit/Utils/Extensions/Collection/Collection+subscript_safe.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift index a0a2d080e7..8830c99996 100644 --- a/CodeEdit/Utils/Extensions/Collection/Collection+subscript_safe.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift @@ -9,7 +9,7 @@ import Foundation extension Collection { /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript (safe index: Index) -> Element? { + public subscript (safe index: Index) -> Element? { indices.contains(index) ? self[index] : nil } } From bc31bd01863d71a646c7298ba9f402ae246eb019 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 18:14:57 +0200 Subject: [PATCH 144/335] Refactor: Source workspace context from SourceControlManager in source-control views --- .../Features/SourceControl/SourceControlManager.swift | 8 +++++--- .../SourceControl/Views/SourceControlFetchView.swift | 5 +---- .../SourceControl/Views/SourceControlPullView.swift | 6 +----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/CodeEdit/Features/SourceControl/SourceControlManager.swift index 923f57dc0a..e6397edea0 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/CodeEdit/Features/SourceControl/SourceControlManager.swift @@ -6,7 +6,6 @@ // import Combine -import CEWorkspaceFileManager import Foundation import OSLog import CodeEditCore @@ -25,14 +24,16 @@ final class SourceControlManager: ObservableObject { let gitClient: GitClientProtocol + /// Reads git configuration. Exposed so source-control views can consult config + /// (e.g. `pull.rebase`) without their own shell-client plumbing. + let gitConfig: GitConfigClient + /// The base URL of the workspace let workspaceURL: URL let eventBus: EventBus var fileEventCancellables: Set = [] - weak var fileManager: CEWorkspaceFileManager? - // MARK: - Git State /// A list of changed files @@ -76,6 +77,7 @@ final class SourceControlManager: ObservableObject { self.workspaceURL = workspaceURL self.eventBus = eventBus gitClient = GitClient(directoryURL: workspaceURL, shellClient: shellClient) + gitConfig = GitConfigClient(shellClient: shellClient) subscribeToWorkspaceFileEvents() Task { try? await validate() } } diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift index fc79b3d3a9..f3ffb50a4a 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import CEWorkspaceFileManager struct SourceControlFetchView: View { @Environment(\.dismiss) @@ -14,11 +13,9 @@ struct SourceControlFetchView: View { @EnvironmentObject var sourceControlManager: SourceControlManager - @Environment(\.workspaceFileManager) - private var workspaceFileManager var projectName: String { - workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" + sourceControlManager.workspaceURL.lastPathComponent } var body: some View { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift index 094a0027c5..a019ca5791 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift +++ b/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift @@ -7,7 +7,6 @@ import CodeEditCore import SwiftUI -import ShellClient struct SourceControlPullView: View { @Environment(\.dismiss) @@ -16,9 +15,6 @@ struct SourceControlPullView: View { @EnvironmentObject var sourceControlManager: SourceControlManager @EnvironmentObject var sourceControlViewModel: SourceControlViewModel - @Environment(\.shellClient) private var shellClient - - private var gitConfig: GitConfigClient { GitConfigClient(shellClient: shellClient ?? ShellClient()) } @State var loading: Bool = false @@ -46,7 +42,7 @@ struct SourceControlPullView: View { .scrollContentBackground(.hidden) .onAppear { Task { - preferRebaseWhenPulling = try await gitConfig.get(key: "pull.rebase", global: true) ?? false + preferRebaseWhenPulling = try await sourceControlManager.gitConfig.get(key: "pull.rebase", global: true) ?? false if preferRebaseWhenPulling { sourceControlViewModel.operationRebase = true } From 261a9ceab3785d430ffd2a5497167bd123c16a8d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 18:16:27 +0200 Subject: [PATCH 145/335] Refactor: Scaffold CESourceControl feature package --- CodeEdit.xcodeproj/project.pbxproj | 7 +++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 ++ .../Accounts/Utils/GitTime.swift | 4 ++- .../Features/CESourceControl/Package.swift | 28 +++++++++++++++++++ .../CESourceControl/CESourceControl.swift | 11 ++++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Packages/Features/CESourceControl/Package.swift create mode 100644 Packages/Features/CESourceControl/Sources/CESourceControl/CESourceControl.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index fd95848ac1..f0de0efa71 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; 58CE15A100000001004BE201 /* CELSP in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE15A000000001004BE200 /* CELSP */; }; + 58CE50C200000002004BE302 /* CESourceControl in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE50C100000002004BE301 /* CESourceControl */; }; 588957132FFA679E004BE116 /* ShellClient in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* ShellClient */; }; 5889571530CE12AB004BE116 /* CEWorkspaceFileManager in Frameworks */ = {isa = PBXBuildFile; productRef = 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */; }; 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; @@ -215,6 +216,7 @@ 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, 588950C52FFA5C05004BE116 /* CESearch in Frameworks */, 58CE15A100000001004BE201 /* CELSP in Frameworks */, + 58CE50C200000002004BE302 /* CESourceControl in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, 6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */, @@ -363,6 +365,7 @@ 5AD0C0DE2D00000000000012 /* CodeEditSettings */, 588950C42FFA5C05004BE116 /* CESearch */, 58CE15A000000001004BE200 /* CELSP */, + 58CE50C100000002004BE301 /* CESourceControl */, 588957122FFA679E004BE116 /* ShellClient */, 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */, 5889639D2FFA9A87004BE116 /* CENotifications */, @@ -1916,6 +1919,10 @@ isa = XCSwiftPackageProductDependency; productName = CELSP; }; + 58CE50C100000002004BE301 /* CESourceControl */ = { + isa = XCSwiftPackageProductDependency; + productName = CESourceControl; + }; 588957122FFA679E004BE116 /* ShellClient */ = { isa = XCSwiftPackageProductDependency; productName = ShellClient; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 7c8186d8a6..19e74b0ad2 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -42,5 +42,8 @@ + + diff --git a/CodeEdit/Features/SourceControl/Accounts/Utils/GitTime.swift b/CodeEdit/Features/SourceControl/Accounts/Utils/GitTime.swift index 8f41b24c91..5f69553ea1 100644 --- a/CodeEdit/Features/SourceControl/Accounts/Utils/GitTime.swift +++ b/CodeEdit/Features/SourceControl/Accounts/Utils/GitTime.swift @@ -16,7 +16,9 @@ enum GitTime { - (https://tools.ietf.org/html/rfc3339) - (https://developer.apple.com/library/mac/qa/qa1480/_index.html) */ - static var rfc3339DateFormatter: DateFormatter = { + // nonisolated(unsafe): configured once here and never mutated afterwards; + // DateFormatter is thread-safe for reading once configuration is complete. + nonisolated(unsafe) static let rfc3339DateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'" formatter.locale = Locale(identifier: "en_US_POSIX") diff --git a/Packages/Features/CESourceControl/Package.swift b/Packages/Features/CESourceControl/Package.swift new file mode 100644 index 0000000000..933708fff5 --- /dev/null +++ b/Packages/Features/CESourceControl/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CESourceControl", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CESourceControl", targets: ["CESourceControl"]) + ], + dependencies: [ + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditSettings"), + .package(path: "../../Foundation/CodeEditUI"), + .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3") + ], + targets: [ + .target( + name: "CESourceControl", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditSettings", package: "CodeEditSettings"), + .product(name: "CodeEditUI", package: "CodeEditUI"), + .product(name: "CodeEditSymbols", package: "CodeEditSymbols") + ] + ) + ] +) diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/CESourceControl.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/CESourceControl.swift new file mode 100644 index 0000000000..f08d5fd4a4 --- /dev/null +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/CESourceControl.swift @@ -0,0 +1,11 @@ +// +// CESourceControl.swift +// CESourceControl +// +// Created by Matthijs Eikelenboom on 12/07/2026. +// + +/// The CESourceControl feature package: git state management (`SourceControlManager`), +/// the git CLI client, source-control operation views, and the GitHub/GitLab/Bitbucket +/// account clients. +enum CESourceControl {} From 7f5323e70ded6b13e380ab125ad0500ae0e9484d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 12 Jul 2026 18:53:37 +0200 Subject: [PATCH 146/335] Refactor: Move the SourceControl feature into the CESourceControl package --- CodeEdit.xcodeproj/project.pbxproj | 20 +++++------ .../Views/ToolbarBranchPicker.swift | 1 + .../Protocols/WorkspaceManaging.swift | 1 + .../Feedback/Model/FeedbackModel.swift | 1 + .../HistoryInspectorModel.swift | 1 + .../HistoryInspectorView.swift | 1 + ...rceControlNavigatorChangesCommitView.swift | 1 + .../SourceControlNavigatorChangesList.swift | 1 + .../SourceControlNavigatorChangesView.swift | 1 + .../SourceControlNavigatorNoRemotesView.swift | 1 + .../SourceControlNavigatorSyncView.swift | 1 + .../History/Views/CommitDetailsView.swift | 1 + .../SourceControlNavigatorHistoryView.swift | 1 + ...SourceControlNavigatorRepositoryView.swift | 1 + .../ChangedFile/GitChangedFileLabel.swift | 1 + .../ChangedFile/GitChangedFileListView.swift | 1 + .../SourceControlNavigatorToolbarBottom.swift | 1 + .../Views/SourceControlNavigatorView.swift | 1 + .../AccountsSettingsSigninView.swift | 1 + .../Models/IgnorePatternModel.swift | 1 + .../SourceControlGeneralView.swift | 1 + .../SourceControlGitView.swift | 1 + .../Features/Welcome/GitCloneButton.swift | 1 + .../SourceControlCommands.swift | 1 + .../Features/Workspace/Models/Workspace.swift | 1 + .../Features/Workspace/WorkspaceFactory.swift | 1 + CodeEdit/WorkspaceSheets.swift | 1 + CodeEdit/WorkspaceView.swift | 1 + .../Documents/DocumentsUnitTests.swift | 1 + .../SourceControl/GitClientTests.swift | 1 + .../GitRefreshActionsTests.swift | 1 + .../SourceControlViewModelTests.swift | 1 + .../Bitbucket/BitBucketAccount+Token.swift | 0 .../Accounts/Bitbucket/BitBucketAccount.swift | 0 .../BitBucketOAuthConfiguration.swift | 0 .../BitBucketTokenConfiguration.swift | 0 .../Model/BitBucketRepositories.swift | 0 .../Bitbucket/Model/BitBucketUser.swift | 2 +- .../Routers/BitBucketOAuthRouter.swift | 0 .../Routers/BitBucketRepositoryRouter.swift | 0 .../Routers/BitBucketTokenRouter.swift | 0 .../Routers/BitBucketUserRouter.swift | 0 .../Accounts/GitHub/GitHubAccount.swift | 4 +-- .../Accounts/GitHub/GitHubConfiguration.swift | 14 ++++---- .../Accounts/GitHub/GitHubOpenness.swift | 2 +- .../Accounts/GitHub/GitHubPreviewHeader.swift | 2 +- .../Model/GitHubAccount+deleteReference.swift | 0 .../Accounts/GitHub/Model/GitHubComment.swift | 0 .../Accounts/GitHub/Model/GitHubFiles.swift | 0 .../Accounts/GitHub/Model/GitHubGist.swift | 0 .../Accounts/GitHub/Model/GitHubIssue.swift | 6 ++-- .../GitHub/Model/GitHubPullRequest.swift | 0 .../GitHub/Model/GitHubRepositories.swift | 0 .../Accounts/GitHub/Model/GitHubReview.swift | 0 .../Accounts/GitHub/Model/GitHubUser.swift | 6 ++-- .../Accounts/GitHub/PublicKey.swift | 0 .../GitHub/Routers/GitHubGistRouter.swift | 0 .../GitHub/Routers/GitHubIssueRouter.swift | 0 .../Routers/GitHubPullRequestRouter.swift | 0 .../Routers/GitHubRepositoryRouter.swift | 0 .../GitHub/Routers/GitHubReviewsRouter.swift | 0 .../GitHub/Routers/GitHubRouter.swift | 0 .../GitHub/Routers/GitHubUserRouter.swift | 0 .../Accounts/GitLab/GitLabAccount.swift | 4 +-- .../Accounts/GitLab/GitLabConfiguration.swift | 12 +++---- .../GitLab/GitLabOAuthConfiguration.swift | 0 .../GitLab/Model/GitLabAccountModel.swift | 0 .../GitLab/Model/GitLabAvatarURL.swift | 0 .../Accounts/GitLab/Model/GitLabCommit.swift | 0 .../Accounts/GitLab/Model/GitLabEvent.swift | 0 .../GitLab/Model/GitLabEventData.swift | 0 .../GitLab/Model/GitLabEventNote.swift | 0 .../GitLab/Model/GitLabGroupAccess.swift | 0 .../GitLab/Model/GitLabNamespace.swift | 0 .../GitLab/Model/GitLabPermissions.swift | 0 .../Accounts/GitLab/Model/GitLabProject.swift | 0 .../GitLab/Model/GitLabProjectAccess.swift | 0 .../GitLab/Model/GitLabProjectHook.swift | 0 .../Accounts/GitLab/Model/GitLabUser.swift | 4 +-- .../GitLab/Routers/GitLabCommitRouter.swift | 0 .../GitLab/Routers/GitLabOAuthRouter.swift | 0 .../GitLab/Routers/GitLabProjectRouter.swift | 0 .../GitLab/Routers/GitLabUserRouter.swift | 0 .../Networking/GitJSONPostRouter.swift | 0 .../Accounts/Networking/GitRouter.swift | 12 +++---- .../Accounts/Networking/GitURLSession.swift | 8 ++--- .../Accounts/Parameters.swift | 0 .../Accounts/Utils/GitTime.swift | 0 .../Utils/String+PercentEncoding.swift | 0 .../Utils/String+QueryParameters.swift | 0 .../Accounts/Utils}/URL+URLParameters.swift | 0 .../Client/GitClient+Branches.swift | 17 ++++++--- .../Client/GitClient+Clone.swift | 4 +-- .../Client/GitClient+Commit.swift | 10 +++--- .../Client/GitClient+CommitHistory.swift | 2 +- .../Client/GitClient+Fetch.swift | 2 +- .../Client/GitClient+Initiate.swift | 2 +- .../Client/GitClient+Pull.swift | 2 +- .../Client/GitClient+Push.swift | 2 +- .../Client/GitClient+Remote.swift | 8 ++--- .../Client/GitClient+Stash.swift | 10 +++--- .../Client/GitClient+Status.swift | 10 +++--- .../Client/GitClient+Validate.swift | 2 +- .../CESourceControl}/Client/GitClient.swift | 8 ++--- .../Client/GitClientProtocol.swift | 5 ++- .../Client/GitConfigClient.swift | 9 ++--- .../Client/GitConfigExtensions.swift | 0 .../Client/GitConfigRepresentable.swift | 2 +- .../Clone/GitCheckoutBranchView.swift | 7 ++-- .../CESourceControl}/Clone/GitCloneView.swift | 7 ++-- .../GitCheckoutBranchViewModel.swift | 2 +- .../Clone/ViewModels/GitCloneViewModel.swift | 2 +- .../SourceControlManager+Alerts.swift | 2 +- ...ourceControlManager+BranchOperations.swift | 12 +++---- .../SourceControlManager+FileEvents.swift | 2 +- .../SourceControlManager+FileOperations.swift | 16 ++++----- ...ourceControlManager+RemoteOperations.swift | 14 ++++---- .../SourceControlManager+Repository.swift | 4 +-- ...SourceControlManager+StashOperations.swift | 6 ++-- .../SourceControlManager.swift | 29 +++++++-------- .../SourceControlViewModel.swift | 36 ++++++++++--------- .../UseCases/CloneRepositoryUseCase.swift | 1 - .../Views}/RegexFormatter.swift | 0 .../Views/RemoteBranchPicker.swift | 1 + .../SourceControlAddExistingRemoteView.swift | 6 ++-- .../Views/SourceControlFetchView.swift | 6 ++-- .../Views/SourceControlNewBranchView.swift | 9 +++-- .../Views/SourceControlPullView.swift | 6 ++-- .../Views/SourceControlPushView.swift | 6 ++-- .../Views/SourceControlRenameBranchView.swift | 9 +++-- .../Views/SourceControlStashView.swift | 6 ++-- .../Views/SourceControlSwitchView.swift | 9 +++-- .../Views}/TrimWhitespaceFormatter.swift | 0 .../CodeEditCore/Domain/Git/GitBranch.swift | 2 +- .../Domain/Git/GitBranchesGroup.swift | 2 +- .../Domain/Git/GitChangedFile.swift | 2 +- .../CodeEditCore/Domain/Git/GitCommit.swift | 2 +- .../CodeEditCore/Domain/Git/GitRemote.swift | 2 +- .../Domain/Git/GitStashEntry.swift | 2 +- .../Extensions/String+SafeOffset.swift | 2 +- 140 files changed, 251 insertions(+), 183 deletions(-) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/BitBucketAccount+Token.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/BitBucketAccount.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/BitBucketTokenConfiguration.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/Model/BitBucketRepositories.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/Model/BitBucketUser.swift (99%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/GitHubAccount.swift (68%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/GitHubConfiguration.swift (93%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/GitHubOpenness.swift (78%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/GitHubPreviewHeader.swift (94%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubComment.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubFiles.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubGist.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubIssue.swift (99%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubPullRequest.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubRepositories.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubReview.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Model/GitHubUser.swift (97%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/PublicKey.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubGistRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubIssueRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubReviewsRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitHub/Routers/GitHubUserRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/GitLabAccount.swift (68%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/GitLabConfiguration.swift (68%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/GitLabOAuthConfiguration.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabAccountModel.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabAvatarURL.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabCommit.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabEvent.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabEventData.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabEventNote.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabGroupAccess.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabNamespace.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabPermissions.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabProject.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabProjectAccess.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabProjectHook.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Model/GitLabUser.swift (97%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Routers/GitLabCommitRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Routers/GitLabOAuthRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Routers/GitLabProjectRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/GitLab/Routers/GitLabUserRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Networking/GitJSONPostRouter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Networking/GitRouter.swift (97%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Networking/GitURLSession.swift (93%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Parameters.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Utils/GitTime.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Utils/String+PercentEncoding.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Accounts/Utils/String+QueryParameters.swift (100%) rename {CodeEdit/Utils/Extensions/URL => Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils}/URL+URLParameters.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Branches.swift (88%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Clone.swift (98%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Commit.swift (90%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+CommitHistory.swift (98%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Fetch.swift (83%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Initiate.swift (83%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Pull.swift (77%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Push.swift (96%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Remote.swift (91%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Stash.swift (88%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Status.swift (97%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient+Validate.swift (92%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClient.swift (90%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitClientProtocol.swift (90%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitConfigClient.swift (86%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitConfigExtensions.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Client/GitConfigRepresentable.swift (93%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Clone/GitCheckoutBranchView.swift (96%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Clone/GitCloneView.swift (97%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Clone/ViewModels/GitCheckoutBranchViewModel.swift (98%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Clone/ViewModels/GitCloneViewModel.swift (99%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+Alerts.swift (95%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+BranchOperations.swift (78%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+FileEvents.swift (98%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+FileOperations.swift (88%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+RemoteOperations.swift (84%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+Repository.swift (84%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager+StashOperations.swift (82%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlManager.swift (71%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/SourceControlViewModel.swift (60%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/UseCases/CloneRepositoryUseCase.swift (99%) rename {CodeEdit/Utils/Formatters => Packages/Features/CESourceControl/Sources/CESourceControl/Views}/RegexFormatter.swift (100%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/RemoteBranchPicker.swift (99%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlAddExistingRemoteView.swift (96%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlFetchView.swift (95%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlNewBranchView.swift (93%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlPullView.swift (97%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlPushView.swift (96%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlRenameBranchView.swift (92%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlStashView.swift (98%) rename {CodeEdit/Features/SourceControl => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/SourceControlSwitchView.swift (94%) rename {CodeEdit/Utils/Formatters => Packages/Features/CESourceControl/Sources/CESourceControl/Views}/TrimWhitespaceFormatter.swift (100%) rename {CodeEdit/Features/Search => Packages/Foundation/CodeEditCore/Sources/CodeEditCore}/Extensions/String+SafeOffset.swift (99%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index f0de0efa71..1aa8f969f3 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -17,11 +17,11 @@ 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; - 58CE15A100000001004BE201 /* CELSP in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE15A000000001004BE200 /* CELSP */; }; - 58CE50C200000002004BE302 /* CESourceControl in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE50C100000002004BE301 /* CESourceControl */; }; 588957132FFA679E004BE116 /* ShellClient in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* ShellClient */; }; 5889571530CE12AB004BE116 /* CEWorkspaceFileManager in Frameworks */ = {isa = PBXBuildFile; productRef = 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */; }; 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; + 58CE15A100000001004BE201 /* CELSP in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE15A000000001004BE200 /* CELSP */; }; + 58CE50C200000002004BE302 /* CESourceControl in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE50C100000002004BE301 /* CESourceControl */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58ED10022FFB0001004BE116 /* CEEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* CEEditor */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; @@ -1915,14 +1915,6 @@ isa = XCSwiftPackageProductDependency; productName = CESearch; }; - 58CE15A000000001004BE200 /* CELSP */ = { - isa = XCSwiftPackageProductDependency; - productName = CELSP; - }; - 58CE50C100000002004BE301 /* CESourceControl */ = { - isa = XCSwiftPackageProductDependency; - productName = CESourceControl; - }; 588957122FFA679E004BE116 /* ShellClient */ = { isa = XCSwiftPackageProductDependency; productName = ShellClient; @@ -1935,6 +1927,14 @@ isa = XCSwiftPackageProductDependency; productName = CENotifications; }; + 58CE15A000000001004BE200 /* CELSP */ = { + isa = XCSwiftPackageProductDependency; + productName = CELSP; + }; + 58CE50C100000002004BE301 /* CESourceControl */ = { + isa = XCSwiftPackageProductDependency; + productName = CESourceControl; + }; 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */ = { isa = XCSwiftPackageProductDependency; productName = CodeEditCore; diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift index d27e24416c..d992c0d86f 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift @@ -5,6 +5,7 @@ // Created by Lukas Pistrol on 21.04.22. // +import CESourceControl import SwiftUI import CodeEditSettings import CEWorkspaceFileManager diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index bc7fc99d39..8b676c2942 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 06.04.26. // +import CESourceControl import Foundation import CEWorkspaceFileManager import CEEditor diff --git a/CodeEdit/Features/Feedback/Model/FeedbackModel.swift b/CodeEdit/Features/Feedback/Model/FeedbackModel.swift index 4aa2f73add..cb9ea480a2 100644 --- a/CodeEdit/Features/Feedback/Model/FeedbackModel.swift +++ b/CodeEdit/Features/Feedback/Model/FeedbackModel.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/04/14. // +import CESourceControl import SwiftUI import CodeEditSettings diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index 584110ba08..0c202d8379 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/04/18. // +import CESourceControl import Foundation import CodeEditSettings import CodeEditCore diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index a29df2ab6f..bb5c7658c4 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -4,6 +4,7 @@ // // Created by Nanashi Li on 2022/03/24. // +import CESourceControl import SwiftUI import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift index 4c37c944f1..516a8fc8a0 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift @@ -5,6 +5,7 @@ // Created by Albert Vinizhanau on 10/19/23. // +import CESourceControl import SwiftUI import CodeEditUI diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift index 20880018d6..420e4eecdb 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 11/18/23. // +import CESourceControl import AppKit import CEWorkspaceFileManager import SwiftUI diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift index ab4b5fc48d..639de7cf83 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/05/20. // +import CESourceControl import SwiftUI import CodeEditUI diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift index 187c8c468f..a27d82af98 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 11/17/23. // +import CESourceControl import SwiftUI struct SourceControlNavigatorNoRemotesView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift index 1c80b2e435..209335ec7c 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift @@ -5,6 +5,7 @@ // Created by Albert Vinizhanau on 10/20/23. // +import CESourceControl import SwiftUI struct SourceControlNavigatorSyncView: View { diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift index d35c322d32..649f726b8d 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 12/27/23. // +import CESourceControl import SwiftUI import CodeEditUI import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift index dbbb978f7c..2e6db5761f 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 12/27/2023. // +import CESourceControl import SwiftUI import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift index 9dd8f90fb3..e680e75980 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/05/20. // +import CESourceControl import SwiftUI import CodeEditUI import CodeEditCore diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index e7ecc1ec27..e958e01733 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 8/23/24. // +import CESourceControl import SwiftUI import ShellClient import CEWorkspaceFileManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift index 481d2a3bd6..b0922c15b2 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/05/20. // +import CESourceControl import SwiftUI import CodeEditSettings import CEWorkspaceFileManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift index 69c776c273..a41876d61a 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/05/20. // +import CESourceControl import SwiftUI import CodeEditUI diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 8d859209b3..b0da5c2d0b 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/05/20. // +import CESourceControl import SwiftUI import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift index 9799a2215e..35cd75cff6 100644 --- a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift +++ b/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 4/5/23. // +import CESourceControl import SwiftUI import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift index b15ca7ff29..9ebbbf84a2 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 11/1/24. // +import CESourceControl import CodeEditCore import Foundation import ShellClient diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index 433f7a1678..d0e5d75929 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -5,6 +5,7 @@ // Created by Raymond Vleeshouwer on 02/04/23. // +import CESourceControl import CodeEditCore import SwiftUI import ShellClient diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index 0c9ec98554..82b2be0c20 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -5,6 +5,7 @@ // Created by Raymond Vleeshouwer on 02/04/23. // +import CESourceControl import CodeEditCore import SwiftUI import ShellClient diff --git a/CodeEdit/Features/Welcome/GitCloneButton.swift b/CodeEdit/Features/Welcome/GitCloneButton.swift index 06963483e1..7c415c904e 100644 --- a/CodeEdit/Features/Welcome/GitCloneButton.swift +++ b/CodeEdit/Features/Welcome/GitCloneButton.swift @@ -5,6 +5,7 @@ // Created by Giorgi Tchelidze on 07.06.25. // +import CESourceControl import CodeEditCore import SwiftUI import ShellClient diff --git a/CodeEdit/Features/WindowCommands/SourceControlCommands.swift b/CodeEdit/Features/WindowCommands/SourceControlCommands.swift index 8ea5be2a31..6905a0c983 100644 --- a/CodeEdit/Features/WindowCommands/SourceControlCommands.swift +++ b/CodeEdit/Features/WindowCommands/SourceControlCommands.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 6/29/24. // +import CESourceControl import SwiftUI struct SourceControlCommands: Commands { diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 8eab2812df..36e11c6d11 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 06.04.26. // +import CESourceControl import AppKit import CEWorkspaceFileManager import CodeEditCore diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 48ebf47924..6ea9ff0a9c 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 07.04.26. // +import CESourceControl import Foundation import CEWorkspaceFileManager import CEEditor diff --git a/CodeEdit/WorkspaceSheets.swift b/CodeEdit/WorkspaceSheets.swift index 5f09e7928c..dc7d735b1a 100644 --- a/CodeEdit/WorkspaceSheets.swift +++ b/CodeEdit/WorkspaceSheets.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 7/1/24. // +import CESourceControl import SwiftUI import CodeEditCore diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 9e4b5fc9ca..fd7d55164d 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 3/10/22. // +import CESourceControl import SwiftUI import CodeEditSettings import CodeEditCore diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 1fc7b84525..eb23b011af 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -8,6 +8,7 @@ import XCTest import CodeEditCore import ShellClient +import CESourceControl import CESearch @testable import CodeEdit diff --git a/CodeEditTests/Features/SourceControl/GitClientTests.swift b/CodeEditTests/Features/SourceControl/GitClientTests.swift index 584d471d3a..8d4f5ee7ba 100644 --- a/CodeEditTests/Features/SourceControl/GitClientTests.swift +++ b/CodeEditTests/Features/SourceControl/GitClientTests.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 9/11/25. // +@testable import CESourceControl import Testing import ShellClient @testable import CodeEdit diff --git a/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift b/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift index c0c14d2283..3ea19292b6 100644 --- a/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift +++ b/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 06/07/2026. // +@testable import CESourceControl import XCTest @testable import CodeEdit diff --git a/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift b/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift index 6ee9d1db32..6084fc6a36 100644 --- a/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift +++ b/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 02/07/2026. // +@testable import CESourceControl import XCTest @testable import CodeEdit diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketAccount.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketAccount.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift similarity index 99% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift index 4cb323504f..8bbe89e90e 100644 --- a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift @@ -37,7 +37,7 @@ class BitBucketEmail: Codable { extension BitBucketAccount { - func me( + public func me( _ session: GitURLSession = URLSession.shared, completion: @escaping (_ response: Result) -> Void ) -> GitURLSessionDataTaskProtocol? { diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubAccount.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift similarity index 68% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubAccount.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift index 39902dc893..aaef39c5a9 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubAccount.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift @@ -9,10 +9,10 @@ import Foundation // TODO: DOCS (Nanashi Li) -struct GitHubAccount { +public struct GitHubAccount { let configuration: GitHubTokenConfiguration - init(_ config: GitHubTokenConfiguration = GitHubTokenConfiguration()) { + public init(_ config: GitHubTokenConfiguration = GitHubTokenConfiguration()) { configuration = config } } diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift similarity index 93% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift index 5566f0f41e..0caec464d5 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubConfiguration.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift @@ -11,12 +11,12 @@ import CodeEditSettings import FoundationNetworking #endif -struct GitHubTokenConfiguration: GitRouterConfiguration { +public struct GitHubTokenConfiguration: GitRouterConfiguration { let provider = SourceControlAccount.Provider.github - var apiEndpoint: String? - var accessToken: String? - let errorDomain: String? = "com.codeedit.models.accounts.github" - let authorizationHeader: String? = "Basic" + public var apiEndpoint: String? + public var accessToken: String? + public let errorDomain: String? = "com.codeedit.models.accounts.github" + public let authorizationHeader: String? = "Basic" /// Custom `Accept` header for API previews. /// @@ -24,12 +24,12 @@ struct GitHubTokenConfiguration: GitRouterConfiguration { /// see: https://developer.github.com/changes/2016-05-12-reactions-api-preview/ private var previewCustomHeaders: [GitHTTPHeader]? - var customHeaders: [GitHTTPHeader]? { + public var customHeaders: [GitHTTPHeader]? { /// More (non-preview) headers can be appended if needed in the future return previewCustomHeaders } - init(_ token: String? = nil, url: String? = nil, previewHeaders: [GitHubPreviewHeader] = []) { + public init(_ token: String? = nil, url: String? = nil, previewHeaders: [GitHubPreviewHeader] = []) { apiEndpoint = url ?? provider.apiURL?.absoluteString accessToken = token?.data(using: .utf8)!.base64EncodedString() previewCustomHeaders = previewHeaders.map { $0.header } diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubOpenness.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift similarity index 78% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubOpenness.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift index 0faea8ea31..03224729b8 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubOpenness.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift @@ -7,7 +7,7 @@ import Foundation -enum GitHubOpenness: String, Codable { +public enum GitHubOpenness: String, Codable { case open case closed case all diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubPreviewHeader.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift similarity index 94% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubPreviewHeader.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift index 0a8c415cf6..b6cac56d23 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/GitHubPreviewHeader.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift @@ -12,7 +12,7 @@ import Foundation /// Some APIs provide additional data for new (preview) APIs if a custom header is added to the request. /// /// - Note: Preview APIs are subject to change. -enum GitHubPreviewHeader { +public enum GitHubPreviewHeader { /// The `Reactions` preview header provides reactions in `Comment`s. case reactions diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubComment.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubComment.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubFiles.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubFiles.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubGist.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubGist.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubIssue.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift similarity index 99% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubIssue.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift index c83a0cef02..cbb349ac6e 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubIssue.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift @@ -10,7 +10,7 @@ import Foundation import FoundationNetworking #endif -class GitHubIssue: Codable { +public class GitHubIssue: Codable { private(set) var id: Int = -1 var url: URL? var repositoryURL: URL? @@ -18,7 +18,7 @@ class GitHubIssue: Codable { var labelsURL: URL? var commentsURL: URL? var eventsURL: URL? - var htmlURL: URL? + public var htmlURL: URL? var number: Int var state: GitHubOpenness? var title: String? @@ -175,7 +175,7 @@ extension GitHubAccount { - parameter completion: Callback for the issue that is created. */ @discardableResult - func postIssue( + public func postIssue( _ session: GitURLSession = URLSession.shared, owner: String, repository: String, diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubRepositories.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubRepositories.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubReview.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubReview.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubUser.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift similarity index 97% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubUser.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift index 563eb5c96c..e5c6c13034 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitHub/Model/GitHubUser.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift @@ -10,7 +10,7 @@ import Foundation import FoundationNetworking #endif -class GitHubUser: Codable { +public class GitHubUser: Codable { private(set) var id: Int = -1 var login: String? var avatarURL: String? @@ -24,7 +24,7 @@ class GitHubUser: Codable { var numberOfPrivateRepos: Int? var nodeID: String? var url: String? - var htmlURL: String? +public var htmlURL: String? var gistsURL: String? var starredURL: String? var subscriptionsURL: String? @@ -102,7 +102,7 @@ extension GitHubAccount { - parameter completion: Callback for the outcome of the fetch. */ @discardableResult - func me( + public func me( _ session: GitURLSession = URLSession.shared, completion: @escaping (_ response: Result) -> Void ) -> GitURLSessionDataTaskProtocol? { diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/PublicKey.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/PublicKey.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabAccount.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift similarity index 68% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabAccount.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift index 51772141c8..5be40e8e33 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabAccount.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift @@ -9,10 +9,10 @@ import Foundation // TODO: DOCS (Nanashi Li) -struct GitLabAccount { +public struct GitLabAccount { let configuration: GitRouterConfiguration - init(_ config: GitRouterConfiguration = GitLabTokenConfiguration()) { + public init(_ config: GitRouterConfiguration = GitLabTokenConfiguration()) { configuration = config } } diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift similarity index 68% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift index 00f65eecd6..2d3d293dcc 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabConfiguration.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift @@ -8,13 +8,13 @@ import Foundation import CodeEditSettings -struct GitLabTokenConfiguration: GitRouterConfiguration { +public struct GitLabTokenConfiguration: GitRouterConfiguration { let provider = SourceControlAccount.Provider.gitlab - var apiEndpoint: String? - var accessToken: String? - let errorDomain: String? = "com.codeedit.models.accounts.gitlab" + public var apiEndpoint: String? + public var accessToken: String? + public let errorDomain: String? = "com.codeedit.models.accounts.gitlab" - init(_ token: String? = nil, url: String? = nil) { + public init(_ token: String? = nil, url: String? = nil) { apiEndpoint = url ?? provider.apiURL?.absoluteString accessToken = token } @@ -26,7 +26,7 @@ struct GitLabPrivateTokenConfiguration: GitRouterConfiguration { var accessToken: String? let errorDomain: String? = "com.codeedit.models.accounts.gitlab" - init(_ token: String? = nil, url: String? = nil) { + public init(_ token: String? = nil, url: String? = nil) { apiEndpoint = url ?? provider.apiURL?.absoluteString accessToken = token } diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabCommit.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabCommit.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabEvent.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabEvent.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabEventData.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabEventData.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabEventNote.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabEventNote.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabNamespace.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabNamespace.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabPermissions.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabPermissions.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabProject.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabProject.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabUser.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift similarity index 97% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabUser.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift index c58d0299eb..a3d41424c9 100644 --- a/CodeEdit/Features/SourceControl/Accounts/GitLab/Model/GitLabUser.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift @@ -7,7 +7,7 @@ import Foundation -class GitLabUser: Codable { +public class GitLabUser: Codable { var id: Int var username: String? var state: String? @@ -62,7 +62,7 @@ extension GitLabAccount { - parameter completion: Callback for the outcome of the fetch. */ @discardableResult - func me( + public func me( _ session: GitURLSession = URLSession.shared, completion: @escaping (_ response: Result) -> Void ) -> GitURLSessionDataTaskProtocol? { diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Networking/GitJSONPostRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Networking/GitJSONPostRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Networking/GitRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift similarity index 97% rename from CodeEdit/Features/SourceControl/Accounts/Networking/GitRouter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift index 2dedbd43f0..962dec7adf 100644 --- a/CodeEdit/Features/SourceControl/Accounts/Networking/GitRouter.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift @@ -18,12 +18,12 @@ enum GitHTTPEncoding: Int { case url, form, json } -struct GitHTTPHeader { +public struct GitHTTPHeader { var headerField: String var value: String } -protocol GitRouterConfiguration { +public protocol GitRouterConfiguration { var apiEndpoint: String? { get } var accessToken: String? { get } var accessTokenFieldName: String? { get } @@ -33,19 +33,19 @@ protocol GitRouterConfiguration { } extension GitRouterConfiguration { - var accessTokenFieldName: String? { + public var accessTokenFieldName: String? { "access_token" } - var authorizationHeader: String? { + public var authorizationHeader: String? { nil } - var errorDomain: String? { + public var errorDomain: String? { "com.codeedit.models.accounts.networking" } - var customHeaders: [GitHTTPHeader]? { + public var customHeaders: [GitHTTPHeader]? { nil } } diff --git a/CodeEdit/Features/SourceControl/Accounts/Networking/GitURLSession.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift similarity index 93% rename from CodeEdit/Features/SourceControl/Accounts/Networking/GitURLSession.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift index 6a8d62bb36..015b981c1c 100644 --- a/CodeEdit/Features/SourceControl/Accounts/Networking/GitURLSession.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift @@ -13,7 +13,7 @@ import FoundationNetworking #endif // TODO: DOCS (Nanashi Li) -protocol GitURLSession { +public protocol GitURLSession { func dataTask( with request: URLRequest, @@ -42,7 +42,7 @@ protocol GitURLSession { #endif } -protocol GitURLSessionDataTaskProtocol { +public protocol GitURLSessionDataTaskProtocol { func resume() } @@ -50,14 +50,14 @@ extension URLSessionDataTask: GitURLSessionDataTaskProtocol {} extension URLSession: GitURLSession { - func dataTask( + public func dataTask( with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void ) -> GitURLSessionDataTaskProtocol { (dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTask) } - func uploadTask( + public func uploadTask( with request: URLRequest, fromData bodyData: Data?, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void diff --git a/CodeEdit/Features/SourceControl/Accounts/Parameters.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Parameters.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Parameters.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Parameters.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Utils/GitTime.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Utils/GitTime.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Utils/String+PercentEncoding.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Utils/String+PercentEncoding.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift diff --git a/CodeEdit/Features/SourceControl/Accounts/Utils/String+QueryParameters.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Utils/String+QueryParameters.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift diff --git a/CodeEdit/Utils/Extensions/URL/URL+URLParameters.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift similarity index 100% rename from CodeEdit/Utils/Extensions/URL/URL+URLParameters.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Branches.swift similarity index 88% rename from CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Branches.swift index f9247f023d..90014b40ee 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Branches.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Branches.swift @@ -8,11 +8,18 @@ import Foundation import CodeEditCore +private extension CharacterSet { + /// Whitespace excluding newlines. Previously leaked in from TextFormation via the + /// app target's cross-file extension visibility; defined locally so this package + /// doesn't depend on an editor-formatting library for a one-liner. + static let whitespacesWithoutNewlines = CharacterSet.whitespacesAndNewlines.subtracting(.newlines) +} + extension GitClient { /// Get branches /// - Parameter remote: If passed, fetches branches for the specified remote /// - Returns: Array of branches - func getBranches(remote: String? = nil) async throws -> [GitBranch] { + public func getBranches(remote: String? = nil) async throws -> [GitBranch] { var command = "branch --format \"%(refname:short)|%(refname)|%(upstream:short) %(upstream:track)\"" if remote != nil { command += " -r" @@ -45,7 +52,7 @@ extension GitClient { } /// Get current branch - func getCurrentBranch() async throws -> GitBranch? { + public func getCurrentBranch() async throws -> GitBranch? { let branchName = try await run("branch --show-current").trimmingCharacters(in: .whitespacesAndNewlines) let output = try await run( "for-each-ref --format=\"%(refname)|%(upstream:short) %(upstream:track)\" refs/heads/\(branchName)" @@ -71,7 +78,7 @@ extension GitClient { } /// Delete branch - func deleteBranch(_ branch: GitBranch) async throws { + public func deleteBranch(_ branch: GitBranch) async throws { if !branch.isLocal { return } @@ -82,13 +89,13 @@ extension GitClient { /// Rename branch /// - Parameter from: Name of the branch to rename /// - Parameter to: New name for branch - func renameBranch(oldName: String, newName: String) async throws { + public func renameBranch(oldName: String, newName: String) async throws { _ = try await run("branch -m \(oldName) \(newName)") } /// Checkout branch /// - Parameter branch: Branch to checkout - func checkoutBranch(_ branch: GitBranch, forceLocal: Bool = false, newName: String? = nil) async throws { + public func checkoutBranch(_ branch: GitBranch, forceLocal: Bool = false, newName: String? = nil) async throws { var command = "checkout " let targetName = newName ?? branch.name diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift similarity index 98% rename from CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift index ab1b00595b..9272c56c2c 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Clone.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift @@ -10,7 +10,7 @@ import Combine import CodeEditCore extension GitClient { - struct CloneProgress { + public struct CloneProgress { let progress: Double let state: GitCloneProgressState } @@ -38,7 +38,7 @@ extension GitClient { /// - remoteUrl: URL of remote repository /// - localPath: Local path to clone /// - Returns: Stream of progress - func cloneRepository( + public func cloneRepository( remoteUrl: URL, localPath: URL ) -> AsyncThrowingMapSequence { diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift similarity index 90% rename from CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift index 752dee2891..e3f185a0ef 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Commit.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift @@ -13,7 +13,7 @@ extension GitClient { /// Commit files /// - Parameters: /// - message: Commit message - func commit(message: String, details: String?) async throws { + public func commit(message: String, details: String?) async throws { let message = message.replacingOccurrences(of: #"""#, with: #"\""#) let command: String @@ -28,24 +28,24 @@ extension GitClient { /// Add file to git /// - Parameter file: File to add - func add(_ files: [URL]) async throws { + public func add(_ files: [URL]) async throws { let output = try await run("add \(files.map { "'\($0.path(percentEncoded: false))'" }.joined(separator: " "))") print(output) } /// Add file to git /// - Parameter file: File to add - func reset(_ files: [URL]) async throws { + public func reset(_ files: [URL]) async throws { _ = try await run("reset \(files.map { "'\($0.path(percentEncoded: false))'" }.joined(separator: " "))") } /// Returns tuple of unsynced commits both ahead and behind - func numberOfUnsyncedCommits() async throws -> (ahead: Int, behind: Int) { + public func numberOfUnsyncedCommits() async throws -> (ahead: Int, behind: Int) { let output = try await run("status -sb --porcelain=v2").trimmingCharacters(in: .whitespacesAndNewlines) return try parseUnsyncedCommitsOutput(from: output) } - func getCommitChangedFiles(commitSHA: String) async throws -> [GitChangedFile] { + public func getCommitChangedFiles(commitSHA: String) async throws -> [GitChangedFile] { do { let output = try await run("diff-tree --no-commit-id --name-status -r \(commitSHA)") let data = output diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+CommitHistory.swift similarity index 98% rename from CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+CommitHistory.swift index 7a3cfc0917..7bf9a3d645 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+CommitHistory.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+CommitHistory.swift @@ -15,7 +15,7 @@ extension GitClient { /// - maxCount: Maximum amount of entries to get /// - fileLocalPath: Optional path of file to get history for /// - Returns: Array of git commits - func getCommitHistory( + public func getCommitHistory( branchName: String? = nil, maxCount: Int? = nil, fileLocalPath: String? = nil, diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Fetch.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Fetch.swift similarity index 83% rename from CodeEdit/Features/SourceControl/Client/GitClient+Fetch.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Fetch.swift index 05964b7921..5d5792cb27 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Fetch.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Fetch.swift @@ -9,7 +9,7 @@ import Foundation extension GitClient { /// Fetch changes to remote - func fetchFromRemote() async throws { + public func fetchFromRemote() async throws { let command = "fetch" _ = try await self.run(command) diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Initiate.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Initiate.swift similarity index 83% rename from CodeEdit/Features/SourceControl/Client/GitClient+Initiate.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Initiate.swift index 2d87d325b9..c8a9bd9604 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Initiate.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Initiate.swift @@ -9,7 +9,7 @@ import Foundation extension GitClient { /// Initiate Git repository - func initiate() async throws { + public func initiate() async throws { _ = try await run("init") } } diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Pull.swift similarity index 77% rename from CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Pull.swift index d5f8c9ca63..9ba88f3ed8 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Pull.swift @@ -9,7 +9,7 @@ import Foundation extension GitClient { /// Pull changes from remote - func pullFromRemote(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { + public func pullFromRemote(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { var command = "pull \(rebase ? "--rebase" : "--no-rebase")" if let remote = remote, let branch = branch { diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Push.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Push.swift similarity index 96% rename from CodeEdit/Features/SourceControl/Client/GitClient+Push.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Push.swift index 4295c075ae..dea73152cf 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Push.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Push.swift @@ -9,7 +9,7 @@ import Foundation extension GitClient { /// Push changes to remote - func pushToRemote( + public func pushToRemote( remote: String? = nil, branch: String? = nil, setUpstream: Bool? = false, diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Remote.swift similarity index 91% rename from CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Remote.swift index 960c5793bb..90d5d0bbbf 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Remote.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Remote.swift @@ -12,7 +12,7 @@ extension GitClient { /// Gets all remotes /// - Parameter name: Name for remote /// - Parameter location: URL string for remote location - func getRemotes() async throws -> [GitRemote] { + public func getRemotes() async throws -> [GitRemote] { let command = "remote -v" let output = try await run(command) let remotes = parseGitRemotes(from: output) @@ -23,13 +23,13 @@ extension GitClient { /// Add existing remote to local git /// - Parameter name: Name for remote /// - Parameter location: URL string for remote location - func addRemote(name: String, location: String) async throws { + public func addRemote(name: String, location: String) async throws { _ = try await run("remote add \(name) \(location)") } /// Remove remote from local git /// - Parameter name: Name for remote to remove - func removeRemote(name: String) async throws { + public func removeRemote(name: String) async throws { _ = try await run("remote rm \(name)") } @@ -39,7 +39,7 @@ extension GitClient { /// > (Reference: https://git-scm.com/docs/git-ls-remote, https://git-scm.com/docs/git-fetch) /// - Returns: A URL if a remote is configured, nil otherwise /// - Throws: `GitClientError.outputError` if the underlying git command fails unexpectedly - func getRemoteURL() async throws -> URL? { + public func getRemoteURL() async throws -> URL? { do { let remote = try await run("ls-remote --get-url") return URL(string: remote.trimmingCharacters(in: .whitespacesAndNewlines)) diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Stash.swift similarity index 88% rename from CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Stash.swift index c17ee607c0..911b2305c4 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Stash.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Stash.swift @@ -10,21 +10,21 @@ import CodeEditCore extension GitClient { /// Add uncommited changes to stash - func stash(message: String?) async throws { + public func stash(message: String?) async throws { let command = message != nil ? "stash save --message=\"\(message ?? "")\"" : "stash" _ = try await self.run(command) } /// Pops the latest entry from stash onto HEAD - func stashPop() async throws { + public func stashPop() async throws { let command = "stash pop" _ = try await self.run(command) } /// Lists all of the entries in stash - func stashList() async throws -> [GitStashEntry] { + public func stashList() async throws -> [GitStashEntry] { let command = "stash list --date=local" let output = try await run(command) let stashEntries = parseGitStashEntries(output) @@ -33,7 +33,7 @@ extension GitClient { } /// Apply stash - func applyStashEntry(_ index: Int?) async throws { + public func applyStashEntry(_ index: Int?) async throws { if let idx = index { _ = try await run("stash apply stash@{\(idx)}") } else { @@ -42,7 +42,7 @@ extension GitClient { } /// Delete stash - func deleteStashEntry(_ index: Int) async throws { + public func deleteStashEntry(_ index: Int) async throws { _ = try await run("stash drop stash@{\(index)}") } } diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift similarity index 97% rename from CodeEdit/Features/SourceControl/Client/GitClient+Status.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift index 59a9002850..1d23755b95 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Status.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift @@ -26,7 +26,7 @@ import CodeEditCore /// information can be included in the same call. extension GitClient { - struct Status { + public struct Status { var changedFiles: [GitChangedFile] var unmergedChanges: [GitChangedFile] var untrackedFiles: [GitChangedFile] @@ -35,7 +35,7 @@ extension GitClient { /// Fetches and parses the git repository's status. /// - Returns: A ``GitClient/Status`` struct with information about the changed files in the repository. /// - Throws: Can throw ``GitClient/GitClientError`` errors if it finds unexpected output. - func getStatus() async throws -> Status { + public func getStatus() async throws -> Status { let output = try await run("status -z --porcelain=2 -u") return try parseStatusString(output) } @@ -43,7 +43,7 @@ extension GitClient { /// Parses a status string from ``getStatus()`` and returns a ``Status`` object if possible. /// - Parameter output: The git output from running `status`. Expects a porcelain v2 string. /// - Returns: A status object if parseable. - func parseStatusString(_ output: borrowing String) throws -> Status { + public func parseStatusString(_ output: borrowing String) throws -> Status { let endsInNull = output.last == Character(UnicodeScalar(0)) let endIndex: String.Index if endsInNull && output.count > 1 { @@ -84,12 +84,12 @@ extension GitClient { } /// Discard changes for file - func discardChanges(for file: URL) async throws { + public func discardChanges(for file: URL) async throws { _ = try await run("restore '\(file.path(percentEncoded: false))'") } /// Discard unstaged changes - func discardAllChanges() async throws { + public func discardAllChanges() async throws { _ = try await run("restore .") } diff --git a/CodeEdit/Features/SourceControl/Client/GitClient+Validate.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Validate.swift similarity index 92% rename from CodeEdit/Features/SourceControl/Client/GitClient+Validate.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Validate.swift index 75d01660c5..847faec855 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient+Validate.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Validate.swift @@ -13,7 +13,7 @@ extension GitClient { /// Runs `git rev-parse --is-inside-work-tree`. /// /// - Returns: True, if git finds a valid repository. - func validate() async -> Bool { + public func validate() async -> Bool { do { let output = try await run("rev-parse --is-inside-work-tree") return output.trimmingCharacters(in: .whitespacesAndNewlines) == "true" diff --git a/CodeEdit/Features/SourceControl/Client/GitClient.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient.swift similarity index 90% rename from CodeEdit/Features/SourceControl/Client/GitClient.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient.swift index f820cc9cf7..44c3fc9298 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClient.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient.swift @@ -10,7 +10,7 @@ import Foundation import CodeEditCore import OSLog -class GitClient: GitClientProtocol { +public final class GitClient: GitClientProtocol { enum GitClientError: Error { case outputError(String) case notGitRepository @@ -47,11 +47,11 @@ class GitClient: GitClientProtocol { self.configClient = GitConfigClient(projectURL: directoryURL, shellClient: shellClient) } - func getConfig(key: String) async throws -> T? { + public func getConfig(key: String) async throws -> T? { return try await configClient.get(key: key, global: false) } - func setConfig(key: String, value: T) async { + public func setConfig(key: String, value: T) async { await configClient.set(key: key, value: value, global: false) } @@ -62,7 +62,7 @@ class GitClient: GitClientProtocol { return try processCommonErrors(output) } - internal typealias LiveCommandStream = AsyncThrowingMapSequence, String> + public typealias LiveCommandStream = AsyncThrowingMapSequence, String> /// Runs a git command in same way as `run`, but returns a async stream of the output internal func runLive(_ command: String) -> LiveCommandStream { diff --git a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClientProtocol.swift similarity index 90% rename from CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClientProtocol.swift index a66fa7c1ab..ed1e002daa 100644 --- a/CodeEdit/Features/SourceControl/Client/GitClientProtocol.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClientProtocol.swift @@ -12,7 +12,10 @@ import CodeEditCore /// /// This protocol decouples the manager from the concrete ``GitClient`` class, /// enabling mock-based testing without hitting the shell. -protocol GitClientProtocol { +/// `Sendable`: implementations must be stateless command executors (the concrete +/// `GitClient` holds only immutable `let` configuration), so they may be used from +/// any concurrency domain. +public protocol GitClientProtocol: Sendable { // MARK: - Repository func validate() async -> Bool diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigClient.swift similarity index 86% rename from CodeEdit/Features/SourceControl/Client/GitConfigClient.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigClient.swift index fb9b207d47..34c5fb77fd 100644 --- a/CodeEdit/Features/SourceControl/Client/GitConfigClient.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigClient.swift @@ -11,7 +11,8 @@ import CodeEditCore /// A client for managing Git configuration settings. /// Provides methods to read and write Git configuration values at both /// project and global levels. -class GitConfigClient { +/// Stateless config reader (immutable configuration only), hence `Sendable`. +public final class GitConfigClient: Sendable { private let projectURL: URL? private let shellClient: ShellClientProtocol @@ -19,7 +20,7 @@ class GitConfigClient { /// - Parameters: /// - projectURL: The project directory URL (if any). /// - shellClient: The client responsible for executing shell commands. - init(projectURL: URL? = nil, shellClient: ShellClientProtocol) { + public init(projectURL: URL? = nil, shellClient: ShellClientProtocol) { self.projectURL = projectURL self.shellClient = shellClient } @@ -47,7 +48,7 @@ class GitConfigClient { /// - 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? { + public 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) @@ -58,7 +59,7 @@ class GitConfigClient { /// - 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 { + public func set(key: String, value: T, global: Bool = false) async { let shouldUnset: Bool if let boolValue = value as? Bool { shouldUnset = !boolValue diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigExtensions.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigExtensions.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Client/GitConfigExtensions.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigExtensions.swift diff --git a/CodeEdit/Features/SourceControl/Client/GitConfigRepresentable.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigRepresentable.swift similarity index 93% rename from CodeEdit/Features/SourceControl/Client/GitConfigRepresentable.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigRepresentable.swift index 1eb55f5023..95cadc61e0 100644 --- a/CodeEdit/Features/SourceControl/Client/GitConfigRepresentable.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigRepresentable.swift @@ -9,7 +9,7 @@ /// /// 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 { +public protocol GitConfigRepresentable { /// Initializes a new instance from a Git configuration value string. /// - Parameter configValue: The configuration value string. init?(configValue: String) diff --git a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift similarity index 96% rename from CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift index c880d9b4d8..d6f43ab666 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCheckoutBranchView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift @@ -8,16 +8,15 @@ import Foundation import SwiftUI import CodeEditCore -import ShellClient -struct GitCheckoutBranchView: View { +public struct GitCheckoutBranchView: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel: GitCheckoutBranchViewModel private var openDocument: (URL) -> Void - init( + public init( repoLocalPath: URL, shellClient: ShellClientProtocol, openDocument: @escaping (URL) -> Void @@ -25,7 +24,7 @@ struct GitCheckoutBranchView: View { _viewModel = .init(wrappedValue: GitCheckoutBranchViewModel(repoPath: repoLocalPath, shellClient: shellClient)) self.openDocument = openDocument } - var body: some View { + public var body: some View { VStack(spacing: 8) { HStack { Image(nsImage: NSApp.applicationIconImage) diff --git a/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCloneView.swift similarity index 97% rename from CodeEdit/Features/SourceControl/Clone/GitCloneView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCloneView.swift index 5875668cae..db5ad7da12 100644 --- a/CodeEdit/Features/SourceControl/Clone/GitCloneView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCloneView.swift @@ -9,9 +9,8 @@ import CodeEditCore import SwiftUI import Foundation import Combine -import ShellClient -struct GitCloneView: View { +public struct GitCloneView: View { @Environment(\.dismiss) private var dismiss @@ -20,7 +19,7 @@ struct GitCloneView: View { private let openBranchView: (URL) -> Void private let openDocument: (URL) -> Void - init( + public init( shellClient: ShellClientProtocol, openBranchView: @escaping (URL) -> Void, openDocument: @escaping (URL) -> Void @@ -30,7 +29,7 @@ struct GitCloneView: View { self.openDocument = openDocument } - var body: some View { + public var body: some View { VStack(spacing: 8) { HStack(alignment: .top) { Image(nsImage: NSApp.applicationIconImage) diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift similarity index 98% rename from CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift index af69013bc4..72a9f50940 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift @@ -7,8 +7,8 @@ import Foundation import CodeEditCore -import ShellClient +@MainActor class GitCheckoutBranchViewModel: ObservableObject { @Published var selectedBranch: GitBranch? @Published var branches: [GitBranch] = [] diff --git a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift similarity index 99% rename from CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift index 96e39502ab..4a5ad673dc 100644 --- a/CodeEdit/Features/SourceControl/Clone/ViewModels/GitCloneViewModel.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift @@ -7,9 +7,9 @@ import CodeEditCore import Foundation -import ShellClient import AppKit +@MainActor class GitCloneViewModel: ObservableObject { @Published var repoUrlStr = "" @Published var isCloning: Bool = false diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Alerts.swift similarity index 95% rename from CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Alerts.swift index d62ee54a7a..00bcb16cdb 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+Alerts.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Alerts.swift @@ -10,7 +10,7 @@ import AppKit /// Alert presentation helpers for source control error handling. extension SourceControlManager { /// Show alert for error - func showAlertForError(title: String, error: Error) async { + public func showAlertForError(title: String, error: Error) async { if let error = error as? GitClient.GitClientError { await showAlert(title: title, message: error.description) return diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+BranchOperations.swift similarity index 78% rename from CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+BranchOperations.swift index 7f3560de9a..5099a3bf43 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+BranchOperations.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+BranchOperations.swift @@ -11,7 +11,7 @@ import CodeEditCore /// Branch-related git operations. extension SourceControlManager { /// Refresh current branch - func refreshCurrentBranch() async { + public func refreshCurrentBranch() async { let currentBranch = try? await gitClient.getCurrentBranch() await MainActor.run { self.currentBranch = currentBranch @@ -19,7 +19,7 @@ extension SourceControlManager { } /// Refresh branches - func refreshBranches() async { + public func refreshBranches() async { let branches = (try? await gitClient.getBranches(remote: nil)) ?? [] await MainActor.run { self.branches = branches @@ -27,27 +27,27 @@ extension SourceControlManager { } /// Checkout branch - func checkoutBranch(branch: GitBranch) async throws { + public func checkoutBranch(branch: GitBranch) async throws { try await gitClient.checkoutBranch(branch, forceLocal: false, newName: nil) await refreshBranches() await refreshCurrentBranch() } /// Create new branch, can be created only from local branch - func newBranch(name: String, from: GitBranch) async throws { + public func newBranch(name: String, from: GitBranch) async throws { try await gitClient.checkoutBranch(from, forceLocal: false, newName: name) await refreshBranches() await refreshCurrentBranch() } /// Rename branch - func renameBranch(oldName: String, newName: String) async throws { + public func renameBranch(oldName: String, newName: String) async throws { try await gitClient.renameBranch(oldName: oldName, newName: newName) await refreshBranches() } /// Delete branch if it's local and not current - func deleteBranch(branch: GitBranch) async throws { + public func deleteBranch(branch: GitBranch) async throws { if !branch.isLocal || branch == currentBranch { return } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileEvents.swift similarity index 98% rename from CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileEvents.swift index 6c41169c1e..2f92585ab9 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+FileEvents.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileEvents.swift @@ -21,7 +21,7 @@ extension SourceControlManager { /// Pure classifier: maps changed workspace-relative paths to the set of git /// refreshes they require. `workspaceRelativePath` is the workspace root's /// `URL.relativePath` (used to anchor `.git/…` checks). - static func gitRefreshActions( + nonisolated static func gitRefreshActions( for paths: [String], workspaceRelativePath root: String ) -> Set { diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileOperations.swift similarity index 88% rename from CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileOperations.swift index c72b05a1a2..55b5c087f7 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+FileOperations.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileOperations.swift @@ -11,7 +11,7 @@ import CodeEditCore /// File status, staging, committing, and discard operations. extension SourceControlManager { /// Refresh all changed files and refresh status in file manager - func refreshAllChangedFiles() async { + public func refreshAllChangedFiles() async { do { let status = try await gitClient.getStatus() @@ -29,7 +29,7 @@ extension SourceControlManager { } /// Get all changed files for a commit - func getCommitChangedFiles(commitSHA: String) async -> [GitChangedFile] { + public func getCommitChangedFiles(commitSHA: String) async -> [GitChangedFile] { do { return try await gitClient.getCommitChangedFiles(commitSHA: commitSHA) } catch { @@ -39,7 +39,7 @@ extension SourceControlManager { } /// Commit files selected by user - func commit(message: String, details: String? = nil) async throws { + public func commit(message: String, details: String? = nil) async throws { try await gitClient.commit(message: message, details: details) await self.refreshAllChangedFiles() @@ -48,18 +48,18 @@ extension SourceControlManager { /// Adds the given URLs to the staged changes. /// - Parameter files: The files to stage. - func add(_ files: [URL]) async throws { + public func add(_ files: [URL]) async throws { try await gitClient.add(files) } /// Removes the given URLs from the staged changes. /// - Parameter files: The URLs to un-stage. - func reset(_ files: [URL]) async throws { + public func reset(_ files: [URL]) async throws { try await gitClient.reset(files) } /// Refresh number of unsynced commits - func refreshNumberOfUnsyncedCommits() async { + public func refreshNumberOfUnsyncedCommits() async { let numberOfUnpushedCommits = (try? await gitClient.numberOfUnsyncedCommits()) ?? (ahead: 0, behind: 0) await MainActor.run { @@ -68,7 +68,7 @@ extension SourceControlManager { } /// Discard changes for file - func discardChanges(for file: URL) { + public func discardChanges(for file: URL) { Task { do { try await gitClient.discardChanges(for: file) @@ -82,7 +82,7 @@ extension SourceControlManager { } /// Discard changes for repository - func discardAllChanges() { + public func discardAllChanges() { Task { do { try await gitClient.discardAllChanges() diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift similarity index 84% rename from CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift index 68bf77554d..0562bb2314 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+RemoteOperations.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift @@ -11,19 +11,19 @@ import CodeEditCore /// Remote, fetch, pull, and push operations. extension SourceControlManager { /// Fetch from remote - func fetch() async throws { + public func fetch() async throws { try await gitClient.fetchFromRemote() await self.refreshNumberOfUnsyncedCommits() } /// Pull changes from remote - func pull(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { + public func pull(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws { try await gitClient.pullFromRemote(remote: remote, branch: branch, rebase: rebase) await self.refreshNumberOfUnsyncedCommits() } /// Push changes to remote - func push( + public func push( remote: String? = nil, branch: String? = nil, setUpstream: Bool = false, @@ -45,7 +45,7 @@ extension SourceControlManager { } /// Get all remotes - func refreshRemotes() async throws { + public func refreshRemotes() async throws { let remotes = (try? await gitClient.getRemotes()) ?? [] await MainActor.run { self.remotes = remotes @@ -73,18 +73,18 @@ extension SourceControlManager { } /// Get branches for a specific remote - func getRemoteBranches(remote: String) async throws -> [GitBranch] { + public func getRemoteBranches(remote: String) async throws -> [GitBranch] { try await gitClient.getBranches(remote: remote) } /// Add existing remote to git - func addRemote(name: String, location: String) async throws { + public func addRemote(name: String, location: String) async throws { try await gitClient.addRemote(name: name, location: location) try await refreshRemotes() } /// Delete remote - func deleteRemote(remote: GitRemote) async throws { + public func deleteRemote(remote: GitRemote) async throws { try await gitClient.removeRemote(name: remote.name) try await refreshRemotes() } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Repository.swift similarity index 84% rename from CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Repository.swift index 860c9b58c8..2bf21b2082 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+Repository.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Repository.swift @@ -10,7 +10,7 @@ import Foundation /// Repository-level git operations. extension SourceControlManager { /// Validate repository - func validate() async throws { + public func validate() async throws { let isGitRepository = await gitClient.validate() await MainActor.run { self.isGitRepository = isGitRepository @@ -18,7 +18,7 @@ extension SourceControlManager { } /// Initiate repository - func initiate() async throws { + public func initiate() async throws { try await gitClient.initiate() } } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+StashOperations.swift similarity index 82% rename from CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+StashOperations.swift index 19bed3e4d1..8f888dfdd2 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager+StashOperations.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+StashOperations.swift @@ -11,7 +11,7 @@ import CodeEditCore /// Stash-related git operations. extension SourceControlManager { /// Refresh stash entries - func refreshStashEntries() async throws { + public func refreshStashEntries() async throws { let stashEntries = (try? await gitClient.stashList()) ?? [] await MainActor.run { self.stashEntries = stashEntries @@ -26,14 +26,14 @@ extension SourceControlManager { } /// Apply stash entry - func applyStashEntry(stashEntry: GitStashEntry) async throws { + public func applyStashEntry(stashEntry: GitStashEntry) async throws { try await gitClient.applyStashEntry(stashEntry.index) try await refreshStashEntries() await refreshAllChangedFiles() } /// Delete stash entry - func deleteStashEntry(stashEntry: GitStashEntry) async throws { + public func deleteStashEntry(stashEntry: GitStashEntry) async throws { try await gitClient.deleteStashEntry(stashEntry.index) try await refreshStashEntries() } diff --git a/CodeEdit/Features/SourceControl/SourceControlManager.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager.swift similarity index 71% rename from CodeEdit/Features/SourceControl/SourceControlManager.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager.swift index e6397edea0..3f43e666a3 100644 --- a/CodeEdit/Features/SourceControl/SourceControlManager.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager.swift @@ -19,17 +19,18 @@ import CodeEditCore /// - `+FileOperations`: status, staging, commit, discard /// - `+Repository`: validate, initiate /// - `+Alerts`: error presentation helpers -final class SourceControlManager: ObservableObject { - let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "SourceControlManager") +@MainActor +public final class SourceControlManager: ObservableObject { + public let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "SourceControlManager") - let gitClient: GitClientProtocol + public let gitClient: GitClientProtocol /// Reads git configuration. Exposed so source-control views can consult config /// (e.g. `pull.rebase`) without their own shell-client plumbing. - let gitConfig: GitConfigClient + public let gitConfig: GitConfigClient /// The base URL of the workspace - let workspaceURL: URL + public let workspaceURL: URL let eventBus: EventBus var fileEventCancellables: Set = [] @@ -37,29 +38,29 @@ final class SourceControlManager: ObservableObject { // MARK: - Git State /// A list of changed files - @Published var changedFiles: [GitChangedFile] = [] + @Published public var changedFiles: [GitChangedFile] = [] /// Current branch - @Published var currentBranch: GitBranch? + @Published public var currentBranch: GitBranch? /// All branches, local and remote - @Published var branches: [GitBranch] = [] + @Published public var branches: [GitBranch] = [] /// All remotes - @Published var remotes: [GitRemote] = [] + @Published public var remotes: [GitRemote] = [] /// All stashed entries - @Published var stashEntries: [GitStashEntry] = [] + @Published public var stashEntries: [GitStashEntry] = [] /// Number of unsynced commits with remote in current branch - @Published var numberOfUnsyncedCommits: (ahead: Int, behind: Int) = (ahead: 0, behind: 0) + @Published public var numberOfUnsyncedCommits: (ahead: Int, behind: Int) = (ahead: 0, behind: 0) /// Is project a git repository - @Published var isGitRepository: Bool = false + @Published public var isGitRepository: Bool = false // MARK: - Computed Properties - var orderedLocalBranches: [GitBranch] { + public var orderedLocalBranches: [GitBranch] { var orderedBranches: [GitBranch] = [currentBranch].compactMap { $0 } let otherBranches = branches.filter { $0.isLocal && $0 != currentBranch } .sorted { $0.name.lowercased() < $1.name.lowercased() } @@ -69,7 +70,7 @@ final class SourceControlManager: ObservableObject { // MARK: - Initialization - init( + public init( workspaceURL: URL, shellClient: ShellClientProtocol, eventBus: EventBus diff --git a/CodeEdit/Features/SourceControl/SourceControlViewModel.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift similarity index 60% rename from CodeEdit/Features/SourceControl/SourceControlViewModel.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift index 3c37901ec6..7cae4d511c 100644 --- a/CodeEdit/Features/SourceControl/SourceControlViewModel.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift @@ -14,65 +14,67 @@ import CodeEditCore /// This view model owns only ephemeral UI state: which sheets are presented, /// alert flags, and the shared operation fields used by the push and pull sheets. @MainActor -final class SourceControlViewModel: ObservableObject { +public final class SourceControlViewModel: ObservableObject { + public init() {} + // MARK: - Sheet State /// Is the push sheet presented - @Published var pushSheetIsPresented: Bool = false { + @Published public var pushSheetIsPresented: Bool = false { didSet { resetOperationFields() } } /// Is the pull sheet presented - @Published var pullSheetIsPresented: Bool = false { + @Published public var pullSheetIsPresented: Bool = false { didSet { resetOperationFields() } } /// Is the fetch sheet presented - @Published var fetchSheetIsPresented: Bool = false + @Published public var fetchSheetIsPresented: Bool = false /// Is the stash sheet presented - @Published var stashSheetIsPresented: Bool = false + @Published public var stashSheetIsPresented: Bool = false /// Is the remote sheet presented - @Published var addExistingRemoteSheetIsPresented: Bool = false + @Published public var addExistingRemoteSheetIsPresented: Bool = false /// Branch to switch to - @Published var switchToBranch: GitBranch? + @Published public var switchToBranch: GitBranch? // MARK: - Operation Fields /// Branch selected for source control operations (shared between push and pull) - @Published var operationBranch: GitBranch? + @Published public var operationBranch: GitBranch? /// Remote selected for source control operations - @Published var operationRemote: GitRemote? + @Published public var operationRemote: GitRemote? /// Rebase boolean set for source control operations - @Published var operationRebase: Bool = false + @Published public var operationRebase: Bool = false /// Force boolean set for source control operations - @Published var operationForce: Bool = false + @Published public var operationForce: Bool = false /// Include tags boolean set for source control operations - @Published var operationIncludeTags: Bool = false + @Published public var operationIncludeTags: Bool = false // MARK: - Alert State /// Is discard all alert presented - @Published var discardAllAlertIsPresented: Bool = false + @Published public var discardAllAlertIsPresented: Bool = false /// Is no changes to stage alert presented - @Published var noChangesToStageAlertIsPresented: Bool = false + @Published public var noChangesToStageAlertIsPresented: Bool = false /// Is no changes to unstage alert presented - @Published var noChangesToUnstageAlertIsPresented: Bool = false + @Published public var noChangesToUnstageAlertIsPresented: Bool = false /// Is no changes to stash alert presented - @Published var noChangesToStashAlertIsPresented: Bool = false + @Published public var noChangesToStashAlertIsPresented: Bool = false /// Is no changes to discard alert presented - @Published var noChangesToDiscardAlertIsPresented: Bool = false + @Published public var noChangesToDiscardAlertIsPresented: Bool = false // MARK: - Private diff --git a/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/CloneRepositoryUseCase.swift similarity index 99% rename from CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/CloneRepositoryUseCase.swift index 379bb93d74..ec8b56a8eb 100644 --- a/CodeEdit/Features/SourceControl/UseCases/CloneRepositoryUseCase.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/CloneRepositoryUseCase.swift @@ -7,7 +7,6 @@ import CodeEditCore import Foundation -import ShellClient /// Validates and orchestrates a `git clone` operation, streaming progress to the caller. final class CloneRepositoryUseCase { diff --git a/CodeEdit/Utils/Formatters/RegexFormatter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/RegexFormatter.swift similarity index 100% rename from CodeEdit/Utils/Formatters/RegexFormatter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/RegexFormatter.swift diff --git a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/RemoteBranchPicker.swift similarity index 99% rename from CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/RemoteBranchPicker.swift index e7c14031c4..5462d376b1 100644 --- a/CodeEdit/Features/SourceControl/Views/RemoteBranchPicker.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/RemoteBranchPicker.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSymbols import CodeEditCore struct RemoteBranchPicker: View { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift similarity index 96% rename from CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift index 2d3e89358b..621339c16b 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlAddExistingRemoteView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift @@ -7,7 +7,9 @@ import SwiftUI -struct SourceControlAddExistingRemoteView: View { +public struct SourceControlAddExistingRemoteView: View { + public init() {} + @EnvironmentObject var sourceControlManager: SourceControlManager @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @Environment(\.dismiss) @@ -22,7 +24,7 @@ struct SourceControlAddExistingRemoteView: View { @FocusState private var focusedField: FocusedField? - var body: some View { + public var body: some View { VStack(spacing: 0) { Form { Section("Add Remote") { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift similarity index 95% rename from CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift index f3ffb50a4a..c71746a385 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlFetchView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift @@ -7,7 +7,9 @@ import SwiftUI -struct SourceControlFetchView: View { +public struct SourceControlFetchView: View { + public init() {} + @Environment(\.dismiss) private var dismiss @@ -18,7 +20,7 @@ struct SourceControlFetchView: View { sourceControlManager.workspaceURL.lastPathComponent } - var body: some View { + public var body: some View { VStack(spacing: 0) { HStack(alignment: .top, spacing: 20) { Image(nsImage: NSApp.applicationIconImage) diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlNewBranchView.swift similarity index 93% rename from CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlNewBranchView.swift index 1396f49002..5cff001fad 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlNewBranchView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlNewBranchView.swift @@ -8,7 +8,8 @@ import SwiftUI import CodeEditCore -struct SourceControlNewBranchView: View { +public struct SourceControlNewBranchView: View { + @Environment(\.dismiss) var dismiss @@ -17,7 +18,11 @@ struct SourceControlNewBranchView: View { @State var name: String = "" @Binding var fromBranch: GitBranch? - var body: some View { + public init(fromBranch: Binding) { + self._fromBranch = fromBranch + } + + public var body: some View { if let branch = fromBranch ?? sourceControlManager.currentBranch { VStack(spacing: 0) { Form { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift similarity index 97% rename from CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift index a019ca5791..cfb2c4ba10 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift @@ -8,7 +8,9 @@ import CodeEditCore import SwiftUI -struct SourceControlPullView: View { +public struct SourceControlPullView: View { + public init() {} + @Environment(\.dismiss) private var dismiss @@ -20,7 +22,7 @@ struct SourceControlPullView: View { @State var preferRebaseWhenPulling: Bool = false - var body: some View { + public var body: some View { VStack(spacing: 0) { Form { Section { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPushView.swift similarity index 96% rename from CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPushView.swift index 4b434f6953..b7c19cbab1 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlPushView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPushView.swift @@ -7,7 +7,9 @@ import SwiftUI -struct SourceControlPushView: View { +public struct SourceControlPushView: View { + public init() {} + @Environment(\.dismiss) private var dismiss @@ -16,7 +18,7 @@ struct SourceControlPushView: View { @State var loading: Bool = false - var body: some View { + public var body: some View { VStack(spacing: 0) { Form { Section { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift similarity index 92% rename from CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift index 1b7be03bbd..2176188175 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlRenameBranchView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift @@ -8,7 +8,8 @@ import SwiftUI import CodeEditCore -struct SourceControlRenameBranchView: View { +public struct SourceControlRenameBranchView: View { + @Environment(\.dismiss) var dismiss @@ -18,7 +19,11 @@ struct SourceControlRenameBranchView: View { @Binding var fromBranch: GitBranch? - var body: some View { + public init(fromBranch: Binding) { + self._fromBranch = fromBranch + } + + public var body: some View { if let branch = fromBranch ?? sourceControlManager.currentBranch { VStack(spacing: 0) { Form { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlStashView.swift similarity index 98% rename from CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlStashView.swift index 428c6566b2..f5b7c31689 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlStashView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlStashView.swift @@ -7,7 +7,9 @@ import SwiftUI -struct SourceControlStashView: View { +public struct SourceControlStashView: View { + public init() {} + @EnvironmentObject var sourceControlManager: SourceControlManager @EnvironmentObject var sourceControlViewModel: SourceControlViewModel @Environment(\.dismiss) @@ -16,7 +18,7 @@ struct SourceControlStashView: View { @State private var message: String = "" @State private var applyStashAfterOperation: Bool = false - var body: some View { + public var body: some View { VStack(spacing: 0) { Form { Section { diff --git a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlSwitchView.swift similarity index 94% rename from CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlSwitchView.swift index 29ac093e5d..007592b1fb 100644 --- a/CodeEdit/Features/SourceControl/Views/SourceControlSwitchView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlSwitchView.swift @@ -8,7 +8,8 @@ import SwiftUI import CodeEditCore -struct SourceControlSwitchView: View { +public struct SourceControlSwitchView: View { + @Environment(\.dismiss) private var dismiss @@ -17,7 +18,11 @@ struct SourceControlSwitchView: View { var branch: GitBranch - var body: some View { + public init(branch: GitBranch) { + self.branch = branch + } + + public var body: some View { VStack(spacing: 0) { HStack(alignment: .top, spacing: 20) { Image(nsImage: NSApp.applicationIconImage) diff --git a/CodeEdit/Utils/Formatters/TrimWhitespaceFormatter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift similarity index 100% rename from CodeEdit/Utils/Formatters/TrimWhitespaceFormatter.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift index ff72401cb1..a72e5ec9e0 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift @@ -7,7 +7,7 @@ import Foundation -public struct GitBranch: Hashable, Identifiable { +public struct GitBranch: Hashable, Identifiable, Sendable { public let name: String public let longName: String public let upstream: String? diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift index 91bdf92b4a..b5f6994720 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift @@ -7,7 +7,7 @@ import Foundation -public struct GitBranchesGroup: Hashable { +public struct GitBranchesGroup: Hashable, Sendable { public let name: String public var branches: [GitBranch] public var shouldNest: Bool { diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift index 587c306efb..23351eb6b9 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a single changed file in the working tree. -public struct GitChangedFile: Identifiable, Hashable { +public struct GitChangedFile: Identifiable, Hashable, Sendable { public var id: String { fileURL.relativePath } /// The status of the file. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift index 59980eddc4..e4b44c9815 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift @@ -8,7 +8,7 @@ import Foundation.NSDate /// Model class to help map commit history log data -public struct GitCommit: Equatable, Hashable, Identifiable { +public struct GitCommit: Equatable, Hashable, Identifiable, Sendable { public var id: UUID public let hash: String public let commitHash: String diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift index 730e92fffb..30a1abc42e 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift @@ -7,7 +7,7 @@ import Foundation -public struct GitRemote: Hashable { +public struct GitRemote: Hashable, Sendable { public let name: String public let pushLocation: String public let fetchLocation: String diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift index 2e87098ebe..52c832dbb3 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift @@ -7,7 +7,7 @@ import Foundation -public struct GitStashEntry: Hashable { +public struct GitStashEntry: Hashable, Sendable { public let index: Int public let message: String public let date: Date diff --git a/CodeEdit/Features/Search/Extensions/String+SafeOffset.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+SafeOffset.swift similarity index 99% rename from CodeEdit/Features/Search/Extensions/String+SafeOffset.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+SafeOffset.swift index ff120d91f0..6f31a8d744 100644 --- a/CodeEdit/Features/Search/Extensions/String+SafeOffset.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+SafeOffset.swift @@ -8,7 +8,7 @@ import Foundation /// Some safer alternative methods to ``String. -extension String { +public extension String { /// Safely returns an offset index in a string. /// Use ``safeOffset(_:offsetBy:)`` to default to limiting to the start or end indexes. /// - Parameters: From 199ddb4bc7c2ad9ed7e56fd274fd271f9b4c2a63 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 13:24:34 +0200 Subject: [PATCH 147/335] Refactor: Decouple TaskManager from CEWorkspaceSettings via TasksConfigurationProviding --- ...Settings+TasksConfigurationProviding.swift | 17 +++++++++++++ CodeEdit/Features/Tasks/TaskManager.swift | 13 +++++----- .../Features/Workspace/WorkspaceFactory.swift | 2 +- .../Documents/DocumentsUnitTests.swift | 2 +- .../Features/Tasks/TaskManagerTests.swift | 24 ++++++++++++------- .../TasksConfigurationProviding.swift | 19 +++++++++++++++ 6 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings+TasksConfigurationProviding.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings+TasksConfigurationProviding.swift b/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings+TasksConfigurationProviding.swift new file mode 100644 index 0000000000..b142bfbdb7 --- /dev/null +++ b/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings+TasksConfigurationProviding.swift @@ -0,0 +1,17 @@ +// +// CEWorkspaceSettings+TasksConfigurationProviding.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import Combine +import CodeEditCore + +extension CEWorkspaceSettings: TasksConfigurationProviding { + var tasks: [CETask] { settings.tasks } + + var tasksPublisher: AnyPublisher<[CETask], Never> { + $settings.map(\.tasks).eraseToAnyPublisher() + } +} diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/CodeEdit/Features/Tasks/TaskManager.swift index e2233902d9..1e668a3286 100644 --- a/CodeEdit/Features/Tasks/TaskManager.swift +++ b/CodeEdit/Features/Tasks/TaskManager.swift @@ -16,20 +16,19 @@ class TaskManager: ObservableObject { @Published var selectedTaskID: UUID? @Published var taskShowingOutput: UUID? - private let settingsStore: CEWorkspaceSettings + private let tasksConfiguration: any TasksConfigurationProviding private var workspaceURL: URL? private var settingsListener: AnyCancellable? private let eventBus: EventBus - init(settingsStore: CEWorkspaceSettings, workspaceURL: URL?, eventBus: EventBus) { + init(tasksConfiguration: any TasksConfigurationProviding, workspaceURL: URL?, eventBus: EventBus) { self.eventBus = eventBus self.workspaceURL = workspaceURL - self.settingsStore = settingsStore + self.tasksConfiguration = tasksConfiguration - settingsListener = settingsStore.$settings - .map(\.tasks) + settingsListener = tasksConfiguration.tasksPublisher .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -54,7 +53,7 @@ class TaskManager: ObservableObject { } var availableTasks: [CETask] { - return settingsStore.settings.tasks + return tasksConfiguration.tasks } func taskStatus(taskID: UUID) -> CETaskStatus { @@ -67,7 +66,7 @@ class TaskManager: ObservableObject { } func executeActiveTask() { - guard let task = settingsStore.settings.tasks.first(where: { $0.id == selectedTaskID }) else { return } + guard let task = tasksConfiguration.tasks.first(where: { $0.id == selectedTaskID }) else { return } Task { await runTask(task: task) } diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 6ea9ff0a9c..eb6267c3f3 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -78,7 +78,7 @@ enum WorkspaceFactory { workspace.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) if let workspaceSettingsManager = workspace.workspaceSettingsManager { workspace.taskManager = TaskManager( - settingsStore: workspaceSettingsManager, + tasksConfiguration: workspaceSettingsManager, workspaceURL: url, eventBus: eventBus ) diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index eb23b011af..d8a906d51a 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -28,7 +28,7 @@ final class DocumentsUnitTests: XCTestCase { hapticFeedbackPerformerMock = NSHapticFeedbackPerformerMock() navigatorViewModel = .init() workspace.taskManager = TaskManager( - settingsStore: CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())), + tasksConfiguration: CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())), workspaceURL: nil, eventBus: EventBus() ) diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index dd38586f8f..bece2c235b 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -6,26 +6,32 @@ // import Foundation +import Combine import CodeEditSettings import Testing import CodeEditCore @testable import CodeEdit +/// In-memory stand-in for `CEWorkspaceSettings`: task configuration without disk I/O. +final class TasksConfigurationStub: TasksConfigurationProviding { + @Published var tasks: [CETask] = [] + var tasksPublisher: AnyPublisher<[CETask], Never> { $tasks.eraseToAnyPublisher() } +} + @MainActor @Suite(.serialized) class TaskManagerTests { var taskManager: TaskManager! - var settingsStore: CEWorkspaceSettings! + var tasksConfiguration: TasksConfigurationStub! init() throws { - settingsStore = CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())) - settingsStore.settings = try JSONDecoder().decode(CEWorkspaceSettingsData.self, from: Data("{}".utf8)) - taskManager = TaskManager(settingsStore: settingsStore, workspaceURL: nil, eventBus: EventBus()) + tasksConfiguration = TasksConfigurationStub() + taskManager = TaskManager(tasksConfiguration: tasksConfiguration, workspaceURL: nil, eventBus: EventBus()) } func testInitialization() { #expect(taskManager != nil) - #expect(taskManager.availableTasks == settingsStore.settings.tasks) + #expect(taskManager.availableTasks == tasksConfiguration.tasks) } @Test @@ -33,7 +39,7 @@ class TaskManagerTests { Settings.shared.preferences.terminal.shell = .zsh let task = CETask(name: "Test Task", command: "echo 'Hello World'") - settingsStore.settings.tasks.append(task) + tasksConfiguration.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() @@ -52,7 +58,7 @@ class TaskManagerTests { Settings.shared.preferences.terminal.shell = .bash let task = CETask(name: "Test Task", command: "echo 'Hello World'") - settingsStore.settings.tasks.append(task) + tasksConfiguration.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() @@ -69,7 +75,7 @@ class TaskManagerTests { @Test(.disabled("Not sure why but tasks run in shells seem to never receive signals.")) func terminateSelectedTask() async throws { let task = CETask(name: "Test Task", command: "sleep 10") - settingsStore.settings.tasks.append(task) + tasksConfiguration.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() @@ -95,7 +101,7 @@ class TaskManagerTests { @Test(.disabled("Not sure why but tasks run in shells seem to never receive signals.")) func suspendAndResumeTask() async throws { let task = CETask(name: "Test Task", command: "sleep 5") - settingsStore.settings.tasks.append(task) + tasksConfiguration.tasks.append(task) taskManager.selectedTaskID = task.id taskManager.executeActiveTask() diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift new file mode 100644 index 0000000000..5c6446faf2 --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift @@ -0,0 +1,19 @@ +// +// TasksConfigurationProviding.swift +// CodeEditCore +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import Combine + +/// Provides the workspace's configured tasks to consumers that shouldn't know +/// where task configuration is stored (currently `.codeedit/settings.json`, +/// loaded by the app-side `CEWorkspaceSettings`). +public protocol TasksConfigurationProviding: AnyObject { + /// The tasks currently configured for the workspace. + var tasks: [CETask] { get } + + /// Emits the task list whenever the workspace configuration changes. + var tasksPublisher: AnyPublisher<[CETask], Never> { get } +} From 23e9657ba6580716cb9467c3f2bfb8272e097d83 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 13:25:42 +0200 Subject: [PATCH 148/335] Refactor: Scaffold CETerminal feature package --- CodeEdit.xcodeproj/project.pbxproj | 7 +++++ CodeEdit.xcworkspace/contents.xcworkspacedata | 3 +++ Packages/Features/CETerminal/Package.swift | 26 +++++++++++++++++++ .../Sources/CETerminal/CETerminal.swift | 11 ++++++++ 4 files changed, 47 insertions(+) create mode 100644 Packages/Features/CETerminal/Package.swift create mode 100644 Packages/Features/CETerminal/Sources/CETerminal/CETerminal.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 1aa8f969f3..0139161724 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 5889639E2FFA9A87004BE116 /* CENotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 5889639D2FFA9A87004BE116 /* CENotifications */; }; 58CE15A100000001004BE201 /* CELSP in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE15A000000001004BE200 /* CELSP */; }; 58CE50C200000002004BE302 /* CESourceControl in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE50C100000002004BE301 /* CESourceControl */; }; + 58CE7E4200000003004BE402 /* CETerminal in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE7E4100000003004BE401 /* CETerminal */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58ED10022FFB0001004BE116 /* CEEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* CEEditor */; }; 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; @@ -217,6 +218,7 @@ 588950C52FFA5C05004BE116 /* CESearch in Frameworks */, 58CE15A100000001004BE201 /* CELSP in Frameworks */, 58CE50C200000002004BE302 /* CESourceControl in Frameworks */, + 58CE7E4200000003004BE402 /* CETerminal in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, 6C9DB9E42D55656300ACD86E /* CodeEditSourceEditor in Frameworks */, @@ -366,6 +368,7 @@ 588950C42FFA5C05004BE116 /* CESearch */, 58CE15A000000001004BE200 /* CELSP */, 58CE50C100000002004BE301 /* CESourceControl */, + 58CE7E4100000003004BE401 /* CETerminal */, 588957122FFA679E004BE116 /* ShellClient */, 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */, 5889639D2FFA9A87004BE116 /* CENotifications */, @@ -1935,6 +1938,10 @@ isa = XCSwiftPackageProductDependency; productName = CESourceControl; }; + 58CE7E4100000003004BE401 /* CETerminal */ = { + isa = XCSwiftPackageProductDependency; + productName = CETerminal; + }; 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */ = { isa = XCSwiftPackageProductDependency; productName = CodeEditCore; diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 19e74b0ad2..4c87b5c153 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -45,5 +45,8 @@ + + diff --git a/Packages/Features/CETerminal/Package.swift b/Packages/Features/CETerminal/Package.swift new file mode 100644 index 0000000000..83f2f81a01 --- /dev/null +++ b/Packages/Features/CETerminal/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CETerminal", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CETerminal", targets: ["CETerminal"]) + ], + dependencies: [ + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditSettings"), + .package(url: "https://github.com/thecoolwinter/SwiftTerm", branch: "codeedit") + ], + targets: [ + .target( + name: "CETerminal", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditSettings", package: "CodeEditSettings"), + .product(name: "SwiftTerm", package: "SwiftTerm") + ] + ) + ] +) diff --git a/Packages/Features/CETerminal/Sources/CETerminal/CETerminal.swift b/Packages/Features/CETerminal/Sources/CETerminal/CETerminal.swift new file mode 100644 index 0000000000..4cef019b5f --- /dev/null +++ b/Packages/Features/CETerminal/Sources/CETerminal/CETerminal.swift @@ -0,0 +1,11 @@ +// +// CETerminal.swift +// CETerminal +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +/// The CETerminal feature package: the SwiftTerm-backed terminal emulator +/// (`TerminalEmulatorView` and the `CETerminalView` family) and the task-running +/// engine (`TaskManager`, `CEActiveTask`) built on top of it. +enum CETerminal {} From 97d2a72a442023f0dd9ed4ff92037caabbf41ae1 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 13:50:59 +0200 Subject: [PATCH 149/335] Refactor: Move TerminalEmulator and Tasks into the CETerminal package Two decoupling fixes surfaced during the move, beyond the planned seam: - TerminalEmulatorView read the app-side ThemeModel singleton directly; extended the existing @Environment(\.currentTheme) seam (from the CodeFileView/ThemeModel decoupling) with a second key, currentDarkTheme, for the terminal's independent dark-appearance override. Both keys are now injected once at the shared WorkspaceView ancestor of the editor and utility areas, rather than only within editorArea. - URL.absolutePath was only defined in the heavyweight CodeEditDocument package; mirrored the one-line extension locally instead of taking that dependency (same pattern as the CESourceControl whitespace-extension fix). TerminalCache, CELocalShellTerminalView, and CEActiveTask are now @MainActor (their state is UI-bound: cached NSViews / a published terminal view). LocalProcess's delegate callbacks are dispatched on DispatchQueue.main by SwiftTerm's own default, so this reflects existing behavior rather than changing it. CELocalShellTerminalView's SwiftTerm protocol conformances use @preconcurrency since SwiftTerm itself isn't concurrency-checked. LocalProcess+sendText.swift moved as-is; it has zero consumers and is a dead-code removal candidate. --- .../ActivityViewer/ActivityViewer.swift | 1 + .../ActivityViewer/Tasks/ActiveTaskView.swift | 1 + .../Tasks/TaskDropDownView.swift | 1 + .../ActivityViewer/Tasks/TaskView.swift | 1 + .../Tasks/TasksPopoverMenuItem.swift | 1 + .../CEWorkspaceSettingsTaskListView.swift | 1 + .../Views/EditCETaskView.swift | 1 + .../Protocols/WorkspaceManaging.swift | 1 + .../Toolbar}/StartTaskToolbarButton.swift | 1 + .../Toolbar}/StartTaskToolbarItem.swift | 1 + .../Toolbar}/StopTaskToolbarButton.swift | 1 + .../Toolbar}/StopTaskToolbarItem.swift | 1 + .../DebugUtility/TaskOutputActionsView.swift | 1 + .../DebugUtility/TaskOutputView.swift | 1 + .../DebugUtility/UtilityAreaDebugView.swift | 1 + .../Models/UtilityAreaTerminal.swift | 1 + .../UtilityAreaTerminalSidebar.swift | 1 + .../UtilityAreaTerminalView.swift | 1 + .../ViewModels/UtilityAreaViewModel.swift | 17 +++-- .../WindowCommands/TasksCommands.swift | 1 + .../Features/Workspace/Models/Workspace.swift | 1 + .../Features/Workspace/WorkspaceFactory.swift | 1 + CodeEdit/WorkspaceView.swift | 3 +- .../Documents/DocumentsUnitTests.swift | 1 + .../Features/Tasks/CEActiveTaskTests.swift | 1 + .../Features/Tasks/TaskManagerTests.swift | 1 + .../ShellIntegrationTests.swift | 1 + .../UtilityAreaViewModelTests.swift | 1 + .../Tasks/Models/CEActiveTask.swift | 29 ++++---- .../Tasks/Models/CETaskStatus.swift | 4 +- .../CETerminal}/Tasks/TaskManager.swift | 36 +++++----- .../Extensions}/LocalProcess+sendText.swift | 0 .../Extensions}/SwiftTerm+Color+Init.swift | 0 .../Extensions/URL+AbsolutePath.swift | 15 ++++ .../TerminalEmulator/Model/CurrentUser.swift | 0 .../TerminalEmulator/Model/Shell.swift | 2 +- .../Model/ShellIntegration.swift | 0 .../Model/TerminalCache.swift | 9 +-- .../Views/CEActiveTaskTerminalView.swift | 6 +- .../Views/CELocalShellTerminalView.swift | 13 ++-- .../Views/CETerminalView.swift | 4 +- .../TerminalEmulatorView+Coordinator.swift | 10 +-- .../Views/TerminalEmulatorView.swift | 71 ++++++++++--------- .../Store/Environment+Theme.swift | 12 ++++ 44 files changed, 161 insertions(+), 96 deletions(-) rename CodeEdit/Features/{Tasks/Views => Documents/Toolbar}/StartTaskToolbarButton.swift (98%) rename CodeEdit/Features/{Tasks/ToolbarItems => Documents/Toolbar}/StartTaskToolbarItem.swift (98%) rename CodeEdit/Features/{Tasks/Views => Documents/Toolbar}/StopTaskToolbarButton.swift (99%) rename CodeEdit/Features/{Tasks/ToolbarItems => Documents/Toolbar}/StopTaskToolbarItem.swift (99%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/Tasks/Models/CEActiveTask.swift (90%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/Tasks/Models/CETaskStatus.swift (91%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/Tasks/TaskManager.swift (84%) rename {CodeEdit/Utils/Extensions/LocalProcess => Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions}/LocalProcess+sendText.swift (100%) rename {CodeEdit/Utils/Extensions/SwiftTerm/Color => Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions}/SwiftTerm+Color+Init.swift (100%) create mode 100644 Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Model/CurrentUser.swift (100%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Model/Shell.swift (97%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Model/ShellIntegration.swift (100%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Model/TerminalCache.swift (81%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Views/CEActiveTaskTerminalView.swift (93%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Views/CELocalShellTerminalView.swift (93%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Views/CETerminalView.swift (93%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift (70%) rename {CodeEdit/Features => Packages/Features/CETerminal/Sources/CETerminal}/TerminalEmulator/Views/TerminalEmulatorView.swift (74%) diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift index e64c8a7c5c..464c2a9582 100644 --- a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift +++ b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CEWorkspaceFileManager /// A view that shows the activity bar and the current status of any executed task diff --git a/CodeEdit/Features/ActivityViewer/Tasks/ActiveTaskView.swift b/CodeEdit/Features/ActivityViewer/Tasks/ActiveTaskView.swift index eab82ba5fd..3eb7b6d39e 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/ActiveTaskView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/ActiveTaskView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal // We need to observe each active task individually because: // 1. Active tasks are nested inside TaskManager. diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift index 156c53021b..db3dd08b8c 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditUI struct TaskDropDownView: View { diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift index 7082056dfc..22ff14541b 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditCore /// `TaskView` represents a single active task and observes its state. diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift index 433ff88f8e..dedd7cfe3f 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditCore /// - Note: This view **cannot** use the `dismiss` environment value to dismiss the sheet. It has to negate the boolean diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift index e6f67d8568..c07add8fcd 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditCore struct CEWorkspaceSettingsTaskListView: View { diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift index 337cb7f00b..4cae363894 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditCore struct EditCETaskView: View { diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift index 8b676c2942..17a6af4c61 100644 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift @@ -11,6 +11,7 @@ import CEWorkspaceFileManager import CEEditor import CENotifications import CESearch +import CETerminal /// Protocol defining the interface that workspace consumers depend on. /// Enables testability via mock implementations and decouples views from the concrete Workspace type. diff --git a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarButton.swift similarity index 98% rename from CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift rename to CodeEdit/Features/Documents/Toolbar/StartTaskToolbarButton.swift index ace008a717..46e3939a02 100644 --- a/CodeEdit/Features/Tasks/Views/StartTaskToolbarButton.swift +++ b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal struct StartTaskToolbarButton: View { @Environment(\.controlActiveState) diff --git a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift similarity index 98% rename from CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift rename to CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift index c1a0f2c8cd..61075800e3 100644 --- a/CodeEdit/Features/Tasks/ToolbarItems/StartTaskToolbarItem.swift +++ b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift @@ -6,6 +6,7 @@ // import AppKit +import CETerminal @available(macOS 26, *) final class StartTaskToolbarItem: NSToolbarItem { diff --git a/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift b/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarButton.swift similarity index 99% rename from CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift rename to CodeEdit/Features/Documents/Toolbar/StopTaskToolbarButton.swift index 0d4a7bcd98..e53da833d4 100644 --- a/CodeEdit/Features/Tasks/Views/StopTaskToolbarButton.swift +++ b/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import Combine struct StopTaskToolbarButton: View { diff --git a/CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift b/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift similarity index 99% rename from CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift rename to CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift index e3e6b6d63b..1df8a104e8 100644 --- a/CodeEdit/Features/Tasks/ToolbarItems/StopTaskToolbarItem.swift +++ b/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift @@ -6,6 +6,7 @@ // import AppKit +import CETerminal import Combine @available(macOS 26, *) diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift b/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift index ae8b9652a2..6e1815f988 100644 --- a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift +++ b/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditUI struct TaskOutputActionsView: View { diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputView.swift b/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputView.swift index c5325280ee..88a88cb9fa 100644 --- a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputView.swift +++ b/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal struct TaskOutputView: View { @ObservedObject var activeTask: CEActiveTask diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift b/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift index dede677f6a..1546ea1201 100644 --- a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift +++ b/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift b/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift index abf9d780d1..ab66a4078d 100644 --- a/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift +++ b/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift @@ -6,6 +6,7 @@ // import Foundation +import CETerminal final class UtilityAreaTerminal: ObservableObject, Identifiable, Equatable { let id: UUID diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index d6981ca6c6..1a9063b1c2 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal /// The view that displays the list of available terminals in the utility area. /// See ``UtilityAreaTerminalView`` for use. diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index d040e9185f..0760c6ca43 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditSettings import CodeEditUI import Cocoa diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index b2ea6bc88e..a0a6f1ee27 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -6,6 +6,7 @@ // import CodeEditCore +import CETerminal import SwiftUI /// # UtilityAreaViewModel @@ -65,7 +66,10 @@ class UtilityAreaViewModel: ObservableObject { func removeTerminals(_ ids: Set) { for (idx, terminal) in terminals.enumerated().reversed() where ids.contains(terminal.id) { - TerminalCache.shared.removeCachedView(terminal.id) + // `UtilityAreaViewModel` isn't statically @MainActor, but is only ever driven from SwiftUI on main. + MainActor.assumeIsolated { + TerminalCache.shared.removeCachedView(terminal.id) + } terminals.remove(at: idx) } @@ -135,8 +139,11 @@ class UtilityAreaViewModel: ObservableObject { let id = UUID() let url = terminals[index].url let shell = terminals[index].shell - if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { - kill(shellPid, SIGKILL) + // `UtilityAreaViewModel` isn't statically @MainActor, but is only ever driven from SwiftUI on main. + MainActor.assumeIsolated { + if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { + kill(shellPid, SIGKILL) + } } terminals[index] = UtilityAreaTerminal( @@ -145,7 +152,9 @@ class UtilityAreaViewModel: ObservableObject { title: shell?.rawValue ?? "terminal", shell: shell ) - TerminalCache.shared.removeCachedView(replacing) + MainActor.assumeIsolated { + TerminalCache.shared.removeCachedView(replacing) + } selectedTerminals = [id] return diff --git a/CodeEdit/Features/WindowCommands/TasksCommands.swift b/CodeEdit/Features/WindowCommands/TasksCommands.swift index ef067b30c8..d0c18a7c11 100644 --- a/CodeEdit/Features/WindowCommands/TasksCommands.swift +++ b/CodeEdit/Features/WindowCommands/TasksCommands.swift @@ -7,6 +7,7 @@ import SwiftUI import Combine +import CETerminal struct TasksCommands: Commands { @UpdatingWindowController var windowController: CodeEditWindowController? diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 36e11c6d11..d96c8add2a 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -12,6 +12,7 @@ import CodeEditCore import CEEditor import CENotifications import CESearch +import CETerminal import SwiftUI import Foundation diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index eb6267c3f3..53facc3dd8 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -10,6 +10,7 @@ import Foundation import CEWorkspaceFileManager import CEEditor import CESearch +import CETerminal /// Constructs and wires the manager/service object graph for a ``Workspace``. /// diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index fd7d55164d..ca42124464 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -60,6 +60,8 @@ struct WorkspaceView: View { } .edgesIgnoringSafeArea(.top) .frame(maxWidth: .infinity, maxHeight: .infinity) + .environment(\.currentTheme, themeModel.selectedTheme ?? themeModel.themes.first!) + .environment(\.currentDarkTheme, themeModel.selectedDarkTheme) .overlay(alignment: .top) { utilityArea(proxy: proxy) } @@ -167,7 +169,6 @@ struct WorkspaceView: View { } } .frame(minHeight: 170 + 29 + 29) - .environment(\.currentTheme, themeModel.selectedTheme ?? themeModel.themes.first!) .collapsable() .collapsed($utilityAreaViewModel.isMaximized) .holdingPriority(.init(1)) diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index d8a906d51a..ee7f17c7de 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -10,6 +10,7 @@ import CodeEditCore import ShellClient import CESourceControl import CESearch +import CETerminal @testable import CodeEdit @MainActor diff --git a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift index 4f787510b1..26f1847553 100644 --- a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift +++ b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift @@ -8,6 +8,7 @@ import Testing import CodeEditCore @testable import CodeEdit +@testable import CETerminal @MainActor @Suite(.serialized) diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index bece2c235b..5560f7643d 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -11,6 +11,7 @@ import CodeEditSettings import Testing import CodeEditCore @testable import CodeEdit +@testable import CETerminal /// In-memory stand-in for `CEWorkspaceSettings`: task configuration without disk I/O. final class TasksConfigurationStub: TasksConfigurationProviding { diff --git a/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift b/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift index 2b160b829c..cd6e6de8d2 100644 --- a/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift +++ b/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import XCTest @testable import CodeEdit +@testable import CETerminal final class ShellIntegrationTests: XCTestCase { func testBash() throws { diff --git a/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift b/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift index 5406cb58f4..b8a0b664f0 100644 --- a/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift +++ b/CodeEditTests/Features/UtilityArea/UtilityAreaViewModelTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import CodeEdit +import CETerminal final class UtilityAreaViewModelTests: XCTestCase { var model: UtilityAreaViewModel! diff --git a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CEActiveTask.swift similarity index 90% rename from CodeEdit/Features/Tasks/Models/CEActiveTask.swift rename to Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CEActiveTask.swift index 0604f59231..2f5b8a0afc 100644 --- a/CodeEdit/Features/Tasks/Models/CEActiveTask.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CEActiveTask.swift @@ -7,21 +7,22 @@ import SwiftUI import Combine -import SwiftTerm +@preconcurrency import SwiftTerm import CodeEditCore /// Stores the state of a task once it's executed -class CEActiveTask: ObservableObject, Identifiable, Hashable { +@MainActor +public class CEActiveTask: ObservableObject, Identifiable, @preconcurrency Hashable { /// The current progress of the task. - @Published var output: CEActiveTaskTerminalView? + @Published public var output: CEActiveTaskTerminalView? var hasOutputBeenConfigured: Bool = false /// The status of the task. - @Published private(set) var status: CETaskStatus = .notRunning + @Published public private(set) var status: CETaskStatus = .notRunning /// The name of the associated task. - let task: CETask + public let task: CETask /// Prevents tasks overwriting each other. /// Say a user cancels one task, then runs it immediately, the cancel message should show and then the @@ -32,7 +33,7 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { task.id.uuidString + "-" + activeTaskID.uuidString } - var workspaceURL: URL? + public var workspaceURL: URL? private let eventBus: EventBus @@ -41,7 +42,6 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { self.eventBus = eventBus } - @MainActor func run(workspaceURL: URL?, shell: Shell? = nil) { self.workspaceURL = workspaceURL self.activeTaskID = UUID() // generate a new ID for this run @@ -55,7 +55,6 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { output = view } - @MainActor func handleProcessFinished(terminationStatus: Int32) { // Shells add 128 to non-zero exit codes. var terminationStatus = terminationStatus @@ -104,16 +103,14 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { deleteStatusTaskNotification() } - @MainActor - func suspend() { + public func suspend() { if let shellPID = output?.runningPID(), status == .running { kill(shellPID, SIGSTOP) updateTaskStatus(to: .stopped) } } - @MainActor - func resume() { + public func resume() { if let shellPID = output?.runningPID(), status == .running { kill(shellPID, SIGCONT) updateTaskStatus(to: .running) @@ -138,8 +135,7 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { } } - @MainActor - func clearOutput() { + public func clearOutput() { output?.terminal.resetToInitialState() output?.feed(text: "") } @@ -170,19 +166,18 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable { )) } - @MainActor func updateTaskStatus(to taskStatus: CETaskStatus) { self.status = taskStatus } - static func == (lhs: CEActiveTask, rhs: CEActiveTask) -> Bool { + public static func == (lhs: CEActiveTask, rhs: CEActiveTask) -> Bool { return lhs.output == rhs.output && lhs.status == rhs.status && lhs.output?.process.shellPid == rhs.output?.process.shellPid && lhs.task == rhs.task } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(output) hasher.combine(status) hasher.combine(task) diff --git a/CodeEdit/Features/Tasks/Models/CETaskStatus.swift b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift similarity index 91% rename from CodeEdit/Features/Tasks/Models/CETaskStatus.swift rename to Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift index 7a42262fc7..fbeb247abe 100644 --- a/CodeEdit/Features/Tasks/Models/CETaskStatus.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift @@ -8,7 +8,7 @@ import SwiftUI /// Enum to represent a task's status -enum CETaskStatus { +public enum CETaskStatus { // default state case notRunning // User suspended the process @@ -19,7 +19,7 @@ enum CETaskStatus { // Processes finished without an error case finished - var color: Color { + public var color: Color { switch self { case .notRunning: return Color.gray case .stopped: return Color.yellow diff --git a/CodeEdit/Features/Tasks/TaskManager.swift b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/TaskManager.swift similarity index 84% rename from CodeEdit/Features/Tasks/TaskManager.swift rename to Packages/Features/CETerminal/Sources/CETerminal/Tasks/TaskManager.swift index 1e668a3286..0d6575292c 100644 --- a/CodeEdit/Features/Tasks/TaskManager.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/TaskManager.swift @@ -11,10 +11,10 @@ import CodeEditCore /// This class handles the execution of tasks @MainActor -class TaskManager: ObservableObject { - @Published var activeTasks: [UUID: CEActiveTask] = [:] - @Published var selectedTaskID: UUID? - @Published var taskShowingOutput: UUID? +public class TaskManager: ObservableObject { + @Published public var activeTasks: [UUID: CEActiveTask] = [:] + @Published public var selectedTaskID: UUID? + @Published public var taskShowingOutput: UUID? private let tasksConfiguration: any TasksConfigurationProviding @@ -23,7 +23,7 @@ class TaskManager: ObservableObject { private let eventBus: EventBus - init(tasksConfiguration: any TasksConfigurationProviding, workspaceURL: URL?, eventBus: EventBus) { + public init(tasksConfiguration: any TasksConfigurationProviding, workspaceURL: URL?, eventBus: EventBus) { self.eventBus = eventBus self.workspaceURL = workspaceURL self.tasksConfiguration = tasksConfiguration @@ -36,7 +36,7 @@ class TaskManager: ObservableObject { } } - var selectedTask: CETask? { + public var selectedTask: CETask? { if let selectedTaskID { return availableTasks.first { $0.id == selectedTaskID } } else { @@ -52,27 +52,27 @@ class TaskManager: ObservableObject { return nil } - var availableTasks: [CETask] { + public var availableTasks: [CETask] { return tasksConfiguration.tasks } - func taskStatus(taskID: UUID) -> CETaskStatus { + public func taskStatus(taskID: UUID) -> CETaskStatus { return self.activeTasks[taskID]?.status ?? .notRunning } - func updateSelectedTaskID() { + public func updateSelectedTaskID() { guard selectedTask == nil else { return } selectedTaskID = availableTasks.first?.id } - func executeActiveTask() { + public func executeActiveTask() { guard let task = tasksConfiguration.tasks.first(where: { $0.id == selectedTaskID }) else { return } Task { await runTask(task: task) } } - func runTask(task: CETask) async { + public func runTask(task: CETask) async { // A process can only be started once, that means we have to renew the Process and Pipe // but don't initialize a new object. if let activeTask = activeTasks[task.id] { @@ -92,7 +92,7 @@ class TaskManager: ObservableObject { } } - func terminateActiveTask() { + public func terminateActiveTask() { guard let taskID = selectedTaskID else { return } @@ -108,7 +108,7 @@ class TaskManager: ObservableObject { /// this method does nothing. /// /// - Parameter taskID: The ID of the task to suspend. - func suspendTask(taskID: UUID) { + public func suspendTask(taskID: UUID) { if let activeTask = activeTasks[taskID] { activeTask.suspend() } @@ -120,7 +120,7 @@ class TaskManager: ObservableObject { /// this method does nothing. /// /// - Parameter taskID: The ID of the task to resume. - func resumeTask(taskID: UUID) { + public func resumeTask(taskID: UUID) { if let activeTask = activeTasks[taskID] { activeTask.resume() } @@ -136,7 +136,7 @@ class TaskManager: ObservableObject { /// or if the task is not currently running, this method does nothing. /// /// - Parameter taskID: The ID of the task to terminate. - func terminateTask(taskID: UUID) { + public func terminateTask(taskID: UUID) { if let activeTask = activeTasks[taskID] { activeTask.terminate() } @@ -153,19 +153,19 @@ class TaskManager: ObservableObject { /// this method does nothing. /// /// - Parameter taskID: The ID of the task to interrupt. - func interruptTask(taskID: UUID) { + public func interruptTask(taskID: UUID) { if let activeTask = activeTasks[taskID] { activeTask.interrupt() } } - func stopAllTasks() { + public func stopAllTasks() { for (id, _) in activeTasks { interruptTask(taskID: id) } } - func deleteTask(taskID: UUID) { + public func deleteTask(taskID: UUID) { terminateTask(taskID: taskID) activeTasks.removeValue(forKey: taskID) } diff --git a/CodeEdit/Utils/Extensions/LocalProcess/LocalProcess+sendText.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift similarity index 100% rename from CodeEdit/Utils/Extensions/LocalProcess/LocalProcess+sendText.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift diff --git a/CodeEdit/Utils/Extensions/SwiftTerm/Color/SwiftTerm+Color+Init.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift similarity index 100% rename from CodeEdit/Utils/Extensions/SwiftTerm/Color/SwiftTerm+Color+Init.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift new file mode 100644 index 0000000000..9a07881bf9 --- /dev/null +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift @@ -0,0 +1,15 @@ +// +// URL+AbsolutePath.swift +// CETerminal +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import Foundation + +extension URL { + /// The non-percent-encoded absolute path. + var absolutePath: String { + absoluteURL.path(percentEncoded: false) + } +} diff --git a/CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift similarity index 100% rename from CodeEdit/Features/TerminalEmulator/Model/CurrentUser.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift diff --git a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/Shell.swift similarity index 97% rename from CodeEdit/Features/TerminalEmulator/Model/Shell.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/Shell.swift index 9dd6257e30..79a5761539 100644 --- a/CodeEdit/Features/TerminalEmulator/Model/Shell.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/Shell.swift @@ -8,7 +8,7 @@ import Foundation /// Shells supported by CodeEdit -enum Shell: String, CaseIterable { +public enum Shell: String, CaseIterable { case bash case zsh diff --git a/CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift similarity index 100% rename from CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift diff --git a/CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift similarity index 81% rename from CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift index f210085249..c43c05e36f 100644 --- a/CodeEdit/Features/TerminalEmulator/Model/TerminalCache.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift @@ -10,8 +10,9 @@ import SwiftTerm /// Stores a mapping of ID -> terminal view for reusing terminal views. /// This allows terminal views to continue to receive data even when not in the view hierarchy. -final class TerminalCache { - static let shared: TerminalCache = TerminalCache() +@MainActor +public final class TerminalCache { + public static let shared: TerminalCache = TerminalCache() /// The cache of terminal views. private var terminals: [UUID: CELocalShellTerminalView] @@ -23,7 +24,7 @@ final class TerminalCache { /// Get a cached terminal view. /// - Parameter id: The ID of the terminal. /// - Returns: The existing terminal, if it exists. - func getTerminalView(_ id: UUID) -> CELocalShellTerminalView? { + public func getTerminalView(_ id: UUID) -> CELocalShellTerminalView? { terminals[id] } @@ -37,7 +38,7 @@ final class TerminalCache { /// Remove any view associated with the terminal id. /// - Parameter id: The ID of the terminal. - func removeCachedView(_ id: UUID) { + public func removeCachedView(_ id: UUID) { terminals[id] = nil } } diff --git a/CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift similarity index 93% rename from CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift index 437e5b405d..66da6b5d41 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift @@ -9,7 +9,7 @@ import AppKit import CodeEditSettings import SwiftTerm -class CEActiveTaskTerminalView: CELocalShellTerminalView { +public class CEActiveTaskTerminalView: CELocalShellTerminalView { var activeTask: CEActiveTask var isUserCommandRunning: Bool { @@ -25,7 +25,7 @@ class CEActiveTaskTerminalView: CELocalShellTerminalView { fatalError("init(coder:) has not been implemented") } - override func startProcess( + override public func startProcess( workspaceURL url: URL?, shell: Shell? = nil, environment: [String] = [], @@ -60,7 +60,7 @@ class CEActiveTaskTerminalView: CELocalShellTerminalView { ) } - override func processTerminated(_ source: LocalProcess, exitCode: Int32?) { + override public func processTerminated(_ source: LocalProcess, exitCode: Int32?) { activeTask.handleProcessFinished(terminationStatus: exitCode ?? 1) } diff --git a/CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift similarity index 93% rename from CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift index d401cfa0db..2cc0393522 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/CELocalShellTerminalView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift @@ -7,7 +7,7 @@ import AppKit import CodeEditSettings -import SwiftTerm +@preconcurrency import SwiftTerm import Foundation /// # Dev Note (please read) @@ -21,7 +21,11 @@ import Foundation /// This has now been updated so that it differs from `LocalProcessTerminalView` in enough important ways that it /// should not be removed in the future even if SwiftTerm has a change in behavior. -protocol CELocalShellTerminalViewDelegate: AnyObject { +/// `@MainActor`: `LocalProcess`'s exit-monitor and read-queue callbacks are dispatched on +/// `DispatchQueue.main` (SwiftTerm's default when no custom queue is supplied), and +/// `TerminalViewDelegate` callbacks originate from AppKit view events — both always on main. +@MainActor +public protocol CELocalShellTerminalViewDelegate: AnyObject { /// This method is invoked to notify that the terminal has been resized to the specified number of columns and rows /// the user interface code might try to adjust the containing scroll view, or if it is a top level window, the /// window itself @@ -49,8 +53,9 @@ protocol CELocalShellTerminalViewDelegate: AnyObject { // MARK: - CELocalShellTerminalView -class CELocalShellTerminalView: CETerminalView, TerminalViewDelegate, LocalProcessDelegate { - var process: LocalProcess! +@MainActor +public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalViewDelegate, @preconcurrency LocalProcessDelegate { + public var process: LocalProcess! override public init(frame: CGRect) { super.init(frame: frame) diff --git a/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift similarity index 93% rename from CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift index 3541da9a52..d79dfca84a 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift @@ -10,8 +10,8 @@ import AppKit /// # Please see dev note in ``CELocalShellTerminalView``! -class CETerminalView: TerminalView { - override func setFrameSize(_ newSize: NSSize) { +public class CETerminalView: TerminalView { + override public func setFrameSize(_ newSize: NSSize) { if newSize != .zero { super.setFrameSize(newSize) } diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift similarity index 70% rename from CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift index ad290c4f44..5fa695ea38 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift @@ -9,7 +9,7 @@ import SwiftUI import SwiftTerm extension TerminalEmulatorView { - final class Coordinator: NSObject, CELocalShellTerminalViewDelegate { + public final class Coordinator: NSObject, CELocalShellTerminalViewDelegate { private let terminalID: UUID public var onTitleChange: (_ title: String) -> Void @@ -22,15 +22,15 @@ extension TerminalEmulatorView { super.init() } - func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {} + public func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {} - func sizeChanged(source: CETerminalView, newCols: Int, newRows: Int) {} + public func sizeChanged(source: CETerminalView, newCols: Int, newRows: Int) {} - func setTerminalTitle(source: CETerminalView, title: String) { + public func setTerminalTitle(source: CETerminalView, title: String) { onTitleChange(title) } - func processTerminated(source: TerminalView, exitCode: Int32?) { + public func processTerminated(source: TerminalView, exitCode: Int32?) { guard let exitCode else { return } diff --git a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift similarity index 74% rename from CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift rename to Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift index cac7fc610a..fd64f060c5 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -18,7 +18,7 @@ import SwiftTerm /// /// Caches the view in the ``TerminalCache`` to keep terminal state when the view is removed from the hierarchy. /// -struct TerminalEmulatorView: NSViewRepresentable { +public struct TerminalEmulatorView: NSViewRepresentable { enum TerminalMode { case shell(shellType: Shell?) case task(activeTask: CEActiveTask) @@ -29,7 +29,8 @@ struct TerminalEmulatorView: NSViewRepresentable { @AppSettings(\.textEditing.font) var fontSettings - @StateObject private var themeModel: ThemeModel = .shared + @Environment(\.currentTheme) private var currentTheme + @Environment(\.currentDarkTheme) private var currentDarkTheme private var font: NSFont { if terminalSettings.useTextEditorFont { @@ -42,8 +43,8 @@ struct TerminalEmulatorView: NSViewRepresentable { private let terminalID: UUID private var url: URL - public var mode: TerminalMode - public var onTitleChange: (_ title: String) -> Void + var mode: TerminalMode + var onTitleChange: (_ title: String) -> Void /// Create an emulator view /// - Parameters: @@ -51,14 +52,14 @@ struct TerminalEmulatorView: NSViewRepresentable { /// - terminalID: The ID of the terminal. Used to restore state when switching away from the view. /// - shellType: The type of shell to use. Overrides any settings or auto-detection. /// - onTitleChange: A callback used when the terminal updates it's title. - init(url: URL, terminalID: UUID, shellType: Shell? = nil, onTitleChange: @escaping (_ title: String) -> Void) { + public init(url: URL, terminalID: UUID, shellType: Shell? = nil, onTitleChange: @escaping (_ title: String) -> Void) { self.url = url self.terminalID = terminalID self.mode = .shell(shellType: shellType) self.onTitleChange = onTitleChange } - init(url: URL, task: CEActiveTask) { + public init(url: URL, task: CEActiveTask) { terminalID = task.task.id self.url = url self.mode = .task(activeTask: task) @@ -86,48 +87,48 @@ struct TerminalEmulatorView: NSViewRepresentable { /// Returns the mapped array of `SwiftTerm.Color` objects of ANSI Colors private var colors: [SwiftTerm.Color] { - if let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? themeModel.selectedDarkTheme - : themeModel.selectedTheme, - let index = themeModel.themes.firstIndex(of: selectedTheme) { - return themeModel.themes[index].terminal.ansiColors.map { color in - SwiftTerm.Color(hex: color) - } + guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance + ? currentDarkTheme + : currentTheme + else { + return [] + } + return selectedTheme.terminal.ansiColors.map { color in + SwiftTerm.Color(hex: color) } - return [] } /// Returns the `cursor` color of the selected theme private var cursorColor: NSColor { - if let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? themeModel.selectedDarkTheme - : themeModel.selectedTheme, - let index = themeModel.themes.firstIndex(of: selectedTheme) { - return NSColor(themeModel.themes[index].terminal.cursor.swiftColor) + guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance + ? currentDarkTheme + : currentTheme + else { + return NSColor(.accentColor) } - return NSColor(.accentColor) + return NSColor(selectedTheme.terminal.cursor.swiftColor) } /// Returns the `selection` color of the selected theme private var selectionColor: NSColor { - if let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? themeModel.selectedDarkTheme - : themeModel.selectedTheme, - let index = themeModel.themes.firstIndex(of: selectedTheme) { - return NSColor(themeModel.themes[index].terminal.selection.swiftColor) + guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance + ? currentDarkTheme + : currentTheme + else { + return NSColor(.accentColor) } - return NSColor(.accentColor) + return NSColor(selectedTheme.terminal.selection.swiftColor) } /// Returns the `text` color of the selected theme private var textColor: NSColor { - if let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? themeModel.selectedDarkTheme - : themeModel.selectedTheme, - let index = themeModel.themes.firstIndex(of: selectedTheme) { - return NSColor(themeModel.themes[index].terminal.text.swiftColor) + guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance + ? currentDarkTheme + : currentTheme + else { + return NSColor(.primary) } - return NSColor(.primary) + return NSColor(selectedTheme.terminal.text.swiftColor) } /// Returns the `background` color of the selected theme @@ -147,7 +148,7 @@ struct TerminalEmulatorView: NSViewRepresentable { // MARK: - NSViewRepresentable /// Inherited from NSViewRepresentable.makeNSView(context:). - func makeNSView(context: Context) -> CELocalShellTerminalView { + public func makeNSView(context: Context) -> CELocalShellTerminalView { let view: CELocalShellTerminalView switch mode { @@ -203,7 +204,7 @@ struct TerminalEmulatorView: NSViewRepresentable { return nil } - func updateNSView(_ view: CELocalShellTerminalView, context: Context) { + public func updateNSView(_ view: CELocalShellTerminalView, context: Context) { view.installColors(self.colors) view.caretColor = cursorColor.withAlphaComponent(0.5) view.caretTextColor = cursorColor.withAlphaComponent(0.5) @@ -218,7 +219,7 @@ struct TerminalEmulatorView: NSViewRepresentable { view.feed(text: "") // send empty character to force colors to be redrawn } - func makeCoordinator() -> Coordinator { + public func makeCoordinator() -> Coordinator { Coordinator(terminalID: terminalID, mode: mode, onTitleChange: onTitleChange) } } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift index 53c1c509c7..ad00355eee 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift @@ -11,9 +11,21 @@ private struct CurrentThemeKey: EnvironmentKey { nonisolated(unsafe) static let defaultValue: Theme? = nil } +private struct CurrentDarkThemeKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: Theme? = nil +} + public extension EnvironmentValues { var currentTheme: Theme? { get { self[CurrentThemeKey.self] } set { self[CurrentThemeKey.self] = newValue } } + + /// The user's saved dark-appearance theme, independent of `currentTheme`. + /// Lets a view force dark colors (e.g. a terminal's "always dark" setting) + /// without following the editor's active light/dark theme. + var currentDarkTheme: Theme? { + get { self[CurrentDarkThemeKey.self] } + set { self[CurrentDarkThemeKey.self] = newValue } + } } From e2958229ac0f620ac5e5c49771d27feb0c73926b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 17:40:50 +0200 Subject: [PATCH 150/335] Refactor: Depend on WorkspaceWindowManaging instead of the concrete manager --- CodeEdit/AppDelegate.swift | 2 +- CodeEdit/Features/Welcome/GitCloneButton.swift | 2 +- CodeEdit/Features/Welcome/NewFileButton.swift | 2 +- .../Features/Welcome/OpenFileOrFolderButton.swift | 2 +- .../Features/WindowCommands/FileCommands.swift | 2 +- .../Models/Environment+AppCommands.swift | 4 ++-- .../Protocols/WorkspaceWindowManaging.swift | 15 +++++++++++++++ .../Workspace/AppFileRelocatorTests.swift | 9 +++++++++ .../Workspace/AppWorkspaceNavigatorTests.swift | 9 +++++++++ 9 files changed, 40 insertions(+), 7 deletions(-) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index d94944a879..17ecd5aeee 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -26,7 +26,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { let dependencies = AppDependencies() var lspService: LSPService { dependencies.lspService } - var windowManager: WorkspaceWindowManager { dependencies.workspaceWindowManager } + var windowManager: any WorkspaceWindowManaging { dependencies.workspaceWindowManager } var eventBus: EventBus { dependencies.eventBus } private lazy var shutdownUseCase = ShutdownApplicationUseCase( diff --git a/CodeEdit/Features/Welcome/GitCloneButton.swift b/CodeEdit/Features/Welcome/GitCloneButton.swift index 7c415c904e..de31155165 100644 --- a/CodeEdit/Features/Welcome/GitCloneButton.swift +++ b/CodeEdit/Features/Welcome/GitCloneButton.swift @@ -16,7 +16,7 @@ struct GitCloneButton: View { @State private var showGitClone = false @State private var showCheckoutBranchItem: URL? - let windowManager: WorkspaceWindowManager + let windowManager: any WorkspaceWindowManaging let shellClient: ShellClientProtocol var dismissWindow: () -> Void diff --git a/CodeEdit/Features/Welcome/NewFileButton.swift b/CodeEdit/Features/Welcome/NewFileButton.swift index b9aa183e13..4180f36a36 100644 --- a/CodeEdit/Features/Welcome/NewFileButton.swift +++ b/CodeEdit/Features/Welcome/NewFileButton.swift @@ -10,7 +10,7 @@ import WelcomeWindow struct NewFileButton: View { - let windowManager: WorkspaceWindowManager + let windowManager: any WorkspaceWindowManaging var dismissWindow: () -> Void diff --git a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift index f0da10648a..1db70a7b45 100644 --- a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift +++ b/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift @@ -13,7 +13,7 @@ struct OpenFileOrFolderButton: View { @Environment(\.openWindow) private var openWindow - let windowManager: WorkspaceWindowManager + let windowManager: any WorkspaceWindowManaging var dismissWindow: () -> Void diff --git a/CodeEdit/Features/WindowCommands/FileCommands.swift b/CodeEdit/Features/WindowCommands/FileCommands.swift index 665243220e..33a36e2318 100644 --- a/CodeEdit/Features/WindowCommands/FileCommands.swift +++ b/CodeEdit/Features/WindowCommands/FileCommands.swift @@ -10,7 +10,7 @@ import SwiftUI struct FileCommands: Commands { static let recentProjectsMenu = RecentProjectsMenu() - let windowManager: WorkspaceWindowManager + let windowManager: any WorkspaceWindowManaging @Environment(\.openWindow) private var openWindow diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 261b0391ae..34d64abeda 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -43,7 +43,7 @@ private struct RegistryManagerKey: EnvironmentKey { } private struct WorkspaceWindowManagerKey: EnvironmentKey { - static let defaultValue: WorkspaceWindowManager? = nil + static let defaultValue: (any WorkspaceWindowManaging)? = nil } private struct EventBusKey: EnvironmentKey { @@ -79,7 +79,7 @@ extension EnvironmentValues { /// The workspace window manager, for flows that open arbitrary files or workspaces /// (e.g. Settings pages opening ~/.gitconfig). Optional: nil in previews. - var workspaceWindowManager: WorkspaceWindowManager? { + var workspaceWindowManager: (any WorkspaceWindowManaging)? { get { self[WorkspaceWindowManagerKey.self] } set { self[WorkspaceWindowManagerKey.self] = newValue } } diff --git a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift index a7a850a238..c134780420 100644 --- a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift +++ b/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift @@ -15,6 +15,21 @@ protocol WorkspaceWindowManaging: AnyObject { func closeWorkspace(_ workspace: Workspace) func workspace(containing url: URL) -> Workspace? func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool + + /// Presents an open panel and opens the chosen workspace or file. + func openDocumentFromPanel() + /// Presents a save panel, creates the file, and opens it. + func newDocumentFromPanel() + /// Opens a workspace or file at the given URL, calling the completion handler on success. + func openDocument(at url: URL, onCompletion: @escaping () -> Void) + /// Opens a dialog to choose a file or folder. (The concrete implementation + /// provides default argument values; protocol requirements cannot.) + func openDocumentWithDialog( + canChooseFiles: Bool, + canChooseDirectories: Bool, + onDialogPresented: (() -> Void)?, + onCancel: (() -> Void)? + ) } extension WorkspaceWindowManaging { diff --git a/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift b/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift index 63a07e8884..41d212de7a 100644 --- a/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppFileRelocatorTests.swift @@ -23,6 +23,15 @@ struct AppFileRelocatorTests { return nil } func openFileInWorkspace(url: URL, asTemporary: Bool) -> Bool { false } + func openDocumentFromPanel() {} + func newDocumentFromPanel() {} + func openDocument(at url: URL, onCompletion: @escaping () -> Void) {} + func openDocumentWithDialog( + canChooseFiles: Bool, + canChooseDirectories: Bool, + onDialogPresented: (() -> Void)?, + onCancel: (() -> Void)? + ) {} } @MainActor diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index e30368dbee..db1c1cb26f 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -25,6 +25,15 @@ struct AppWorkspaceNavigatorTests { opened.append((url, asTemporary)) return true } + func openDocumentFromPanel() {} + func newDocumentFromPanel() {} + func openDocument(at url: URL, onCompletion: @escaping () -> Void) {} + func openDocumentWithDialog( + canChooseFiles: Bool, + canChooseDirectories: Bool, + onDialogPresented: (() -> Void)?, + onCancel: (() -> Void)? + ) {} } @MainActor From c45fc45c4a8f5d7d63bdfbca22cf99bdcfc57d92 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 17:43:05 +0200 Subject: [PATCH 151/335] Refactor: Depend on LSPServiceProtocol in command consumers --- CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift | 4 ++-- .../Features/Workspace/UseCases/CloseWorkspaceUseCase.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift index b9fb909967..ec76867e53 100644 --- a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift @@ -17,12 +17,12 @@ import CodeEditDocument /// standalone-window view, and `LSPService` lifecycle notifications. @MainActor final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { - private let lspService: LSPService + private let lspService: any LSPServiceProtocol private let windowManager: WorkspaceWindowManaging private let languageServices: LanguageServicesProvider init( - lspService: LSPService, + lspService: any LSPServiceProtocol, windowManager: WorkspaceWindowManaging, languageServices: LanguageServicesProvider ) { diff --git a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift index 496bc80886..5e9a79c923 100644 --- a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift @@ -12,9 +12,9 @@ import Foundation @MainActor final class CloseWorkspaceUseCase { - private let lspService: LSPService + private let lspService: any LSPServiceProtocol - init(lspService: LSPService) { + init(lspService: any LSPServiceProtocol) { self.lspService = lspService } From 2b1855773566777b9b2cff72ed8003e31a094f83 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 19:51:57 +0200 Subject: [PATCH 152/335] Refactor: Construct Workspace complete via WorkspaceFactory.make MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace's managers are non-optional for its lifetime; the two-phase populate pattern, the bare test init, and the unused WorkspaceManaging protocol are gone. tearDown() is cleanup-only — WorkspaceLifecycleTests guards deallocation instead of defensive nil-ing. --- .../UseCases/AcceptDroppedFilesUseCase.swift | 2 +- .../UseCases/MoveFileUseCase.swift | 6 +- .../CodeEditSplitViewController.swift | 22 ++- .../CodeEditWindowController+Panels.swift | 4 +- .../CodeEditWindowController+Toolbar.swift | 8 +- .../CodeEditWindowController.swift | 24 +-- .../CodeEditWindowControllerExtensions.swift | 23 ++- .../Protocols/WorkspaceManaging.swift | 38 ----- .../Toolbar/StartTaskToolbarItem.swift | 4 +- .../Toolbar/StopTaskToolbarItem.swift | 4 +- .../OutlineView/FileSystemTableViewCell.swift | 2 +- .../OutlineView/ProjectNavigatorMenu.swift | 4 +- .../ProjectNavigatorMenuActions.swift | 18 +-- .../ProjectNavigatorOutlineView.swift | 5 +- ...ewController+NSOutlineViewDataSource.swift | 10 +- ...ViewController+NSOutlineViewDelegate.swift | 12 +- ...troller+OutlineTableViewCellDelegate.swift | 2 +- .../ProjectNavigatorViewController.swift | 14 +- .../ChangedFile/GitChangedFileLabel.swift | 2 - .../WindowCommands/EditorCommands.swift | 2 +- .../WindowCommands/NavigateCommands.swift | 2 +- .../WindowControllerPropertyWrapper.swift | 6 +- .../WindowCommands/ViewCommands.swift | 2 +- .../Features/Workspace/Models/Workspace.swift | 146 +++++++++--------- .../Services/AppWorkspaceNavigator.swift | 2 +- .../Services/WorkspaceWindowManager.swift | 10 +- .../UseCases/CloseWorkspaceUseCase.swift | 4 +- .../UseCases/OpenWorkspaceUseCase.swift | 6 +- .../UseCases/ShutdownApplicationUseCase.swift | 2 +- .../Features/Workspace/WorkspaceFactory.swift | 104 +++++++------ .../Documents/DocumentsUnitTests.swift | 16 +- .../LSP/LanguageServer+CodeFileDocument.swift | 10 +- .../AppWorkspaceNavigatorTests.swift | 11 +- .../Workspace/WorkspaceLifecycleTests.swift | 35 +++++ .../Helpers/TestWorkspaceFactory.swift | 22 +++ 35 files changed, 297 insertions(+), 287 deletions(-) delete mode 100644 CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift create mode 100644 CodeEditTests/Features/Workspace/WorkspaceLifecycleTests.swift create mode 100644 CodeEditTests/Helpers/TestWorkspaceFactory.swift diff --git a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift index 886fb40654..ce8900c451 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift @@ -41,7 +41,7 @@ final class AcceptDroppedFilesUseCase { } // Resolve the source: either an existing workspace file, or treat as external - let source = workspace.workspaceFileManager?.getFile(url.path) + let source = workspace.workspaceFileManager.getFile(url.path) ?? CEWorkspaceFile(url: URL(fileURLWithPath: url.path)) // Handle existing destination via the supplied confirmation closure diff --git a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift index 20e511b5ef..3373305499 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift @@ -16,7 +16,7 @@ import CodeEditCore final class MoveFileUseCase { func execute(file: CEWorkspaceFile, to destination: URL, in workspace: Workspace) throws -> CEWorkspaceFile? { - guard let newFile = try workspace.workspaceFileManager?.move(file: file, to: destination) else { + guard let newFile = try workspace.workspaceFileManager.move(file: file, to: destination) else { return nil } @@ -25,10 +25,10 @@ final class MoveFileUseCase { } if !file.isFolder { - workspace.editorManager?.editorLayout.closeAllTabs(of: file) + workspace.editorManager.editorLayout.closeAllTabs(of: file) } workspace.listenerModel.highlightedFileItem = newFile - workspace.editorManager?.openTab(item: newFile) + workspace.editorManager.openTab(item: newFile) return newFile } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 1d6ce77b9c..de0eeb7c4d 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -66,20 +66,18 @@ final class CodeEditSplitViewController: NSSplitViewController { return } - guard let workspace, - let navigatorViewModel, - let editorManager = workspace.editorManager, - let statusBarViewModel = workspace.statusBarViewModel, - let utilityAreaModel = workspace.utilityAreaModel, - let projectNavigatorViewModel = workspace.projectNavigatorViewModel, - let sourceControlManager = workspace.sourceControlManager, - let sourceControlViewModel = workspace.sourceControlViewModel, - let searchState = workspace.searchState, - let taskManager = workspace.taskManager else { - // swiftlint:disable:next line_length - assertionFailure("Missing a workspace model: workspace=\(workspace == nil), navigator=\(navigatorViewModel == nil), editorManager=\(workspace?.editorManager == nil), statusBarModel=\(workspace?.statusBarViewModel == nil), utilityAreaModel=\(workspace?.utilityAreaModel == nil), taskManager=\(workspace?.taskManager == nil)") + guard let workspace, let navigatorViewModel else { + assertionFailure("Missing workspace=\(workspace == nil) or navigator=\(navigatorViewModel == nil)") return } + let editorManager = workspace.editorManager + let statusBarViewModel = workspace.statusBarViewModel + let utilityAreaModel = workspace.utilityAreaModel + let projectNavigatorViewModel = workspace.projectNavigatorViewModel + let sourceControlManager = workspace.sourceControlManager + let sourceControlViewModel = workspace.sourceControlViewModel + let searchState = workspace.searchState + let taskManager = workspace.taskManager splitView.translatesAutoresizingMaskIntoConstraints = false diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift index ea9feebb2e..5a18e5a298 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift @@ -79,10 +79,10 @@ extension CodeEditWindowController { toggle: { self.toggleLastPanel(shouldAnimate: false) } ), PanelDescriptor( - isCollapsed: { self.workspace?.utilityAreaModel?.isCollapsed ?? true }, + isCollapsed: { self.workspace?.utilityAreaModel.isCollapsed ?? true }, getPrevCollapsed: { self.prevUtilityAreaCollapsed }, setPrevCollapsed: { self.prevUtilityAreaCollapsed = $0 }, - toggle: { self.workspace?.utilityAreaModel?.togglePanel(animation: false) } + toggle: { self.workspace?.utilityAreaModel.togglePanel(animation: false) } ), PanelDescriptor( isCollapsed: { self.toolbarCollapsed }, diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index 9af47c04f8..699fc4e933 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -103,14 +103,14 @@ extension CodeEditWindowController { func toggleToolbar() { toolbarCollapsed.toggle() - workspace?.statePersistence?.set(key: .toolbarCollapsed, value: toolbarCollapsed) + workspace?.statePersistence.set(key: .toolbarCollapsed, value: toolbarCollapsed) updateToolbarVisibility() } func updateToolbarVisibility() { if toolbarCollapsed { window?.titleVisibility = .visible - window?.title = workspace?.workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" + window?.title = workspace?.workspaceFileManager.folderUrl.lastPathComponent ?? "Empty" window?.toolbar = nil } else { window?.titleVisibility = .hidden @@ -182,10 +182,10 @@ extension CodeEditWindowController { guard #available(macOS 26, *) else { fatalError("Unified task sidebar item used on pre-tahoe platform.") } - guard let workspace, - let stop = StopTaskToolbarItem(workspace: workspace) else { + guard let workspace else { return nil } + let stop = StopTaskToolbarItem(workspace: workspace) let start = StartTaskToolbarItem(workspace: workspace, commandManager: dependencies.commandManager) let group = NSToolbarItemGroup(itemIdentifier: .taskSidebarItem) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 3984adf97b..98b643367a 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -52,7 +52,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs window?.delegate = self guard let workspace else { return } self.workspace = workspace - self.toolbarCollapsed = workspace.statePersistence?.get(.toolbarCollapsed) as? Bool ?? false + self.toolbarCollapsed = workspace.statePersistence.get(.toolbarCollapsed) as? Bool ?? false guard let splitViewController = setupSplitView(with: workspace) else { fatalError("Failed to set up content view.") } @@ -122,11 +122,12 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs @IBAction func saveDocument(_ sender: Any) { guard let codeFile = getSelectedCodeFile() else { return } codeFile.save(sender) - workspace?.editorManager?.activeEditor.temporaryTab = nil + workspace?.editorManager.activeEditor.temporaryTab = nil } @IBAction func openCommandPalette(_ sender: Any) { - if let workspace, let state = workspace.commandsPaletteState { + if let workspace { + let state = workspace.commandsPaletteState if let commandPalettePanel { if commandPalettePanel.isKeyWindow { commandPalettePanel.close() @@ -163,14 +164,15 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs if let navigatorViewModel = navigatorSidebarViewModel, let searchTab = navigatorViewModel.tabItems.first(where: { $0 == .search }) { DispatchQueue.main.async { - self.workspace?.searchState?.shouldFocusSearchField = true + self.workspace?.searchState.shouldFocusSearchField = true navigatorViewModel.setNavigatorTab(tab: searchTab) } } } @IBAction func openQuickly(_ sender: Any?) { - if let workspace, let state = workspace.openQuicklyViewModel { + if let workspace { + let state = workspace.openQuicklyViewModel if let quickOpenPanel { if quickOpenPanel.isKeyWindow { quickOpenPanel.close() @@ -189,7 +191,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs panel.close() self.panelOpen = false } openFile: { file in - workspace.editorManager?.openTab(item: file) + workspace.editorManager.openTab(item: file) } .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileProvider, workspace.workspaceFileManager) @@ -207,20 +209,20 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs @IBAction func closeCurrentTab(_ sender: Any) { if self.panelOpen { return } - if (workspace?.editorManager?.activeEditor.tabs ?? []).isEmpty { + if (workspace?.editorManager.activeEditor.tabs ?? []).isEmpty { self.closeActiveEditor(self) } else { - workspace?.editorManager?.activeEditor.closeSelectedTab() + workspace?.editorManager.activeEditor.closeSelectedTab() } } @IBAction func closeActiveEditor(_ sender: Any) { - if workspace?.editorManager?.editorLayout.findSomeEditor( - except: workspace?.editorManager?.activeEditor + if workspace?.editorManager.editorLayout.findSomeEditor( + except: workspace?.editorManager.activeEditor ) == nil { NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: NSApp.keyWindow, from: nil) } else { - workspace?.editorManager?.activeEditor.close() + workspace?.editorManager.activeEditor.close() } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 2cd5c091cc..609019bab8 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -36,7 +36,7 @@ extension CodeEditWindowController { // Listen to changes in all tabs/files internal func listenToDocumentEdited(workspace: Workspace) { - guard let editorManager = workspace.editorManager else { return } + let editorManager = workspace.editorManager editorManager.$activeEditor .flatMap({ editor in editor.$tabs @@ -73,24 +73,21 @@ extension CodeEditWindowController { // Recalculate documentEdited by checking if any tab/file is edited private func updateDocumentEdited(workspace: Workspace) { - let hasEditedDocuments = !(workspace - .editorManager - .map({ editorManager in - editorManager - .editorLayout - .gatherOpenFiles() - .filter({ editorManager.document(for: $0)?.isDocumentEdited == true }) - })? - .isEmpty ?? true) + let editorManager = workspace.editorManager + let hasEditedDocuments = !editorManager + .editorLayout + .gatherOpenFiles() + .filter({ editorManager.document(for: $0)?.isDocumentEdited == true }) + .isEmpty self.setDocumentEdited(hasEditedDocuments) } @IBAction func openWorkspaceSettings(_ sender: Any) { guard let window = window, - let workspace = workspace, - let workspaceSettingsManager = workspace.workspaceSettingsManager, - let taskManager = workspace.taskManager + let workspace = workspace else { return } + let workspaceSettingsManager = workspace.workspaceSettingsManager + let taskManager = workspace.taskManager if let workspaceSettingsWindow, workspaceSettingsWindow.isVisible { workspaceSettingsWindow.makeKeyAndOrderFront(self) diff --git a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift b/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift deleted file mode 100644 index 17a6af4c61..0000000000 --- a/CodeEdit/Features/Documents/Protocols/WorkspaceManaging.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// WorkspaceManaging.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 06.04.26. -// - -import CESourceControl -import Foundation -import CEWorkspaceFileManager -import CEEditor -import CENotifications -import CESearch -import CETerminal - -/// Protocol defining the interface that workspace consumers depend on. -/// Enables testability via mock implementations and decouples views from the concrete Workspace type. -protocol WorkspaceManaging: AnyObject, ObservableObject { - var fileURL: URL? { get } - var displayName: String { get } - var workspaceFileManager: CEWorkspaceFileManager? { get } - var editorManager: EditorManager? { get } - var statusBarViewModel: StatusBarViewModel? { get } - var utilityAreaModel: UtilityAreaViewModel? { get } - var searchState: SearchState? { get } - var openQuicklyViewModel: OpenQuicklyViewModel? { get } - var commandsPaletteState: QuickActionsViewModel? { get } - var sourceControlManager: SourceControlManager? { get } - var sourceControlViewModel: SourceControlViewModel? { get } - var taskManager: TaskManager? { get } - var workspaceSettingsManager: CEWorkspaceSettings? { get } - var statePersistence: WorkspaceStatePersistence? { get } - var listenerModel: WorkspaceNotificationModel { get } - var undoRegistration: UndoManagerRegistration { get } - var notificationPanel: NotificationPanelViewModel { get } - var taskNotificationHandler: TaskNotificationHandler { get } - var projectNavigatorViewModel: ProjectNavigatorViewModel? { get } -} diff --git a/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift index 61075800e3..3d09a8f708 100644 --- a/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift +++ b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift @@ -14,7 +14,7 @@ final class StartTaskToolbarItem: NSToolbarItem { private let commandManager: CommandManaging private var utilityAreaCollapsed: Bool { - workspace?.utilityAreaModel?.isCollapsed ?? true + workspace?.utilityAreaModel.isCollapsed ?? true } init(workspace: Workspace, commandManager: CommandManaging) { @@ -41,7 +41,7 @@ final class StartTaskToolbarItem: NSToolbarItem { if utilityAreaCollapsed { commandManager.executeCommand("open.drawer") } - workspace?.utilityAreaModel?.selectedTab = .debugConsole + workspace?.utilityAreaModel.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID } } diff --git a/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift b/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift index 1df8a104e8..6927e53c8b 100644 --- a/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift +++ b/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift @@ -22,8 +22,8 @@ final class StopTaskToolbarItem: NSToolbarItem { private var statusListener: AnyCancellable? private var otherListeners: Set = [] - init?(workspace: Workspace) { - guard let taskManager = workspace.taskManager else { return nil } + init(workspace: Workspace) { + let taskManager = workspace.taskManager self.workspace = workspace super.init(itemIdentifier: NSToolbarItem.Identifier("StopTaskToolbarItem")) diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift index cac5eee5ae..e3836833b6 100644 --- a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift @@ -168,7 +168,7 @@ extension FileSystemTableViewCell: NSTextFieldDelegate { let newURL = fileItem.url .deletingLastPathComponent() .appending(path: textField?.stringValue ?? "") - try workspace?.workspaceFileManager?.move(file: fileItem, to: newURL) + try workspace?.workspaceFileManager.move(file: fileItem, to: newURL) } else { textField?.stringValue = fileItem.labelFileName() } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 66f14951bc..4a93ded6c9 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -75,12 +75,12 @@ final class ProjectNavigatorMenu: NSMenu { let rename = menuItem("Rename", action: #selector(renameFile)) let trash = menuItem("Move to Trash", action: - item.url != workspace?.workspaceFileManager?.folderUrl + item.url != workspace?.workspaceFileManager.folderUrl ? #selector(trash) : nil) // trash has to be the previous menu item for delete.isAlternate to work correctly let delete = menuItem("Delete Immediately...", action: - item.url != workspace?.workspaceFileManager?.folderUrl + item.url != workspace?.workspaceFileManager.folderUrl ? #selector(delete) : nil) delete.keyEquivalentModifierMask = .option delete.isAlternate = true diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index 6a5c92535c..b48824be28 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -89,7 +89,7 @@ extension ProjectNavigatorMenu { func newFile() { guard let item else { return } do { - if let newFile = try workspace?.workspaceFileManager?.addFile(fileName: "untitled", toFile: item) { + if let newFile = try workspace?.workspaceFileManager.addFile(fileName: "untitled", toFile: item) { workspace?.listenerModel.highlightedFileItem = newFile sender.workspaceNavigator.open(file: newFile, asTemporary: false) } @@ -124,7 +124,7 @@ extension ProjectNavigatorMenu { do { let clipBoardContent = NSPasteboard.general.string(forType: .string)?.data(using: .utf8) if let clipBoardContent, !clipBoardContent.isEmpty, let newFile = try workspace? - .workspaceFileManager? + .workspaceFileManager .addFile( fileName: "untitled", toFile: item, @@ -147,7 +147,7 @@ extension ProjectNavigatorMenu { func newFolder() { guard let item else { return } do { - if let newFolder = try workspace?.workspaceFileManager?.addFolder(folderName: "untitled", toFile: item) { + if let newFolder = try workspace?.workspaceFileManager.addFolder(folderName: "untitled", toFile: item) { workspace?.listenerModel.highlightedFileItem = newFolder } } catch { @@ -160,7 +160,7 @@ extension ProjectNavigatorMenu { /// Creates a new folder with the items selected. @objc func newFolderFromSelection() { - guard let workspace, let workspaceFileManager = workspace.workspaceFileManager else { return } + guard let workspaceFileManager = workspace?.workspaceFileManager else { return } let selectedItems = selectedItems() guard let parent = selectedItems.first?.parent else { return } @@ -198,7 +198,7 @@ extension ProjectNavigatorMenu { // Was likely already trashed (eg selecting files in a folder and deleting the folder and files) return } - try workspace?.workspaceFileManager?.trash(file: item) + try workspace?.workspaceFileManager.trash(file: item) } reloadData() } catch { @@ -233,10 +233,10 @@ extension ProjectNavigatorMenu { do { if selectedItems.count == 1 { try selectedItems.forEach { item in - try workspace?.workspaceFileManager?.delete(file: item) + try workspace?.workspaceFileManager.delete(file: item) } } else { - try workspace?.workspaceFileManager?.batchDelete(files: selectedItems) + try workspace?.workspaceFileManager.batchDelete(files: selectedItems) } withAnimation { @@ -258,7 +258,7 @@ extension ProjectNavigatorMenu { func duplicate() { do { try selectedItems().forEach { item in - try workspace?.workspaceFileManager?.duplicate(file: item) + try workspace?.workspaceFileManager.duplicate(file: item) } reloadData() } catch { @@ -281,7 +281,7 @@ extension ProjectNavigatorMenu { /// Copies the relative path of the selected files @objc func copyRelativePath() { - guard let rootPath = workspace?.workspaceFileManager?.folderUrl else { + guard let rootPath = workspace?.workspaceFileManager.folderUrl else { return } let paths = selectedItems().map { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index d9827bdf2b..399ac9c970 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -31,7 +31,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { controller.iconColor = prefs.preferences.general.fileIconStyle controller.activeEditorState = activeEditorState controller.workspaceNavigator = workspaceNavigator - workspace.workspaceFileManager?.addObserver(context.coordinator) + workspace.workspaceFileManager.addObserver(context.coordinator) context.coordinator.controller = controller context.coordinator.observeActiveFile(activeEditorState) @@ -69,7 +69,8 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { self?.controller?.reveal(fileItem) }) .store(in: &cancellables) - if let projectNavigatorViewModel = workspace.projectNavigatorViewModel { + do { + let projectNavigatorViewModel = workspace.projectNavigatorViewModel projectNavigatorViewModel.$navigatorFilter .throttle(for: 0.1, scheduler: RunLoop.main, latest: true) .sink { [weak self] _ in diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index d2d25bf8d7..7fab85a80a 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -15,15 +15,15 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { if let cachedChildren = filteredContentChildren[item] { return cachedChildren .sorted { lhs, rhs in - workspace?.projectNavigatorViewModel?.sortFoldersOnTop == true + workspace?.projectNavigatorViewModel.sortFoldersOnTop == true ? lhs.isFolder && !rhs.isFolder : lhs.name < rhs.name } } - if let workspace, let children = workspace.workspaceFileManager?.childrenOfFile(item) { - let navigatorFilter = workspace.projectNavigatorViewModel?.navigatorFilter ?? "" - let sourceControlFilter = workspace.projectNavigatorViewModel?.sourceControlFilter ?? false - let sortFoldersOnTop = workspace.projectNavigatorViewModel?.sortFoldersOnTop ?? true + if let workspace, let children = workspace.workspaceFileManager.childrenOfFile(item) { + let navigatorFilter = workspace.projectNavigatorViewModel.navigatorFilter ?? "" + let sourceControlFilter = workspace.projectNavigatorViewModel.sourceControlFilter ?? false + let sortFoldersOnTop = workspace.projectNavigatorViewModel.sortFoldersOnTop ?? true if !navigatorFilter.isEmpty || sourceControlFilter { let filteredChildren = children.filter { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index ba1d99196c..e1a1ad6bea 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -31,7 +31,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { frame: frameRect, item: item as? CEWorkspaceFile, delegate: self, - navigatorFilter: workspace?.projectNavigatorViewModel?.navigatorFilter + navigatorFilter: workspace?.projectNavigatorViewModel.navigatorFilter ) return cell } @@ -63,13 +63,13 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineViewItemDidExpand(_ notification: Notification) { /// Save expanded items' state to restore when finish filtering. guard let workspace else { return } - if workspace.projectNavigatorViewModel?.navigatorFilter.isEmpty ?? true, + if workspace.projectNavigatorViewModel.navigatorFilter.isEmpty ?? true, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { expandedItems.insert(item) } guard let id = activeEditorState?.selectedFile?.id, - let item = workspace.workspaceFileManager?.getFile(id, createIfNotFound: true), + let item = workspace.workspaceFileManager.getFile(id, createIfNotFound: true), /// update outline selection only if the parent of selected item match with expanded item item.parent === notification.userInfo?["NSObject"] as? CEWorkspaceFile else { return @@ -83,7 +83,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineViewItemDidCollapse(_ notification: Notification) { /// Save expanded items' state to restore when finish filtering. guard let workspace else { return } - if workspace.projectNavigatorViewModel?.navigatorFilter.isEmpty ?? true, + if workspace.projectNavigatorViewModel.navigatorFilter.isEmpty ?? true, let item = notification.userInfo?["NSObject"] as? CEWorkspaceFile { expandedItems.remove(item) } @@ -91,7 +91,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? { guard let id = object as? CEWorkspaceFile.ID, - let item = workspace?.workspaceFileManager?.getFile(id, createIfNotFound: true) else { return nil } + let item = workspace?.workspaceFileManager.getFile(id, createIfNotFound: true) else { return nil } return item } @@ -107,7 +107,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { /// - forcesReveal: The boolean to indicates whether or not it should force to reveal the selected file. func select(by id: EditorTabID, forcesReveal: Bool) { guard case .codeEditor(let path) = id, - let item = workspace?.workspaceFileManager?.getFile(path, createIfNotFound: true) else { + let item = workspace?.workspaceFileManager.getFile(path, createIfNotFound: true) else { return } // If the user has set "Reveal file on selection change" to on or it is forced to reveal, diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index 7d1ac09338..ed65e381b1 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -28,7 +28,7 @@ extension ProjectNavigatorViewController: OutlineTableViewCellDelegate { func copyFile(file: CEWorkspaceFile, to destination: URL) { do { - try workspace?.workspaceFileManager?.copy(file: file, to: destination) + try workspace?.workspaceFileManager.copy(file: file, to: destination) } catch { let alert = NSAlert(error: error) alert.addButton(withTitle: "Dismiss") diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 40c111977a..ba11cdbe98 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -30,8 +30,8 @@ final class ProjectNavigatorViewController: NSViewController { /// /// Also creates a top level item "root" which represents the projects root directory and automatically expands it. var content: [CEWorkspaceFile] { - guard let folderURL = workspace?.workspaceFileManager?.folderUrl else { return [] } - guard let root = workspace?.workspaceFileManager?.getFile(folderURL.path) else { return [] } + guard let folderURL = workspace?.workspaceFileManager.folderUrl else { return [] } + guard let root = workspace?.workspaceFileManager.getFile(folderURL.path) else { return [] } return [root] } @@ -74,7 +74,7 @@ final class ProjectNavigatorViewController: NSViewController { var shouldReloadAfterDoneEditing: Bool = false var filterIsEmpty: Bool { - workspace?.projectNavigatorViewModel?.navigatorFilter + workspace?.projectNavigatorViewModel.navigatorFilter .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true } @@ -88,7 +88,7 @@ final class ProjectNavigatorViewController: NSViewController { self.outlineView.dataSource = self self.outlineView.delegate = self self.outlineView.autosaveExpandedItems = true - self.outlineView.autosaveName = workspace?.workspaceFileManager?.folderUrl.path ?? "" + self.outlineView.autosaveName = workspace?.workspaceFileManager.folderUrl.path ?? "" self.outlineView.headerView = nil self.outlineView.menu = ProjectNavigatorMenu(self) self.outlineView.menu?.delegate = self @@ -207,7 +207,7 @@ final class ProjectNavigatorViewController: NSViewController { guard let workspace else { return } /// If the filter is empty, show all items and restore the expanded state. - if workspace.projectNavigatorViewModel?.sourceControlFilter == true || !filterIsEmpty { + if workspace.projectNavigatorViewModel.sourceControlFilter == true || !filterIsEmpty { outlineView.autosaveExpandedItems = false /// Expand all items for search. outlineView.expandItem(outlineView.item(atRow: 0), expandChildren: true) @@ -243,7 +243,7 @@ final class ProjectNavigatorViewController: NSViewController { return true } - if let children = workspace?.workspaceFileManager?.childrenOfFile(item) { + if let children = workspace?.workspaceFileManager.childrenOfFile(item) { return children.contains { fileSearchMatches(filter, for: $0, sourceControlFilter: sourceControlFilter) } } @@ -256,7 +256,7 @@ final class ProjectNavigatorViewController: NSViewController { private func saveAllContentChildren(for item: CEWorkspaceFile) { guard item.isFolder, filteredContentChildren[item] == nil else { return } - if let children = workspace?.workspaceFileManager?.childrenOfFile(item) { + if let children = workspace?.workspaceFileManager.childrenOfFile(item) { filteredContentChildren[item] = children for child in children.filter({ $0.isFolder }) { saveAllContentChildren(for: child) diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift index e958e01733..31425bb4da 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift @@ -45,7 +45,6 @@ struct GitChangedFileLabel: View { originalFilename: nil )) .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), eventBus: EventBus())) - .environmentObject(Workspace()) GitChangedFileLabel(file: GitChangedFile( status: .none, @@ -54,6 +53,5 @@ struct GitChangedFileLabel: View { originalFilename: "app2.jsx" )) .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), eventBus: EventBus())) - .environmentObject(Workspace()) }.padding() } diff --git a/CodeEdit/Features/WindowCommands/EditorCommands.swift b/CodeEdit/Features/WindowCommands/EditorCommands.swift index d37ff60d10..5b58a7036c 100644 --- a/CodeEdit/Features/WindowCommands/EditorCommands.swift +++ b/CodeEdit/Features/WindowCommands/EditorCommands.swift @@ -13,7 +13,7 @@ struct EditorCommands: Commands { @UpdatingWindowController var windowController: CodeEditWindowController? private var editor: Editor? { - windowController?.workspace?.editorManager?.activeEditor + windowController?.workspace?.editorManager.activeEditor } var body: some Commands { diff --git a/CodeEdit/Features/WindowCommands/NavigateCommands.swift b/CodeEdit/Features/WindowCommands/NavigateCommands.swift index b7b51ae3f3..20e73f97ba 100644 --- a/CodeEdit/Features/WindowCommands/NavigateCommands.swift +++ b/CodeEdit/Features/WindowCommands/NavigateCommands.swift @@ -12,7 +12,7 @@ struct NavigateCommands: Commands { @UpdatingWindowController var windowController: CodeEditWindowController? private var editor: Editor? { - windowController?.workspace?.editorManager?.activeEditor + windowController?.workspace?.editorManager.activeEditor } var body: some Commands { diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index 00f2f8acd4..56a6d4db5d 100644 --- a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -60,18 +60,18 @@ struct UpdatingWindowController: DynamicProperty { } .store(in: &cancellables) - controller?.workspace?.utilityAreaModel?.objectWillChange.sink { [weak self] in + controller?.workspace?.utilityAreaModel.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } .store(in: &cancellables) - let activeEditor = controller?.workspace?.editorManager?.activeEditor + let activeEditor = controller?.workspace?.editorManager.activeEditor activeEditor?.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } .store(in: &cancellables) - controller?.workspace?.taskManager?.objectWillChange.sink { [weak self] in + controller?.workspace?.taskManager.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } .store(in: &cancellables) diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 9165714e22..4b9d2caee4 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -110,7 +110,7 @@ extension ViewCommands { } var utilityAreaCollapsed: Bool { - windowController?.workspace?.utilityAreaModel?.isCollapsed ?? true + windowController?.workspace?.utilityAreaModel.isCollapsed ?? true } var toolbarCollapsed: Bool { diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index d96c8add2a..8615738d57 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -17,85 +17,95 @@ import SwiftUI import Foundation /// A plain model representing an open workspace (folder). -/// Replaces `WorkspaceDocument` (NSDocument) with no framework coupling. +/// Constructed complete by ``WorkspaceFactory/make(url:dependencies:)`` — every +/// manager is non-optional for the workspace's lifetime. @MainActor -final class Workspace: ObservableObject, WorkspaceManaging { - var projectNavigatorViewModel: ProjectNavigatorViewModel? = ProjectNavigatorViewModel() - - var fileURL: URL? - var displayName: String = "" - - var workspaceFileManager: CEWorkspaceFileManager? - var editorManager: EditorManager? = EditorManager() - var statusBarViewModel: StatusBarViewModel? = StatusBarViewModel() - var utilityAreaModel: UtilityAreaViewModel? = UtilityAreaViewModel() - var searchState: SearchState? - var openQuicklyViewModel: OpenQuicklyViewModel? - var commandsPaletteState: QuickActionsViewModel? - var listenerModel: WorkspaceNotificationModel = .init() - var sourceControlManager: SourceControlManager? - var sourceControlViewModel: SourceControlViewModel? - - var taskManager: TaskManager? - var workspaceSettingsManager: CEWorkspaceSettings? - var taskNotificationHandler: TaskNotificationHandler - - var statePersistence: WorkspaceStatePersistence? - - var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() - - var notificationPanel: NotificationPanelViewModel +final class Workspace: ObservableObject { + let fileURL: URL + let displayName: String + + let editorManager: EditorManager + let workspaceFileManager: CEWorkspaceFileManager + let sourceControlManager: SourceControlManager + let sourceControlViewModel: SourceControlViewModel + let searchState: SearchState + let taskManager: TaskManager + let workspaceSettingsManager: CEWorkspaceSettings + let statePersistence: WorkspaceStatePersistence + let undoRegistration: UndoManagerRegistration + + // Window-UI models — Phase B moves these to CodeEditWindowController. + let statusBarViewModel: StatusBarViewModel + let utilityAreaModel: UtilityAreaViewModel + let openQuicklyViewModel: OpenQuicklyViewModel + let commandsPaletteState: QuickActionsViewModel + let notificationPanel: NotificationPanelViewModel + let taskNotificationHandler: TaskNotificationHandler + + // Navigator-coupled — stay until the Navigator feature is packaged + // (consumed by the ProjectNavigator AppKit cluster and by-workspace command paths). + let listenerModel: WorkspaceNotificationModel + let projectNavigatorViewModel: ProjectNavigatorViewModel /// The original (possibly bookmark-derived) security-scoped URL whose access is held for this /// workspace's lifetime. Set by `WorkspaceFactory` when the URL is security-scoped (e.g. opened /// from recents in the sandbox); released in ``tearDown()``. var securityScopedURL: URL? - // MARK: - Initialization - - init(url: URL, dependencies: AppDependencies) { - self.taskNotificationHandler = TaskNotificationHandler(eventBus: dependencies.eventBus) - self.notificationPanel = NotificationPanelViewModel( - notificationManager: dependencies.notificationManager, - eventBus: dependencies.eventBus - ) - WorkspaceFactory.populate(self, url: url, dependencies: dependencies) - } - - /// Minimal initializer for testing. Does not set up workspace state. - internal init() { - let eventBus = EventBus() - self.taskNotificationHandler = TaskNotificationHandler(eventBus: eventBus) - self.notificationPanel = NotificationPanelViewModel( - notificationManager: NotificationManager(eventBus: eventBus), - eventBus: eventBus - ) + // swiftlint:disable:next function_parameter_count + init( + fileURL: URL, + displayName: String, + editorManager: EditorManager, + workspaceFileManager: CEWorkspaceFileManager, + sourceControlManager: SourceControlManager, + sourceControlViewModel: SourceControlViewModel, + searchState: SearchState, + taskManager: TaskManager, + workspaceSettingsManager: CEWorkspaceSettings, + statePersistence: WorkspaceStatePersistence, + undoRegistration: UndoManagerRegistration, + statusBarViewModel: StatusBarViewModel, + utilityAreaModel: UtilityAreaViewModel, + openQuicklyViewModel: OpenQuicklyViewModel, + commandsPaletteState: QuickActionsViewModel, + notificationPanel: NotificationPanelViewModel, + taskNotificationHandler: TaskNotificationHandler, + listenerModel: WorkspaceNotificationModel, + projectNavigatorViewModel: ProjectNavigatorViewModel, + securityScopedURL: URL? + ) { + self.fileURL = fileURL + self.displayName = displayName + self.editorManager = editorManager + self.workspaceFileManager = workspaceFileManager + self.sourceControlManager = sourceControlManager + self.sourceControlViewModel = sourceControlViewModel + self.searchState = searchState + self.taskManager = taskManager + self.workspaceSettingsManager = workspaceSettingsManager + self.statePersistence = statePersistence + self.undoRegistration = undoRegistration + self.statusBarViewModel = statusBarViewModel + self.utilityAreaModel = utilityAreaModel + self.openQuicklyViewModel = openQuicklyViewModel + self.commandsPaletteState = commandsPaletteState + self.notificationPanel = notificationPanel + self.taskNotificationHandler = taskNotificationHandler + self.listenerModel = listenerModel + self.projectNavigatorViewModel = projectNavigatorViewModel + self.securityScopedURL = securityScopedURL } // MARK: - Tear Down + /// Cleanup-only: saves restoration state and releases external resources. + /// Members are no longer nil-ed — `WorkspaceLifecycleTests` guards against leaks instead. func tearDown() { - if let statePersistence { - editorManager?.saveRestorationState(statePersistence) - utilityAreaModel?.saveRestorationState(statePersistence) - } - - statusBarViewModel = nil - utilityAreaModel = nil - searchState = nil - editorManager = nil - openQuicklyViewModel = nil - commandsPaletteState = nil - sourceControlManager = nil - sourceControlViewModel = nil - projectNavigatorViewModel = nil - workspaceFileManager?.cleanUp() - workspaceFileManager = nil - workspaceSettingsManager?.cleanUp() - workspaceSettingsManager = nil - taskManager = nil - statePersistence = nil - + editorManager.saveRestorationState(statePersistence) + utilityAreaModel.saveRestorationState(statePersistence) + workspaceFileManager.cleanUp() + workspaceSettingsManager.cleanUp() securityScopedURL?.stopAccessingSecurityScopedResource() securityScopedURL = nil } @@ -103,7 +113,6 @@ final class Workspace: ObservableObject, WorkspaceManaging { // MARK: - Unsaved Changes func hasUnsavedChanges() -> Bool { - guard let editorManager else { return false } let editedFiles = editorManager.editorLayout .gatherOpenFiles() .compactMap { editorManager.document(for: $0) } @@ -114,7 +123,6 @@ final class Workspace: ObservableObject, WorkspaceManaging { /// Prompts the user to save any unsaved files before closing. /// Returns `true` if all files are clean and the workspace can close, `false` if the user cancelled. func promptSaveUnsavedFiles() -> Bool { - guard let editorManager else { return true } let editedCodeFiles = editorManager.editorLayout .gatherOpenFiles() .compactMap { editorManager.document(for: $0) } diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift index 3a70233f5d..dc226e2290 100644 --- a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift +++ b/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift @@ -32,6 +32,6 @@ final class AppWorkspaceNavigator: WorkspaceNavigator { @MainActor func closeTab(file: CEWorkspaceFile) { - windowManager.workspace(containing: file.url)?.editorManager?.editorLayout.closeAllTabs(of: file) + windowManager.workspace(containing: file.url)?.editorManager.editorLayout.closeAllTabs(of: file) } } diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index f21a980fa9..21a919d468 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -40,7 +40,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { func openWorkspace(at url: URL) throws { // Check if this workspace is already open - if let existing = openWorkspaces.first(where: { $0.fileURL?.standardizedFileURL == url.standardizedFileURL }) { + if let existing = openWorkspaces.first(where: { $0.fileURL.standardizedFileURL == url.standardizedFileURL }) { focusWorkspace(existing) return } @@ -79,7 +79,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { func workspace(containing url: URL) -> Workspace? { openWorkspaces.first { workspace in - workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) != nil + workspace.workspaceFileManager.getFile(url.absolutePath, createIfNotFound: true) != nil } } @@ -89,10 +89,10 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { guard !url.isFolder else { return false } for workspace in openWorkspaces.sorted(by: { - ($0.fileURL?.sharedComponents(url) ?? 0) > ($1.fileURL?.sharedComponents(url) ?? 0) + $0.fileURL.sharedComponents(url) > $1.fileURL.sharedComponents(url) }) { - if let newFile = workspace.workspaceFileManager?.getFile(url.absolutePath, createIfNotFound: true) { - workspace.editorManager?.openTab(item: newFile, asTemporary: asTemporary) + if let newFile = workspace.workspaceFileManager.getFile(url.absolutePath, createIfNotFound: true) { + workspace.editorManager.openTab(item: newFile, asTemporary: asTemporary) focusWorkspace(workspace) return true } diff --git a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift index 5e9a79c923..5ac977bb39 100644 --- a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift @@ -19,9 +19,7 @@ final class CloseWorkspaceUseCase { } func execute(workspace: Workspace) { - if let path = workspace.fileURL?.absoluteURL.path() { - lspService.closeWorkspace(path) - } + lspService.closeWorkspace(workspace.fileURL.absoluteURL.path()) workspace.tearDown() } } diff --git a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift index a6f4baeea7..6487a37622 100644 --- a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift @@ -23,7 +23,7 @@ final class OpenWorkspaceUseCase { } func execute(url: URL) -> Result { - let workspace = Workspace(url: url, dependencies: dependencies) + let workspace = WorkspaceFactory.make(url: url, dependencies: dependencies) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 1400, height: 900), @@ -39,7 +39,7 @@ final class OpenWorkspaceUseCase { ) // Restore saved window geometry, or use default centered frame - if let rectString = workspace.statePersistence?.get(.workspaceWindowSize) as? String { + if let rectString = workspace.statePersistence.get(.workspaceWindowSize) as? String { window.setFrame(NSRectFromString(rectString), display: true, animate: false) } else { window.setFrame(NSRect(x: 0, y: 0, width: 1400, height: 900), display: true, animate: false) @@ -47,7 +47,7 @@ final class OpenWorkspaceUseCase { } window.setAccessibilityIdentifier("workspace") - window.setAccessibilityDocument(workspace.fileURL?.absoluteString) + window.setAccessibilityDocument(workspace.fileURL.absoluteString) return Result(workspace: workspace, window: window, windowController: windowController) } diff --git a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift index 4e860bc1b6..2a565dc881 100644 --- a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift @@ -30,7 +30,7 @@ final class ShutdownApplicationUseCase { let workspaces = windowManager.openWorkspaces // Save workspace paths for recovery on next launch - let projects: [String] = workspaces.compactMap { $0.fileURL?.path } + let projects: [String] = workspaces.map { $0.fileURL.path } UserDefaults.standard.set(projects, forKey: AppDelegate.recoverWorkspacesKey) // Check for unsaved changes and prompt the user diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index 53facc3dd8..d77ef8ca82 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -9,6 +9,7 @@ import CESourceControl import Foundation import CEWorkspaceFileManager import CEEditor +import CENotifications import CESearch import CETerminal @@ -20,22 +21,16 @@ enum WorkspaceFactory { private static let ignoredFilesAndDirectories: Set = [".DS_Store"] - /// Populates all manager properties on `workspace` for the given workspace URL. - /// - /// - Parameters: - /// - workspace: The workspace to populate. Its `editorManager`, - /// `statusBarViewModel`, `utilityAreaModel`, `listenerModel`, - /// `taskNotificationHandler`, `undoRegistration`, and `notificationPanel` - /// must already be initialized (they are set at declaration time). - /// - url: The root URL of the workspace folder. + /// Builds a fully-populated ``Workspace`` for the given folder URL. @MainActor - static func populate(_ workspace: Workspace, url: URL, dependencies: AppDependencies) { + static func make(url: URL, dependencies: AppDependencies) -> Workspace { // Begin security-scoped access on the original (possibly bookmark-derived) URL so a // sandboxed build can read a workspace opened from recents. `startAccessingSecurityScopedResource` // returns `false` for non-scoped URLs (e.g. from the open panel / Powerbox), which access // fine without it. Released in `Workspace.tearDown`. + var securityScopedURL: URL? if url.startAccessingSecurityScopedResource() { - workspace.securityScopedURL = url + securityScopedURL = url } // Normalize the URL to always end with "/" @@ -44,60 +39,69 @@ enum WorkspaceFactory { url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") } - workspace.fileURL = url - workspace.displayName = url.lastPathComponent - workspace.statePersistence = WorkspaceStatePersistence(workspaceURL: url) - - // --- Phase 1: Source control + file manager (dependency chain) --- - guard let editorManager = workspace.editorManager else { - assertionFailure("EditorManager must be initialized before calling populate") - return - } - - let shellClient = dependencies.shellClient let eventBus = dependencies.eventBus + let statePersistence = WorkspaceStatePersistence(workspaceURL: url) + let editorManager = EditorManager() let sourceControlManager = SourceControlManager( workspaceURL: url, - shellClient: shellClient, + shellClient: dependencies.shellClient, eventBus: eventBus ) - let workspaceFileManager = CEWorkspaceFileManager( folderUrl: url, ignoredFilesAndFolders: ignoredFilesAndDirectories, eventBus: eventBus ) + let searchState = SearchState(workspaceURL: url, eventBus: eventBus) + let workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) + let taskManager = TaskManager( + tasksConfiguration: workspaceSettingsManager, + workspaceURL: url, + eventBus: eventBus + ) + let undoRegistration = UndoManagerRegistration() + + // Observer registration + workspaceFileManager.addObserver(undoRegistration) + undoRegistration.editorManager = editorManager - workspace.sourceControlManager = sourceControlManager - workspace.sourceControlViewModel = SourceControlViewModel() - workspace.workspaceFileManager = workspaceFileManager + // Window-UI models (Phase B moves these to CodeEditWindowController) + let utilityAreaModel = UtilityAreaViewModel() - // --- Phase 2: Independent managers --- - workspace.searchState = SearchState(workspaceURL: url, eventBus: eventBus) - workspace.openQuicklyViewModel = OpenQuicklyViewModel(fileURL: url) - workspace.commandsPaletteState = QuickActionsViewModel(commandManager: dependencies.commandManager) - workspace.workspaceSettingsManager = CEWorkspaceSettings(workspaceURL: url) - if let workspaceSettingsManager = workspace.workspaceSettingsManager { - workspace.taskManager = TaskManager( - tasksConfiguration: workspaceSettingsManager, - workspaceURL: url, + let workspace = Workspace( + fileURL: url, + displayName: url.lastPathComponent, + editorManager: editorManager, + workspaceFileManager: workspaceFileManager, + sourceControlManager: sourceControlManager, + sourceControlViewModel: SourceControlViewModel(), + searchState: searchState, + taskManager: taskManager, + workspaceSettingsManager: workspaceSettingsManager, + statePersistence: statePersistence, + undoRegistration: undoRegistration, + statusBarViewModel: StatusBarViewModel(), + utilityAreaModel: utilityAreaModel, + openQuicklyViewModel: OpenQuicklyViewModel(fileURL: url), + commandsPaletteState: QuickActionsViewModel(commandManager: dependencies.commandManager), + notificationPanel: NotificationPanelViewModel( + notificationManager: dependencies.notificationManager, eventBus: eventBus - ) - } - workspace.taskNotificationHandler.workspaceURL = url + ), + taskNotificationHandler: TaskNotificationHandler(workspaceURL: url, eventBus: eventBus), + listenerModel: WorkspaceNotificationModel(), + projectNavigatorViewModel: ProjectNavigatorViewModel(), + securityScopedURL: securityScopedURL + ) - // --- Phase 3: Observer registration --- - workspaceFileManager.addObserver(workspace.undoRegistration) - workspace.undoRegistration.editorManager = editorManager + // State restoration + editorManager.restoreFromState( + statePersistence: statePersistence, + fileManager: workspaceFileManager, + findReplaceQuery: searchState.query + ) + utilityAreaModel.restoreFromState(statePersistence) - // --- Phase 4: State restoration --- - if let statePersistence = workspace.statePersistence { - editorManager.restoreFromState( - statePersistence: statePersistence, - fileManager: workspaceFileManager, - findReplaceQuery: workspace.searchState?.query - ) - workspace.utilityAreaModel?.restoreFromState(statePersistence) - } + return workspace } } diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index ee7f17c7de..bfceaa797b 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -20,7 +20,7 @@ final class DocumentsUnitTests: XCTestCase { private var hapticFeedbackPerformerMock: NSHapticFeedbackPerformerMock! private var navigatorViewModel: NavigatorAreaViewModel! private var window: NSWindow! - private var workspace = Workspace() + private var workspace: Workspace! // MARK: - Lifecycle @@ -28,18 +28,8 @@ final class DocumentsUnitTests: XCTestCase { super.setUp() hapticFeedbackPerformerMock = NSHapticFeedbackPerformerMock() navigatorViewModel = .init() - workspace.taskManager = TaskManager( - tasksConfiguration: CEWorkspaceSettings(workspaceURL: URL(filePath: NSTemporaryDirectory())), - workspaceURL: nil, - eventBus: EventBus() - ) - workspace.sourceControlManager = SourceControlManager( - workspaceURL: URL(filePath: "/tmp"), - shellClient: ShellClient(), - eventBus: EventBus() - ) - workspace.sourceControlViewModel = SourceControlViewModel() - workspace.searchState = SearchState(workspaceURL: URL(filePath: "/tmp"), eventBus: EventBus()) + // swiftlint:disable:next force_try + workspace = try! TestWorkspaceFactory.make() window = NSWindow() splitViewController = .init( workspace: workspace, diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 974fc74f02..4c5d568f8a 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -111,16 +111,12 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { let windowManager = appDependencies.workspaceWindowManager try windowManager.openWorkspace(at: tempTestDir) guard let workspace = windowManager.openWorkspaces.first(where: { - $0.fileURL?.standardizedFileURL.path() == tempTestDir.standardizedFileURL.path() + $0.fileURL.standardizedFileURL.path() == tempTestDir.standardizedFileURL.path() }) else { XCTFail("Workspace was not registered with the window manager") fatalError("Workspace was not registered with the window manager") // never runs } - guard let fileManager = workspace.workspaceFileManager else { - XCTFail("No File Manager") - fatalError("No File Manager") // never runs - } - return (workspace, fileManager) + return (workspace, workspace.workspaceFileManager) } @MainActor @@ -201,7 +197,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { withContentsOf: file.url, ofType: "public.swift-source" ) - workspace.editorManager?.setDocument(codeFile, for: file) + workspace.editorManager.setDocument(codeFile, for: file) NSDocumentController.shared.addDocument(codeFile) await waitForClientState( diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index db1c1cb26f..4d44a5bc90 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -52,8 +52,8 @@ struct AppWorkspaceNavigatorTests { @MainActor @Test - func revealSetsHighlightedFileItemOnCorrectWorkspace() { - let workspace = Workspace() + func revealSetsHighlightedFileItemOnCorrectWorkspace() throws { + let workspace = try TestWorkspaceFactory.make() let mock = MockWindowManager() mock.stubbedWorkspace = workspace let navigator = AppWorkspaceNavigator(windowManager: mock) @@ -66,10 +66,9 @@ struct AppWorkspaceNavigatorTests { @MainActor @Test - func closeTabClosesFileInEditorLayout() { - let workspace = Workspace() - let editorManager = EditorManager() - workspace.editorManager = editorManager + func closeTabClosesFileInEditorLayout() throws { + let workspace = try TestWorkspaceFactory.make() + let editorManager = workspace.editorManager let mock = MockWindowManager() mock.stubbedWorkspace = workspace let navigator = AppWorkspaceNavigator(windowManager: mock) diff --git a/CodeEditTests/Features/Workspace/WorkspaceLifecycleTests.swift b/CodeEditTests/Features/Workspace/WorkspaceLifecycleTests.swift new file mode 100644 index 0000000000..6ca7b4d041 --- /dev/null +++ b/CodeEditTests/Features/Workspace/WorkspaceLifecycleTests.swift @@ -0,0 +1,35 @@ +// +// WorkspaceLifecycleTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import XCTest +import CEEditor +import CETerminal +@testable import CodeEdit + +@MainActor +final class WorkspaceLifecycleTests: XCTestCase { + /// tearDown() no longer nils members; this guards that nothing retains the + /// workspace graph after close (a strong manager→Workspace back-reference, + /// or a manager self-cycle via a `sink` without `[weak self]`). + func testWorkspaceAndManagersDeallocateAfterTearDown() throws { + weak var weakWorkspace: Workspace? + weak var weakEditorManager: EditorManager? + weak var weakTaskManager: TaskManager? + + try autoreleasepool { + let workspace = try TestWorkspaceFactory.make() + weakWorkspace = workspace + weakEditorManager = workspace.editorManager + weakTaskManager = workspace.taskManager + workspace.tearDown() + } + + XCTAssertNil(weakWorkspace, "Workspace leaked after tearDown") + XCTAssertNil(weakEditorManager, "EditorManager leaked after tearDown") + XCTAssertNil(weakTaskManager, "TaskManager leaked after tearDown") + } +} diff --git a/CodeEditTests/Helpers/TestWorkspaceFactory.swift b/CodeEditTests/Helpers/TestWorkspaceFactory.swift new file mode 100644 index 0000000000..408a14a8d7 --- /dev/null +++ b/CodeEditTests/Helpers/TestWorkspaceFactory.swift @@ -0,0 +1,22 @@ +// +// TestWorkspaceFactory.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import Foundation +@testable import CodeEdit + +/// Builds a real `Workspace` via `WorkspaceFactory.make` on a fresh temp directory. +/// `AppDependencies` is all-lazy, so a per-test instance is cheap. +@MainActor +enum TestWorkspaceFactory { + static func make(dependencies: AppDependencies? = nil) throws -> Workspace { + let dependencies = dependencies ?? AppDependencies() + let dir = URL(filePath: NSTemporaryDirectory()) + .appending(path: "workspace-tests-\(UUID().uuidString)", directoryHint: .isDirectory) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + return WorkspaceFactory.make(url: dir, dependencies: dependencies) + } +} From 2222fd93b23521a10803ae8746cf741e1ee4b5f7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 20:01:14 +0200 Subject: [PATCH 153/335] Refactor: Move window-UI models from Workspace to CodeEditWindowController Workspace now holds project-scoped services only (plus the two navigator-coupled models deferred to Navigator packaging). The window controller owns utility-area/status-bar/open-quickly/command-palette/ notification-panel/task-notification state and its restoration. --- .../CodeEditSplitViewController.swift | 17 +++++++-- .../CodeEditWindowController+Panels.swift | 4 +- .../CodeEditWindowController+Toolbar.swift | 10 +++-- .../CodeEditWindowController.swift | 37 +++++++++++++++---- .../Toolbar/StartTaskToolbarItem.swift | 8 ++-- .../WindowCommands/TasksCommands.swift | 2 +- .../WindowControllerPropertyWrapper.swift | 2 +- .../WindowCommands/ViewCommands.swift | 2 +- .../Features/Workspace/Models/Workspace.swift | 21 ----------- .../Services/WorkspaceWindowManager.swift | 2 +- .../Features/Workspace/WorkspaceFactory.swift | 13 ------- .../Documents/DocumentsUnitTests.swift | 8 ++++ 12 files changed, 68 insertions(+), 58 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index de0eeb7c4d..94ce07b0c5 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -26,6 +26,11 @@ final class CodeEditSplitViewController: NSSplitViewController { private weak var statePersistence: (any WorkspaceStatePersisting)? private unowned var hapticPerformer: NSHapticFeedbackPerformer + // Window-UI models, owned by the window controller and injected here for the SwiftUI trees. + private let statusBarViewModel: StatusBarViewModel + private let utilityAreaModel: UtilityAreaViewModel + private let notificationPanel: NotificationPanelViewModel + /// Per-window active-file read-model, retained so its Combine subscription lives with the window. private var activeEditorState: AppActiveEditorState? @@ -42,6 +47,9 @@ final class CodeEditSplitViewController: NSSplitViewController { navigatorViewModel: NavigatorAreaViewModel, windowRef: NSWindow, dependencies: AppDependencies, + statusBarViewModel: StatusBarViewModel, + utilityAreaModel: UtilityAreaViewModel, + notificationPanel: NotificationPanelViewModel, hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer ) { self.dependencies = dependencies @@ -49,6 +57,9 @@ final class CodeEditSplitViewController: NSSplitViewController { self.navigatorViewModel = navigatorViewModel self.windowRef = windowRef self.statePersistence = workspace.statePersistence + self.statusBarViewModel = statusBarViewModel + self.utilityAreaModel = utilityAreaModel + self.notificationPanel = notificationPanel self.hapticPerformer = hapticPerformer super.init(nibName: nil, bundle: nil) } @@ -71,8 +82,6 @@ final class CodeEditSplitViewController: NSSplitViewController { return } let editorManager = workspace.editorManager - let statusBarViewModel = workspace.statusBarViewModel - let utilityAreaModel = workspace.utilityAreaModel let projectNavigatorViewModel = workspace.projectNavigatorViewModel let sourceControlManager = workspace.sourceControlManager let sourceControlViewModel = workspace.sourceControlViewModel @@ -119,7 +128,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(sourceControlViewModel) .environmentObject(workspace.listenerModel) .environmentObject(workspace.undoRegistration) - .environmentObject(workspace.notificationPanel) + .environmentObject(notificationPanel) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) @@ -189,7 +198,7 @@ final class CodeEditSplitViewController: NSSplitViewController { ) as? Bool ?? true } - workspace?.notificationPanel.updateToolbarItem() + notificationPanel.updateToolbarItem() } // MARK: - NSSplitViewDelegate diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift index 5a18e5a298..4fb79852d7 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift @@ -79,10 +79,10 @@ extension CodeEditWindowController { toggle: { self.toggleLastPanel(shouldAnimate: false) } ), PanelDescriptor( - isCollapsed: { self.workspace?.utilityAreaModel.isCollapsed ?? true }, + isCollapsed: { self.utilityAreaModel.isCollapsed }, getPrevCollapsed: { self.prevUtilityAreaCollapsed }, setPrevCollapsed: { self.prevUtilityAreaCollapsed = $0 }, - toggle: { self.workspace?.utilityAreaModel.togglePanel(animation: false) } + toggle: { self.utilityAreaModel.togglePanel(animation: false) } ), PanelDescriptor( isCollapsed: { self.toolbarCollapsed }, diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index 699fc4e933..5e41966e83 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -186,7 +186,11 @@ extension CodeEditWindowController { return nil } let stop = StopTaskToolbarItem(workspace: workspace) - let start = StartTaskToolbarItem(workspace: workspace, commandManager: dependencies.commandManager) + let start = StartTaskToolbarItem( + workspace: workspace, + utilityAreaModel: utilityAreaModel, + commandManager: dependencies.commandManager + ) let group = NSToolbarItemGroup(itemIdentifier: .taskSidebarItem) group.isBordered = true @@ -217,7 +221,6 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.startTaskSidebarItem) guard let taskManager = workspace?.taskManager else { return nil } - guard let utilityAreaModel = workspace?.utilityAreaModel else { return nil } let view = NSHostingView( rootView: StartTaskToolbarButton(taskManager: taskManager) @@ -232,7 +235,7 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: .notificationItem) guard let workspace = workspace else { return nil } let view = NSHostingView( - rootView: NotificationToolbarItem().environmentObject(workspace.notificationPanel) + rootView: NotificationToolbarItem().environmentObject(notificationPanel) ) toolbarItem.view = view return toolbarItem @@ -242,7 +245,6 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.activityViewer) toolbarItem.visibilityPriority = .user guard let workspaceSettingsManager = workspace?.workspaceSettingsManager, - let taskNotificationHandler = workspace?.taskNotificationHandler, let taskManager = workspace?.taskManager else { return nil } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 98b643367a..c1ad0790ac 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -9,6 +9,7 @@ import Cocoa import CodeEditDocument import CodeEditSettings import CEEditor +import CENotifications import SwiftUI import CodeEditUI import Combine @@ -36,6 +37,14 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs var commandPalettePanel: SearchPanel? var navigatorSidebarViewModel: NavigatorAreaViewModel? + // Window-UI models: window-scoped state, owned here (1:1 with the workspace). + let statusBarViewModel = StatusBarViewModel() + let utilityAreaModel = UtilityAreaViewModel() + let openQuicklyViewModel: OpenQuicklyViewModel + let commandsPaletteState: QuickActionsViewModel + let notificationPanel: NotificationPanelViewModel + let taskNotificationHandler: TaskNotificationHandler + internal var cancellables = [AnyCancellable]() var splitViewController: CodeEditSplitViewController? { @@ -44,15 +53,25 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs init( window: NSWindow?, - workspace: Workspace?, + workspace: Workspace, dependencies: AppDependencies ) { self.dependencies = dependencies + self.workspace = workspace + self.openQuicklyViewModel = OpenQuicklyViewModel(fileURL: workspace.fileURL) + self.commandsPaletteState = QuickActionsViewModel(commandManager: dependencies.commandManager) + self.notificationPanel = NotificationPanelViewModel( + notificationManager: dependencies.notificationManager, + eventBus: dependencies.eventBus + ) + self.taskNotificationHandler = TaskNotificationHandler( + workspaceURL: workspace.fileURL, + eventBus: dependencies.eventBus + ) super.init(window: window) window?.delegate = self - guard let workspace else { return } - self.workspace = workspace self.toolbarCollapsed = workspace.statePersistence.get(.toolbarCollapsed) as? Bool ?? false + utilityAreaModel.restoreFromState(workspace.statePersistence) guard let splitViewController = setupSplitView(with: workspace) else { fatalError("Failed to set up content view.") } @@ -109,7 +128,10 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs workspace: workspace, navigatorViewModel: navigatorModel, windowRef: window, - dependencies: dependencies + dependencies: dependencies, + statusBarViewModel: statusBarViewModel, + utilityAreaModel: utilityAreaModel, + notificationPanel: notificationPanel ) } @@ -126,8 +148,8 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } @IBAction func openCommandPalette(_ sender: Any) { - if let workspace { - let state = workspace.commandsPaletteState + do { + let state = commandsPaletteState if let commandPalettePanel { if commandPalettePanel.isKeyWindow { commandPalettePanel.close() @@ -172,7 +194,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs @IBAction func openQuickly(_ sender: Any?) { if let workspace { - let state = workspace.openQuicklyViewModel + let state = openQuicklyViewModel if let quickOpenPanel { if quickOpenPanel.isKeyWindow { quickOpenPanel.close() @@ -251,6 +273,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs // Notify the window manager to clean up workspace state if let workspace { + utilityAreaModel.saveRestorationState(workspace.statePersistence) dependencies.workspaceWindowManager.closeWorkspace(workspace) } workspace = nil diff --git a/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift index 3d09a8f708..e8706bccb6 100644 --- a/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift +++ b/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift @@ -11,14 +11,16 @@ import CETerminal @available(macOS 26, *) final class StartTaskToolbarItem: NSToolbarItem { private weak var workspace: Workspace? + private weak var utilityAreaModel: UtilityAreaViewModel? private let commandManager: CommandManaging private var utilityAreaCollapsed: Bool { - workspace?.utilityAreaModel.isCollapsed ?? true + utilityAreaModel?.isCollapsed ?? true } - init(workspace: Workspace, commandManager: CommandManaging) { + init(workspace: Workspace, utilityAreaModel: UtilityAreaViewModel, commandManager: CommandManaging) { self.workspace = workspace + self.utilityAreaModel = utilityAreaModel self.commandManager = commandManager super.init(itemIdentifier: NSToolbarItem.Identifier("StartTaskToolbarItem")) @@ -41,7 +43,7 @@ final class StartTaskToolbarItem: NSToolbarItem { if utilityAreaCollapsed { commandManager.executeCommand("open.drawer") } - workspace?.utilityAreaModel.selectedTab = .debugConsole + utilityAreaModel?.selectedTab = .debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID } } diff --git a/CodeEdit/Features/WindowCommands/TasksCommands.swift b/CodeEdit/Features/WindowCommands/TasksCommands.swift index d0c18a7c11..b540b286c7 100644 --- a/CodeEdit/Features/WindowCommands/TasksCommands.swift +++ b/CodeEdit/Features/WindowCommands/TasksCommands.swift @@ -94,7 +94,7 @@ struct TasksCommands: Commands { } private func showOutput() { - guard let utilityAreaModel = windowController?.workspace?.utilityAreaModel else { + guard let utilityAreaModel = windowController?.utilityAreaModel else { return } if utilityAreaModel.isCollapsed { diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift index 56a6d4db5d..93e8c22ea5 100644 --- a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift +++ b/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift @@ -60,7 +60,7 @@ struct UpdatingWindowController: DynamicProperty { } .store(in: &cancellables) - controller?.workspace?.utilityAreaModel.objectWillChange.sink { [weak self] in + controller?.utilityAreaModel.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } .store(in: &cancellables) diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 4b9d2caee4..277133f9f5 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -110,7 +110,7 @@ extension ViewCommands { } var utilityAreaCollapsed: Bool { - windowController?.workspace?.utilityAreaModel.isCollapsed ?? true + windowController?.utilityAreaModel.isCollapsed ?? true } var toolbarCollapsed: Bool { diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 8615738d57..5042d0d422 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -34,14 +34,6 @@ final class Workspace: ObservableObject { let statePersistence: WorkspaceStatePersistence let undoRegistration: UndoManagerRegistration - // Window-UI models — Phase B moves these to CodeEditWindowController. - let statusBarViewModel: StatusBarViewModel - let utilityAreaModel: UtilityAreaViewModel - let openQuicklyViewModel: OpenQuicklyViewModel - let commandsPaletteState: QuickActionsViewModel - let notificationPanel: NotificationPanelViewModel - let taskNotificationHandler: TaskNotificationHandler - // Navigator-coupled — stay until the Navigator feature is packaged // (consumed by the ProjectNavigator AppKit cluster and by-workspace command paths). let listenerModel: WorkspaceNotificationModel @@ -65,12 +57,6 @@ final class Workspace: ObservableObject { workspaceSettingsManager: CEWorkspaceSettings, statePersistence: WorkspaceStatePersistence, undoRegistration: UndoManagerRegistration, - statusBarViewModel: StatusBarViewModel, - utilityAreaModel: UtilityAreaViewModel, - openQuicklyViewModel: OpenQuicklyViewModel, - commandsPaletteState: QuickActionsViewModel, - notificationPanel: NotificationPanelViewModel, - taskNotificationHandler: TaskNotificationHandler, listenerModel: WorkspaceNotificationModel, projectNavigatorViewModel: ProjectNavigatorViewModel, securityScopedURL: URL? @@ -86,12 +72,6 @@ final class Workspace: ObservableObject { self.workspaceSettingsManager = workspaceSettingsManager self.statePersistence = statePersistence self.undoRegistration = undoRegistration - self.statusBarViewModel = statusBarViewModel - self.utilityAreaModel = utilityAreaModel - self.openQuicklyViewModel = openQuicklyViewModel - self.commandsPaletteState = commandsPaletteState - self.notificationPanel = notificationPanel - self.taskNotificationHandler = taskNotificationHandler self.listenerModel = listenerModel self.projectNavigatorViewModel = projectNavigatorViewModel self.securityScopedURL = securityScopedURL @@ -103,7 +83,6 @@ final class Workspace: ObservableObject { /// Members are no longer nil-ed — `WorkspaceLifecycleTests` guards against leaks instead. func tearDown() { editorManager.saveRestorationState(statePersistence) - utilityAreaModel.saveRestorationState(statePersistence) workspaceFileManager.cleanUp() workspaceSettingsManager.cleanUp() securityScopedURL?.stopAccessingSecurityScopedResource() diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 21a919d468..d75c21602c 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -49,7 +49,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { openWorkspaces.append(result.workspace) windowControllers[ObjectIdentifier(result.workspace)] = result.windowController - let notificationPanel = result.workspace.notificationPanel + let notificationPanel = result.windowController.notificationPanel notificationPanel.windowController = result.windowController // App shell owns window-toolbar mutation; the package signals a refresh via this hook. notificationPanel.onToolbarUpdateRequested = { [weak notificationPanel] in diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index d77ef8ca82..be993e51df 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -65,9 +65,6 @@ enum WorkspaceFactory { workspaceFileManager.addObserver(undoRegistration) undoRegistration.editorManager = editorManager - // Window-UI models (Phase B moves these to CodeEditWindowController) - let utilityAreaModel = UtilityAreaViewModel() - let workspace = Workspace( fileURL: url, displayName: url.lastPathComponent, @@ -80,15 +77,6 @@ enum WorkspaceFactory { workspaceSettingsManager: workspaceSettingsManager, statePersistence: statePersistence, undoRegistration: undoRegistration, - statusBarViewModel: StatusBarViewModel(), - utilityAreaModel: utilityAreaModel, - openQuicklyViewModel: OpenQuicklyViewModel(fileURL: url), - commandsPaletteState: QuickActionsViewModel(commandManager: dependencies.commandManager), - notificationPanel: NotificationPanelViewModel( - notificationManager: dependencies.notificationManager, - eventBus: eventBus - ), - taskNotificationHandler: TaskNotificationHandler(workspaceURL: url, eventBus: eventBus), listenerModel: WorkspaceNotificationModel(), projectNavigatorViewModel: ProjectNavigatorViewModel(), securityScopedURL: securityScopedURL @@ -100,7 +88,6 @@ enum WorkspaceFactory { fileManager: workspaceFileManager, findReplaceQuery: searchState.query ) - utilityAreaModel.restoreFromState(statePersistence) return workspace } diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index bfceaa797b..4524c6b58a 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -8,6 +8,7 @@ import XCTest import CodeEditCore import ShellClient +import CENotifications import CESourceControl import CESearch import CETerminal @@ -31,11 +32,18 @@ final class DocumentsUnitTests: XCTestCase { // swiftlint:disable:next force_try workspace = try! TestWorkspaceFactory.make() window = NSWindow() + let eventBus = EventBus() splitViewController = .init( workspace: workspace, navigatorViewModel: navigatorViewModel, windowRef: window, dependencies: AppDependencies(), + statusBarViewModel: StatusBarViewModel(), + utilityAreaModel: UtilityAreaViewModel(), + notificationPanel: NotificationPanelViewModel( + notificationManager: NotificationManager(eventBus: eventBus), + eventBus: eventBus + ), hapticPerformer: hapticFeedbackPerformerMock ) splitViewController.viewDidLoad() From 94c8a1b68483fbdb6c35be71fd1e106ba4a947bc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 20:31:58 +0200 Subject: [PATCH 154/335] Refactor: Move ToolbarBranchPicker into CESourceControl The workspaceFileManager dependency reduces to a fallback title string, so the package's zero-service-products manifest stays intact. --- .../CodeEditWindowController+Toolbar.swift | 3 ++- .../Features/CodeEditUI/CodeEditUITests.swift | 5 +++-- .../Views/ToolbarBranchPicker.swift | 20 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) rename {CodeEdit/Features/CodeEditUI => Packages/Features/CESourceControl/Sources/CESourceControl}/Views/ToolbarBranchPicker.swift (93%) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index 5e41966e83..3a72d25c7b 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -6,6 +6,7 @@ // import AppKit +import CESourceControl import CEWorkspaceFileManager import SwiftUI import Combine @@ -167,7 +168,7 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker) let view = NSHostingView( rootView: ToolbarBranchPicker( - workspaceFileManager: workspace?.workspaceFileManager, + fallbackTitle: workspace?.workspaceFileManager.folderUrl.lastPathComponent ?? "Empty", sourceControlManager: workspace?.sourceControlManager ) ) diff --git a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift index b14a39dbde..5eac606e3e 100644 --- a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift +++ b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift @@ -6,6 +6,7 @@ // @testable import CodeEdit +import CESourceControl import CodeEditUI import Foundation import SnapshotTesting @@ -88,7 +89,7 @@ final class CodeEditUIUnitTests: XCTestCase { func testBranchPickerLight() throws { let view = ToolbarBranchPicker( - workspaceFileManager: nil, + fallbackTitle: "Empty", sourceControlManager: nil ) let hosting = NSHostingView(rootView: view) @@ -99,7 +100,7 @@ final class CodeEditUIUnitTests: XCTestCase { func testBranchPickerDark() throws { let view = ToolbarBranchPicker( - workspaceFileManager: nil, + fallbackTitle: "Empty", sourceControlManager: nil ) let hosting = NSHostingView(rootView: view) diff --git a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/ToolbarBranchPicker.swift similarity index 93% rename from CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/ToolbarBranchPicker.swift index d992c0d86f..0e17118b11 100644 --- a/CodeEdit/Features/CodeEditUI/Views/ToolbarBranchPicker.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/ToolbarBranchPicker.swift @@ -5,18 +5,16 @@ // Created by Lukas Pistrol on 21.04.22. // -import CESourceControl import SwiftUI import CodeEditSettings -import CEWorkspaceFileManager import CodeEditCore import CodeEditSymbols import CodeEditUI import Combine /// A view that pops up a branch picker. -struct ToolbarBranchPicker: View { - private weak var workspaceFileManager: CEWorkspaceFileManager? +public struct ToolbarBranchPicker: View { + private let fallbackTitle: String private weak var sourceControlManager: SourceControlManager? @Environment(\.controlActiveState) @@ -26,17 +24,17 @@ struct ToolbarBranchPicker: View { @State private var displayPopover: Bool = false @State private var currentBranch: GitBranch? - /// Initializes the ``ToolbarBranchPicker`` with an instance of a `WorkspaceClient` - /// - Parameter workspace: An instance of the current `WorkspaceClient` - init( - workspaceFileManager: CEWorkspaceFileManager?, + /// Initializes the picker with the workspace's display name (shown when no + /// branch is available) and its source-control manager. + public init( + fallbackTitle: String, sourceControlManager: SourceControlManager? ) { - self.workspaceFileManager = workspaceFileManager + self.fallbackTitle = fallbackTitle self.sourceControlManager = sourceControlManager } - var body: some View { + public var body: some View { HStack(alignment: .center, spacing: 7) { Group { if currentBranch != nil { @@ -105,7 +103,7 @@ struct ToolbarBranchPicker: View { } private var title: String { - workspaceFileManager?.folderUrl.lastPathComponent ?? "Empty" + fallbackTitle } // MARK: Popover View From d01bda605296ae9cc9aba94910c669389cd24b00 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 20:35:22 +0200 Subject: [PATCH 155/335] Refactor: Promote KeyValueTable, actionBar, and SearchPanelView to the CodeEditUI package All three are pure presentation atoms (SwiftUI + CodeEditUI-internal styles only). actionBar escapes Settings/Pages/GeneralSettings and travels with its long-flagged companion KeyValueTable. NSTableViewWrapper rides along as SearchPanelView's internal helper (sole consumer; same-target visibility hid the dependency from the import-based coupling check) with the CodeEditCore [safe:] subscript inlined to keep the package's Symbols-only charter. --- .../CEWorkspaceSettings/Views/CETaskFormView.swift | 1 + CodeEdit/Features/Commands/Views/QuickActionsView.swift | 1 + CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift | 1 + CodeEdit/Features/Settings/Views/GlobPatternList.swift | 1 + .../Sources/CodeEditUI/Styles}/View+actionBar.swift | 3 +-- .../Sources}/CodeEditUI/Views/KeyValueTable.swift | 1 - .../Sources/CodeEditUI}/Views/NSTableViewWrapper.swift | 4 +++- .../Sources}/CodeEditUI/Views/SearchPanelView.swift | 7 +++---- 8 files changed, 11 insertions(+), 8 deletions(-) rename {CodeEdit/Features/Settings/Pages/GeneralSettings => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles}/View+actionBar.swift (95%) rename {CodeEdit/Features => Packages/Foundation/CodeEditUI/Sources}/CodeEditUI/Views/KeyValueTable.swift (99%) rename {CodeEdit/Features/OpenQuickly => Packages/Foundation/CodeEditUI/Sources/CodeEditUI}/Views/NSTableViewWrapper.swift (96%) rename {CodeEdit/Features => Packages/Foundation/CodeEditUI/Sources}/CodeEditUI/Views/SearchPanelView.swift (97%) diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift index 4261fb4116..e405a4b0b5 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore struct CETaskFormView: View { diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/Views/QuickActionsView.swift index b5c3ea967e..f9ea7646c5 100644 --- a/CodeEdit/Features/Commands/Views/QuickActionsView.swift +++ b/CodeEdit/Features/Commands/Views/QuickActionsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditCore /// Quick actions view diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift index ccb6ad5319..abee2083f5 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CEWorkspaceFileManager import CodeEditCore diff --git a/CodeEdit/Features/Settings/Views/GlobPatternList.swift b/CodeEdit/Features/Settings/Views/GlobPatternList.swift index 9e455963e0..3d1e17a234 100644 --- a/CodeEdit/Features/Settings/Views/GlobPatternList.swift +++ b/CodeEdit/Features/Settings/Views/GlobPatternList.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI import CodeEditSettings struct GlobPatternList: View { diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift similarity index 95% rename from CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift index 0cb266b14b..208444cc16 100644 --- a/CodeEdit/Features/Settings/Pages/GeneralSettings/View+actionBar.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift @@ -6,9 +6,8 @@ // import SwiftUI -import CodeEditUI -extension View { +public extension View { func actionBar(@ViewBuilder content: () -> Content) -> some View { self .padding(.bottom, 24) diff --git a/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/KeyValueTable.swift similarity index 99% rename from CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/KeyValueTable.swift index eeccb1eccb..f57669ef3f 100644 --- a/CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/KeyValueTable.swift @@ -6,7 +6,6 @@ // import SwiftUI -import CodeEditUI public struct KeyValueItem: Identifiable, Equatable { public let id = UUID() diff --git a/CodeEdit/Features/OpenQuickly/Views/NSTableViewWrapper.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/NSTableViewWrapper.swift similarity index 96% rename from CodeEdit/Features/OpenQuickly/Views/NSTableViewWrapper.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/NSTableViewWrapper.swift index 3be6aeb6b1..564bf4d1b7 100644 --- a/CodeEdit/Features/OpenQuickly/Views/NSTableViewWrapper.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/NSTableViewWrapper.swift @@ -132,7 +132,9 @@ struct NSTableViewWrapper: NSViewR func tableViewSelectionDidChange(_ notification: Notification) { if let view = notification.object as? NSTableView { - let newSelection = parent.data[safe: view.selectedRow] + let newSelection = parent.data.indices.contains(view.selectedRow) + ? parent.data[view.selectedRow] + : nil if newSelection != parent.selection { parent.selection = newSelection } diff --git a/CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanelView.swift similarity index 97% rename from CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanelView.swift index 49b5178f44..0255a72e1b 100644 --- a/CodeEdit/Features/CodeEditUI/Views/SearchPanelView.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanelView.swift @@ -7,9 +7,8 @@ import Foundation import SwiftUI -import CodeEditUI -struct SearchPanelView: View { +public struct SearchPanelView: View { @ViewBuilder let rowViewBuilder: ((Option) -> RowView) @ViewBuilder let previewViewBuilder: ((Option) -> PreviewView)? @@ -27,7 +26,7 @@ struct SearchPanelView, @@ -52,7 +51,7 @@ struct SearchPanelView Date: Mon, 13 Jul 2026 20:36:49 +0200 Subject: [PATCH 156/335] Refactor: Dissolve the app-side CodeEditUI folder WorkspacePanelView/TabBar (window-area chrome, SettingsData-parameterized) move to Documents/WorkspacePanel; the folder that shadowed the CodeEditUI package name is gone. --- .../Views => Documents/WorkspacePanel}/WorkspacePanelTabBar.swift | 0 .../Views => Documents/WorkspacePanel}/WorkspacePanelView.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/Features/{CodeEditUI/Views => Documents/WorkspacePanel}/WorkspacePanelTabBar.swift (100%) rename CodeEdit/Features/{CodeEditUI/Views => Documents/WorkspacePanel}/WorkspacePanelView.swift (100%) diff --git a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelTabBar.swift b/CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelTabBar.swift similarity index 100% rename from CodeEdit/Features/CodeEditUI/Views/WorkspacePanelTabBar.swift rename to CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelTabBar.swift diff --git a/CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift b/CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelView.swift similarity index 100% rename from CodeEdit/Features/CodeEditUI/Views/WorkspacePanelView.swift rename to CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelView.swift From dd67b13337c41b8332c05f76b7f059e22dfb2fb9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 20:49:33 +0200 Subject: [PATCH 157/335] Refactor: Drop Workspace's vestigial NotificationCenter deinit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing registers Workspace as an observer since the WorkspaceDocument era; the NotificationCenter→EventBus migration is verified complete (zero custom Notification.Names anywhere; remaining NC use is platform notifications only). --- CodeEdit/Features/Workspace/Models/Workspace.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Models/Workspace.swift index 5042d0d422..8e3e4d557a 100644 --- a/CodeEdit/Features/Workspace/Models/Workspace.swift +++ b/CodeEdit/Features/Workspace/Models/Workspace.swift @@ -140,8 +140,4 @@ final class Workspace: ObservableObject { let mutablePointer = UnsafeMutablePointer(opaquePtr) mutablePointer.pointee = shouldClose } - - deinit { - NotificationCenter.default.removeObserver(self) - } } From dc5dfebb1c642ba6e1569b794f91233887647acf Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 21:24:50 +0200 Subject: [PATCH 158/335] Refactor: Split RegistryViewState out of RegistryManager The manager is now a pure command service behind RegistryManaging (no ObservableObject); the Settings extension pages observe the owned RegistryViewState and issue commands through the protocol-typed env key. The views read installedLanguageServers via their own @AppSettings, the established reactive idiom for Settings pages. --- .../LanguageServerInstallView.swift | 15 +++++--- .../Extensions/LanguageServerRowView.swift | 21 ++++++++---- .../Extensions/LanguageServersView.swift | 11 +++--- CodeEdit/Features/Settings/SettingsView.swift | 2 +- .../Models/Environment+AppCommands.swift | 4 +-- CodeEditTests/Features/LSP/Registry.swift | 2 +- .../Registry/Protocols/RegistryManaging.swift | 9 ++--- .../RegistryManager+HandleRegistryFile.swift | 8 ++--- .../CELSP/Registry/RegistryManager.swift | 34 ++++++++----------- .../CELSP/Registry/RegistryViewState.swift | 24 +++++++++++++ 10 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 Packages/Features/CELSP/Sources/CELSP/Registry/RegistryViewState.swift diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift index 3c39a6ccfd..3e39632c6b 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift @@ -7,13 +7,18 @@ import CELSP import SwiftUI +import CodeEditSettings import CodeEditUI /// A view for initiating a package install and monitoring progress. struct LanguageServerInstallView: View { @Environment(\.dismiss) var dismiss - @EnvironmentObject private var registryManager: RegistryManager + @Environment(\.registryManager) + private var registryManager + + @AppSettings(\.languageServers.installedLanguageServers) + private var installedLanguageServers @ObservedObject var operation: PackageManagerInstallOperation @@ -30,7 +35,7 @@ struct LanguageServerInstallView: View { presenting: operation.waitingForConfirmation ) { _ in Button("Cancel") { - registryManager.cancelInstallation() + registryManager?.cancelInstallation() } Button("Continue") { operation.confirmCurrentStep() @@ -67,7 +72,7 @@ struct LanguageServerInstallView: View { .buttonStyle(.bordered) Button { do { - try registryManager.startInstallation(operation: operation) + try registryManager?.startInstallation(operation: operation) } catch { // Display the error NSAlert(error: error).runModal() @@ -78,7 +83,7 @@ struct LanguageServerInstallView: View { .buttonStyle(.borderedProminent) case .running: Button { - registryManager.cancelInstallation() + registryManager?.cancelInstallation() dismiss() } label: { Text("Cancel") @@ -150,7 +155,7 @@ struct LanguageServerInstallView: View { @ViewBuilder private var progressSection: some View { Section { LabeledContent("Step") { - if registryManager.installedLanguageServers[operation.package.name] != nil { + if installedLanguageServers[operation.package.name] != nil { HStack(spacing: 4) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift index 7b26162a30..a06ac00ca4 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift @@ -7,6 +7,7 @@ import CELSP import SwiftUI +import CodeEditSettings import CodeEditCore private let iconSize: CGFloat = 26 @@ -17,10 +18,10 @@ struct LanguageServerRowView: View, Equatable { let onInstall: (() async -> Void) private var isInstalled: Bool { - registryManager.installedLanguageServers[package.name] != nil + installedLanguageServers[package.name] != nil } private var isEnabled: Bool { - registryManager.installedLanguageServers[package.name]?.isEnabled ?? false + installedLanguageServers[package.name]?.isEnabled ?? false } @State private var isHovering: Bool = false @@ -31,7 +32,13 @@ struct LanguageServerRowView: View, Equatable { @State private var showMore: Bool = false - @EnvironmentObject var registryManager: RegistryManager + @EnvironmentObject var registryState: RegistryViewState + + @Environment(\.registryManager) + private var registryManager + + @AppSettings(\.languageServers.installedLanguageServers) + private var installedLanguageServers init( package: RegistryItem, @@ -123,7 +130,7 @@ struct LanguageServerRowView: View, Equatable { private func installationButton() -> some View { if isInstalled { installedRow() - } else if registryManager.runningInstall?.package.name == package.name { + } else if registryState.runningInstall?.package.name == package.name { isInstallingRow() } else if isHovering { isHoveringRow() @@ -147,7 +154,7 @@ struct LanguageServerRowView: View, Equatable { "", isOn: Binding( get: { isEnabled }, - set: { registryManager.setPackageEnabled(packageName: package.name, enabled: $0) } + set: { registryManager?.setPackageEnabled(packageName: package.name, enabled: $0) } ) ) .toggleStyle(.switch) @@ -197,7 +204,7 @@ struct LanguageServerRowView: View, Equatable { } label: { Text("Install") } - .disabled(registryManager.isInstalling) + .disabled(registryState.isInstalling) } @ViewBuilder @@ -222,7 +229,7 @@ struct LanguageServerRowView: View, Equatable { isRemoving = true Task { do { - try await registryManager.removeLanguageServer(packageName: package.name) + try await registryManager?.removeLanguageServer(packageName: package.name) await MainActor.run { isRemoving = false } diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 324a9ab7c2..2cc28d2762 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -11,7 +11,8 @@ import CodeEditCore /// Displays a searchable list of packages from the ``RegistryManager``. struct LanguageServersView: View { - @ObservedObject var registryManager: RegistryManager + let registryManager: any RegistryManaging + @ObservedObject var registryState: RegistryViewState @StateObject private var searchModel = FuzzySearchUIModel() @State private var searchText: String = "" @State private var selectedInstall: PackageManagerInstallOperation? @@ -21,7 +22,7 @@ struct LanguageServersView: View { var body: some View { Group { SettingsForm { - if registryManager.isDownloadingRegistry { + if registryState.isDownloadingRegistry { HStack { Spacer() ProgressView() @@ -31,7 +32,7 @@ struct LanguageServersView: View { } Section { - List(searchModel.items ?? registryManager.registryItems, id: \.name) { item in + List(searchModel.items ?? registryState.registryItems, id: \.name) { item in LanguageServerRowView( package: item, onCancel: { @@ -50,7 +51,7 @@ struct LanguageServersView: View { } .searchable(text: $searchText) .onChange(of: searchText) { _, newValue in - searchModel.searchTextUpdated(searchText: newValue, allItems: registryManager.registryItems) + searchModel.searchTextUpdated(searchText: newValue, allItems: registryState.registryItems) } } header: { Label( @@ -63,7 +64,7 @@ struct LanguageServersView: View { LanguageServerInstallView(operation: operation) } } - .environmentObject(registryManager) + .environmentObject(registryState) } private func getInfoString() -> AttributedString { diff --git a/CodeEdit/Features/Settings/SettingsView.swift b/CodeEdit/Features/Settings/SettingsView.swift index dd1d442267..1a19099bb2 100644 --- a/CodeEdit/Features/Settings/SettingsView.swift +++ b/CodeEdit/Features/Settings/SettingsView.swift @@ -203,7 +203,7 @@ struct SettingsView: View { LocationsSettingsView() case .languageServers: if let registryManager { - LanguageServersView(registryManager: registryManager) + LanguageServersView(registryManager: registryManager, registryState: registryManager.viewState) } case .developer: DeveloperSettingsView() diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 34d64abeda..8ed34b6d62 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -39,7 +39,7 @@ private struct LSPServiceKey: EnvironmentKey { } private struct RegistryManagerKey: EnvironmentKey { - static let defaultValue: RegistryManager? = nil + static let defaultValue: (any RegistryManaging)? = nil } private struct WorkspaceWindowManagerKey: EnvironmentKey { @@ -72,7 +72,7 @@ extension EnvironmentValues { } /// The language-server registry. Optional: registry UI is empty in previews. Injected by the app shell. - var registryManager: RegistryManager? { + var registryManager: (any RegistryManaging)? { get { self[RegistryManagerKey.self] } set { self[RegistryManagerKey.self] = newValue } } diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index 282a5e9bcf..50bf830293 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -27,7 +27,7 @@ struct RegistryTests { func registryDownload() async throws { await registry.downloadRegistryItems() - #expect(registry.downloadError == nil) + #expect(registry.viewState.downloadError == nil) let registryJsonPath = registry.installPath.appending(path: "registry.json") let checksumPath = registry.installPath.appending(path: "checksums.txt") diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift index 594730ea66..86fe2ae35c 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift @@ -11,12 +11,13 @@ import CodeEditCore /// Protocol for managing the language server registry. /// -/// Note: `@Published` properties are not included because consumers -/// need the concrete type for SwiftUI observation. Use `RegistryManager` directly in views. +/// A pure command service: observable presentation state lives on the +/// concrete ``RegistryViewState`` exposed via ``viewState`` (views observe +/// that; a view-state is concrete by nature). @MainActor -public protocol RegistryManaging: AnyObject, ObservableObject { +public protocol RegistryManaging: AnyObject { + var viewState: RegistryViewState { get } var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] { get } - var isInstalling: Bool { get } func setPackageEnabled(packageName: String, enabled: Bool) func removeLanguageServer(packageName: String) async throws diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift index 08b1fc1920..20498fd1a1 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift @@ -11,8 +11,8 @@ import CodeEditCore extension RegistryManager { /// Downloads the latest registry func downloadRegistryItems() async { - isDownloadingRegistry = true - defer { isDownloadingRegistry = false } + viewState.isDownloadingRegistry = true + defer { viewState.isDownloadingRegistry = false } let registryData, checksumData: Data do { @@ -47,7 +47,7 @@ extension RegistryManager { try FileManager.default.removeItem(at: tempZipURL) try checksumData.write(to: checksumDestination) - downloadError = nil + viewState.downloadError = nil } catch { handleUpdateError(RegistryManagerError.writeFailed(error: error)) return @@ -65,7 +65,7 @@ extension RegistryManager { } func handleUpdateError(_ error: Error) { - self.downloadError = error + self.viewState.downloadError = error if let regError = error as? RegistryManagerError { switch regError { case .installationRunning: diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift index 7105e65a65..912bcfdc4a 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift @@ -13,7 +13,7 @@ import Combine import CodeEditCore @MainActor -public final class RegistryManager: ObservableObject, RegistryManaging { +public final class RegistryManager: RegistryManaging { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RegistryManager") let installPath = Settings.shared.baseURL.appending(path: "Language Servers") @@ -27,17 +27,11 @@ public final class RegistryManager: ObservableObject, RegistryManaging { string: "https://github.com/mason-org/mason-registry/releases/latest/download/checksums.txt" )! - @Published public var isDownloadingRegistry: Bool = false - /// Holds an errors found while downloading the registry file. Needs a UI to dismiss, is logged. - @Published public var downloadError: Error? - /// Any currently running installation operation. - @Published public var runningInstall: PackageManagerInstallOperation? - private var installTask: Task? + /// Observable presentation state for the Settings extension pages. + /// The manager owns and feeds it; views observe it instead of the manager. + public let viewState = RegistryViewState() - /// Indicates if the manager is currently installing a package. - public var isInstalling: Bool { - installTask != nil - } + private var installTask: Task? /// Reference to cached registry data. Will be removed from memory after a certain amount of time. private var cachedRegistry: CachedRegistry? @@ -45,8 +39,6 @@ public final class RegistryManager: ObservableObject, RegistryManaging { /// nonisolated(unsafe): scheduled and invalidated on the main actor; also /// invalidated from `deinit`, which cannot be actor-isolated. private nonisolated(unsafe) var cleanupTimer: Timer? - /// Public access to registry items with cache management - @Published public private(set) var registryItems: [RegistryItem] = [] @AppSettings(\.languageServers.installedLanguageServers) public var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] @@ -109,7 +101,7 @@ public final class RegistryManager: ObservableObject, RegistryManaging { // MARK: - Install public func installOperation(package: RegistryItem) throws -> PackageManagerInstallOperation { - guard !isInstalling else { + guard !viewState.isInstalling else { throw RegistryManagerError.installationRunning } guard let method = package.installMethod, @@ -122,7 +114,7 @@ public final class RegistryManager: ObservableObject, RegistryManaging { /// Starts the actual installation process for a package public func startInstallation(operation installOperation: PackageManagerInstallOperation) throws { - guard !isInstalling else { + guard !viewState.isInstalling else { throw RegistryManagerError.installationRunning } @@ -135,12 +127,14 @@ public final class RegistryManager: ObservableObject, RegistryManaging { } private func installPackage(operation: PackageManagerInstallOperation, method: InstallationMethod) { + viewState.isInstalling = true installTask = Task { [weak self] in defer { self?.installTask = nil - self?.runningInstall = nil + self?.viewState.isInstalling = false + self?.viewState.runningInstall = nil } - self?.runningInstall = operation + self?.viewState.runningInstall = operation // Add to activity viewer let activityTitle = "\(operation.package.name)\("@" + (method.version ?? "latest"))" @@ -170,9 +164,11 @@ public final class RegistryManager: ObservableObject, RegistryManaging { /// Cancel the currently running installation public func cancelInstallation() { - runningInstall?.cancel() + viewState.runningInstall?.cancel() installTask?.cancel() installTask = nil + viewState.isInstalling = false + viewState.runningInstall = nil } /// Updates the activity viewer with the status of the language server installation @@ -215,7 +211,7 @@ public final class RegistryManager: ObservableObject, RegistryManaging { } } - registryItems = items + viewState.registryItems = items } } diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryViewState.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryViewState.swift new file mode 100644 index 0000000000..315d002431 --- /dev/null +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryViewState.swift @@ -0,0 +1,24 @@ +// +// RegistryViewState.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import Foundation +import CodeEditCore + +/// Observable presentation state for the language-server registry. +/// Owned and fed by ``RegistryManager``; observed by the Settings extension +/// pages. Split out so the manager itself is a pure command service. +@MainActor +public final class RegistryViewState: ObservableObject { + @Published public internal(set) var isDownloadingRegistry: Bool = false + /// Holds any error found while downloading the registry file. Needs a UI to dismiss, is logged. + @Published public internal(set) var downloadError: Error? + /// Any currently running installation operation. + @Published public internal(set) var runningInstall: PackageManagerInstallOperation? + /// Indicates if the manager is currently installing a package. + @Published public internal(set) var isInstalling: Bool = false + @Published public internal(set) var registryItems: [RegistryItem] = [] +} From 98716ddedf737ce427201eaf0f6e2f4830ab815e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 21:28:52 +0200 Subject: [PATCH 159/335] Refactor: Split LanguageServerListState out of LSPService LSPService is no longer ObservableObject; the output-source picker observes the owned list state and the lspService environment key is deleted (the picker was its only reader). --- .../View/UtilityAreaOutputSourcePicker.swift | 24 +++++------ .../Models/Environment+AppCommands.swift | 16 ++++---- .../Sources/CELSP/Service/LSPService.swift | 16 +++++++- .../Service/LanguageServerListState.swift | 41 +++++++++++++++++++ 4 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerListState.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index 91e894834c..6f24a2a8d8 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -23,9 +23,10 @@ struct UtilityAreaOutputSourcePicker: View { @ObservedObject var extensionManager = ExtensionManager.shared - @Environment(\.lspService) var lspService + @Environment(\.languageServerListState) + private var languageServerListState @State private var updater: UUID = UUID() - @State private var languageServerClients: [LSPService.LanguageServerType] = [] + @State private var languageServerClients: [RunningLanguageServer] = [] var body: some View { Picker("Output Source", selection: $selectedSource) { @@ -67,26 +68,21 @@ struct UtilityAreaOutputSourcePicker: View { .labelsHidden() .controlSize(.small) .onAppear { - updateLanguageServers(lspService?.languageClients ?? [:]) + updateLanguageServers(languageServerListState?.runningServers ?? []) } .onReceive( - lspService?.$languageClients.eraseToAnyPublisher() ?? Just([:]).eraseToAnyPublisher() - ) { clients in - updateLanguageServers(clients) + languageServerListState?.$runningServers.eraseToAnyPublisher() ?? Just([]).eraseToAnyPublisher() + ) { servers in + updateLanguageServers(servers) } .onReceive(extensionManager.$extensions) { _ in updater = UUID() } } - func updateLanguageServers(_ clients: [LSPService.ClientKey: LSPService.LanguageServerType]) { - languageServerClients = clients - .compactMap { (key, value) in - if key.workspacePath == workspaceFileURL?.absolutePath { - return value - } - return nil - } + func updateLanguageServers(_ servers: [RunningLanguageServer]) { + languageServerClients = servers + .filter { $0.workspacePath == workspaceFileURL?.absolutePath } .sorted(by: { $0.languageId.rawValue < $1.languageId.rawValue }) if selectedSource == nil, let client = languageServerClients.first { selectedSource = Sources.languageServer(client.logContainer) diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift index 8ed34b6d62..a2768fc6a8 100644 --- a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift +++ b/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift @@ -34,8 +34,8 @@ private struct ShellClientKey: EnvironmentKey { static let defaultValue: ShellClientProtocol? = nil } -private struct LSPServiceKey: EnvironmentKey { - static let defaultValue: LSPService? = nil +private struct LanguageServerListStateKey: EnvironmentKey { + static let defaultValue: LanguageServerListState? = nil } private struct RegistryManagerKey: EnvironmentKey { @@ -65,10 +65,10 @@ extension EnvironmentValues { set { self[ShellClientKey.self] = newValue } } - /// The LSP service. Optional: language-server UI is empty in previews. Injected by the app shell. - var lspService: LSPService? { - get { self[LSPServiceKey.self] } - set { self[LSPServiceKey.self] = newValue } + /// The observable list of running language servers. Optional: empty in previews. + var languageServerListState: LanguageServerListState? { + get { self[LanguageServerListStateKey.self] } + set { self[LanguageServerListStateKey.self] = newValue } } /// The language-server registry. Optional: registry UI is empty in previews. Injected by the app shell. @@ -97,7 +97,7 @@ extension View { func appServices(_ dependencies: AppDependencies) -> some View { environment(\.commandManager, dependencies.commandManager) .environment(\.shellClient, dependencies.shellClient) - .environment(\.lspService, dependencies.lspService) + .environment(\.languageServerListState, dependencies.lspService.serverListState) .environment(\.registryManager, dependencies.registryManager) .environment(\.eventBus, dependencies.eventBus) .environment(\.workspaceWindowManager, dependencies.workspaceWindowManager) @@ -114,7 +114,7 @@ extension Scene { func appServices(_ dependencies: AppDependencies) -> some Scene { environment(\.commandManager, dependencies.commandManager) .environment(\.shellClient, dependencies.shellClient) - .environment(\.lspService, dependencies.lspService) + .environment(\.languageServerListState, dependencies.lspService.serverListState) .environment(\.registryManager, dependencies.registryManager) .environment(\.eventBus, dependencies.eventBus) .environment(\.workspaceWindowManager, dependencies.workspaceWindowManager) diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift index e07f926610..074d038ddc 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift @@ -100,11 +100,15 @@ import CodeEditLanguages /// } /// ``` @MainActor -public final class LSPService: ObservableObject, LSPServiceProtocol { +public final class LSPService: LSPServiceProtocol { public typealias LanguageServerType = LanguageServer let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") + /// Observable list of running servers for UI (the output-source picker). + /// The service owns and feeds it; views observe it instead of the service. + public let serverListState = LanguageServerListState() + public struct ClientKey: Hashable, Equatable, Sendable { public let languageId: LanguageIdentifier public let workspacePath: String @@ -116,7 +120,7 @@ public final class LSPService: ObservableObject, LSPServiceProtocol { } /// Holds the active language clients - @Published public var languageClients: [ClientKey: LanguageServerType] = [:] + public var languageClients: [ClientKey: LanguageServerType] = [:] /// Holds the language server configurations for all the installed language servers var languageConfigs: [LanguageIdentifier: LanguageServerBinary] = [:] /// Holds all the event listeners for each active language client @@ -211,6 +215,11 @@ public final class LSPService: ObservableObject, LSPServiceProtocol { } ) languageClients[ClientKey(languageId, workspacePath)] = server + serverListState.add(RunningLanguageServer( + workspacePath: workspacePath, + languageId: languageId, + logContainer: server.logContainer + )) logger.info("Successfully started \(languageId.rawValue) language server") self.startListeningToEvents(for: ClientKey(languageId, workspacePath)) @@ -288,6 +297,7 @@ public final class LSPService: ObservableObject, LSPServiceProtocol { for (key, _) in clientKeys { self.languageClients.removeValue(forKey: key) } + self.serverListState.removeAll(workspacePath: workspacePath) } } @@ -310,6 +320,7 @@ public final class LSPService: ObservableObject, LSPServiceProtocol { throw error } languageClients.removeValue(forKey: ClientKey(languageId, workspacePath)) + serverListState.remove(workspacePath: workspacePath, languageId: languageId) logger.info("Server stopped for language \(languageId.rawValue)") stopListeningToEvents(for: ClientKey(languageId, workspacePath)) @@ -327,6 +338,7 @@ public final class LSPService: ObservableObject, LSPServiceProtocol { } } languageClients.removeAll() + serverListState.removeAll() eventListeningTasks.forEach { (_, value) in value.cancel() } diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerListState.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerListState.swift new file mode 100644 index 0000000000..4bb019eb8d --- /dev/null +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerListState.swift @@ -0,0 +1,41 @@ +// +// LanguageServerListState.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 13/07/2026. +// + +import Foundation +import LanguageServerProtocol + +/// A running language server, as presented to UI (the output-source picker). +public struct RunningLanguageServer: Identifiable { + public let workspacePath: String + public let languageId: LanguageIdentifier + public let logContainer: LanguageServerLogContainer + public var id: String { workspacePath + languageId.rawValue } +} + +/// Observable list of running language servers. Owned and fed by +/// ``LSPService``; observed by the utility-area output-source picker. +@MainActor +public final class LanguageServerListState: ObservableObject { + @Published public private(set) var runningServers: [RunningLanguageServer] = [] + + func add(_ server: RunningLanguageServer) { + runningServers.removeAll { $0.id == server.id } + runningServers.append(server) + } + + func remove(workspacePath: String, languageId: LanguageIdentifier) { + runningServers.removeAll { $0.workspacePath == workspacePath && $0.languageId == languageId } + } + + func removeAll(workspacePath: String) { + runningServers.removeAll { $0.workspacePath == workspacePath } + } + + func removeAll() { + runningServers.removeAll() + } +} From 39e2d202dd22477ea149cbdfd13b99557012212a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 21:50:11 +0200 Subject: [PATCH 160/335] Fix: Restore workspace-reopen-on-launch and file recents Two regressions from the NSDocument-removal rewrite, found in the recents/window-restoration audit: - ShutdownApplicationUseCase still saved open-workspace paths to recover.workspaces on quit, but the launch-time read was dropped with the old AppDelegate. Workspace windows are plain NSWindows, so NSQuitAlwaysKeepsWindows cannot restore them. AppDelegate now reopens the saved workspaces on launch, falling back to handleOpen() only when none were restored. - The deleted CodeEditDocumentController noted every opened URL to RecentsStore; afterward only folders were noted. OpenDocumentUseCase now notes file opens (both the in-workspace and standalone-document paths), so the Open Recent file section and Welcome list populate again. --- CodeEdit/AppDelegate.swift | 23 ++++++++++++++++++- .../UseCases/OpenDocumentUseCase.swift | 8 ++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 17ecd5aeee..aab9042d0d 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -78,7 +78,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } if needToHandleOpen { - self.handleOpen() + // Reopen the workspaces that were open at last quit (saved by + // ShutdownApplicationUseCase). Workspace windows are plain NSWindows, + // so NSQuitAlwaysKeepsWindows cannot restore them itself. + var restoredWorkspace = false + if let projects = UserDefaults.standard.array( + forKey: AppDelegate.recoverWorkspacesKey + ) as? [String] { + for path in projects { + do { + try self.windowManager.openWorkspace(at: URL(fileURLWithPath: path)) + restoredWorkspace = true + } catch { + self.logger.error( + "Failed to restore workspace at \(path): \(error.localizedDescription)" + ) + } + } + } + + if !restoredWorkspace { + self.handleOpen() + } } } } diff --git a/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift b/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift index f147ef7cbb..bddc0e4929 100644 --- a/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift @@ -6,6 +6,7 @@ // import AppKit +import WelcomeWindow /// Routes a URL to the appropriate opener: a workspace (folder), an existing workspace's file, /// or a standalone document via NSDocumentController. @@ -20,15 +21,20 @@ final class OpenDocumentUseCase { func execute(url: URL, onCompletion: @escaping () -> Void) { do { if url.isFolder { + // Folders are noted as recents in `WorkspaceWindowManager.openWorkspace`. try windowManager.openWorkspace(at: url) onCompletion() } else if windowManager.openFileInWorkspace(url: url) { + RecentsStore.documentOpened(at: url) onCompletion() } else { NSDocumentController.shared.openDocument( withContentsOf: url, display: true ) { _, _, error in - if error == nil { onCompletion() } + if error == nil { + RecentsStore.documentOpened(at: url) + onCompletion() + } } } } catch { From 6abef3aea43af1f6cb3fd036b3ca75af4f6c7d67 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 13 Jul 2026 22:01:06 +0200 Subject: [PATCH 161/335] Refactor: Load the language-server registry lazily on first use RegistryManager.init no longer reads the catalog from disk or kicks off a network download at app launch (it was forced eagerly by the appServices env injection). The catalog now loads via loadRegistryIfNeeded() when Settings -> Extensions first appears; the page already shows the download spinner. --- .../Extensions/LanguageServersView.swift | 3 +++ .../Registry/Protocols/RegistryManaging.swift | 1 + .../CELSP/Registry/RegistryManager.swift | 20 ++++++++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift index 2cc28d2762..3cae27c66f 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift @@ -65,6 +65,9 @@ struct LanguageServersView: View { } } .environmentObject(registryState) + .task { + registryManager.loadRegistryIfNeeded() + } } private func getInfoString() -> AttributedString { diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift index 86fe2ae35c..2b3b4eb9d2 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift @@ -19,6 +19,7 @@ public protocol RegistryManaging: AnyObject { var viewState: RegistryViewState { get } var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] { get } + func loadRegistryIfNeeded() func setPackageEnabled(packageName: String, enabled: Bool) func removeLanguageServer(packageName: String) async throws func installOperation(package: RegistryItem) throws -> PackageManagerInstallOperation diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift index 912bcfdc4a..4ccb61202b 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift @@ -51,7 +51,21 @@ public final class RegistryManager: RegistryManaging { self.eventBus = eventBus self.errorNotifier = errorNotifier self.shellClient = shellClient - // Load the registry items from disk again after cache expires + } + + deinit { + cleanupTimer?.invalidate() + } + + private var didStartInitialLoad = false + + /// Loads the registry catalog on first demand (from disk, else network). + /// Idempotent — safe to call on every appearance of the Extensions page. + /// The cache-expiry timer set by `setRegistryItems` continues the refresh + /// cycle after the first load. + public func loadRegistryIfNeeded() { + guard !didStartInitialLoad else { return } + didStartInitialLoad = true if let items = loadItemsFromDisk() { setRegistryItems(items) } else { @@ -61,10 +75,6 @@ public final class RegistryManager: RegistryManaging { } } - deinit { - cleanupTimer?.invalidate() - } - // MARK: - Enable/Disable public func setPackageEnabled(packageName: String, enabled: Bool) { From fd08a0d9489c07945ce1f7138f55f70c00399f46 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 13:25:14 +0200 Subject: [PATCH 162/335] Refactor: Move the fuzzy-search algorithm into CodeEditCore FuzzySearchable + String+Normalise + String+LengthOfMatchingPrefix join FuzzySearchModels in Core, making the synchronous matching algorithm a reusable Foundation utility (packages can now fuzzy-match without app-tier code). The dependency-bound async pieces (Collection+FuzzySearch on CollectionConcurrencyKit, FuzzySearchUIModel on swift-async-algorithms) stay app-side; FuzzySearchUIModel gains the explicit AsyncAlgorithms import it was reaching via workspace leaky-import. The three FuzzySearchable conformances' witnesses become public to satisfy the now-public protocol. --- .../Features/Search/FuzzySearch/FuzzySearchUIModel.swift | 2 ++ .../Pages/Extensions/RegistryItem+FuzzySearchable.swift | 2 +- .../Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift | 3 ++- CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift | 3 ++- .../Features/Search/FuzzySearch/FuzzySearchTests.swift | 1 + .../CodeEditCore/Domain/Search}/FuzzySearchable.swift | 5 ++--- .../Domain/Search}/String+LengthOfMatchingPrefix.swift | 0 .../CodeEditCore/Domain/Search}/String+Normalise.swift | 3 +-- 8 files changed, 11 insertions(+), 8 deletions(-) rename {CodeEdit/Features/Search/FuzzySearch => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search}/FuzzySearchable.swift (97%) rename {CodeEdit/Features/Search/FuzzySearch => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search}/String+LengthOfMatchingPrefix.swift (100%) rename {CodeEdit/Features/Search/FuzzySearch => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search}/String+Normalise.swift (96%) diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift index 5006fa58f2..4c4ae42c6a 100644 --- a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift +++ b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift @@ -7,6 +7,8 @@ import Foundation import Combine +import AsyncAlgorithms +import CodeEditCore @MainActor final class FuzzySearchUIModel: ObservableObject { diff --git a/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift b/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift index 5713cc52a2..5a979db6e3 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift @@ -9,5 +9,5 @@ import CELSP import CodeEditCore extension RegistryItem: FuzzySearchable { - var searchableString: String { name } + public var searchableString: String { name } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift index ae17ba8c55..999f3f6dd5 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift @@ -7,9 +7,10 @@ import Foundation import CodeEditSettings +import CodeEditCore extension Theme: FuzzySearchable { - var searchableString: String { + public var searchableString: String { return id } } diff --git a/CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift b/CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift index ef6c363565..8759431a6f 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift @@ -6,9 +6,10 @@ // import Foundation +import CodeEditCore extension URL: FuzzySearchable { - var searchableString: String { + public var searchableString: String { return self.lastPathComponent } } diff --git a/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift b/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift index 9a9ba41781..6f8fad5190 100644 --- a/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift +++ b/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CodeEditCore @testable import CodeEdit final class FuzzySearchTests: XCTestCase { diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift similarity index 97% rename from CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift index afae806871..9aa12ae1e3 100644 --- a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchable.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift @@ -6,10 +6,9 @@ // import Foundation -import CodeEditCore /// A protocol defining the requirements for an object that can be searched using fuzzy matching. -protocol FuzzySearchable { +public protocol FuzzySearchable { var searchableString: String { get } /// Performs a fuzzy search on the conforming object's searchable string. @@ -22,7 +21,7 @@ protocol FuzzySearchable { func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult } -extension FuzzySearchable { +public extension FuzzySearchable { func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult { let compareString = characters.characters diff --git a/CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift similarity index 100% rename from CodeEdit/Features/Search/FuzzySearch/String+LengthOfMatchingPrefix.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift diff --git a/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift similarity index 96% rename from CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift index e4486e2b5d..d2672abf72 100644 --- a/CodeEdit/Features/Search/FuzzySearch/String+Normalise.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift @@ -6,9 +6,8 @@ // import Foundation -import CodeEditCore -extension String { +public extension String { /// Normalises the characters of the string by converting them to ASCII representation. /// Each character is transformed into its ASCII equivalent, and the resulting array /// of FuzzySearchCharacter objects contains both the original and normalised content. From 81f3509643470aebbbad27df188c30007a601d22 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 13:27:21 +0200 Subject: [PATCH 163/335] Refactor: Promote QuickSearchResultLabel to the CodeEditUI package The command-palette / open-quickly result label is a pure NSViewRepresentable presentation atom; it joins the shared UI tier beside SearchPanelView. This empties the app-side Features/Search/Views folder. --- .../Views/OpenQuicklyListItemView.swift | 1 + .../CodeEditUI}/Views/QuickSearchResultLabel.swift | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) rename {CodeEdit/Features/Search => Packages/Foundation/CodeEditUI/Sources/CodeEditUI}/Views/QuickSearchResultLabel.swift (82%) diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyListItemView.swift b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyListItemView.swift index 6acd23e381..61fc93b16e 100644 --- a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyListItemView.swift +++ b/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyListItemView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditUI struct OpenQuicklyListItemView: View { private let baseDirectory: URL diff --git a/CodeEdit/Features/Search/Views/QuickSearchResultLabel.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift similarity index 82% rename from CodeEdit/Features/Search/Views/QuickSearchResultLabel.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift index 9cb60c02c4..3dba2ba6f7 100644 --- a/CodeEdit/Features/Search/Views/QuickSearchResultLabel.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift @@ -10,12 +10,22 @@ import SwiftUI /// Implementation of command palette entity. While swiftui does not allow to use NSMutableAttributeStrings, /// the only way to fallback to UIKit and have NSViewRepresentable to be a bridge between UIKit and SwiftUI. /// Highlights currently entered text query -struct QuickSearchResultLabel: NSViewRepresentable { +public struct QuickSearchResultLabel: NSViewRepresentable { let labelName: String let charactersToHighlight: [NSRange] let maximumNumberOfLines: Int = 1 var nsLabelName: NSAttributedString? + public init( + labelName: String, + charactersToHighlight: [NSRange], + nsLabelName: NSAttributedString? = nil + ) { + self.labelName = labelName + self.charactersToHighlight = charactersToHighlight + self.nsLabelName = nsLabelName + } + public func makeNSView(context: Context) -> some NSTextField { let label = NSTextField(wrappingLabelWithString: labelName) label.translatesAutoresizingMaskIntoConstraints = false @@ -41,7 +51,7 @@ struct QuickSearchResultLabel: NSViewRepresentable { return attribText } - func updateNSView(_ nsView: NSViewType, context: Context) { + public func updateNSView(_ nsView: NSViewType, context: Context) { nsView.textColor = if nsLabelName == nil && charactersToHighlight.isEmpty { .controlTextColor } else { From 111dfa748620b5ce18edef1b2f315fcf580192ef Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 15:14:23 +0200 Subject: [PATCH 164/335] Refactor: Fold the workspace window shell into Features/Workspace The misleadingly-named Documents/ (a WorkspaceDocument/NSDocument-era fossil) is dissolved into Workspace/, reuniting the window controller + split view controller + toolbar + panels (now Workspace/Window/) with the WorkspaceWindowManager that owns them. WorkspaceStatePersistence and AppCodeFileDocumentDelegate move to Workspace/ and Workspace/Services/ respectively. Folder-only move (filesystem-synchronized groups); no code changes. --- .../Services}/AppCodeFileDocumentDelegate.swift | 0 .../Window}/Controllers/CodeEditSplitViewController.swift | 0 .../Window}/Controllers/CodeEditWindowController+Panels.swift | 0 .../Window}/Controllers/CodeEditWindowController+Toolbar.swift | 0 .../Window}/Controllers/CodeEditWindowController.swift | 0 .../Window}/Controllers/CodeEditWindowControllerExtensions.swift | 0 .../Window}/Controllers/NotificationPanelViewModel+Toolbar.swift | 0 .../Window}/Toolbar/StartTaskToolbarButton.swift | 0 .../Window}/Toolbar/StartTaskToolbarItem.swift | 0 .../Window}/Toolbar/StopTaskToolbarButton.swift | 0 .../Window}/Toolbar/StopTaskToolbarItem.swift | 0 .../Window}/WorkspacePanel/WorkspacePanelTabBar.swift | 0 .../Window}/WorkspacePanel/WorkspacePanelView.swift | 0 .../WorkspaceStatePersistence.swift | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/Features/{Documents => Workspace/Services}/AppCodeFileDocumentDelegate.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Controllers/CodeEditSplitViewController.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Controllers/CodeEditWindowController+Panels.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Controllers/CodeEditWindowController+Toolbar.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Controllers/CodeEditWindowController.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Controllers/CodeEditWindowControllerExtensions.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Controllers/NotificationPanelViewModel+Toolbar.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Toolbar/StartTaskToolbarButton.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Toolbar/StartTaskToolbarItem.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Toolbar/StopTaskToolbarButton.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/Toolbar/StopTaskToolbarItem.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/WorkspacePanel/WorkspacePanelTabBar.swift (100%) rename CodeEdit/Features/{Documents => Workspace/Window}/WorkspacePanel/WorkspacePanelView.swift (100%) rename CodeEdit/Features/{Documents/WorkspaceDocument => Workspace}/WorkspaceStatePersistence.swift (100%) diff --git a/CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Workspace/Services/AppCodeFileDocumentDelegate.swift similarity index 100% rename from CodeEdit/Features/Documents/AppCodeFileDocumentDelegate.swift rename to CodeEdit/Features/Workspace/Services/AppCodeFileDocumentDelegate.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Workspace/Window/Controllers/CodeEditSplitViewController.swift similarity index 100% rename from CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift rename to CodeEdit/Features/Workspace/Window/Controllers/CodeEditSplitViewController.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Panels.swift similarity index 100% rename from CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift rename to CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Panels.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Toolbar.swift similarity index 100% rename from CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift rename to CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Toolbar.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController.swift similarity index 100% rename from CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift rename to CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowControllerExtensions.swift similarity index 100% rename from CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift rename to CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowControllerExtensions.swift diff --git a/CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Workspace/Window/Controllers/NotificationPanelViewModel+Toolbar.swift similarity index 100% rename from CodeEdit/Features/Documents/Controllers/NotificationPanelViewModel+Toolbar.swift rename to CodeEdit/Features/Workspace/Window/Controllers/NotificationPanelViewModel+Toolbar.swift diff --git a/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarButton.swift b/CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarButton.swift similarity index 100% rename from CodeEdit/Features/Documents/Toolbar/StartTaskToolbarButton.swift rename to CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarButton.swift diff --git a/CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift b/CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarItem.swift similarity index 100% rename from CodeEdit/Features/Documents/Toolbar/StartTaskToolbarItem.swift rename to CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarItem.swift diff --git a/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarButton.swift b/CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarButton.swift similarity index 100% rename from CodeEdit/Features/Documents/Toolbar/StopTaskToolbarButton.swift rename to CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarButton.swift diff --git a/CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift b/CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarItem.swift similarity index 100% rename from CodeEdit/Features/Documents/Toolbar/StopTaskToolbarItem.swift rename to CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarItem.swift diff --git a/CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelTabBar.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelTabBar.swift rename to CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelTabBar.swift diff --git a/CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelView.swift b/CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelView.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspacePanel/WorkspacePanelView.swift rename to CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelView.swift diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift b/CodeEdit/Features/Workspace/WorkspaceStatePersistence.swift similarity index 100% rename from CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceStatePersistence.swift rename to CodeEdit/Features/Workspace/WorkspaceStatePersistence.swift From e053c8471a6150a67a7ec6f03775474a4e01474e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 15:16:11 +0200 Subject: [PATCH 165/335] Refactor: Rename DocumentsUnitTests to WorkspaceWindowTests The class exercises the split view controller / window setup, not documents; it joins the other Workspace tests. Search/indexer tests stay in the test Documents/ folder (separate cleanup). --- .../Mocks/NSHapticFeedbackPerformerMock.swift | 0 .../WorkspaceWindowTests.swift} | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename CodeEditTests/Features/{Documents => Workspace}/Mocks/NSHapticFeedbackPerformerMock.swift (100%) rename CodeEditTests/Features/{Documents/DocumentsUnitTests.swift => Workspace/WorkspaceWindowTests.swift} (98%) diff --git a/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift b/CodeEditTests/Features/Workspace/Mocks/NSHapticFeedbackPerformerMock.swift similarity index 100% rename from CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift rename to CodeEditTests/Features/Workspace/Mocks/NSHapticFeedbackPerformerMock.swift diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Workspace/WorkspaceWindowTests.swift similarity index 98% rename from CodeEditTests/Features/Documents/DocumentsUnitTests.swift rename to CodeEditTests/Features/Workspace/WorkspaceWindowTests.swift index 4524c6b58a..0fbe52ae76 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Workspace/WorkspaceWindowTests.swift @@ -1,5 +1,5 @@ // -// DocumentsUnitTests.swift +// WorkspaceWindowTests.swift // CodeEditTests // // Created by YAPRYNTSEV Aleksey on 31.12.2022. @@ -15,7 +15,7 @@ import CETerminal @testable import CodeEdit @MainActor -final class DocumentsUnitTests: XCTestCase { +final class WorkspaceWindowTests: XCTestCase { // Properties private var splitViewController: CodeEditSplitViewController! private var hapticFeedbackPerformerMock: NSHapticFeedbackPerformerMock! From 4d6dcfa3c0d63b195979ca40ec12ce091e744792 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 15:26:28 +0200 Subject: [PATCH 166/335] Refactor: Move SoftwareUpdater to the app-shell root --- CodeEdit/{Features/Settings => }/SoftwareUpdater.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/{Features/Settings => }/SoftwareUpdater.swift (100%) diff --git a/CodeEdit/Features/Settings/SoftwareUpdater.swift b/CodeEdit/SoftwareUpdater.swift similarity index 100% rename from CodeEdit/Features/Settings/SoftwareUpdater.swift rename to CodeEdit/SoftwareUpdater.swift From 21fba345be615c03b254f03ecaf31e30148bba4d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:26:58 +0200 Subject: [PATCH 167/335] Refactor: Delete dead Utils extensions and their orphaned tests --- CodeEdit.xcodeproj/project.pbxproj | 25 -------- .../Services/WorkspaceWindowManager.swift | 20 +++++++ .../Extensions/Array/Array+SortURLs.swift | 21 ------- .../Extensions/NSWindow/NSWindow+Child.swift | 25 -------- .../String+AppearancesOfSubstring.swift | 39 ------------ .../Extensions/String/String+Character.swift | 21 ------- .../Extensions/String/String+Ranges.swift | 27 --------- .../String/String+RemoveOccurrences.swift | 23 ------- .../Extensions/String/String+SHA256.swift | 35 ----------- .../Extensions/URL/URL+componentCompare.swift | 36 ----------- .../Extensions/View/View+focusedValue.swift | 18 ------ .../ZipFoundation+ErrorDescrioption.swift | 60 ------------------- .../Utils/UnitTests_Extensions.swift | 36 ----------- 13 files changed, 20 insertions(+), 366 deletions(-) delete mode 100644 CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift delete mode 100644 CodeEdit/Utils/Extensions/NSWindow/NSWindow+Child.swift delete mode 100644 CodeEdit/Utils/Extensions/String/String+AppearancesOfSubstring.swift delete mode 100644 CodeEdit/Utils/Extensions/String/String+Character.swift delete mode 100644 CodeEdit/Utils/Extensions/String/String+Ranges.swift delete mode 100644 CodeEdit/Utils/Extensions/String/String+RemoveOccurrences.swift delete mode 100644 CodeEdit/Utils/Extensions/String/String+SHA256.swift delete mode 100644 CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift delete mode 100644 CodeEdit/Utils/Extensions/View/View+focusedValue.swift delete mode 100644 CodeEdit/Utils/Extensions/ZipFoundation/ZipFoundation+ErrorDescrioption.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 0139161724..2524f86aaf 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 283BDCBD2972EEBD002AFF81 /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 283BDCBC2972EEBD002AFF81 /* Package.resolved */; }; 284DC8512978BA2600BF2770 /* .all-contributorsrc in Resources */ = {isa = PBXBuildFile; fileRef = 284DC8502978BA2600BF2770 /* .all-contributorsrc */; }; 2BE487F428245162003F3F64 /* OpenWithCodeEdit.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 30818CB42D4E563900967860 /* ZIPFoundation */; }; 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; @@ -182,7 +181,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 302AD7FF2D8054D500231E16 /* ZIPFoundation in Frameworks */, 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */, 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, @@ -353,7 +351,6 @@ 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */, 6CB94D022CA1205100E8651C /* AsyncAlgorithms */, 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */, - 30818CB42D4E563900967860 /* ZIPFoundation */, 6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */, 5EACE6212DF4BF08005E08B8 /* WelcomeWindow */, 5E4485602DF600D9008BBE69 /* AboutWindow */, @@ -474,7 +471,6 @@ 303E88452C276FD100EEA8D9 /* XCRemoteSwiftPackageReference "LanguageClient" */, 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, - 30ED7B722DD299E600ACC922 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, 5EACE6202DF4BF08005E08B8 /* XCRemoteSwiftPackageReference "WelcomeWindow" */, 5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */, 6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, @@ -1738,14 +1734,6 @@ minimumVersion = 0.13.2; }; }; - 30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/weichsel/ZIPFoundation"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.19; - }; - }; 30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ChimeHQ/LanguageServerProtocol"; @@ -1762,14 +1750,6 @@ minimumVersion = 0.8.0; }; }; - 30ED7B722DD299E600ACC922 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/weichsel/ZIPFoundation"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.19; - }; - }; 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git"; @@ -1890,11 +1870,6 @@ package = 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */; productName = CodeEditSymbols; }; - 30818CB42D4E563900967860 /* ZIPFoundation */ = { - isa = XCSwiftPackageProductDependency; - package = 30818CB32D4E563900967860 /* XCRemoteSwiftPackageReference "ZIPFoundation" */; - productName = ZIPFoundation; - }; 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */ = { isa = XCSwiftPackageProductDependency; package = 30CB648F2C16CA8100CC8A9E /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */; diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index d75c21602c..5b6b7a5c60 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -203,3 +203,23 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { } } } + +extension URL { + /// Compares this url with another, counting the number of shared path components. Stops counting once a + /// different component is found. + /// + /// - Note: URL treats a leading `/` as a component, so `/Users` and `/` will return `1`. + /// - Parameter other: The URL to compare against. + /// - Returns: The number of shared components. + func sharedComponents(_ other: URL) -> Int { + var count = 0 + for (component, otherComponent) in zip(pathComponents, other.pathComponents) { + if component == otherComponent { + count += 1 + } else { + return count + } + } + return count + } +} diff --git a/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift b/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift deleted file mode 100644 index 823b92bd67..0000000000 --- a/CodeEdit/Utils/Extensions/Array/Array+SortURLs.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Array+FileSystem.FileItem.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 07/02/2023. -// - -import Foundation - -extension Array where Element: Hashable { - - /// Checks the difference between two given items. - /// - Parameter other: Other element - /// - Returns: symmetricDifference - func difference(from other: [Element]) -> [Element] { - let thisSet = Set(self) - let otherSet = Set(other) - return Array(thisSet.symmetricDifference(otherSet)) - } - -} diff --git a/CodeEdit/Utils/Extensions/NSWindow/NSWindow+Child.swift b/CodeEdit/Utils/Extensions/NSWindow/NSWindow+Child.swift deleted file mode 100644 index 2686fb13d3..0000000000 --- a/CodeEdit/Utils/Extensions/NSWindow/NSWindow+Child.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSWindow+Child.swift -// CodeEdit -// -// Created by Axel Martinez on 8/4/24. -// - -import AppKit - -extension NSWindow { - func addCenteredChildWindow(_ childWindow: NSWindow, over parentWindow: NSWindow) { - let parentFrame = parentWindow.frame - let parentCenterX = parentFrame.origin.x + (parentFrame.size.width / 2) - let parentCenterY = parentFrame.origin.y + (parentFrame.size.height / 2) - - let childWidth = childWindow.frame.size.width - let childHeight = childWindow.frame.size.height - let newChildOriginX = parentCenterX - (childWidth / 2) - let newChildOriginY = parentCenterY - (childHeight / 2) - - childWindow.setFrameOrigin(NSPoint(x: newChildOriginX, y: newChildOriginY)) - - parentWindow.addChildWindow(childWindow, ordered: .above) - } -} diff --git a/CodeEdit/Utils/Extensions/String/String+AppearancesOfSubstring.swift b/CodeEdit/Utils/Extensions/String/String+AppearancesOfSubstring.swift deleted file mode 100644 index c557a9df13..0000000000 --- a/CodeEdit/Utils/Extensions/String/String+AppearancesOfSubstring.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// String+AppearancesOfSubstring.swift -// CodeEdit -// -// Created by Tommy Ludwig on 24.11.23. -// - -import Foundation - -extension String { - /// Finds the appearances of a substring within the string. - /// - Parameters: - /// - substring: The substring to search for within the string. - /// - toLeft: The optional number of characters to include to the left of each found substring appearance. - /// - toRight: The optional number of characters to include to the right of each found substring appearance. - /// - /// - Returns: An array of ranges representing the appearances of the substring within the string. - func appearancesOfSubstring(substring: String, toLeft: Int=0, toRight: Int=0) -> [Range] { - guard !substring.isEmpty && self.contains(substring) else { return [] } - var appearances: [Range] = [] - for (index, character) in self.enumerated() where character == substring.first { - let startOfFoundCharacter = self.index(self.startIndex, offsetBy: index) - guard index + substring.count < self.count else { continue } - let lengthOfFoundCharacter = self.index(self.startIndex, offsetBy: (substring.count + index)) - if self[startOfFoundCharacter.. Character? { - guard index < self.count else { - return nil - } - - return self[self.index(self.startIndex, offsetBy: index)] - } -} diff --git a/CodeEdit/Utils/Extensions/String/String+Ranges.swift b/CodeEdit/Utils/Extensions/String/String+Ranges.swift deleted file mode 100644 index 26a84adcf8..0000000000 --- a/CodeEdit/Utils/Extensions/String/String+Ranges.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// String+Ranges.swift -// CodeEdit -// -// Created by Ziyuan Zhao on 2022/3/21. -// - -import Foundation - -extension StringProtocol where Index == String.Index { - func ranges( - of substring: T, - options: String.CompareOptions = [], - locale: Locale? = nil - ) -> [Range] { - var ranges: [Range] = [] - while let result = range( - of: substring, - options: options, - range: (ranges.last?.upperBound ?? startIndex).. String { - self.replacingOccurrences(of: "\n", with: "") - } - - /// Removes all `space` characters in a `String` - /// - Returns: A String - func removingSpaces() -> String { - self.replacingOccurrences(of: " ", with: "") - } -} diff --git a/CodeEdit/Utils/Extensions/String/String+SHA256.swift b/CodeEdit/Utils/Extensions/String/String+SHA256.swift deleted file mode 100644 index 89c368a032..0000000000 --- a/CodeEdit/Utils/Extensions/String/String+SHA256.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// String+SHA256.swift -// CodeEditModules/CodeEditUtils -// -// Created by Debdut Karmakar on 6/9/22. -// - -import Foundation -import CryptoKit - -extension String { - - /// Returns a SHA256 encrypted String of the input String - /// - /// - Parameters: - /// - trim: If `true` the input string will be trimmed from whitespaces and new-lines. Defaults to `false`. - /// - caseSensitive: If `false` the input string will be converted to lowercase characters. Defaults to `true`. - /// - Returns: A String in HEX format - func sha256(trim: Bool = false, caseSensitive: Bool = true) -> String { - var string = self - - // trim whitespaces & new lines if specified - if trim { string = string.trimmingCharacters(in: .whitespacesAndNewlines) } - - // make string lowercased if not case sensitive - if !caseSensitive { string = string.lowercased() } - - // compute the hash - // (note that `String.data(using: .utf8)!` is safe since it will never fail) - let computed = SHA256.hash(data: string.data(using: .utf8)!) - - // map the result to a hex string and return - return computed.compactMap { String(format: "%02x", $0) }.joined() - } -} diff --git a/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift b/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift deleted file mode 100644 index f5f7949e84..0000000000 --- a/CodeEdit/Utils/Extensions/URL/URL+componentCompare.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// URL+componentCompare.swift -// CodeEdit -// -// Created by Khan Winter on 10/22/24. -// - -import Foundation - -extension URL { - /// Compare a URL using its path components. - /// - Parameter other: The URL to compare to - /// - Returns: `true` if the URL points to the same path on disk. Regardless of query parameters, trailing - /// slashes, etc. - func componentCompare(_ other: URL) -> Bool { - return self.pathComponents == other.pathComponents - } - - /// Compares this url with another, counting the number of shared path components. Stops counting once a - /// different component is found. - /// - /// - Note: URL treats a leading `/` as a component, so `/Users` and `/` will return `1`. - /// - Parameter other: The URL to compare against. - /// - Returns: The number of shared components. - func sharedComponents(_ other: URL) -> Int { - var count = 0 - for (component, otherComponent) in zip(pathComponents, other.pathComponents) { - if component == otherComponent { - count += 1 - } else { - return count - } - } - return count - } -} diff --git a/CodeEdit/Utils/Extensions/View/View+focusedValue.swift b/CodeEdit/Utils/Extensions/View/View+focusedValue.swift deleted file mode 100644 index 8e46de9017..0000000000 --- a/CodeEdit/Utils/Extensions/View/View+focusedValue.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// View+focusedValue.swift -// CodeEdit -// -// Created by Wouter Hennen on 18/06/2023. -// - -import SwiftUI - -extension View { - func focusedValue( - _ keyPath: WritableKeyPath, - disabled: Bool, - _ value: Value - ) -> some View { - focusedValue(keyPath, disabled ? nil : value) - } -} diff --git a/CodeEdit/Utils/Extensions/ZipFoundation/ZipFoundation+ErrorDescrioption.swift b/CodeEdit/Utils/Extensions/ZipFoundation/ZipFoundation+ErrorDescrioption.swift deleted file mode 100644 index c9b76cb5f9..0000000000 --- a/CodeEdit/Utils/Extensions/ZipFoundation/ZipFoundation+ErrorDescrioption.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// ZipFoundation+ErrorDescrioption.swift -// CodeEdit -// -// Created by Khan Winter on 8/14/25. -// - -import Foundation -import ZIPFoundation - -extension Archive.ArchiveError: @retroactive LocalizedError { - public var errorDescription: String? { - switch self { - case .unreadableArchive: - "Unreadable archive." - case .unwritableArchive: - "Unwritable archive." - case .invalidEntryPath: - "Invalid entry path." - case .invalidCompressionMethod: - "Invalid compression method." - case .invalidCRC32: - "Invalid checksum." - case .cancelledOperation: - "Operation cancelled." - case .invalidBufferSize: - "Invalid buffer size." - case .invalidEntrySize: - "Invalid entry size." - case .invalidLocalHeaderDataOffset, - .invalidLocalHeaderSize, - .invalidCentralDirectoryOffset, - .invalidCentralDirectorySize, - .invalidCentralDirectoryEntryCount, - .missingEndOfCentralDirectoryRecord: - "Invalid file detected." - case .uncontainedSymlink: - "Uncontained symlink detected." - } - } - - public var failureReason: String? { - return switch self { - case .invalidLocalHeaderDataOffset: - "Invalid local header data offset." - case .invalidLocalHeaderSize: - "Invalid local header size." - case .invalidCentralDirectoryOffset: - "Invalid central directory offset." - case .invalidCentralDirectorySize: - "Invalid central directory size." - case .invalidCentralDirectoryEntryCount: - "Invalid central directory entry count." - case .missingEndOfCentralDirectoryRecord: - "Missing end of central directory record." - default: - nil - } - } -} diff --git a/CodeEditTests/Utils/UnitTests_Extensions.swift b/CodeEditTests/Utils/UnitTests_Extensions.swift index 26fd7e01c3..4cfbd2b2f3 100644 --- a/CodeEditTests/Utils/UnitTests_Extensions.swift +++ b/CodeEditTests/Utils/UnitTests_Extensions.swift @@ -99,42 +99,6 @@ final class CodeEditUtilsExtensionsUnitTests: XCTestCase { XCTAssertEqual(result, md5) } - // MARK: - STRING + SHA256 - - func testSHA256GenerationCaseSensitive() throws { - let testString = "CodeEdit" - let md5 = testString.sha256(caseSensitive: true) - - let result = "52125689c088f1783e53c48db78a4fe7b3fa10b12d8fba205fcf054e5ef3789a" - XCTAssertEqual(result, md5) - } - - func test256Generation() throws { - let testString = "CodeEdit" - let md5 = testString.sha256(caseSensitive: false) - - let result = "7c3f327eab3860fc823a99623b348afbf1d7aebaec5d21289fbaeab0f6340e4a" - XCTAssertEqual(result, md5) - } - - // MARK: - STRING + REMOVING OCCURRENCES - - func testRemovingNewLines() throws { - let string = "Hello, \nWorld!" - let withoutNewLines = string.removingNewLines() - - let result = "Hello, World!" - XCTAssertEqual(result, withoutNewLines) - } - - func testRemovingSpaces() throws { - let string = "Hello, World!" - let withoutSpaces = string.removingSpaces() - - let result = "Hello,World!" - XCTAssertEqual(result, withoutSpaces) - } - // MARK: - STRING + VALID FILE NAME func testValidFileName() { From 5ae5ff26e117da46b0762567653d26bd0d60f4c5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:31:14 +0200 Subject: [PATCH 168/335] Refactor: Promote URL helpers to CodeEditCore and CECircularProgressView to CodeEditUI --- .../TaskNotificationsDetailView.swift | 1 + .../Extensions/LanguageServerRowView.swift | 1 + .../View/UtilityAreaOutputSourcePicker.swift | 1 + .../Extensions/URL/URL+absolutePath.swift | 14 ----------- .../Restoration/EditorStateRestoration.swift | 1 + .../Sources/CELSP/Service/LSPService.swift | 1 + .../Views/CEActiveTaskTerminalView.swift | 1 + .../Views/CELocalShellTerminalView.swift | 1 + .../Domain/Workspace/CEWorkspaceFile.swift | 23 +++---------------- .../Extensions/URL+AbsolutePath.swift | 6 ++--- .../Extensions/URL+ResourceValues.swift | 6 ++--- .../CodeEditDocument/URL+AbsolutePath.swift | 15 ------------ .../Views}/CECircularProgressView.swift | 11 ++++++--- 13 files changed, 24 insertions(+), 58 deletions(-) delete mode 100644 CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift rename Packages/{Features/CETerminal/Sources/CETerminal/TerminalEmulator => Foundation/CodeEditCore/Sources/CodeEditCore}/Extensions/URL+AbsolutePath.swift (67%) rename CodeEdit/Utils/Extensions/URL/URL+ResouceValues.swift => Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift (88%) delete mode 100644 Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift rename {CodeEdit/Features/ActivityViewer/Notifications => Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views}/CECircularProgressView.swift (89%) diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift index bbc97de62e..0b6ee5ec89 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift @@ -5,6 +5,7 @@ // Created by Tommy Ludwig on 21.06.24. // +import CodeEditUI import CodeEditCore import SwiftUI diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift index a06ac00ca4..19120aa533 100644 --- a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift +++ b/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 2/2/25. // +import CodeEditUI import CELSP import SwiftUI import CodeEditSettings diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift index 6f24a2a8d8..65c6d20968 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 7/18/25. // +import CodeEditCore import CELSP import SwiftUI import Combine diff --git a/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift b/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift deleted file mode 100644 index 8270af913f..0000000000 --- a/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// URL+LanguageServer.swift -// CodeEdit -// -// Created by Khan Winter on 9/8/24. -// - -import Foundation - -extension URL { - var absolutePath: String { - absoluteURL.path(percentEncoded: false) - } -} diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift index 519560ef06..f0c9a3d33a 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 6/20/25. // +import CodeEditCore import Foundation import GRDB import CodeEditSourceEditor diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift index 074d038ddc..fe15cd88bd 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 2/7/24. // +import CodeEditCore import os.log import CodeEditSettings import CodeEditDocument diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift index 66da6b5d41..777f2aa35c 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 7/14/25. // +import CodeEditCore import AppKit import CodeEditSettings import SwiftTerm diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift index 2cc0393522..2960663cf4 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 8/7/24. // +import CodeEditCore import AppKit import CodeEditSettings @preconcurrency import SwiftTerm diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift index 26bab146a8..c3e58fd1b2 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift @@ -34,7 +34,7 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable /// Returns the resolved symlink url of this object. public lazy var resolvedURL: URL = { - Self.isSymbolicLink(url) ? url.resolvingSymlinksInPath() : url + url.isSymbolicLink ? url.resolvingSymlinksInPath() : url }() /// Returns a parent ``CEWorkspaceFile``. `nil` for the top-level item. @@ -50,7 +50,7 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable /// True if the resource is a directory. public lazy var isFolder: Bool = { - Self.isDirectory(resolvedURL) + resolvedURL.isFolder }() /// True if this directory has no contents. (Check ``isFolder`` first.) @@ -69,7 +69,7 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable public var doesExist: Bool { Self.fileManager.fileExists(atPath: self.url.path) } /// The file's UTType. - public var contentType: UTType? { Self.contentType(url) } + public var contentType: UTType? { url.contentType } public init( id: String, @@ -129,23 +129,6 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable /// `FileManager.default` is documented thread-safe; the shared instance is only read from here. nonisolated(unsafe) public static let fileManager = FileManager.default - private static func resourceValues(_ url: URL) -> URLResourceValues? { - try? url.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey, .contentTypeKey]) - } - - private static func isDirectory(_ url: URL) -> Bool { - resourceValues(url)?.isDirectory ?? false - } - - private static func isSymbolicLink(_ url: URL) -> Bool { - let values = resourceValues(url) - return (values?.isSymbolicLink ?? false) || (values?.contentType ?? .item) == .aliasFile - } - - private static func contentType(_ url: URL) -> UTType? { - resourceValues(url)?.contentType - } - // MARK: Comparable / Hashable public static func == (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift similarity index 67% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift index 9a07881bf9..b8f4d082a4 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/URL+AbsolutePath.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift @@ -1,13 +1,13 @@ // // URL+AbsolutePath.swift -// CETerminal +// CodeEditCore // -// Created by Matthijs Eikelenboom on 13/07/2026. +// Created by Matthijs Eikelenboom on 14/07/2026. // import Foundation -extension URL { +public extension URL { /// The non-percent-encoded absolute path. var absolutePath: String { absoluteURL.path(percentEncoded: false) diff --git a/CodeEdit/Utils/Extensions/URL/URL+ResouceValues.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift similarity index 88% rename from CodeEdit/Utils/Extensions/URL/URL+ResouceValues.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift index 94285d567b..58ab492716 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+ResouceValues.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift @@ -1,6 +1,6 @@ // -// URL+ResouceValues.swift -// CodeEdit +// URL+ResourceValues.swift +// CodeEditCore // // Created by Axel Martinez on 27/6/24. // @@ -8,7 +8,7 @@ import Foundation import UniformTypeIdentifiers -extension URL { +public extension URL { fileprivate var resourceValues: URLResourceValues? { try? self.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey, .contentTypeKey]) } diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift deleted file mode 100644 index 6fa5b6473f..0000000000 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/URL+AbsolutePath.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// URL+AbsolutePath.swift -// CodeEditDocument -// -// Created by Matthijs Eikelenboom. -// - -import Foundation - -extension URL { - /// The non-percent-encoded absolute path. - public var absolutePath: String { - absoluteURL.path(percentEncoded: false) - } -} diff --git a/CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CECircularProgressView.swift similarity index 89% rename from CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift rename to Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CECircularProgressView.swift index e6580f36d0..d3d37f6637 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CECircularProgressView.swift @@ -1,13 +1,13 @@ // // CECircularProgressView.swift -// CodeEdit +// CodeEditUI // // Created by Tommy Ludwig on 21.06.24. // import SwiftUI -struct CECircularProgressView: View { +public struct CECircularProgressView: View { @State private var isAnimating = false @State private var previousValue: Bool = false @@ -16,7 +16,12 @@ struct CECircularProgressView: View { let lineWidth: CGFloat = 2 - var body: some View { + public init(progress: Double? = nil, currentTaskCount: Int = 1) { + self.progress = progress + self.currentTaskCount = currentTaskCount + } + + public var body: some View { Circle() .stroke(style: StrokeStyle(lineWidth: lineWidth)) .foregroundStyle(.tertiary) From 1c6e53aae3c06765ffea6daf8452417fea520763 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:33:20 +0200 Subject: [PATCH 169/335] Refactor: Own a single SoftwareUpdater in AppDependencies --- CodeEdit/AppDelegate.swift | 3 +-- CodeEdit/AppDependencies.swift | 5 +++++ CodeEdit/CodeEditApp.swift | 4 +--- CodeEdit/Features/Settings/SettingsWindow.swift | 6 +++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index aab9042d0d..0ca63dd54f 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -18,7 +18,6 @@ import OSLog @MainActor final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "AppDelegate") - private let updater = SoftwareUpdater() @Environment(\.openWindow) var openWindow @@ -210,7 +209,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } @IBAction private func checkForUpdates(_ sender: Any) { - updater.checkForUpdates() + dependencies.softwareUpdater.checkForUpdates() } /// Tries to focus a window with specified view content type. diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/AppDependencies.swift index 24aa1bd344..c97b5b8dae 100644 --- a/CodeEdit/AppDependencies.swift +++ b/CodeEdit/AppDependencies.swift @@ -35,6 +35,11 @@ final class AppDependencies { private(set) lazy var notificationManager: NotificationManaging = NotificationManager(eventBus: eventBus) + /// The single Sparkle updater controller. One instance app-wide: the "Check for + /// Updates" menu action and the Settings auto-update toggles must observe the same + /// `SPUUpdater`, or their state drifts apart. + private(set) lazy var softwareUpdater = SoftwareUpdater() + private(set) lazy var lspService: LSPService = { let service = LSPService() // Property-injected (not init-injected): the window manager's construction consumes diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index 31c886d183..fc46c01c47 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -17,8 +17,6 @@ struct CodeEditApp: App { @NSApplicationDelegateAdaptor var appdelegate: AppDelegate @ObservedObject var settings = Settings.shared - let updater: SoftwareUpdater = SoftwareUpdater() - init() { NSMenuItem.swizzle() NSSplitViewItem.swizzle() @@ -83,7 +81,7 @@ struct CodeEditApp: App { footer: { AboutFooterView() } ) - SettingsWindow() + SettingsWindow(updater: appdelegate.dependencies.softwareUpdater) .commands { CodeEditCommands(dependencies: appdelegate.dependencies) } diff --git a/CodeEdit/Features/Settings/SettingsWindow.swift b/CodeEdit/Features/Settings/SettingsWindow.swift index ab34f579bb..011e627f97 100644 --- a/CodeEdit/Features/Settings/SettingsWindow.swift +++ b/CodeEdit/Features/Settings/SettingsWindow.swift @@ -8,7 +8,11 @@ import SwiftUI struct SettingsWindow: Scene { - private let updater = SoftwareUpdater() + private let updater: SoftwareUpdater + + init(updater: SoftwareUpdater) { + self.updater = updater + } var body: some Scene { Window("Settings", id: SceneID.settings.rawValue) { From 1c16caf28be2b146e9ff8b74b3055f62822c9f14 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:35:39 +0200 Subject: [PATCH 170/335] Refactor: Rename app UseCases to doer-style role nouns --- CodeEdit/AppDelegate.swift | 6 +++--- ...dFilesUseCase.swift => FileDropHandler.swift} | 4 ++-- .../{MoveFileUseCase.swift => FileMover.swift} | 4 ++-- ...rViewController+NSOutlineViewDataSource.swift | 4 ++-- ...Controller+OutlineTableViewCellDelegate.swift | 4 ++-- .../Workspace/Services/AppFileRelocator.swift | 4 ++-- .../Services/WorkspaceWindowManager.swift | 16 ++++++++-------- ...wift => ApplicationShutdownCoordinator.swift} | 4 ++-- ...ocumentUseCase.swift => DocumentOpener.swift} | 4 ++-- ...kspaceUseCase.swift => WorkspaceCloser.swift} | 4 ++-- ...kspaceUseCase.swift => WorkspaceOpener.swift} | 4 ++-- ...orStateUseCase.swift => EditorRestorer.swift} | 0 ...itoryUseCase.swift => RepositoryCloner.swift} | 0 13 files changed, 29 insertions(+), 29 deletions(-) rename CodeEdit/Features/CEWorkspace/UseCases/{AcceptDroppedFilesUseCase.swift => FileDropHandler.swift} (96%) rename CodeEdit/Features/CEWorkspace/UseCases/{MoveFileUseCase.swift => FileMover.swift} (94%) rename CodeEdit/Features/Workspace/UseCases/{ShutdownApplicationUseCase.swift => ApplicationShutdownCoordinator.swift} (96%) rename CodeEdit/Features/Workspace/UseCases/{OpenDocumentUseCase.swift => DocumentOpener.swift} (95%) rename CodeEdit/Features/Workspace/UseCases/{CloseWorkspaceUseCase.swift => WorkspaceCloser.swift} (87%) rename CodeEdit/Features/Workspace/UseCases/{OpenWorkspaceUseCase.swift => WorkspaceOpener.swift} (96%) rename Packages/Features/CEEditor/Sources/CEEditor/UseCases/{RestoreEditorStateUseCase.swift => EditorRestorer.swift} (100%) rename Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/{CloneRepositoryUseCase.swift => RepositoryCloner.swift} (100%) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 0ca63dd54f..81a91864f4 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -28,7 +28,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { var windowManager: any WorkspaceWindowManaging { dependencies.workspaceWindowManager } var eventBus: EventBus { dependencies.eventBus } - private lazy var shutdownUseCase = ShutdownApplicationUseCase( + private lazy var shutdownCoordinator = ApplicationShutdownCoordinator( windowManager: dependencies.workspaceWindowManager, eventBus: dependencies.eventBus ) @@ -78,7 +78,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { if needToHandleOpen { // Reopen the workspaces that were open at last quit (saved by - // ShutdownApplicationUseCase). Workspace windows are plain NSWindows, + // ApplicationShutdownCoordinator). Workspace windows are plain NSWindows, // so NSQuitAlwaysKeepsWindows cannot restore them itself. var restoredWorkspace = false if let projects = UserDefaults.standard.array( @@ -180,7 +180,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { /// /// All paths _must_ call `NSApplication.shared.reply(toApplicationShouldTerminate: true)` as soon as possible. func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - guard shutdownUseCase.execute() else { + guard shutdownCoordinator.execute() else { return .terminateCancel } diff --git a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/FileDropHandler.swift similarity index 96% rename from CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift rename to CodeEdit/Features/CEWorkspace/UseCases/FileDropHandler.swift index ce8900c451..4b8606f1d0 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/AcceptDroppedFilesUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/FileDropHandler.swift @@ -1,5 +1,5 @@ // -// AcceptDroppedFilesUseCase.swift +// FileDropHandler.swift // CodeEdit // // Created by Matthijs Eikelenboom on 15/04/26. @@ -11,7 +11,7 @@ import CodeEditCore /// Resolves dropped file URLs into copy/move operations, handling source resolution and replace conflicts. @MainActor -final class AcceptDroppedFilesUseCase { +final class FileDropHandler { struct Operation { let source: CEWorkspaceFile diff --git a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift b/CodeEdit/Features/CEWorkspace/UseCases/FileMover.swift similarity index 94% rename from CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift rename to CodeEdit/Features/CEWorkspace/UseCases/FileMover.swift index 3373305499..1160c80028 100644 --- a/CodeEdit/Features/CEWorkspace/UseCases/MoveFileUseCase.swift +++ b/CodeEdit/Features/CEWorkspace/UseCases/FileMover.swift @@ -1,5 +1,5 @@ // -// MoveFileUseCase.swift +// FileMover.swift // CodeEdit // // Created by Matthijs Eikelenboom on 15/04/26. @@ -13,7 +13,7 @@ import CodeEditCore /// /// Returns the resolved new file (for non-folder moves) so the caller can update its UI. @MainActor -final class MoveFileUseCase { +final class FileMover { func execute(file: CEWorkspaceFile, to destination: URL, in workspace: Workspace) throws -> CEWorkspaceFile? { guard let newFile = try workspace.workspaceFileManager.move(file: file, to: destination) else { diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift index 7fab85a80a..45f42ba826 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift @@ -105,11 +105,11 @@ extension ProjectNavigatorViewController: NSOutlineViewDataSource { guard let fileItemDestination = item as? CEWorkspaceFile, let workspace else { return false } - let useCase = AcceptDroppedFilesUseCase() + let dropHandler = FileDropHandler() let isCopy = info.draggingSourceOperationMask == .copy do { - let operations = try useCase.execute( + let operations = try dropHandler.execute( urls: fileItemURLS, destinationParent: fileItemDestination, isCopyOperation: isCopy, diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index ed65e381b1..1f290d073d 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -16,8 +16,8 @@ extension ProjectNavigatorViewController: OutlineTableViewCellDelegate { func moveFile(file: CEWorkspaceFile, to destination: URL) { guard let workspace else { return } do { - let useCase = MoveFileUseCase() - _ = try useCase.execute(file: file, to: destination, in: workspace) + let fileMover = FileMover() + _ = try fileMover.execute(file: file, to: destination, in: workspace) outlineView.reloadItem(file.parent, reloadChildren: true) } catch { let alert = NSAlert(error: error) diff --git a/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift b/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift index a683e167f9..2730c83bb9 100644 --- a/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift +++ b/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift @@ -10,7 +10,7 @@ import CodeEditCore import CEWorkspaceFileManager /// App-shell binding of the `FileRelocator` command. Resolves the workspace that -/// owns the file and delegates to `MoveFileUseCase`, which moves the file and +/// owns the file and delegates to `FileMover`, which moves the file and /// reconciles open tabs. final class AppFileRelocator: FileRelocator { private let windowManager: WorkspaceWindowManaging @@ -22,6 +22,6 @@ final class AppFileRelocator: FileRelocator { @MainActor func relocate(file: CEWorkspaceFile, to destination: URL) throws -> CEWorkspaceFile? { guard let workspace = windowManager.workspace(containing: file.url) else { return nil } - return try MoveFileUseCase().execute(file: file, to: destination, in: workspace) + return try FileMover().execute(file: file, to: destination, in: workspace) } } diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift index 5b6b7a5c60..6f6b720b13 100644 --- a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift +++ b/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift @@ -20,14 +20,14 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { private let dependencies: AppDependencies private var eventBus: EventBus { dependencies.eventBus } - private let openWorkspaceUseCase: OpenWorkspaceUseCase - private let closeWorkspaceUseCase: CloseWorkspaceUseCase - private lazy var openDocumentUseCase = OpenDocumentUseCase(windowManager: self) + private let workspaceOpener: WorkspaceOpener + private let workspaceCloser: WorkspaceCloser + private lazy var documentOpener = DocumentOpener(windowManager: self) init(dependencies: AppDependencies) { self.dependencies = dependencies - self.openWorkspaceUseCase = OpenWorkspaceUseCase(dependencies: dependencies) - self.closeWorkspaceUseCase = CloseWorkspaceUseCase(lspService: dependencies.lspService) + self.workspaceOpener = WorkspaceOpener(dependencies: dependencies) + self.workspaceCloser = WorkspaceCloser(lspService: dependencies.lspService) } /// All currently open workspaces. @@ -45,7 +45,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { return } - let result = openWorkspaceUseCase.execute(url: url) + let result = workspaceOpener.execute(url: url) openWorkspaces.append(result.workspace) windowControllers[ObjectIdentifier(result.workspace)] = result.windowController @@ -64,7 +64,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { // MARK: - Close Workspace func closeWorkspace(_ workspace: Workspace) { - closeWorkspaceUseCase.execute(workspace: workspace) + workspaceCloser.execute(workspace: workspace) let id = ObjectIdentifier(workspace) windowControllers.removeValue(forKey: id) @@ -150,7 +150,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { /// Opens a workspace or file at the given URL, calling the completion handler on success. func openDocument(at url: URL, onCompletion: @escaping () -> Void) { - openDocumentUseCase.execute(url: url, onCompletion: onCompletion) + documentOpener.execute(url: url, onCompletion: onCompletion) } /// Opens a dialog to choose a file or folder, with optional configuration. diff --git a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift b/CodeEdit/Features/Workspace/UseCases/ApplicationShutdownCoordinator.swift similarity index 96% rename from CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift rename to CodeEdit/Features/Workspace/UseCases/ApplicationShutdownCoordinator.swift index 2a565dc881..adee24a129 100644 --- a/CodeEdit/Features/Workspace/UseCases/ShutdownApplicationUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/ApplicationShutdownCoordinator.swift @@ -1,5 +1,5 @@ // -// ShutdownApplicationUseCase.swift +// ApplicationShutdownCoordinator.swift // CodeEdit // // Created by Matthijs Eikelenboom on 12/04/26. @@ -15,7 +15,7 @@ import CodeEditCore /// Language server shutdown is handled separately by the caller (AppDelegate) /// since it's async and tied to the NSApplication reply lifecycle. @MainActor -final class ShutdownApplicationUseCase { +final class ApplicationShutdownCoordinator { private let windowManager: WorkspaceWindowManaging private let eventBus: EventBus diff --git a/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift b/CodeEdit/Features/Workspace/UseCases/DocumentOpener.swift similarity index 95% rename from CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift rename to CodeEdit/Features/Workspace/UseCases/DocumentOpener.swift index bddc0e4929..51b501f1c9 100644 --- a/CodeEdit/Features/Workspace/UseCases/OpenDocumentUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/DocumentOpener.swift @@ -1,5 +1,5 @@ // -// OpenDocumentUseCase.swift +// DocumentOpener.swift // CodeEdit // // Created by Matthijs Eikelenboom on 15/04/26. @@ -11,7 +11,7 @@ import WelcomeWindow /// Routes a URL to the appropriate opener: a workspace (folder), an existing workspace's file, /// or a standalone document via NSDocumentController. @MainActor -final class OpenDocumentUseCase { +final class DocumentOpener { private let windowManager: WorkspaceWindowManaging init(windowManager: WorkspaceWindowManaging) { diff --git a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/WorkspaceCloser.swift similarity index 87% rename from CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift rename to CodeEdit/Features/Workspace/UseCases/WorkspaceCloser.swift index 5ac977bb39..e052c865d2 100644 --- a/CodeEdit/Features/Workspace/UseCases/CloseWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/WorkspaceCloser.swift @@ -1,5 +1,5 @@ // -// CloseWorkspaceUseCase.swift +// WorkspaceCloser.swift // CodeEdit // // Created by Matthijs Eikelenboom on 12/04/26. @@ -10,7 +10,7 @@ import Foundation /// Coordinates cleanup when a workspace is closed (LSP shutdown + workspace teardown). @MainActor -final class CloseWorkspaceUseCase { +final class WorkspaceCloser { private let lspService: any LSPServiceProtocol diff --git a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift b/CodeEdit/Features/Workspace/UseCases/WorkspaceOpener.swift similarity index 96% rename from CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift rename to CodeEdit/Features/Workspace/UseCases/WorkspaceOpener.swift index 6487a37622..a237f4bdff 100644 --- a/CodeEdit/Features/Workspace/UseCases/OpenWorkspaceUseCase.swift +++ b/CodeEdit/Features/Workspace/UseCases/WorkspaceOpener.swift @@ -1,5 +1,5 @@ // -// OpenWorkspaceUseCase.swift +// WorkspaceOpener.swift // CodeEdit // // Created by Matthijs Eikelenboom on 12/04/26. @@ -9,7 +9,7 @@ import AppKit /// Creates and configures a workspace, window, and window controller for a given URL. @MainActor -final class OpenWorkspaceUseCase { +final class WorkspaceOpener { private let dependencies: AppDependencies init(dependencies: AppDependencies) { diff --git a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/UseCases/RestoreEditorStateUseCase.swift rename to Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/CloneRepositoryUseCase.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/CloneRepositoryUseCase.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift From bc86ce166836ff73d8a53ec477644ba45a2e3196 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:35:39 +0200 Subject: [PATCH 171/335] Refactor: Rename package UseCases to doer-style role nouns --- .../EditorLayout/EditorLayout+StateRestoration.swift | 4 ++-- .../Sources/CEEditor/UseCases/EditorRestorer.swift | 6 +++--- .../Clone/ViewModels/GitCloneViewModel.swift | 12 ++++++------ .../CESourceControl/UseCases/RepositoryCloner.swift | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift index b21a0e556d..b28eccafb5 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -30,8 +30,8 @@ extension EditorManager { } } - let useCase = RestoreEditorStateUseCase() - switch useCase.execute( + let restorer = EditorRestorer() + switch restorer.execute( statePersistence: statePersistence, fileManager: fileManager, findReplaceQuery: findReplaceQuery, diff --git a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift index d09a4d623c..365e444c63 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift @@ -1,5 +1,5 @@ // -// RestoreEditorStateUseCase.swift +// EditorRestorer.swift // CodeEdit // // Created by Matthijs Eikelenboom on 15/04/26. @@ -11,7 +11,7 @@ import OSLog import OrderedCollections /// Restores an editor layout from persisted state, resolving file references against the current file manager. -public final class RestoreEditorStateUseCase { +public final class EditorRestorer { public enum Outcome { /// Persisted state was loaded and resolved successfully. @@ -22,7 +22,7 @@ public final class RestoreEditorStateUseCase { case noChange } - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RestoreEditorStateUseCase") + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "EditorRestorer") /// Decodes persisted editor state, validates it, and resolves file references. public func execute( diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift index 4a5ad673dc..e712d3976d 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift @@ -18,11 +18,11 @@ class GitCloneViewModel: ObservableObject { var cloningTask: Task? let shellClient: ShellClientProtocol - private let useCase: CloneRepositoryUseCase + private let cloner: RepositoryCloner init(shellClient: ShellClientProtocol) { self.shellClient = shellClient - self.useCase = CloneRepositoryUseCase(shellClient: shellClient) + self.cloner = RepositoryCloner(shellClient: shellClient) } /// Check if url is valid @@ -53,7 +53,7 @@ class GitCloneViewModel: ObservableObject { /// Clone repository func cloneRepository(completionHandler: @escaping (URL) -> Void) { do { - try useCase.verifyGitInstalled() + try cloner.verifyGitInstalled() } catch { showAlert(alertMsg: "Git installation not found.", infoText: error.localizedDescription) return @@ -61,7 +61,7 @@ class GitCloneViewModel: ObservableObject { let parsed: (remoteUrl: URL, suggestedName: String) do { - parsed = try useCase.parse(repoUrl: repoUrlStr) + parsed = try cloner.parse(repoUrl: repoUrlStr) } catch { showAlert(alertMsg: "Invalid URL", infoText: error.localizedDescription) return @@ -73,7 +73,7 @@ class GitCloneViewModel: ObservableObject { let progressStream: AsyncThrowingMapSequence do { - progressStream = try useCase.execute(remoteUrl: parsed.remoteUrl, localPath: localPath) + progressStream = try cloner.execute(remoteUrl: parsed.remoteUrl, localPath: localPath) } catch { showAlert(alertMsg: "Failed to clone", infoText: error.localizedDescription) return @@ -120,7 +120,7 @@ class GitCloneViewModel: ObservableObject { private func deleteTemporaryFolder(localPath: URL) { do { - try useCase.cleanup(localPath: localPath) + try cloner.cleanup(localPath: localPath) } catch { showAlert(alertMsg: "Failed to delete folder", infoText: "\(error)") } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift index ec8b56a8eb..1a2ac57c6e 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift @@ -1,5 +1,5 @@ // -// CloneRepositoryUseCase.swift +// RepositoryCloner.swift // CodeEdit // // Created by Matthijs Eikelenboom on 15/04/26. @@ -9,7 +9,7 @@ import CodeEditCore import Foundation /// Validates and orchestrates a `git clone` operation, streaming progress to the caller. -final class CloneRepositoryUseCase { +final class RepositoryCloner { private let shellClient: ShellClientProtocol init(shellClient: ShellClientProtocol) { From bf2df63ff2fcf4733e689735728175bb6b656462 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:37:38 +0200 Subject: [PATCH 172/335] Refactor: Fold CEWorkspace into Workspace and restructure by purpose --- .../{Features/Workspace/Models => }/Environment+AppCommands.swift | 0 .../{Services => Adapters}/AppCodeFileDocumentDelegate.swift | 0 .../Workspace/{Services => Adapters}/AppErrorNotifier.swift | 0 .../Workspace/{Services => Adapters}/AppFileRelocator.swift | 0 .../Workspace/{Services => Adapters}/AppWorkspaceFileOpener.swift | 0 .../Workspace/{Services => Adapters}/AppWorkspaceNavigator.swift | 0 .../Features/Workspace/{Models => }/Environment+Workspace.swift | 0 .../Models => Workspace/Files}/CEWorkspaceFile+Presentation.swift | 0 .../Models => Workspace/Files}/CEWorkspaceFileIcon.swift | 0 .../UseCases => Workspace/Files}/FileDropHandler.swift | 0 .../{CEWorkspace/UseCases => Workspace/Files}/FileMover.swift | 0 .../Window/{Controllers => }/CodeEditSplitViewController.swift | 0 .../{Controllers => }/CodeEditWindowController+Panels.swift | 0 .../{Controllers => }/CodeEditWindowController+Toolbar.swift | 0 .../Window/{Controllers => }/CodeEditWindowController.swift | 0 .../{Controllers => }/CodeEditWindowControllerExtensions.swift | 0 .../{Controllers => }/NotificationPanelViewModel+Toolbar.swift | 0 .../ApplicationShutdownCoordinator.swift | 0 .../Workspace/{UseCases => WindowManagement}/DocumentOpener.swift | 0 .../{UseCases => WindowManagement}/WorkspaceCloser.swift | 0 .../{UseCases => WindowManagement}/WorkspaceOpener.swift | 0 .../{Services => WindowManagement}/WorkspaceWindowManager.swift | 0 .../{Protocols => WindowManagement}/WorkspaceWindowManaging.swift | 0 CodeEdit/Features/Workspace/{Models => }/Workspace.swift | 0 .../Workspace/{Models => }/WorkspaceNotificationModel.swift | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/{Features/Workspace/Models => }/Environment+AppCommands.swift (100%) rename CodeEdit/Features/Workspace/{Services => Adapters}/AppCodeFileDocumentDelegate.swift (100%) rename CodeEdit/Features/Workspace/{Services => Adapters}/AppErrorNotifier.swift (100%) rename CodeEdit/Features/Workspace/{Services => Adapters}/AppFileRelocator.swift (100%) rename CodeEdit/Features/Workspace/{Services => Adapters}/AppWorkspaceFileOpener.swift (100%) rename CodeEdit/Features/Workspace/{Services => Adapters}/AppWorkspaceNavigator.swift (100%) rename CodeEdit/Features/Workspace/{Models => }/Environment+Workspace.swift (100%) rename CodeEdit/Features/{CEWorkspace/Models => Workspace/Files}/CEWorkspaceFile+Presentation.swift (100%) rename CodeEdit/Features/{CEWorkspace/Models => Workspace/Files}/CEWorkspaceFileIcon.swift (100%) rename CodeEdit/Features/{CEWorkspace/UseCases => Workspace/Files}/FileDropHandler.swift (100%) rename CodeEdit/Features/{CEWorkspace/UseCases => Workspace/Files}/FileMover.swift (100%) rename CodeEdit/Features/Workspace/Window/{Controllers => }/CodeEditSplitViewController.swift (100%) rename CodeEdit/Features/Workspace/Window/{Controllers => }/CodeEditWindowController+Panels.swift (100%) rename CodeEdit/Features/Workspace/Window/{Controllers => }/CodeEditWindowController+Toolbar.swift (100%) rename CodeEdit/Features/Workspace/Window/{Controllers => }/CodeEditWindowController.swift (100%) rename CodeEdit/Features/Workspace/Window/{Controllers => }/CodeEditWindowControllerExtensions.swift (100%) rename CodeEdit/Features/Workspace/Window/{Controllers => }/NotificationPanelViewModel+Toolbar.swift (100%) rename CodeEdit/Features/Workspace/{UseCases => WindowManagement}/ApplicationShutdownCoordinator.swift (100%) rename CodeEdit/Features/Workspace/{UseCases => WindowManagement}/DocumentOpener.swift (100%) rename CodeEdit/Features/Workspace/{UseCases => WindowManagement}/WorkspaceCloser.swift (100%) rename CodeEdit/Features/Workspace/{UseCases => WindowManagement}/WorkspaceOpener.swift (100%) rename CodeEdit/Features/Workspace/{Services => WindowManagement}/WorkspaceWindowManager.swift (100%) rename CodeEdit/Features/Workspace/{Protocols => WindowManagement}/WorkspaceWindowManaging.swift (100%) rename CodeEdit/Features/Workspace/{Models => }/Workspace.swift (100%) rename CodeEdit/Features/Workspace/{Models => }/WorkspaceNotificationModel.swift (100%) diff --git a/CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift b/CodeEdit/Environment+AppCommands.swift similarity index 100% rename from CodeEdit/Features/Workspace/Models/Environment+AppCommands.swift rename to CodeEdit/Environment+AppCommands.swift diff --git a/CodeEdit/Features/Workspace/Services/AppCodeFileDocumentDelegate.swift b/CodeEdit/Features/Workspace/Adapters/AppCodeFileDocumentDelegate.swift similarity index 100% rename from CodeEdit/Features/Workspace/Services/AppCodeFileDocumentDelegate.swift rename to CodeEdit/Features/Workspace/Adapters/AppCodeFileDocumentDelegate.swift diff --git a/CodeEdit/Features/Workspace/Services/AppErrorNotifier.swift b/CodeEdit/Features/Workspace/Adapters/AppErrorNotifier.swift similarity index 100% rename from CodeEdit/Features/Workspace/Services/AppErrorNotifier.swift rename to CodeEdit/Features/Workspace/Adapters/AppErrorNotifier.swift diff --git a/CodeEdit/Features/Workspace/Services/AppFileRelocator.swift b/CodeEdit/Features/Workspace/Adapters/AppFileRelocator.swift similarity index 100% rename from CodeEdit/Features/Workspace/Services/AppFileRelocator.swift rename to CodeEdit/Features/Workspace/Adapters/AppFileRelocator.swift diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift b/CodeEdit/Features/Workspace/Adapters/AppWorkspaceFileOpener.swift similarity index 100% rename from CodeEdit/Features/Workspace/Services/AppWorkspaceFileOpener.swift rename to CodeEdit/Features/Workspace/Adapters/AppWorkspaceFileOpener.swift diff --git a/CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift b/CodeEdit/Features/Workspace/Adapters/AppWorkspaceNavigator.swift similarity index 100% rename from CodeEdit/Features/Workspace/Services/AppWorkspaceNavigator.swift rename to CodeEdit/Features/Workspace/Adapters/AppWorkspaceNavigator.swift diff --git a/CodeEdit/Features/Workspace/Models/Environment+Workspace.swift b/CodeEdit/Features/Workspace/Environment+Workspace.swift similarity index 100% rename from CodeEdit/Features/Workspace/Models/Environment+Workspace.swift rename to CodeEdit/Features/Workspace/Environment+Workspace.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift b/CodeEdit/Features/Workspace/Files/CEWorkspaceFile+Presentation.swift similarity index 100% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile+Presentation.swift rename to CodeEdit/Features/Workspace/Files/CEWorkspaceFile+Presentation.swift diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift b/CodeEdit/Features/Workspace/Files/CEWorkspaceFileIcon.swift similarity index 100% rename from CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileIcon.swift rename to CodeEdit/Features/Workspace/Files/CEWorkspaceFileIcon.swift diff --git a/CodeEdit/Features/CEWorkspace/UseCases/FileDropHandler.swift b/CodeEdit/Features/Workspace/Files/FileDropHandler.swift similarity index 100% rename from CodeEdit/Features/CEWorkspace/UseCases/FileDropHandler.swift rename to CodeEdit/Features/Workspace/Files/FileDropHandler.swift diff --git a/CodeEdit/Features/CEWorkspace/UseCases/FileMover.swift b/CodeEdit/Features/Workspace/Files/FileMover.swift similarity index 100% rename from CodeEdit/Features/CEWorkspace/UseCases/FileMover.swift rename to CodeEdit/Features/Workspace/Files/FileMover.swift diff --git a/CodeEdit/Features/Workspace/Window/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Controllers/CodeEditSplitViewController.swift rename to CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift diff --git a/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Workspace/Window/CodeEditWindowController+Panels.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Panels.swift rename to CodeEdit/Features/Workspace/Window/CodeEditWindowController+Panels.swift diff --git a/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Workspace/Window/CodeEditWindowController+Toolbar.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController+Toolbar.swift rename to CodeEdit/Features/Workspace/Window/CodeEditWindowController+Toolbar.swift diff --git a/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Workspace/Window/CodeEditWindowController.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowController.swift rename to CodeEdit/Features/Workspace/Window/CodeEditWindowController.swift diff --git a/CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Workspace/Window/CodeEditWindowControllerExtensions.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Controllers/CodeEditWindowControllerExtensions.swift rename to CodeEdit/Features/Workspace/Window/CodeEditWindowControllerExtensions.swift diff --git a/CodeEdit/Features/Workspace/Window/Controllers/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/Features/Workspace/Window/NotificationPanelViewModel+Toolbar.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Controllers/NotificationPanelViewModel+Toolbar.swift rename to CodeEdit/Features/Workspace/Window/NotificationPanelViewModel+Toolbar.swift diff --git a/CodeEdit/Features/Workspace/UseCases/ApplicationShutdownCoordinator.swift b/CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift similarity index 100% rename from CodeEdit/Features/Workspace/UseCases/ApplicationShutdownCoordinator.swift rename to CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift diff --git a/CodeEdit/Features/Workspace/UseCases/DocumentOpener.swift b/CodeEdit/Features/Workspace/WindowManagement/DocumentOpener.swift similarity index 100% rename from CodeEdit/Features/Workspace/UseCases/DocumentOpener.swift rename to CodeEdit/Features/Workspace/WindowManagement/DocumentOpener.swift diff --git a/CodeEdit/Features/Workspace/UseCases/WorkspaceCloser.swift b/CodeEdit/Features/Workspace/WindowManagement/WorkspaceCloser.swift similarity index 100% rename from CodeEdit/Features/Workspace/UseCases/WorkspaceCloser.swift rename to CodeEdit/Features/Workspace/WindowManagement/WorkspaceCloser.swift diff --git a/CodeEdit/Features/Workspace/UseCases/WorkspaceOpener.swift b/CodeEdit/Features/Workspace/WindowManagement/WorkspaceOpener.swift similarity index 100% rename from CodeEdit/Features/Workspace/UseCases/WorkspaceOpener.swift rename to CodeEdit/Features/Workspace/WindowManagement/WorkspaceOpener.swift diff --git a/CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift b/CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManager.swift similarity index 100% rename from CodeEdit/Features/Workspace/Services/WorkspaceWindowManager.swift rename to CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManager.swift diff --git a/CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift b/CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManaging.swift similarity index 100% rename from CodeEdit/Features/Workspace/Protocols/WorkspaceWindowManaging.swift rename to CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManaging.swift diff --git a/CodeEdit/Features/Workspace/Models/Workspace.swift b/CodeEdit/Features/Workspace/Workspace.swift similarity index 100% rename from CodeEdit/Features/Workspace/Models/Workspace.swift rename to CodeEdit/Features/Workspace/Workspace.swift diff --git a/CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift b/CodeEdit/Features/Workspace/WorkspaceNotificationModel.swift similarity index 100% rename from CodeEdit/Features/Workspace/Models/WorkspaceNotificationModel.swift rename to CodeEdit/Features/Workspace/WorkspaceNotificationModel.swift From 6fb8266efc1063b93bcff265ea8f0e8b0670ec28 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 17:40:39 +0200 Subject: [PATCH 173/335] Refactor: Rationalize feature folders to purpose-first grouping --- .../Acknowledgements/{Views => }/AcknowledgementRowView.swift | 0 .../About/Acknowledgements/{Views => }/AcknowledgementsView.swift | 0 .../{ViewModels => }/AcknowledgementsViewModel.swift | 0 .../Acknowledgements/{Views => }/ParsePackagesResolved.swift | 0 .../Features/About/Contributors/{Model => }/Contributor.swift | 0 .../About}/OperatingSystemVersion+String.swift | 0 .../Features/CEWorkspaceSettings/{Views => }/AddCETaskView.swift | 0 .../Features/CEWorkspaceSettings/{Views => }/CETaskFormView.swift | 0 .../CEWorkspaceSettings+TasksConfigurationProviding.swift | 0 .../CEWorkspaceSettings/{Models => }/CEWorkspaceSettings.swift | 0 .../{Views => }/CEWorkspaceSettingsTaskListView.swift | 0 .../CEWorkspaceSettings/{Views => }/CEWorkspaceSettingsView.swift | 0 .../Features/CEWorkspaceSettings/{Views => }/EditCETaskView.swift | 0 .../{Views => }/EnvironmentVariableListItem.swift | 0 CodeEdit/Features/Commands/{Views => }/QuickActionsView.swift | 0 .../Commands/{ViewModels => }/QuickActionsViewModel.swift | 0 CodeEdit/Features/Feedback/{Model => }/FeedbackIssueArea.swift | 0 CodeEdit/Features/Feedback/{Model => }/FeedbackModel.swift | 0 CodeEdit/Features/Feedback/{HelperView => }/FeedbackToolbar.swift | 0 CodeEdit/Features/Feedback/{Model => }/FeedbackType.swift | 0 .../Feedback/{Controllers => }/FeedbackWindowController.swift | 0 .../Features/InspectorArea/{Views => }/InspectorAreaView.swift | 0 .../InspectorArea/{ViewModels => }/InspectorAreaViewModel.swift | 0 CodeEdit/Features/InspectorArea/{Views => }/InspectorField.swift | 0 .../Features/InspectorArea/{Views => }/InspectorSection.swift | 0 CodeEdit/Features/InspectorArea/{Models => }/InspectorTab.swift | 0 .../InspectorArea/{Views => }/NoSelectionInspectorView.swift | 0 .../Features/Keybindings/{Protocols => }/CommandManaging.swift | 0 .../Features/Keybindings/{Protocols => }/KeybindingManaging.swift | 0 .../Features/NavigatorArea/{Views => }/NavigatorAreaView.swift | 0 .../NavigatorArea/{ViewModels => }/NavigatorAreaViewModel.swift | 0 CodeEdit/Features/NavigatorArea/{Models => }/NavigatorTab.swift | 0 .../ProjectNavigator/{Models => }/ProjectNavigatorViewModel.swift | 0 .../{Views => }/SourceControlNavigatorChangesCommitView.swift | 0 .../Changes/{Views => }/SourceControlNavigatorChangesList.swift | 0 .../Changes/{Views => }/SourceControlNavigatorChangesView.swift | 0 .../Changes/{Views => }/SourceControlNavigatorNoRemotesView.swift | 0 .../Changes/{Views => }/SourceControlNavigatorSyncView.swift | 0 .../{Views/ChangedFile => }/GitChangedFileLabel.swift | 0 .../{Views/ChangedFile => }/GitChangedFileListView.swift | 0 .../History/{Views => }/CommitDetailsHeaderView.swift | 0 .../History/{Views => }/CommitDetailsView.swift | 0 .../History/{Views => }/CommitListItemView.swift | 0 .../History/{Views => }/SourceControlNavigatorHistoryView.swift | 0 .../SourceControlNavigator/History}/String+MD5.swift | 0 .../Repository/{Models => }/RepoOutlineGroupItem.swift | 0 .../{Views => }/SourceControlNavigatorRepositoryItem.swift | 0 .../SourceControlNavigatorRepositoryView+contextMenu.swift | 0 .../SourceControlNavigatorRepositoryView+outlineGroupData.swift | 0 .../{Views => }/SourceControlNavigatorRepositoryView.swift | 0 .../{Views => }/SourceControlNavigatorToolbarBottom.swift | 0 .../{Views => }/SourceControlNavigatorView.swift | 0 .../OpenQuickly/{Views => }/OpenQuicklyListItemView.swift | 0 .../Features/OpenQuickly/{Views => }/OpenQuicklyPreviewView.swift | 0 CodeEdit/Features/OpenQuickly/{Views => }/OpenQuicklyView.swift | 0 .../OpenQuickly/{ViewModels => }/OpenQuicklyViewModel.swift | 0 .../URL => Features/OpenQuickly}/URL+FuzzySearchable.swift | 0 .../URL => Features/OpenQuickly}/URL+Identifiable.swift | 0 CodeEdit/Features/Settings/{Views => }/ExternalLink.swift | 0 CodeEdit/Features/Settings/{Views => }/FontWeightPicker.swift | 0 CodeEdit/Features/Settings/{Views => }/GlobPatternList.swift | 0 CodeEdit/Features/Settings/{Views => }/GlobPatternListItem.swift | 0 .../Extensions/Int => Features/Settings}/Int+HexString.swift | 0 .../Settings/{Views => }/InvisibleCharacterWarningList.swift | 0 CodeEdit/Features/Settings/{Views => }/MonospacedFontPicker.swift | 0 CodeEdit/Features/Settings/{Models => }/PageAndSettings.swift | 0 .../Settings/Pages/AccountsSettings}/Font+Caption3.swift | 0 .../AccountsSettings/{Models => }/SourceControlAccount+Icon.swift | 0 .../Pages/LocationsSettings/{Models => }/LocationsSettings.swift | 0 .../Pages/SearchSettings/{Models => }/SearchSettingsModel.swift | 0 .../SourceControlSettings/{Models => }/IgnorePatternModel.swift | 0 .../Settings/Pages/SourceControlSettings}/Limiter.swift | 0 .../Pages/ThemeSettings/{Models => }/Theme+FuzzySearchable.swift | 0 .../Pages/ThemeSettings/{Models => }/ThemeModel+CRUD.swift | 0 .../Pages/ThemeSettings/{Models => }/ThemeModel+Export.swift | 0 .../Settings/Pages/ThemeSettings/{Models => }/ThemeModel.swift | 0 .../Pages/ThemeSettings/{Models => }/ThemeRepository.swift | 0 .../Features/Settings/{Models => }/SearchableSettingsPage.swift | 0 CodeEdit/Features/Settings/{Views => }/SettingsColorPicker.swift | 0 .../Settings/{Models => }/SettingsData+CommandRegistration.swift | 0 .../Settings/{Models => }/SettingsData+KeybindingReconcile.swift | 0 CodeEdit/Features/Settings/{Models => }/SettingsData+Search.swift | 0 CodeEdit/Features/Settings/{Views => }/SettingsForm.swift | 0 CodeEdit/Features/Settings/{Models => }/SettingsInjector.swift | 0 CodeEdit/Features/Settings/{Models => }/SettingsPage.swift | 0 CodeEdit/Features/Settings/{Views => }/SettingsPageView.swift | 0 .../Features/Settings/{Models => }/SettingsSearchResult.swift | 0 CodeEdit/Features/Settings/{Models => }/SettingsSidebarFix.swift | 0 .../Settings}/String+HighlightOccurrences.swift | 0 .../Settings/{Views => }/View+ConstrainHeightToWindow.swift | 0 .../Features/Settings/{Views => }/View+HideSidebarToggle.swift | 0 .../{Views => }/View+NavigationBarBackButtonVisible.swift | 0 .../Features/Settings/{Views => }/WarningCharactersView.swift | 0 CodeEdit/Features/StatusBar/{Models => }/ImageDimensions.swift | 0 CodeEdit/Features/StatusBar/{Views => }/StatusBarIcon.swift | 0 .../{Views => }/StatusBarItems/StatusBarBreakpointButton.swift | 0 .../{Views => }/StatusBarItems/StatusBarCursorPositionLabel.swift | 0 .../{Views => }/StatusBarItems/StatusBarEncodingSelector.swift | 0 .../{Views => }/StatusBarItems/StatusBarFileInfoView.swift | 0 .../{Views => }/StatusBarItems/StatusBarIndentSelector.swift | 0 .../{Views => }/StatusBarItems/StatusBarLineEndSelector.swift | 0 .../StatusBar/{Views => }/StatusBarItems/StatusBarMenuStyle.swift | 0 .../StatusBarItems/StatusBarToggleUtilityAreaButton.swift | 0 .../StatusBar/StatusBarItems}/View+isHovering.swift | 0 CodeEdit/Features/StatusBar/{Views => }/StatusBarView.swift | 0 .../Features/StatusBar/{ViewModels => }/StatusBarViewModel.swift | 0 .../{Model/Sources => }/ExtensionUtilityAreaOutputSource.swift | 0 .../{Model/Sources => }/InternalDevelopmentOutputSource.swift | 0 .../Sources => }/LanguageServerLogContainer+UtilityArea.swift | 0 .../OutputUtility/{Model => }/UtilityAreaLogLevel.swift | 0 .../OutputUtility/{View => }/UtilityAreaOutputLogList.swift | 0 .../OutputUtility/{Model => }/UtilityAreaOutputSource.swift | 0 .../OutputUtility/{View => }/UtilityAreaOutputSourcePicker.swift | 0 .../OutputUtility/{View => }/UtilityAreaOutputView.swift | 0 CodeEdit/Features/UtilityArea/{Views => }/PaneToolbar.swift | 0 .../{Models => TerminalUtility}/UtilityAreaTerminal.swift | 0 CodeEdit/Features/UtilityArea/{Models => }/UtilityAreaTab.swift | 0 .../Features/UtilityArea/{Views => }/UtilityAreaTabView.swift | 0 .../UtilityArea/{ViewModels => }/UtilityAreaTabViewModel.swift | 0 CodeEdit/Features/UtilityArea/{Views => }/UtilityAreaView.swift | 0 .../UtilityArea/{ViewModels => }/UtilityAreaViewModel.swift | 0 CodeEdit/Features/UtilityArea/{Views => }/View+paneToolbar.swift | 0 CodeEdit/Features/WindowCommands/{Utils => }/CommandsFixes.swift | 0 .../{Utils => }/FirstResponderPropertyWrapper.swift | 0 CodeEdit/{Utils => Features/WindowCommands}/FocusedValues.swift | 0 .../WindowCommands/{Utils => }/KeyWindowControllerObserver.swift | 0 .../Features/WindowCommands/{Utils => }/RecentProjectsMenu.swift | 0 .../{Utils => }/WindowControllerPropertyWrapper.swift | 0 .../{Utils/Extensions/NSApplication => }/NSApp+openWindow.swift | 0 CodeEdit/Utils/{Extensions/Bundle => }/Bundle+Info.swift | 0 CodeEdit/Utils/{Extensions/Date => }/Date+Formatted.swift | 0 .../{Extensions/NSTableView => }/NSTableView+Background.swift | 0 CodeEdit/{Utils => }/withTimeout.swift | 0 133 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/Features/About/Acknowledgements/{Views => }/AcknowledgementRowView.swift (100%) rename CodeEdit/Features/About/Acknowledgements/{Views => }/AcknowledgementsView.swift (100%) rename CodeEdit/Features/About/Acknowledgements/{ViewModels => }/AcknowledgementsViewModel.swift (100%) rename CodeEdit/Features/About/Acknowledgements/{Views => }/ParsePackagesResolved.swift (100%) rename CodeEdit/Features/About/Contributors/{Model => }/Contributor.swift (100%) rename CodeEdit/{Utils/Extensions/OperatingSystemVersion => Features/About}/OperatingSystemVersion+String.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Views => }/AddCETaskView.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Views => }/CETaskFormView.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Models => }/CEWorkspaceSettings+TasksConfigurationProviding.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Models => }/CEWorkspaceSettings.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Views => }/CEWorkspaceSettingsTaskListView.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Views => }/CEWorkspaceSettingsView.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Views => }/EditCETaskView.swift (100%) rename CodeEdit/Features/CEWorkspaceSettings/{Views => }/EnvironmentVariableListItem.swift (100%) rename CodeEdit/Features/Commands/{Views => }/QuickActionsView.swift (100%) rename CodeEdit/Features/Commands/{ViewModels => }/QuickActionsViewModel.swift (100%) rename CodeEdit/Features/Feedback/{Model => }/FeedbackIssueArea.swift (100%) rename CodeEdit/Features/Feedback/{Model => }/FeedbackModel.swift (100%) rename CodeEdit/Features/Feedback/{HelperView => }/FeedbackToolbar.swift (100%) rename CodeEdit/Features/Feedback/{Model => }/FeedbackType.swift (100%) rename CodeEdit/Features/Feedback/{Controllers => }/FeedbackWindowController.swift (100%) rename CodeEdit/Features/InspectorArea/{Views => }/InspectorAreaView.swift (100%) rename CodeEdit/Features/InspectorArea/{ViewModels => }/InspectorAreaViewModel.swift (100%) rename CodeEdit/Features/InspectorArea/{Views => }/InspectorField.swift (100%) rename CodeEdit/Features/InspectorArea/{Views => }/InspectorSection.swift (100%) rename CodeEdit/Features/InspectorArea/{Models => }/InspectorTab.swift (100%) rename CodeEdit/Features/InspectorArea/{Views => }/NoSelectionInspectorView.swift (100%) rename CodeEdit/Features/Keybindings/{Protocols => }/CommandManaging.swift (100%) rename CodeEdit/Features/Keybindings/{Protocols => }/KeybindingManaging.swift (100%) rename CodeEdit/Features/NavigatorArea/{Views => }/NavigatorAreaView.swift (100%) rename CodeEdit/Features/NavigatorArea/{ViewModels => }/NavigatorAreaViewModel.swift (100%) rename CodeEdit/Features/NavigatorArea/{Models => }/NavigatorTab.swift (100%) rename CodeEdit/Features/NavigatorArea/ProjectNavigator/{Models => }/ProjectNavigatorViewModel.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/{Views => }/SourceControlNavigatorChangesCommitView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/{Views => }/SourceControlNavigatorChangesList.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/{Views => }/SourceControlNavigatorChangesView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/{Views => }/SourceControlNavigatorNoRemotesView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/{Views => }/SourceControlNavigatorSyncView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/{Views/ChangedFile => }/GitChangedFileLabel.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/{Views/ChangedFile => }/GitChangedFileListView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/{Views => }/CommitDetailsHeaderView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/{Views => }/CommitDetailsView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/{Views => }/CommitListItemView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/{Views => }/SourceControlNavigatorHistoryView.swift (100%) rename CodeEdit/{Utils/Extensions/String => Features/NavigatorArea/SourceControlNavigator/History}/String+MD5.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/{Models => }/RepoOutlineGroupItem.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/{Views => }/SourceControlNavigatorRepositoryItem.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/{Views => }/SourceControlNavigatorRepositoryView+contextMenu.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/{Views => }/SourceControlNavigatorRepositoryView+outlineGroupData.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/{Views => }/SourceControlNavigatorRepositoryView.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/{Views => }/SourceControlNavigatorToolbarBottom.swift (100%) rename CodeEdit/Features/NavigatorArea/SourceControlNavigator/{Views => }/SourceControlNavigatorView.swift (100%) rename CodeEdit/Features/OpenQuickly/{Views => }/OpenQuicklyListItemView.swift (100%) rename CodeEdit/Features/OpenQuickly/{Views => }/OpenQuicklyPreviewView.swift (100%) rename CodeEdit/Features/OpenQuickly/{Views => }/OpenQuicklyView.swift (100%) rename CodeEdit/Features/OpenQuickly/{ViewModels => }/OpenQuicklyViewModel.swift (100%) rename CodeEdit/{Utils/Extensions/URL => Features/OpenQuickly}/URL+FuzzySearchable.swift (100%) rename CodeEdit/{Utils/Extensions/URL => Features/OpenQuickly}/URL+Identifiable.swift (100%) rename CodeEdit/Features/Settings/{Views => }/ExternalLink.swift (100%) rename CodeEdit/Features/Settings/{Views => }/FontWeightPicker.swift (100%) rename CodeEdit/Features/Settings/{Views => }/GlobPatternList.swift (100%) rename CodeEdit/Features/Settings/{Views => }/GlobPatternListItem.swift (100%) rename CodeEdit/{Utils/Extensions/Int => Features/Settings}/Int+HexString.swift (100%) rename CodeEdit/Features/Settings/{Views => }/InvisibleCharacterWarningList.swift (100%) rename CodeEdit/Features/Settings/{Views => }/MonospacedFontPicker.swift (100%) rename CodeEdit/Features/Settings/{Models => }/PageAndSettings.swift (100%) rename CodeEdit/{Utils/Extensions/Text => Features/Settings/Pages/AccountsSettings}/Font+Caption3.swift (100%) rename CodeEdit/Features/Settings/Pages/AccountsSettings/{Models => }/SourceControlAccount+Icon.swift (100%) rename CodeEdit/Features/Settings/Pages/LocationsSettings/{Models => }/LocationsSettings.swift (100%) rename CodeEdit/Features/Settings/Pages/SearchSettings/{Models => }/SearchSettingsModel.swift (100%) rename CodeEdit/Features/Settings/Pages/SourceControlSettings/{Models => }/IgnorePatternModel.swift (100%) rename CodeEdit/{Utils => Features/Settings/Pages/SourceControlSettings}/Limiter.swift (100%) rename CodeEdit/Features/Settings/Pages/ThemeSettings/{Models => }/Theme+FuzzySearchable.swift (100%) rename CodeEdit/Features/Settings/Pages/ThemeSettings/{Models => }/ThemeModel+CRUD.swift (100%) rename CodeEdit/Features/Settings/Pages/ThemeSettings/{Models => }/ThemeModel+Export.swift (100%) rename CodeEdit/Features/Settings/Pages/ThemeSettings/{Models => }/ThemeModel.swift (100%) rename CodeEdit/Features/Settings/Pages/ThemeSettings/{Models => }/ThemeRepository.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SearchableSettingsPage.swift (100%) rename CodeEdit/Features/Settings/{Views => }/SettingsColorPicker.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsData+CommandRegistration.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsData+KeybindingReconcile.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsData+Search.swift (100%) rename CodeEdit/Features/Settings/{Views => }/SettingsForm.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsInjector.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsPage.swift (100%) rename CodeEdit/Features/Settings/{Views => }/SettingsPageView.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsSearchResult.swift (100%) rename CodeEdit/Features/Settings/{Models => }/SettingsSidebarFix.swift (100%) rename CodeEdit/{Utils/Extensions/String => Features/Settings}/String+HighlightOccurrences.swift (100%) rename CodeEdit/Features/Settings/{Views => }/View+ConstrainHeightToWindow.swift (100%) rename CodeEdit/Features/Settings/{Views => }/View+HideSidebarToggle.swift (100%) rename CodeEdit/Features/Settings/{Views => }/View+NavigationBarBackButtonVisible.swift (100%) rename CodeEdit/Features/Settings/{Views => }/WarningCharactersView.swift (100%) rename CodeEdit/Features/StatusBar/{Models => }/ImageDimensions.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarIcon.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarBreakpointButton.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarCursorPositionLabel.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarEncodingSelector.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarFileInfoView.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarIndentSelector.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarLineEndSelector.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarMenuStyle.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarItems/StatusBarToggleUtilityAreaButton.swift (100%) rename CodeEdit/{Utils/Extensions/View => Features/StatusBar/StatusBarItems}/View+isHovering.swift (100%) rename CodeEdit/Features/StatusBar/{Views => }/StatusBarView.swift (100%) rename CodeEdit/Features/StatusBar/{ViewModels => }/StatusBarViewModel.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{Model/Sources => }/ExtensionUtilityAreaOutputSource.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{Model/Sources => }/InternalDevelopmentOutputSource.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{Model/Sources => }/LanguageServerLogContainer+UtilityArea.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{Model => }/UtilityAreaLogLevel.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{View => }/UtilityAreaOutputLogList.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{Model => }/UtilityAreaOutputSource.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{View => }/UtilityAreaOutputSourcePicker.swift (100%) rename CodeEdit/Features/UtilityArea/OutputUtility/{View => }/UtilityAreaOutputView.swift (100%) rename CodeEdit/Features/UtilityArea/{Views => }/PaneToolbar.swift (100%) rename CodeEdit/Features/UtilityArea/{Models => TerminalUtility}/UtilityAreaTerminal.swift (100%) rename CodeEdit/Features/UtilityArea/{Models => }/UtilityAreaTab.swift (100%) rename CodeEdit/Features/UtilityArea/{Views => }/UtilityAreaTabView.swift (100%) rename CodeEdit/Features/UtilityArea/{ViewModels => }/UtilityAreaTabViewModel.swift (100%) rename CodeEdit/Features/UtilityArea/{Views => }/UtilityAreaView.swift (100%) rename CodeEdit/Features/UtilityArea/{ViewModels => }/UtilityAreaViewModel.swift (100%) rename CodeEdit/Features/UtilityArea/{Views => }/View+paneToolbar.swift (100%) rename CodeEdit/Features/WindowCommands/{Utils => }/CommandsFixes.swift (100%) rename CodeEdit/Features/WindowCommands/{Utils => }/FirstResponderPropertyWrapper.swift (100%) rename CodeEdit/{Utils => Features/WindowCommands}/FocusedValues.swift (100%) rename CodeEdit/Features/WindowCommands/{Utils => }/KeyWindowControllerObserver.swift (100%) rename CodeEdit/Features/WindowCommands/{Utils => }/RecentProjectsMenu.swift (100%) rename CodeEdit/Features/WindowCommands/{Utils => }/WindowControllerPropertyWrapper.swift (100%) rename CodeEdit/{Utils/Extensions/NSApplication => }/NSApp+openWindow.swift (100%) rename CodeEdit/Utils/{Extensions/Bundle => }/Bundle+Info.swift (100%) rename CodeEdit/Utils/{Extensions/Date => }/Date+Formatted.swift (100%) rename CodeEdit/Utils/{Extensions/NSTableView => }/NSTableView+Background.swift (100%) rename CodeEdit/{Utils => }/withTimeout.swift (100%) diff --git a/CodeEdit/Features/About/Acknowledgements/Views/AcknowledgementRowView.swift b/CodeEdit/Features/About/Acknowledgements/AcknowledgementRowView.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/Views/AcknowledgementRowView.swift rename to CodeEdit/Features/About/Acknowledgements/AcknowledgementRowView.swift diff --git a/CodeEdit/Features/About/Acknowledgements/Views/AcknowledgementsView.swift b/CodeEdit/Features/About/Acknowledgements/AcknowledgementsView.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/Views/AcknowledgementsView.swift rename to CodeEdit/Features/About/Acknowledgements/AcknowledgementsView.swift diff --git a/CodeEdit/Features/About/Acknowledgements/ViewModels/AcknowledgementsViewModel.swift b/CodeEdit/Features/About/Acknowledgements/AcknowledgementsViewModel.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/ViewModels/AcknowledgementsViewModel.swift rename to CodeEdit/Features/About/Acknowledgements/AcknowledgementsViewModel.swift diff --git a/CodeEdit/Features/About/Acknowledgements/Views/ParsePackagesResolved.swift b/CodeEdit/Features/About/Acknowledgements/ParsePackagesResolved.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/Views/ParsePackagesResolved.swift rename to CodeEdit/Features/About/Acknowledgements/ParsePackagesResolved.swift diff --git a/CodeEdit/Features/About/Contributors/Model/Contributor.swift b/CodeEdit/Features/About/Contributors/Contributor.swift similarity index 100% rename from CodeEdit/Features/About/Contributors/Model/Contributor.swift rename to CodeEdit/Features/About/Contributors/Contributor.swift diff --git a/CodeEdit/Utils/Extensions/OperatingSystemVersion/OperatingSystemVersion+String.swift b/CodeEdit/Features/About/OperatingSystemVersion+String.swift similarity index 100% rename from CodeEdit/Utils/Extensions/OperatingSystemVersion/OperatingSystemVersion+String.swift rename to CodeEdit/Features/About/OperatingSystemVersion+String.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/AddCETaskView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift rename to CodeEdit/Features/CEWorkspaceSettings/AddCETaskView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift b/CodeEdit/Features/CEWorkspaceSettings/CETaskFormView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift rename to CodeEdit/Features/CEWorkspaceSettings/CETaskFormView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings+TasksConfigurationProviding.swift b/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings+TasksConfigurationProviding.swift rename to CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings.swift b/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Models/CEWorkspaceSettings.swift rename to CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsTaskListView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift rename to CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsTaskListView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift b/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift rename to CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/EditCETaskView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Views/EditCETaskView.swift rename to CodeEdit/Features/CEWorkspaceSettings/EditCETaskView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/EnvironmentVariableListItem.swift b/CodeEdit/Features/CEWorkspaceSettings/EnvironmentVariableListItem.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/Views/EnvironmentVariableListItem.swift rename to CodeEdit/Features/CEWorkspaceSettings/EnvironmentVariableListItem.swift diff --git a/CodeEdit/Features/Commands/Views/QuickActionsView.swift b/CodeEdit/Features/Commands/QuickActionsView.swift similarity index 100% rename from CodeEdit/Features/Commands/Views/QuickActionsView.swift rename to CodeEdit/Features/Commands/QuickActionsView.swift diff --git a/CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift b/CodeEdit/Features/Commands/QuickActionsViewModel.swift similarity index 100% rename from CodeEdit/Features/Commands/ViewModels/QuickActionsViewModel.swift rename to CodeEdit/Features/Commands/QuickActionsViewModel.swift diff --git a/CodeEdit/Features/Feedback/Model/FeedbackIssueArea.swift b/CodeEdit/Features/Feedback/FeedbackIssueArea.swift similarity index 100% rename from CodeEdit/Features/Feedback/Model/FeedbackIssueArea.swift rename to CodeEdit/Features/Feedback/FeedbackIssueArea.swift diff --git a/CodeEdit/Features/Feedback/Model/FeedbackModel.swift b/CodeEdit/Features/Feedback/FeedbackModel.swift similarity index 100% rename from CodeEdit/Features/Feedback/Model/FeedbackModel.swift rename to CodeEdit/Features/Feedback/FeedbackModel.swift diff --git a/CodeEdit/Features/Feedback/HelperView/FeedbackToolbar.swift b/CodeEdit/Features/Feedback/FeedbackToolbar.swift similarity index 100% rename from CodeEdit/Features/Feedback/HelperView/FeedbackToolbar.swift rename to CodeEdit/Features/Feedback/FeedbackToolbar.swift diff --git a/CodeEdit/Features/Feedback/Model/FeedbackType.swift b/CodeEdit/Features/Feedback/FeedbackType.swift similarity index 100% rename from CodeEdit/Features/Feedback/Model/FeedbackType.swift rename to CodeEdit/Features/Feedback/FeedbackType.swift diff --git a/CodeEdit/Features/Feedback/Controllers/FeedbackWindowController.swift b/CodeEdit/Features/Feedback/FeedbackWindowController.swift similarity index 100% rename from CodeEdit/Features/Feedback/Controllers/FeedbackWindowController.swift rename to CodeEdit/Features/Feedback/FeedbackWindowController.swift diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/InspectorAreaView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift rename to CodeEdit/Features/InspectorArea/InspectorAreaView.swift diff --git a/CodeEdit/Features/InspectorArea/ViewModels/InspectorAreaViewModel.swift b/CodeEdit/Features/InspectorArea/InspectorAreaViewModel.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/ViewModels/InspectorAreaViewModel.swift rename to CodeEdit/Features/InspectorArea/InspectorAreaViewModel.swift diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorField.swift b/CodeEdit/Features/InspectorArea/InspectorField.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/Views/InspectorField.swift rename to CodeEdit/Features/InspectorArea/InspectorField.swift diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorSection.swift b/CodeEdit/Features/InspectorArea/InspectorSection.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/Views/InspectorSection.swift rename to CodeEdit/Features/InspectorArea/InspectorSection.swift diff --git a/CodeEdit/Features/InspectorArea/Models/InspectorTab.swift b/CodeEdit/Features/InspectorArea/InspectorTab.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/Models/InspectorTab.swift rename to CodeEdit/Features/InspectorArea/InspectorTab.swift diff --git a/CodeEdit/Features/InspectorArea/Views/NoSelectionInspectorView.swift b/CodeEdit/Features/InspectorArea/NoSelectionInspectorView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/Views/NoSelectionInspectorView.swift rename to CodeEdit/Features/InspectorArea/NoSelectionInspectorView.swift diff --git a/CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift b/CodeEdit/Features/Keybindings/CommandManaging.swift similarity index 100% rename from CodeEdit/Features/Keybindings/Protocols/CommandManaging.swift rename to CodeEdit/Features/Keybindings/CommandManaging.swift diff --git a/CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift b/CodeEdit/Features/Keybindings/KeybindingManaging.swift similarity index 100% rename from CodeEdit/Features/Keybindings/Protocols/KeybindingManaging.swift rename to CodeEdit/Features/Keybindings/KeybindingManaging.swift diff --git a/CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift b/CodeEdit/Features/NavigatorArea/NavigatorAreaView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/Views/NavigatorAreaView.swift rename to CodeEdit/Features/NavigatorArea/NavigatorAreaView.swift diff --git a/CodeEdit/Features/NavigatorArea/ViewModels/NavigatorAreaViewModel.swift b/CodeEdit/Features/NavigatorArea/NavigatorAreaViewModel.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ViewModels/NavigatorAreaViewModel.swift rename to CodeEdit/Features/NavigatorArea/NavigatorAreaViewModel.swift diff --git a/CodeEdit/Features/NavigatorArea/Models/NavigatorTab.swift b/CodeEdit/Features/NavigatorArea/NavigatorTab.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/Models/NavigatorTab.swift rename to CodeEdit/Features/NavigatorArea/NavigatorTab.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/Models/ProjectNavigatorViewModel.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/Models/ProjectNavigatorViewModel.swift rename to CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesCommitView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesList.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorChangesView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorNoRemotesView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/Views/SourceControlNavigatorSyncView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileLabel.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/ChangedFile/GitChangedFileListView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsHeaderView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitDetailsView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorHistoryView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift diff --git a/CodeEdit/Utils/Extensions/String/String+MD5.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/String+MD5.swift similarity index 100% rename from CodeEdit/Utils/Extensions/String/String+MD5.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/String+MD5.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Models/RepoOutlineGroupItem.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryItem.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+contextMenu.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+outlineGroupData.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+outlineGroupData.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorToolbarBottom.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift rename to CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyListItemView.swift b/CodeEdit/Features/OpenQuickly/OpenQuicklyListItemView.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/Views/OpenQuicklyListItemView.swift rename to CodeEdit/Features/OpenQuickly/OpenQuicklyListItemView.swift diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/Views/OpenQuicklyPreviewView.swift rename to CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift diff --git a/CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift b/CodeEdit/Features/OpenQuickly/OpenQuicklyView.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/Views/OpenQuicklyView.swift rename to CodeEdit/Features/OpenQuickly/OpenQuicklyView.swift diff --git a/CodeEdit/Features/OpenQuickly/ViewModels/OpenQuicklyViewModel.swift b/CodeEdit/Features/OpenQuickly/OpenQuicklyViewModel.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/ViewModels/OpenQuicklyViewModel.swift rename to CodeEdit/Features/OpenQuickly/OpenQuicklyViewModel.swift diff --git a/CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift b/CodeEdit/Features/OpenQuickly/URL+FuzzySearchable.swift similarity index 100% rename from CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift rename to CodeEdit/Features/OpenQuickly/URL+FuzzySearchable.swift diff --git a/CodeEdit/Utils/Extensions/URL/URL+Identifiable.swift b/CodeEdit/Features/OpenQuickly/URL+Identifiable.swift similarity index 100% rename from CodeEdit/Utils/Extensions/URL/URL+Identifiable.swift rename to CodeEdit/Features/OpenQuickly/URL+Identifiable.swift diff --git a/CodeEdit/Features/Settings/Views/ExternalLink.swift b/CodeEdit/Features/Settings/ExternalLink.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/ExternalLink.swift rename to CodeEdit/Features/Settings/ExternalLink.swift diff --git a/CodeEdit/Features/Settings/Views/FontWeightPicker.swift b/CodeEdit/Features/Settings/FontWeightPicker.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/FontWeightPicker.swift rename to CodeEdit/Features/Settings/FontWeightPicker.swift diff --git a/CodeEdit/Features/Settings/Views/GlobPatternList.swift b/CodeEdit/Features/Settings/GlobPatternList.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/GlobPatternList.swift rename to CodeEdit/Features/Settings/GlobPatternList.swift diff --git a/CodeEdit/Features/Settings/Views/GlobPatternListItem.swift b/CodeEdit/Features/Settings/GlobPatternListItem.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/GlobPatternListItem.swift rename to CodeEdit/Features/Settings/GlobPatternListItem.swift diff --git a/CodeEdit/Utils/Extensions/Int/Int+HexString.swift b/CodeEdit/Features/Settings/Int+HexString.swift similarity index 100% rename from CodeEdit/Utils/Extensions/Int/Int+HexString.swift rename to CodeEdit/Features/Settings/Int+HexString.swift diff --git a/CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift b/CodeEdit/Features/Settings/InvisibleCharacterWarningList.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/InvisibleCharacterWarningList.swift rename to CodeEdit/Features/Settings/InvisibleCharacterWarningList.swift diff --git a/CodeEdit/Features/Settings/Views/MonospacedFontPicker.swift b/CodeEdit/Features/Settings/MonospacedFontPicker.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/MonospacedFontPicker.swift rename to CodeEdit/Features/Settings/MonospacedFontPicker.swift diff --git a/CodeEdit/Features/Settings/Models/PageAndSettings.swift b/CodeEdit/Features/Settings/PageAndSettings.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/PageAndSettings.swift rename to CodeEdit/Features/Settings/PageAndSettings.swift diff --git a/CodeEdit/Utils/Extensions/Text/Font+Caption3.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/Font+Caption3.swift similarity index 100% rename from CodeEdit/Utils/Extensions/Text/Font+Caption3.swift rename to CodeEdit/Features/Settings/Pages/AccountsSettings/Font+Caption3.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount+Icon.swift b/CodeEdit/Features/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/Models/SourceControlAccount+Icon.swift rename to CodeEdit/Features/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift diff --git a/CodeEdit/Features/Settings/Pages/LocationsSettings/Models/LocationsSettings.swift b/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettings.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/LocationsSettings/Models/LocationsSettings.swift rename to CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettings.swift diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift b/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsModel.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SearchSettings/Models/SearchSettingsModel.swift rename to CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsModel.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/IgnorePatternModel.swift rename to CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift diff --git a/CodeEdit/Utils/Limiter.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/Limiter.swift similarity index 100% rename from CodeEdit/Utils/Limiter.swift rename to CodeEdit/Features/Settings/Pages/SourceControlSettings/Limiter.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme+FuzzySearchable.swift rename to CodeEdit/Features/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift rename to CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+Export.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+Export.swift rename to CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+Export.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift rename to CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeRepository.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeRepository.swift rename to CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeRepository.swift diff --git a/CodeEdit/Features/Settings/Models/SearchableSettingsPage.swift b/CodeEdit/Features/Settings/SearchableSettingsPage.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SearchableSettingsPage.swift rename to CodeEdit/Features/Settings/SearchableSettingsPage.swift diff --git a/CodeEdit/Features/Settings/Views/SettingsColorPicker.swift b/CodeEdit/Features/Settings/SettingsColorPicker.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/SettingsColorPicker.swift rename to CodeEdit/Features/Settings/SettingsColorPicker.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift b/CodeEdit/Features/Settings/SettingsData+CommandRegistration.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsData+CommandRegistration.swift rename to CodeEdit/Features/Settings/SettingsData+CommandRegistration.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift b/CodeEdit/Features/Settings/SettingsData+KeybindingReconcile.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsData+KeybindingReconcile.swift rename to CodeEdit/Features/Settings/SettingsData+KeybindingReconcile.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsData+Search.swift b/CodeEdit/Features/Settings/SettingsData+Search.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsData+Search.swift rename to CodeEdit/Features/Settings/SettingsData+Search.swift diff --git a/CodeEdit/Features/Settings/Views/SettingsForm.swift b/CodeEdit/Features/Settings/SettingsForm.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/SettingsForm.swift rename to CodeEdit/Features/Settings/SettingsForm.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsInjector.swift b/CodeEdit/Features/Settings/SettingsInjector.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsInjector.swift rename to CodeEdit/Features/Settings/SettingsInjector.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsPage.swift b/CodeEdit/Features/Settings/SettingsPage.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsPage.swift rename to CodeEdit/Features/Settings/SettingsPage.swift diff --git a/CodeEdit/Features/Settings/Views/SettingsPageView.swift b/CodeEdit/Features/Settings/SettingsPageView.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/SettingsPageView.swift rename to CodeEdit/Features/Settings/SettingsPageView.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsSearchResult.swift b/CodeEdit/Features/Settings/SettingsSearchResult.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsSearchResult.swift rename to CodeEdit/Features/Settings/SettingsSearchResult.swift diff --git a/CodeEdit/Features/Settings/Models/SettingsSidebarFix.swift b/CodeEdit/Features/Settings/SettingsSidebarFix.swift similarity index 100% rename from CodeEdit/Features/Settings/Models/SettingsSidebarFix.swift rename to CodeEdit/Features/Settings/SettingsSidebarFix.swift diff --git a/CodeEdit/Utils/Extensions/String/String+HighlightOccurrences.swift b/CodeEdit/Features/Settings/String+HighlightOccurrences.swift similarity index 100% rename from CodeEdit/Utils/Extensions/String/String+HighlightOccurrences.swift rename to CodeEdit/Features/Settings/String+HighlightOccurrences.swift diff --git a/CodeEdit/Features/Settings/Views/View+ConstrainHeightToWindow.swift b/CodeEdit/Features/Settings/View+ConstrainHeightToWindow.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/View+ConstrainHeightToWindow.swift rename to CodeEdit/Features/Settings/View+ConstrainHeightToWindow.swift diff --git a/CodeEdit/Features/Settings/Views/View+HideSidebarToggle.swift b/CodeEdit/Features/Settings/View+HideSidebarToggle.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/View+HideSidebarToggle.swift rename to CodeEdit/Features/Settings/View+HideSidebarToggle.swift diff --git a/CodeEdit/Features/Settings/Views/View+NavigationBarBackButtonVisible.swift b/CodeEdit/Features/Settings/View+NavigationBarBackButtonVisible.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/View+NavigationBarBackButtonVisible.swift rename to CodeEdit/Features/Settings/View+NavigationBarBackButtonVisible.swift diff --git a/CodeEdit/Features/Settings/Views/WarningCharactersView.swift b/CodeEdit/Features/Settings/WarningCharactersView.swift similarity index 100% rename from CodeEdit/Features/Settings/Views/WarningCharactersView.swift rename to CodeEdit/Features/Settings/WarningCharactersView.swift diff --git a/CodeEdit/Features/StatusBar/Models/ImageDimensions.swift b/CodeEdit/Features/StatusBar/ImageDimensions.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Models/ImageDimensions.swift rename to CodeEdit/Features/StatusBar/ImageDimensions.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift b/CodeEdit/Features/StatusBar/StatusBarIcon.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarIcon.swift rename to CodeEdit/Features/StatusBar/StatusBarIcon.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarBreakpointButton.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarBreakpointButton.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarEncodingSelector.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarEncodingSelector.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarFileInfoView.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarIndentSelector.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarIndentSelector.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarIndentSelector.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarIndentSelector.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarLineEndSelector.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarLineEndSelector.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarMenuStyle.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarMenuStyle.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarMenuStyle.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarMenuStyle.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift diff --git a/CodeEdit/Utils/Extensions/View/View+isHovering.swift b/CodeEdit/Features/StatusBar/StatusBarItems/View+isHovering.swift similarity index 100% rename from CodeEdit/Utils/Extensions/View/View+isHovering.swift rename to CodeEdit/Features/StatusBar/StatusBarItems/View+isHovering.swift diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarView.swift b/CodeEdit/Features/StatusBar/StatusBarView.swift similarity index 100% rename from CodeEdit/Features/StatusBar/Views/StatusBarView.swift rename to CodeEdit/Features/StatusBar/StatusBarView.swift diff --git a/CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift b/CodeEdit/Features/StatusBar/StatusBarViewModel.swift similarity index 100% rename from CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift rename to CodeEdit/Features/StatusBar/StatusBarViewModel.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/ExtensionUtilityAreaOutputSource.swift b/CodeEdit/Features/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/ExtensionUtilityAreaOutputSource.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/InternalDevelopmentOutputSource.swift b/CodeEdit/Features/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/InternalDevelopmentOutputSource.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer+UtilityArea.swift b/CodeEdit/Features/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer+UtilityArea.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaLogLevel.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaLogLevel.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputLogList.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputLogList.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaOutputSource.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaOutputSource.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift rename to CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift diff --git a/CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift b/CodeEdit/Features/UtilityArea/PaneToolbar.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift rename to CodeEdit/Features/UtilityArea/PaneToolbar.swift diff --git a/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminal.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift diff --git a/CodeEdit/Features/UtilityArea/Models/UtilityAreaTab.swift b/CodeEdit/Features/UtilityArea/UtilityAreaTab.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Models/UtilityAreaTab.swift rename to CodeEdit/Features/UtilityArea/UtilityAreaTab.swift diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTabView.swift b/CodeEdit/Features/UtilityArea/UtilityAreaTabView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/UtilityAreaTabView.swift rename to CodeEdit/Features/UtilityArea/UtilityAreaTabView.swift diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaTabViewModel.swift b/CodeEdit/Features/UtilityArea/UtilityAreaTabViewModel.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaTabViewModel.swift rename to CodeEdit/Features/UtilityArea/UtilityAreaTabViewModel.swift diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift b/CodeEdit/Features/UtilityArea/UtilityAreaView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift rename to CodeEdit/Features/UtilityArea/UtilityAreaView.swift diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/UtilityAreaViewModel.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift rename to CodeEdit/Features/UtilityArea/UtilityAreaViewModel.swift diff --git a/CodeEdit/Features/UtilityArea/Views/View+paneToolbar.swift b/CodeEdit/Features/UtilityArea/View+paneToolbar.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/View+paneToolbar.swift rename to CodeEdit/Features/UtilityArea/View+paneToolbar.swift diff --git a/CodeEdit/Features/WindowCommands/Utils/CommandsFixes.swift b/CodeEdit/Features/WindowCommands/CommandsFixes.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/Utils/CommandsFixes.swift rename to CodeEdit/Features/WindowCommands/CommandsFixes.swift diff --git a/CodeEdit/Features/WindowCommands/Utils/FirstResponderPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/FirstResponderPropertyWrapper.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/Utils/FirstResponderPropertyWrapper.swift rename to CodeEdit/Features/WindowCommands/FirstResponderPropertyWrapper.swift diff --git a/CodeEdit/Utils/FocusedValues.swift b/CodeEdit/Features/WindowCommands/FocusedValues.swift similarity index 100% rename from CodeEdit/Utils/FocusedValues.swift rename to CodeEdit/Features/WindowCommands/FocusedValues.swift diff --git a/CodeEdit/Features/WindowCommands/Utils/KeyWindowControllerObserver.swift b/CodeEdit/Features/WindowCommands/KeyWindowControllerObserver.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/Utils/KeyWindowControllerObserver.swift rename to CodeEdit/Features/WindowCommands/KeyWindowControllerObserver.swift diff --git a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift b/CodeEdit/Features/WindowCommands/RecentProjectsMenu.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift rename to CodeEdit/Features/WindowCommands/RecentProjectsMenu.swift diff --git a/CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift b/CodeEdit/Features/WindowCommands/WindowControllerPropertyWrapper.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/Utils/WindowControllerPropertyWrapper.swift rename to CodeEdit/Features/WindowCommands/WindowControllerPropertyWrapper.swift diff --git a/CodeEdit/Utils/Extensions/NSApplication/NSApp+openWindow.swift b/CodeEdit/NSApp+openWindow.swift similarity index 100% rename from CodeEdit/Utils/Extensions/NSApplication/NSApp+openWindow.swift rename to CodeEdit/NSApp+openWindow.swift diff --git a/CodeEdit/Utils/Extensions/Bundle/Bundle+Info.swift b/CodeEdit/Utils/Bundle+Info.swift similarity index 100% rename from CodeEdit/Utils/Extensions/Bundle/Bundle+Info.swift rename to CodeEdit/Utils/Bundle+Info.swift diff --git a/CodeEdit/Utils/Extensions/Date/Date+Formatted.swift b/CodeEdit/Utils/Date+Formatted.swift similarity index 100% rename from CodeEdit/Utils/Extensions/Date/Date+Formatted.swift rename to CodeEdit/Utils/Date+Formatted.swift diff --git a/CodeEdit/Utils/Extensions/NSTableView/NSTableView+Background.swift b/CodeEdit/Utils/NSTableView+Background.swift similarity index 100% rename from CodeEdit/Utils/Extensions/NSTableView/NSTableView+Background.swift rename to CodeEdit/Utils/NSTableView+Background.swift diff --git a/CodeEdit/Utils/withTimeout.swift b/CodeEdit/withTimeout.swift similarity index 100% rename from CodeEdit/Utils/withTimeout.swift rename to CodeEdit/withTimeout.swift From 139cf9eb1ed4af06819d92ea622a2d1263c80cfc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 19:04:07 +0200 Subject: [PATCH 174/335] Style: Apply SwiftLint autocorrect fixes --- CodeEdit/WorkspaceView.swift | 2 +- .../Sources/CEEditor/Views/PDFFileView.swift | 2 +- .../DocumentSync/LSPContentCoordinator.swift | 4 +-- .../PackageManagerInstallOperation.swift | 2 +- .../CELSP/Registry/RegistryManager.swift | 2 +- .../Service/LanguageServerLogContainer.swift | 1 - .../SourceControlViewModel.swift | 1 - .../UseCases/RepositoryCloner.swift | 1 - .../Views/SourceControlFetchView.swift | 1 - .../Views/SourceControlPullView.swift | 1 - .../CodeEditDocument/CodeFileDocument.swift | 26 +++++++++---------- .../LanguageServicesProvider.swift | 2 +- 12 files changed, 20 insertions(+), 25 deletions(-) diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index ca42124464..97ade329b1 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -100,7 +100,7 @@ struct WorkspaceView: View { .task { // Only refresh git data if source control is enabled guard sourceControlIsEnabled else { return } - + do { try await sourceControlManager.refreshRemotes() try await sourceControlManager.refreshStashEntries() diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift index 60058807a6..3400a05094 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift @@ -46,7 +46,7 @@ struct PDFFileView: NSViewRepresentable { /// - Returns: A modified `pdfView` if a valid PDF was created, or an unmodified `pdfView` if it could not create a /// valid PDF. @discardableResult - private func attachPDFDocumentToView (_ pdfView: PDFView) -> PDFView { + private func attachPDFDocumentToView(_ pdfView: PDFView) -> PDFView { guard let pdfDocument = PDFDocument(url: fileURL) else { // What can happen is the view doesn't redraw, so whatever was in the editor area view remains as is. return pdfView diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift b/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift index 691bd7bb18..9ad60536f8 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift @@ -33,8 +33,8 @@ class LSPContentCoordinator: @preconcurren // nonisolated(unsafe): assigned on the main actor during setup; read from the // detached debounce task (`languageServer`, `sequenceContinuation`) and from // `deinit` (`task`, `sequenceContinuation`), which cannot be actor-isolated. - private nonisolated(unsafe) var sequenceContinuation: AsyncStream.Continuation? - private nonisolated(unsafe) var task: Task? + nonisolated(unsafe) private var sequenceContinuation: AsyncStream.Continuation? + nonisolated(unsafe) private var task: Task? nonisolated(unsafe) weak var languageServer: LanguageServer? var documentURI: String? diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift index 39a3221e6e..7007da503d 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift @@ -36,7 +36,7 @@ public final class PackageManagerInstallOperation: ObservableObject, Identifiabl } } - public nonisolated var id: String { package.name } + nonisolated public var id: String { package.name } public let package: RegistryItem public let steps: [PackageManagerInstallStep] diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift index 4ccb61202b..12864a5d4a 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift @@ -38,7 +38,7 @@ public final class RegistryManager: RegistryManaging { /// Timer to clear expired cache. /// nonisolated(unsafe): scheduled and invalidated on the main actor; also /// invalidated from `deinit`, which cannot be actor-isolated. - private nonisolated(unsafe) var cleanupTimer: Timer? + nonisolated(unsafe) private var cleanupTimer: Timer? @AppSettings(\.languageServers.installedLanguageServers) public var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift b/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift index 65167a81c0..34097b1113 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift @@ -23,7 +23,6 @@ public final class LanguageServerLogContainer: @unchecked Sendable { log.message } - public var date: Date = Date() public var subsystem: String? public var category: String? diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift index 7cae4d511c..700fd7294c 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift @@ -17,7 +17,6 @@ import CodeEditCore public final class SourceControlViewModel: ObservableObject { public init() {} - // MARK: - Sheet State /// Is the push sheet presented diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift index 1a2ac57c6e..a6cb24c991 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift @@ -16,7 +16,6 @@ final class RepositoryCloner { self.shellClient = shellClient } - enum Failure: Error, LocalizedError { case gitNotInstalled case invalidUrl diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift index c71746a385..362482ecf5 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift @@ -15,7 +15,6 @@ public struct SourceControlFetchView: View { @EnvironmentObject var sourceControlManager: SourceControlManager - var projectName: String { sourceControlManager.workspaceURL.lastPathComponent } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift index cfb2c4ba10..52832d8635 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift @@ -17,7 +17,6 @@ public struct SourceControlPullView: View { @EnvironmentObject var sourceControlManager: SourceControlManager @EnvironmentObject var sourceControlViewModel: SourceControlViewModel - @State var loading: Bool = false @State var preferRebaseWhenPulling: Bool = false diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift index 986d570464..017d8eab48 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift @@ -116,15 +116,15 @@ public final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - NSDocument - public override static var autosavesInPlace: Bool { + override public static var autosavesInPlace: Bool { isAutoSaveOnProvider() } - public override var autosavingFileType: String? { + override public var autosavingFileType: String? { Self.isAutoSaveOnProvider() ? fileType : nil } - public override func makeWindowControllers() { + override public func makeWindowControllers() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 750, height: 800), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], @@ -150,7 +150,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - Data - public override func data(ofType _: String) throws -> Data { + override public func data(ofType _: String) throws -> Data { guard let sourceEncoding, let data = (content?.string as NSString?)?.data(using: sourceEncoding.nsValue) else { Self.logger.error("Failed to encode contents to \(self.sourceEncoding.debugDescription)") throw CodeFileError.failedToEncode @@ -162,7 +162,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { /// This function is used for decoding files. /// It should not throw error as unsupported files can still be opened by QLPreviewView. - public override func read(from data: Data, ofType _: String) throws { + override public func read(from data: Data, ofType _: String) throws { var nsString: NSString? let rawEncoding = NSString.stringEncoding( for: data, @@ -214,7 +214,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { /// - Note: This is inefficient memory-wise. We could do a diff of the file and only register the /// mutations that would recreate the diff. However, that would instead be CPU intensive. /// Tradeoffs. - private nonisolated func registerContentChangeUndo(fileURL: URL?, nsString: NSString, content: NSTextStorage) { + nonisolated private func registerContentChangeUndo(fileURL: URL?, nsString: NSString, content: NSTextStorage) { guard let fileURL else { return } // The delegate's undo registry is main-actor isolated. Capture only Sendable primitives and build // the (non-Sendable) `TextMutation` on the main actor so nothing non-Sendable crosses the boundary. @@ -239,7 +239,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - Autosave /// Triggered when change occurred - public override func updateChangeCount(_ change: NSDocument.ChangeType) { + override public func updateChangeCount(_ change: NSDocument.ChangeType) { super.updateChangeCount(change) if CodeFileDocument.autosavesInPlace { @@ -250,7 +250,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { } /// Triggered when changes saved - public override func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) { + override public func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) { super.updateChangeCount(withToken: changeCountToken, for: saveOperation) if CodeFileDocument.autosavesInPlace { @@ -265,7 +265,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { /// /// All operations are done with the ``autosaveTimerLock`` acquired (including the scheduled autosave) to ensure /// correct timing when scheduling or cancelling timers. - public override func scheduleAutosaving() { + override public func scheduleAutosaving() { autosaveTimerLock.withLock { if self.hasUnautosavedChanges { guard autosaveTimer == nil else { return } @@ -292,7 +292,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { /// we continue. /// To determine if we can reload the file, we check if the document has outstanding edits. If not, we reload the /// file. - public override func presentedItemDidChange() { + override public func presentedItemDidChange() { if fileModificationDate != getModificationDate() { guard isDocumentEdited else { fileModificationDate = getModificationDate() @@ -326,14 +326,14 @@ public final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - Close - public override func close() { + override public func close() { super.close() if let fileURL { notifyLSPDidClose(fileURL) } } - public override func save(_ sender: Any?) { + override public func save(_ sender: Any?) { guard let fileURL else { super.save(sender) return @@ -350,7 +350,7 @@ public final class CodeFileDocument: NSDocument, ObservableObject { } } - public override func fileNameExtension( + override public func fileNameExtension( forType typeName: String, saveOperation: NSDocument.SaveOperationType ) -> String? { diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift index 968bc027a2..1d3a7a521e 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift @@ -26,7 +26,7 @@ public protocol LanguageServicesProvider: AnyObject { } public final class NoOpLanguageServicesProvider: LanguageServicesProvider { - public nonisolated init() {} + nonisolated public init() {} @MainActor public func languageServices(for document: CodeFileDocument) -> LanguageServices { From d1c82119f71a6aa2883165214315bd599f44a90e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 19:10:38 +0200 Subject: [PATCH 175/335] Style: Resolve all mechanical SwiftLint violations --- .../FileInspector/FileInspectorView.swift | 19 +++-- .../HistoryInspectorView.swift | 3 +- .../Features/InspectorArea/InspectorTab.swift | 2 +- ...InternalDevelopmentNotificationsView.swift | 3 +- .../Features/NavigatorArea/NavigatorTab.swift | 2 +- .../OutlineView/ProjectNavigatorMenu.swift | 3 +- .../ProjectNavigatorOutlineView.swift | 6 +- .../ProjectNavigatorToolbarBottom.swift | 6 +- .../SourceControlNavigatorChangesList.swift | 3 +- .../GitChangedFileLabel.swift | 12 ++- .../OpenQuickly/OpenQuicklyPreviewView.swift | 3 +- .../SourceControlGeneralView.swift | 3 +- .../SourceControlGitView.swift | 6 +- .../Pages/ThemeSettings/ThemeModel+CRUD.swift | 2 +- CodeEdit/Features/Settings/SettingsView.swift | 3 +- .../StatusBarFileInfoView.swift | 3 +- .../Window/CodeEditSplitViewController.swift | 81 ++++++++++++------- .../ApplicationShutdownCoordinator.swift | 6 +- CodeEdit/Features/Workspace/Workspace.swift | 1 - .../Features/Workspace/WorkspaceFactory.swift | 35 ++++---- .../CEWorkspaceFileManagerEventsTests.swift | 16 ++-- .../Features/Tasks/CEActiveTaskTests.swift | 5 +- .../EditorLayout+StateRestoration.swift | 2 +- .../Sources/CEEditor/Models/FileIcon.swift | 3 +- .../Sources/CEEditor/Views/CodeFileView.swift | 3 +- .../CEEditor/Views/FilePreviewView.swift | 10 ++- .../CEEditor/Views/WindowCodeFileView.swift | 3 +- .../DocumentSync/LSPContentCoordinator.swift | 4 +- .../FindNavigatorResultList.swift | 3 +- .../Accounts/Utils/GitTime.swift | 4 +- .../Views/SourceControlPullView.swift | 3 +- .../Views/CELocalShellTerminalView.swift | 3 +- .../Views/TerminalEmulatorView.swift | 13 ++- .../Infrastructure/ActiveCursorState.swift | 3 +- .../Infrastructure/ErrorNotifying.swift | 6 +- .../Infrastructure/FileEditorOverrides.swift | 15 ++-- .../Infrastructure/FileRelocator.swift | 3 +- .../Infrastructure/WorkspaceNavigator.swift | 18 +++-- .../CodeEditDocument/CodeFileDocument.swift | 5 +- .../Models/GeneralSettings.swift | 12 ++- .../Models/LanguageServerSettings.swift | 4 +- .../Models/TextEditingSettings.swift | 3 +- .../SplitView/Environment+ContentInsets.swift | 3 +- 43 files changed, 227 insertions(+), 119 deletions(-) diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift index d8e6dee0e5..9645afc06d 100644 --- a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift @@ -10,10 +10,13 @@ import CodeEditCore import CodeEditLanguages struct FileInspectorView: View { - @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.activeEditorState) + private var activeEditorState - @Environment(\.fileEditorOverrides) private var fileEditorOverrides - @Environment(\.fileRelocator) private var fileRelocator + @Environment(\.fileEditorOverrides) + private var fileEditorOverrides + @Environment(\.fileRelocator) + private var fileRelocator @AppSettings(\.textEditing) private var textEditing @@ -207,7 +210,10 @@ struct FileInspectorView: View { } .onChange(of: defaultTabWidth) { _, newValue in if let file { - fileEditorOverrides.setDefaultTabWidth(newValue == textEditing.defaultTabWidth ? nil : newValue, for: file) + fileEditorOverrides.setDefaultTabWidth( + newValue == textEditing.defaultTabWidth ? nil : newValue, + for: file + ) } } } @@ -216,7 +222,10 @@ struct FileInspectorView: View { Toggle("Wrap lines", isOn: $wrapLines) .onChange(of: wrapLines) { _, newValue in if let file { - fileEditorOverrides.setWrapLines(newValue == textEditing.wrapLinesToEditorWidth ? nil : newValue, for: file) + fileEditorOverrides.setWrapLines( + newValue == textEditing.wrapLinesToEditorWidth ? nil : newValue, + for: file + ) } } } diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift index bb5c7658c4..50c0f5f27f 100644 --- a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -16,7 +16,8 @@ struct HistoryInspectorView: View { @EnvironmentObject private var sourceControlManager: SourceControlManager - @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.activeEditorState) + private var activeEditorState @ObservedObject private var model: HistoryInspectorModel diff --git a/CodeEdit/Features/InspectorArea/InspectorTab.swift b/CodeEdit/Features/InspectorArea/InspectorTab.swift index f9311cea32..284c3fdfc9 100644 --- a/CodeEdit/Features/InspectorArea/InspectorTab.swift +++ b/CodeEdit/Features/InspectorArea/InspectorTab.swift @@ -29,7 +29,7 @@ enum InspectorTab: WorkspacePanelTab { } var id: String { - if case .uiExtension(let endpoint, let data) = self { + if case let .uiExtension(endpoint, data) = self { return endpoint.bundleIdentifier + data.sceneID } return title diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift index 5834f0a64d..ef100dd74c 100644 --- a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift @@ -9,7 +9,8 @@ import SwiftUI import CENotifications struct InternalDevelopmentNotificationsView: View { - @Environment(\.notificationManager) private var notificationManager + @Environment(\.notificationManager) + private var notificationManager enum IconType: String, CaseIterable { case symbol = "Symbol" diff --git a/CodeEdit/Features/NavigatorArea/NavigatorTab.swift b/CodeEdit/Features/NavigatorArea/NavigatorTab.swift index de585a51f8..27f5a721a2 100644 --- a/CodeEdit/Features/NavigatorArea/NavigatorTab.swift +++ b/CodeEdit/Features/NavigatorArea/NavigatorTab.swift @@ -29,7 +29,7 @@ enum NavigatorTab: WorkspacePanelTab { } var id: String { - if case .uiExtension(let endpoint, let data) = self { + if case let .uiExtension(endpoint, data) = self { return endpoint.bundleIdentifier + data.sceneID } return title diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 4a93ded6c9..6cca6f2d45 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -49,7 +49,8 @@ final class ProjectNavigatorMenu: NSMenu { /// Configures the menu based on the current selection in the outline view. /// - Menu items get added depending on the amount of selected items. - @MainActor private func setupMenu() { // swiftlint:disable:this function_body_length + @MainActor + private func setupMenu() { // swiftlint:disable:this function_body_length guard let item else { return } let showInFinder = menuItem("Show in Finder", action: #selector(showInFinder)) diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 399ac9c970..7e7f71a16a 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -18,8 +18,10 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @EnvironmentObject var workspace: Workspace @EnvironmentObject var editorManager: EditorManager - @Environment(\.activeEditorState) private var activeEditorState - @Environment(\.workspaceNavigator) private var workspaceNavigator + @Environment(\.activeEditorState) + private var activeEditorState + @Environment(\.workspaceNavigator) + private var workspaceNavigator @StateObject var prefs: CodeEditSettings.Settings = .shared diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 50058a5943..4722b34f28 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -17,8 +17,10 @@ struct ProjectNavigatorToolbarBottom: View { @Environment(\.colorScheme) private var colorScheme - @Environment(\.activeEditorState) private var activeEditorState - @Environment(\.workspaceNavigator) private var workspaceNavigator + @Environment(\.activeEditorState) + private var activeEditorState + @Environment(\.workspaceNavigator) + private var workspaceNavigator @EnvironmentObject var listenerModel: WorkspaceNotificationModel @EnvironmentObject var projectNavigatorViewModel: ProjectNavigatorViewModel diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift index 420e4eecdb..6af32edcfe 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift @@ -13,7 +13,8 @@ import CodeEditCore struct SourceControlNavigatorChangesList: View { @EnvironmentObject var sourceControlManager: SourceControlManager - @Environment(\.workspaceNavigator) private var workspaceNavigator + @Environment(\.workspaceNavigator) + private var workspaceNavigator @Environment(\.workspaceFileManager) private var workspaceFileManager diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift index 31425bb4da..8fc4b594c1 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift @@ -44,7 +44,11 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: nil )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), eventBus: EventBus())) + .environmentObject(SourceControlManager( + workspaceURL: URL(filePath: "/Users/CodeEdit"), + shellClient: ShellClient(), + eventBus: EventBus() + )) GitChangedFileLabel(file: GitChangedFile( status: .none, @@ -52,6 +56,10 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: "app2.jsx" )) - .environmentObject(SourceControlManager(workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), eventBus: EventBus())) + .environmentObject(SourceControlManager( + workspaceURL: URL(filePath: "/Users/CodeEdit"), + shellClient: ShellClient(), + eventBus: EventBus() + )) }.padding() } diff --git a/CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift b/CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift index e47c5c43ed..3a38ccbd45 100644 --- a/CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift +++ b/CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift @@ -11,7 +11,8 @@ import CodeEditCore struct OpenQuicklyPreviewView: View { let item: CEWorkspaceFile - @Environment(\.filePreview) private var filePreview + @Environment(\.filePreview) + private var filePreview var body: some View { filePreview(item) diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index d0e5d75929..351fa070f5 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -15,7 +15,8 @@ struct SourceControlGeneralView: View { @AppSettings(\.sourceControl.general) var settings - @Environment(\.shellClient) private var shellClient + @Environment(\.shellClient) + private var shellClient private var gitConfig: GitConfigClient { GitConfigClient(shellClient: shellClient ?? ShellClient()) } diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift index 82b2be0c20..4be4f10ae0 100644 --- a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift +++ b/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift @@ -15,8 +15,10 @@ struct SourceControlGitView: View { @AppSettings(\.sourceControl.git) var git - @Environment(\.shellClient) private var shellClient - @Environment(\.workspaceWindowManager) private var windowManager + @Environment(\.shellClient) + private var shellClient + @Environment(\.workspaceWindowManager) + private var windowManager private var gitConfig: GitConfigClient { GitConfigClient(shellClient: shellClient ?? ShellClient()) } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift index a0a51dcb54..afebdb39ad 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift @@ -11,7 +11,7 @@ import UniformTypeIdentifiers extension ThemeModel { /// Loads all available themes from disk, applies overrides, and selects the initial theme. - func loadThemes() throws { // swiftlint:disable:this function_body_length + func loadThemes() throws { themes.removeAll() let prefs = Settings.shared.preferences diff --git a/CodeEdit/Features/Settings/SettingsView.swift b/CodeEdit/Features/Settings/SettingsView.swift index 1a19099bb2..b330f3353c 100644 --- a/CodeEdit/Features/Settings/SettingsView.swift +++ b/CodeEdit/Features/Settings/SettingsView.swift @@ -10,7 +10,8 @@ import CodeEditSettings /// A struct for settings struct SettingsView: View { - @Environment(\.registryManager) private var registryManager + @Environment(\.registryManager) + private var registryManager @StateObject var model = SettingsViewModel() @Environment(\.colorScheme) diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift index 1eb01d1cf2..aca59a26a2 100644 --- a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift +++ b/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift @@ -13,7 +13,8 @@ import UniformTypeIdentifiers struct StatusBarFileInfoView: View { @EnvironmentObject private var statusBarViewModel: StatusBarViewModel - @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.activeEditorState) + private var activeEditorState @State private var fileSize: Int? @State private var dimensions: ImageDimensions? diff --git a/CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift b/CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift index 94ce07b0c5..f584ffc33f 100644 --- a/CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift @@ -81,51 +81,73 @@ final class CodeEditSplitViewController: NSSplitViewController { assertionFailure("Missing workspace=\(workspace == nil) or navigator=\(navigatorViewModel == nil)") return } - let editorManager = workspace.editorManager - let projectNavigatorViewModel = workspace.projectNavigatorViewModel - let sourceControlManager = workspace.sourceControlManager - let sourceControlViewModel = workspace.sourceControlViewModel - let searchState = workspace.searchState - let taskManager = workspace.taskManager splitView.translatesAutoresizingMaskIntoConstraints = false - let activeEditorState = AppActiveEditorState(editorManager: editorManager) + let activeEditorState = AppActiveEditorState(editorManager: workspace.editorManager) self.activeEditorState = activeEditorState - let activeCursorState = AppActiveCursorState(editorManager: editorManager) + let activeCursorState = AppActiveCursorState(editorManager: workspace.editorManager) self.activeCursorState = activeCursorState - let fileEditorOverrides = AppFileEditorOverrides(editorManager: editorManager) + let fileEditorOverrides = AppFileEditorOverrides(editorManager: workspace.editorManager) self.fileEditorOverrides = fileEditorOverrides - let navigator = makeNavigator(view: SettingsInjector { + addSplitViewItem(makeNavigatorItem( + workspace: workspace, + navigatorViewModel: navigatorViewModel, + activeEditorState: activeEditorState + )) + addSplitViewItem(makeMainContentItem( + workspace: workspace, + windowRef: windowRef, + activeEditorState: activeEditorState, + activeCursorState: activeCursorState + )) + addSplitViewItem(makeInspectorItem( + workspace: workspace, + activeEditorState: activeEditorState, + fileEditorOverrides: fileEditorOverrides + )) + } + + private func makeNavigatorItem( + workspace: Workspace, + navigatorViewModel: NavigatorAreaViewModel, + activeEditorState: AppActiveEditorState + ) -> NSSplitViewItem { + makeNavigator(view: SettingsInjector { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) - .environmentObject(editorManager) + .environmentObject(workspace.editorManager) .environmentObject(workspace.listenerModel) - .environmentObject(projectNavigatorViewModel) - .environmentObject(sourceControlManager) - .environmentObject(sourceControlViewModel) - .environmentObject(searchState) + .environmentObject(workspace.projectNavigatorViewModel) + .environmentObject(workspace.sourceControlManager) + .environmentObject(workspace.sourceControlViewModel) + .environmentObject(workspace.searchState) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.workspaceFileURL, workspace.fileURL) .environment(\.activeEditorState, activeEditorState) .appServices(dependencies) }) + } - addSplitViewItem(navigator) - + private func makeMainContentItem( + workspace: Workspace, + windowRef: NSWindow, + activeEditorState: AppActiveEditorState, + activeCursorState: AppActiveCursorState + ) -> NSSplitViewItem { let workspaceView = SettingsInjector { WindowObserver(window: WindowBox(value: windowRef)) { WorkspaceView() - .environmentObject(editorManager) + .environmentObject(workspace.editorManager) .environmentObject(statusBarViewModel) .environmentObject(utilityAreaModel) - .environmentObject(taskManager) - .environmentObject(sourceControlManager) - .environmentObject(sourceControlViewModel) + .environmentObject(workspace.taskManager) + .environmentObject(workspace.sourceControlManager) + .environmentObject(workspace.sourceControlViewModel) .environmentObject(workspace.listenerModel) .environmentObject(workspace.undoRegistration) .environmentObject(notificationPanel) @@ -142,21 +164,24 @@ final class CodeEditSplitViewController: NSSplitViewController { let mainContent = NSSplitViewItem(viewController: NSHostingController(rootView: workspaceView)) mainContent.titlebarSeparatorStyle = .line mainContent.minimumThickness = 200 + return mainContent + } - addSplitViewItem(mainContent) - - let inspector = makeInspector(view: SettingsInjector { + private func makeInspectorItem( + workspace: Workspace, + activeEditorState: AppActiveEditorState, + fileEditorOverrides: AppFileEditorOverrides + ) -> NSSplitViewItem { + makeInspector(view: SettingsInjector { InspectorAreaView(viewModel: InspectorAreaViewModel()) - .environmentObject(editorManager) - .environmentObject(sourceControlManager) + .environmentObject(workspace.editorManager) + .environmentObject(workspace.sourceControlManager) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.activeEditorState, activeEditorState) .environment(\.fileEditorOverrides, fileEditorOverrides) .appServices(dependencies) }) - - addSplitViewItem(inspector) } private func makeNavigator(view: some View) -> NSSplitViewItem { diff --git a/CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift b/CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift index adee24a129..a0610adc19 100644 --- a/CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift +++ b/CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift @@ -36,10 +36,8 @@ final class ApplicationShutdownCoordinator { // Check for unsaved changes and prompt the user let hasUnsavedChanges = workspaces.contains { $0.hasUnsavedChanges() } if hasUnsavedChanges { - for workspace in workspaces { - if !workspace.promptSaveUnsavedFiles() { - return false // User cancelled - } + for workspace in workspaces where !workspace.promptSaveUnsavedFiles() { + return false // User cancelled } } diff --git a/CodeEdit/Features/Workspace/Workspace.swift b/CodeEdit/Features/Workspace/Workspace.swift index 8e3e4d557a..97d89a8b98 100644 --- a/CodeEdit/Features/Workspace/Workspace.swift +++ b/CodeEdit/Features/Workspace/Workspace.swift @@ -44,7 +44,6 @@ final class Workspace: ObservableObject { /// from recents in the sandbox); released in ``tearDown()``. var securityScopedURL: URL? - // swiftlint:disable:next function_parameter_count init( fileURL: URL, displayName: String, diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/Features/Workspace/WorkspaceFactory.swift index be993e51df..eca21a2505 100644 --- a/CodeEdit/Features/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/Features/Workspace/WorkspaceFactory.swift @@ -24,20 +24,7 @@ enum WorkspaceFactory { /// Builds a fully-populated ``Workspace`` for the given folder URL. @MainActor static func make(url: URL, dependencies: AppDependencies) -> Workspace { - // Begin security-scoped access on the original (possibly bookmark-derived) URL so a - // sandboxed build can read a workspace opened from recents. `startAccessingSecurityScopedResource` - // returns `false` for non-scoped URLs (e.g. from the open panel / Powerbox), which access - // fine without it. Released in `Workspace.tearDown`. - var securityScopedURL: URL? - if url.startAccessingSecurityScopedResource() { - securityScopedURL = url - } - - // Normalize the URL to always end with "/" - var url = url - if !url.absoluteString.hasSuffix("/") { - url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") - } + let (url, securityScopedURL) = prepareWorkspaceURL(url) let eventBus = dependencies.eventBus let statePersistence = WorkspaceStatePersistence(workspaceURL: url) @@ -91,4 +78,24 @@ enum WorkspaceFactory { return workspace } + + /// Claims security-scoped access and normalizes the workspace URL. + /// + /// Begins security-scoped access on the original (possibly bookmark-derived) URL so a + /// sandboxed build can read a workspace opened from recents. `startAccessingSecurityScopedResource` + /// returns `false` for non-scoped URLs (e.g. from the open panel / Powerbox), which access + /// fine without it. Released in `Workspace.tearDown`. The returned URL always ends with "/". + private static func prepareWorkspaceURL(_ url: URL) -> (url: URL, securityScopedURL: URL?) { + var securityScopedURL: URL? + if url.startAccessingSecurityScopedResource() { + securityScopedURL = url + } + + var url = url + if !url.absoluteString.hasSuffix("/") { + url = URL(filePath: url.absoluteURL.path(percentEncoded: false) + "/") + } + + return (url, securityScopedURL) + } } diff --git a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift index e847ed9649..40933e5004 100644 --- a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift +++ b/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileManagerEventsTests.swift @@ -26,19 +26,19 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { func testAppliesGitStatusChangedEvent() throws { let bus = EventBus() - let fm = CEWorkspaceFileManager( + let manager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], eventBus: bus ) let key = directory.appending(path: "changed.swift").relativePath - XCTAssertNotNil(fm.getFile(key), "file should be cached after init") + XCTAssertNotNil(manager.getFile(key), "file should be cached after init") bus.publish(GitStatusChangedEvent(workspaceURL: directory, changed: [key: .modified])) let expectation = expectation(description: "status applied") DispatchQueue.main.async { - XCTAssertEqual(fm.getFile(key)?.gitStatus, .modified) + XCTAssertEqual(manager.getFile(key)?.gitStatus, .modified) expectation.fulfill() } wait(for: [expectation], timeout: 2) @@ -46,19 +46,19 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { func testClearsStaleGitStatus() throws { let bus = EventBus() - let fm = CEWorkspaceFileManager( + let manager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], eventBus: bus ) let key = directory.appending(path: "changed.swift").relativePath - fm.getFile(key)?.gitStatus = .modified + manager.getFile(key)?.gitStatus = .modified bus.publish(GitStatusChangedEvent(workspaceURL: directory, changed: [:])) let expectation = expectation(description: "status cleared") DispatchQueue.main.async { - XCTAssertNil(fm.getFile(key)?.gitStatus) + XCTAssertNil(manager.getFile(key)?.gitStatus) expectation.fulfill() } wait(for: [expectation], timeout: 2) @@ -66,7 +66,7 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { func testIgnoresEventsForOtherWorkspaces() throws { let bus = EventBus() - let fm = CEWorkspaceFileManager( + let manager = CEWorkspaceFileManager( folderUrl: directory, ignoredFilesAndFolders: [], eventBus: bus @@ -77,7 +77,7 @@ final class CEWorkspaceFileManagerEventsTests: XCTestCase { let expectation = expectation(description: "no apply") DispatchQueue.main.async { - XCTAssertNil(fm.getFile(key)?.gitStatus) + XCTAssertNil(manager.getFile(key)?.gitStatus) expectation.fulfill() } wait(for: [expectation], timeout: 2) diff --git a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift index 26f1847553..6434053944 100644 --- a/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift +++ b/CodeEditTests/Features/Tasks/CEActiveTaskTests.swift @@ -54,7 +54,10 @@ class CEActiveTaskTests { func testHandleProcessFinished(_ shell: Shell) async throws { // CETask is a value type, so build a fresh active task around the failing command // rather than mutating `task` after `activeTask` already copied it. - let activeTask = CEActiveTask(task: CETask(name: "Test Task", command: "aNon-existentCommand"), eventBus: EventBus()) + let activeTask = CEActiveTask( + task: CETask(name: "Test Task", command: "aNon-existentCommand"), + eventBus: EventBus() + ) activeTask.run(workspaceURL: nil, shell: shell) activeTask.waitForExit() diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift index b28eccafb5..59b2c6938a 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -37,7 +37,7 @@ extension EditorManager { findReplaceQuery: findReplaceQuery, editorManager: self ) { - case .restored(let layout, let activeEditor): + case let .restored(layout, activeEditor): self.editorLayout = layout self.activeEditor = activeEditor switchToActiveEditor() diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift index 2662acf0c6..ab8fb6bf93 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift @@ -10,7 +10,8 @@ import CodeEditCore enum FileIcon { - static func fileIcon(fileType: FileType?) -> String { // swiftlint:disable:this cyclomatic_complexity function_body_length + // swiftlint:disable:next cyclomatic_complexity function_body_length + static func fileIcon(fileType: FileType?) -> String { switch fileType { case .json, .yml, .resolved: return "doc.json" diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift index 4bdcf6d5ec..70a52ecf23 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift @@ -69,7 +69,8 @@ struct CodeFileView: View { @EnvironmentObject var undoRegistration: UndoManagerRegistration - @Environment(\.currentTheme) private var injectedTheme + @Environment(\.currentTheme) + private var injectedTheme @State private var treeSitter = TreeSitterClient() diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift index 39f2f2a852..8daccda23a 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift @@ -27,11 +27,17 @@ public struct FilePreviewView: View { self._document = .init(wrappedValue: doc ?? .init()) } - @Environment(\.languageServices) private var languageServices + @Environment(\.languageServices) + private var languageServices public var body: some View { if let utType = document.utType, utType.conforms(to: .text) { - CodeFileView(editorInstance: editorInstance, codeFile: document, languageServices: languageServices, isEditable: false) + CodeFileView( + editorInstance: editorInstance, + codeFile: document, + languageServices: languageServices, + isEditable: false + ) .environmentObject(undoRegistration) } else { NonTextFileView(fileDocument: document) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift index 3f027ae601..59944c9e70 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift @@ -27,7 +27,8 @@ public struct WindowCodeFileView: View { self.codeFile = codeFile } - @Environment(\.languageServices) private var languageServices + @Environment(\.languageServices) + private var languageServices public var body: some View { if let utType = codeFile.utType, utType.conforms(to: .text) { diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift b/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift index 9ad60536f8..1263c74793 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift @@ -21,7 +21,9 @@ import LanguageServerProtocol /// keeps an async stream around for the duration of its lifetime. The stream is sent edit notifications, which are then /// chunked into 250ms timed groups before being sent to the ``LanguageServer``. @MainActor -class LSPContentCoordinator: @preconcurrency TextViewCoordinator, @preconcurrency TextViewDelegate { +class LSPContentCoordinator< + DocumentType: LanguageServerDocument +>: @preconcurrency TextViewCoordinator, @preconcurrency TextViewDelegate { // Required to avoid a large_tuple lint error private struct SequenceElement: Sendable { let uri: String diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift index e6d1a9d2e3..2fafd03341 100644 --- a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift +++ b/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift @@ -11,7 +11,8 @@ import Combine struct FindNavigatorResultList: NSViewControllerRepresentable { @EnvironmentObject var state: SearchState - @Environment(\.workspaceFileOpener) private var fileOpener + @Environment(\.workspaceFileOpener) + private var fileOpener let configuration: FindNavigatorConfiguration diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift index 5f69553ea1..2b593fb970 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift @@ -10,14 +10,14 @@ import Foundation // TODO: DOCS (Nanashi Li) enum GitTime { + // nonisolated(unsafe): configured once here and never mutated afterwards; + // DateFormatter is thread-safe for reading once configuration is complete. /** A date formatter for RFC 3339 style timestamps. Uses POSIX locale and GMT timezone so that date values are parsed as absolutes. - (https://tools.ietf.org/html/rfc3339) - (https://developer.apple.com/library/mac/qa/qa1480/_index.html) */ - // nonisolated(unsafe): configured once here and never mutated afterwards; - // DateFormatter is thread-safe for reading once configuration is complete. nonisolated(unsafe) static let rfc3339DateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'" diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift index 52832d8635..40509fe437 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift @@ -43,7 +43,8 @@ public struct SourceControlPullView: View { .scrollContentBackground(.hidden) .onAppear { Task { - preferRebaseWhenPulling = try await sourceControlManager.gitConfig.get(key: "pull.rebase", global: true) ?? false + preferRebaseWhenPulling = try await sourceControlManager.gitConfig + .get(key: "pull.rebase", global: true) ?? false if preferRebaseWhenPulling { sourceControlViewModel.operationRebase = true } diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift index 2960663cf4..888826ccdc 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift @@ -55,7 +55,8 @@ public protocol CELocalShellTerminalViewDelegate: AnyObject { // MARK: - CELocalShellTerminalView @MainActor -public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalViewDelegate, @preconcurrency LocalProcessDelegate { +public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalViewDelegate, + @preconcurrency LocalProcessDelegate { public var process: LocalProcess! override public init(frame: CGRect) { diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift index fd64f060c5..b1f817b637 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -29,8 +29,10 @@ public struct TerminalEmulatorView: NSViewRepresentable { @AppSettings(\.textEditing.font) var fontSettings - @Environment(\.currentTheme) private var currentTheme - @Environment(\.currentDarkTheme) private var currentDarkTheme + @Environment(\.currentTheme) + private var currentTheme + @Environment(\.currentDarkTheme) + private var currentDarkTheme private var font: NSFont { if terminalSettings.useTextEditorFont { @@ -52,7 +54,12 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// - terminalID: The ID of the terminal. Used to restore state when switching away from the view. /// - shellType: The type of shell to use. Overrides any settings or auto-detection. /// - onTitleChange: A callback used when the terminal updates it's title. - public init(url: URL, terminalID: UUID, shellType: Shell? = nil, onTitleChange: @escaping (_ title: String) -> Void) { + public init( + url: URL, + terminalID: UUID, + shellType: Shell? = nil, + onTitleChange: @escaping (_ title: String) -> Void + ) { self.url = url self.terminalID = terminalID self.mode = .shell(shellType: shellType) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift index 87a5143b5a..d391367659 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift @@ -17,7 +17,8 @@ public protocol ActiveCursorState: AnyObject { @MainActor var cursorPositionsPublisher: AnyPublisher<[EditorCursorPosition], Never> { get } /// Number of lines contained by `range` in the active editor's live text view. /// Returns 0 when there is no active editor or the lines cannot be resolved. - @MainActor func linesInRange(_ range: NSRange) -> Int + @MainActor + func linesInRange(_ range: NSRange) -> Int } /// Default used when no cursor state is injected (tests, previews); reports no cursor. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift index 0b2b97be25..5765d0b10e 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift @@ -10,10 +10,12 @@ import Foundation /// Posts a user-visible error notification. A one-method seam so packages can /// surface errors without depending on the CENotifications feature package. public protocol ErrorNotifying: AnyObject { - @MainActor func postError(title: String, description: String) + @MainActor + func postError(title: String, description: String) } public final class NoOpErrorNotifier: ErrorNotifying { public init() {} - @MainActor public func postError(title: String, description: String) {} + @MainActor + public func postError(title: String, description: String) {} } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift index c6082b4938..a6e8a1d6a5 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift @@ -12,11 +12,16 @@ import Foundation /// `EditorManager` / `CodeFileDocument`. Workspace-scoped: one per window. public protocol FileEditorOverrides: AnyObject { /// Current overrides for `file`; all fields `nil` when the file has no open document. - @MainActor func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues - @MainActor func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) - @MainActor func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) - @MainActor func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) - @MainActor func setLanguageId(_ value: String?, for file: CEWorkspaceFile) + @MainActor + func overrides(for file: CEWorkspaceFile) -> FileEditorOverrideValues + @MainActor + func setIndentOption(_ value: IndentOption?, for file: CEWorkspaceFile) + @MainActor + func setDefaultTabWidth(_ value: Int?, for file: CEWorkspaceFile) + @MainActor + func setWrapLines(_ value: Bool?, for file: CEWorkspaceFile) + @MainActor + func setLanguageId(_ value: String?, for file: CEWorkspaceFile) } /// Default used when no editor is injected (tests, previews); no overrides, no-op writes. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift index 9fcb4e88fa..98af9373b8 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift @@ -12,7 +12,8 @@ import Foundation /// Inspector rename/relocate without depending on the Editor feature or holding /// a `Workspace`. Returns the resolved new file (nil for folders or unresolved workspace). public protocol FileRelocator: AnyObject { - @MainActor func relocate(file: CEWorkspaceFile, to destination: URL) throws -> CEWorkspaceFile? + @MainActor + func relocate(file: CEWorkspaceFile, to destination: URL) throws -> CEWorkspaceFile? } /// Default used when no app shell is present (tests, previews); performs no move. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift index ec77fc33ec..e18904548b 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift @@ -13,19 +13,25 @@ public protocol WorkspaceNavigator: AnyObject { /// Open `file` in its owning workspace's active editor. /// - Parameter asTemporary: open as a temporary (preview) tab, replaced by the next /// temporary open, rather than a pinned tab. - @MainActor func open(file: CEWorkspaceFile, asTemporary: Bool) + @MainActor + func open(file: CEWorkspaceFile, asTemporary: Bool) /// Highlight `file` in the project navigator without opening it. - @MainActor func reveal(file: CEWorkspaceFile) + @MainActor + func reveal(file: CEWorkspaceFile) /// Close all tabs showing `file` across every editor split. - @MainActor func closeTab(file: CEWorkspaceFile) + @MainActor + func closeTab(file: CEWorkspaceFile) } /// Default no-op used until the app registers a real implementation. public final class NoOpWorkspaceNavigator: WorkspaceNavigator { public init() {} - @MainActor public func open(file: CEWorkspaceFile, asTemporary: Bool) {} - @MainActor public func reveal(file: CEWorkspaceFile) {} - @MainActor public func closeTab(file: CEWorkspaceFile) {} + @MainActor + public func open(file: CEWorkspaceFile, asTemporary: Bool) {} + @MainActor + public func reveal(file: CEWorkspaceFile) {} + @MainActor + public func closeTab(file: CEWorkspaceFile) {} } diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift index 017d8eab48..1ad721d655 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift @@ -250,7 +250,10 @@ public final class CodeFileDocument: NSDocument, ObservableObject { } /// Triggered when changes saved - override public func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) { + override public func updateChangeCount( + withToken changeCountToken: Any, + for saveOperation: NSDocument.SaveOperationType + ) { super.updateChangeCount(withToken: changeCountToken, for: saveOperation) if CodeFileDocument.autosavesInPlace { diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift index 6af057da2f..37f21ab0c3 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift @@ -28,7 +28,8 @@ extension SettingsData { @CodableDefault public var dimEditorsWithoutFocus = false /// The show file extensions behavior of the app - @CodableDefault public var fileExtensionsVisibility: FileExtensionsVisibility = .showAll + @CodableDefault public var fileExtensionsVisibility: + FileExtensionsVisibility = .showAll /// The file extensions collection to display @CodableDefault public var shownFileExtensions: FileExtensions = .default @@ -40,16 +41,19 @@ extension SettingsData { @CodableDefault public var fileIconStyle: FileIconStyle = .color /// The position for the navigator sidebar tab bar - @CodableDefault public var navigatorTabBarPosition: SidebarTabBarPosition = .top + @CodableDefault public var navigatorTabBarPosition: + SidebarTabBarPosition = .top /// The position for the inspector sidebar tab bar - @CodableDefault public var inspectorTabBarPosition: SidebarTabBarPosition = .top + @CodableDefault public var inspectorTabBarPosition: + SidebarTabBarPosition = .top /// The reopen behavior of the app @CodableDefault public var reopenBehavior: ReopenBehavior = .welcome /// Decides what the app does after a workspace is closed - @CodableDefault public var reopenWindowAfterClose: ReopenWindowBehavior = .doNothing + @CodableDefault public var reopenWindowAfterClose: + ReopenWindowBehavior = .doNothing /// The size of the project navigator @CodableDefault public var projectNavigatorSize: ProjectNavigatorSize = .medium diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift index 531ebf5474..abcced41da 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift @@ -11,8 +11,8 @@ extension SettingsData { public struct LanguageServerSettings: Codable, Hashable { /// Stores the currently installed language servers. The key is the name of the language server. - @CodableDefault - public var installedLanguageServers: [String: InstalledLanguageServer] = [:] + @CodableDefault public var installedLanguageServers: + [String: InstalledLanguageServer] = [:] /// Default initializer public init() {} diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift index e2101d771a..695beb15ee 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift @@ -195,7 +195,8 @@ extension SettingsData { } public struct WarningCharacters: Equatable, Hashable, Codable { - nonisolated(unsafe) public static let `default`: WarningCharacters = WarningCharacters(enabled: true, characters: [ + nonisolated(unsafe) public static let `default`: WarningCharacters = + WarningCharacters(enabled: true, characters: [ 0x0003: "End of text", 0x00A0: "Non-breaking space", diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift index bdf7cbadc8..53c51e9a5e 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift @@ -8,7 +8,8 @@ import SwiftUI public struct EdgeInsetsEnvironmentKey: EnvironmentKey { - nonisolated(unsafe) public static var defaultValue: EdgeInsets = EdgeInsets(top: 1, leading: 0, bottom: 0, trailing: 0) + nonisolated(unsafe) public static var defaultValue: EdgeInsets = + EdgeInsets(top: 1, leading: 0, bottom: 0, trailing: 0) } public extension EnvironmentValues { From 438869f4081eb4a81ca89aaf140e757ebb9f63e7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 14 Jul 2026 19:16:56 +0200 Subject: [PATCH 176/335] Docs: Document the public API of all local packages --- .../Sources/CEEditor/Environment+SplitEditor.swift | 1 + .../CEEditor/Models/Editor/Editor+TabSwitch.swift | 2 ++ .../EditorLayout/EditorLayout+StateRestoration.swift | 3 +++ .../CEEditor/Models/Environment+ActiveEditor.swift | 1 + .../Sources/CEEditor/Models/Theme+EditorTheme.swift | 1 + .../Sources/CEEditor/UseCases/EditorRestorer.swift | 1 + .../Sources/CEEditor/Views/EditorLayoutView.swift | 1 + .../CELSP/LanguageServer/LanguageServer.swift | 1 + .../Install/InstallStepConfirmation.swift | 3 +++ .../Sources/CESearch/Extensions/Array+Index.swift | 2 ++ .../Indexer/SearchIndexer+AsyncController.swift | 3 +++ .../Accounts/Bitbucket/Model/BitBucketUser.swift | 5 +++++ .../Accounts/GitHub/GitHubAccount.swift | 2 ++ .../Accounts/GitLab/GitLabAccount.swift | 2 ++ .../Accounts/Networking/GitRouter.swift | 12 ++++++++++++ .../Accounts/Networking/GitURLSession.swift | 7 +++++++ .../CESourceControl/Client/GitClient+Clone.swift | 1 + .../CESourceControl/Client/GitClient+Commit.swift | 3 +++ .../CESourceControl/Client/GitClient+Status.swift | 1 + .../CETerminal/Tasks/Models/CETaskStatus.swift | 10 ++++++---- .../TerminalEmulator/Model/TerminalCache.swift | 1 + .../CodeEditCore/Domain/Search/FuzzySearchable.swift | 3 +++ .../CodeEditCore/Extensions/String+Escaped.swift | 1 + .../CodeEditCore/Extensions/URL+FileName.swift | 1 + .../CodeEditCore/Extensions/URL+ResourceValues.swift | 3 +++ .../CodeEditDocument/LanguageServicesProvider.swift | 4 ++++ .../Sources/CodeEditSettings/Loopable.swift | 1 + .../CodeEditSettings/Models/Theme+Color.swift | 2 ++ .../Sources/CodeEditSettings/Store/AppSettings.swift | 2 ++ .../CodeEditSettings/Store/CodableDefault.swift | 2 ++ .../Sources/CodeEditSettings/Store/Color+HEX.swift | 9 +++++++++ .../CodeEditSettings/Store/Environment+Theme.swift | 2 ++ .../EnvironmentKeys/Environment+IsFullscreen.swift | 2 ++ .../EnvironmentKeys/Environment+ModifierKeys.swift | 2 ++ .../EnvironmentKeys/Environment+Window.swift | 6 ++++++ .../Sources/CodeEditUI/LayoutMetrics.swift | 2 ++ .../Sources/CodeEditUI/Styles/BlurButtonStyle.swift | 2 ++ .../Sources/CodeEditUI/Styles/IconButtonStyle.swift | 4 ++++ .../Sources/CodeEditUI/Styles/IconToggleStyle.swift | 4 ++++ .../Sources/CodeEditUI/Styles/View+actionBar.swift | 2 ++ .../Views/SplitView/Environment+ContentInsets.swift | 2 ++ .../Views/SplitView/SplitViewModifiers.swift | 4 ++++ .../CodeEditUI/Views/SplitView/SplitViewReader.swift | 5 +++++ .../CodeEditUI/Views/SplitView/Variadic.swift | 2 ++ .../Sources/CodeEditUI/Views/View+if.swift | 5 +++++ 45 files changed, 131 insertions(+), 4 deletions(-) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift b/Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift index 81e56a0df1..e8e2956bbd 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift @@ -12,6 +12,7 @@ public struct SplitEditorEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// A closure that splits the current editor towards the given edge, inserting the provided editor. var splitEditor: SplitEditorEnvironmentKey.Value { get { self[SplitEditorEnvironmentKey.self] } set { self[SplitEditorEnvironmentKey.self] = newValue } diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift index ae6587b596..6539308404 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift @@ -8,6 +8,7 @@ import Foundation extension Editor { + /// Selects the tab after the current one, wrapping around to the first tab when at the end. public func selectNextTab() { guard let currentTab = selectedTab, let currentIndex = tabs.firstIndex(of: currentTab) else { return } let nextIndex = tabs.index(after: currentIndex) @@ -19,6 +20,7 @@ extension Editor { } } + /// Selects the tab before the current one, wrapping around to the last tab when at the beginning. public func selectPreviousTab() { guard let currentTab = selectedTab, let currentIndex = tabs.firstIndex(of: currentTab) else { return } let previousIndex = tabs.index(before: currentIndex) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift index 59b2c6938a..68de7b710d 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift @@ -48,6 +48,9 @@ extension EditorManager { } } + /// Encodes the current editor layout and active editor, storing it with the persistence service + /// for `restoreFromState` to load on the next launch. + /// - Parameter statePersistence: The persistence service to save the captured state to. public func saveRestorationState(_ statePersistence: any WorkspaceStatePersisting) { if let data = try? JSONEncoder().encode( EditorRestorationState(activeEditor: activeEditor.id, groups: editorLayout) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift index f89c11e8c3..1c55c54ce2 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift @@ -12,6 +12,7 @@ public struct ActiveEditorEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// Whether the editor this view belongs to is the focused editor in the window. var isActiveEditor: Bool { get { self[ActiveEditorEnvironmentKey.self] } set { self[ActiveEditorEnvironmentKey.self] = newValue } diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift index 2abf5f12b7..2875a081bc 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift @@ -10,6 +10,7 @@ import CodeEditSourceEditor import AppKit public extension Theme.EditorColors { + /// Bridges the settings theme's editor colors to a source editor `EditorTheme`, converting in both directions. var editorTheme: EditorTheme { get { .init( diff --git a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift index 365e444c63..8345065f10 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift @@ -13,6 +13,7 @@ import OrderedCollections /// Restores an editor layout from persisted state, resolving file references against the current file manager. public final class EditorRestorer { + /// The result of a restoration attempt, telling the caller how to proceed. public enum Outcome { /// Persisted state was loaded and resolved successfully. case restored(layout: EditorLayout, activeEditor: Editor) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift b/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift index 4196bff0ac..1ca49a5a4b 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift @@ -98,6 +98,7 @@ public struct BelowToolbarEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// The vertical edges at which this editor layout borders the window, used to adjust chrome near the toolbar. var isEditorLayoutAtEdge: BelowToolbarEnvironmentKey.Value { get { self[BelowToolbarEnvironmentKey.self] } set { self[BelowToolbarEnvironmentKey.self] = newValue } diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift index f422631436..9ee535d9fe 100644 --- a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift +++ b/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift @@ -46,6 +46,7 @@ public class LanguageServer { /// The configuration options this server supports. var serverCapabilities: ServerCapabilities + /// Buffers log messages received from the server for display in the language server UI. public var logContainer: LanguageServerLogContainer /// An instance of a language server, that may or may not be initialized diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift index c2f82b08b1..6404a939d8 100644 --- a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift +++ b/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift @@ -5,7 +5,10 @@ // Created by Khan Winter on 8/8/25. // +/// Whether a package installation step requires the user's confirmation before it executes. public enum InstallStepConfirmation { + /// No confirmation is needed; the step can run immediately. case none + /// The user must approve the step before it runs; `message` describes what will happen. case required(message: String) } diff --git a/Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift b/Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift index 69b73c4c12..705a32217c 100644 --- a/Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift +++ b/Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift @@ -6,10 +6,12 @@ // public extension Array { + /// The second element of the array, or `nil` if the array has fewer than two elements. var second: Element? { self.count > 1 ? self[1] : nil } + /// The third element of the array, or `nil` if the array has fewer than three elements. var third: Element? { self.count > 2 ? self[2] : nil } diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift index 3e5ff04b53..f8d1e0147e 100644 --- a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift +++ b/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift @@ -15,6 +15,9 @@ extension SearchIndexer { private let addQueue = DispatchQueue(label: "app.codeedit.CodeEdit.AddFilesToIndex", attributes: .concurrent) private let searchQueue = DispatchQueue(label: "app.codeedit.CodeEdit.SearchIndex", attributes: .concurrent) + /// Create an async manager wrapping an index. + /// + /// - Parameter index: The index to perform asynchronous operations on. public init(index: SearchIndexer) { self.index = index } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift index 8bbe89e90e..81630558a9 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift @@ -37,6 +37,11 @@ class BitBucketEmail: Codable { extension BitBucketAccount { + /// Fetches the profile of the currently authenticated Bitbucket user. + /// - Parameters: + /// - session: The session used to make the request; defaults to the shared session. + /// - completion: Called with the user on success, or the request error on failure. + /// - Returns: The started network task, or `nil` if the request could not be constructed. public func me( _ session: GitURLSession = URLSession.shared, completion: @escaping (_ response: Result) -> Void diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift index aaef39c5a9..fa7adf5410 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift @@ -9,9 +9,11 @@ import Foundation // TODO: DOCS (Nanashi Li) +/// Entry point for GitHub API requests, bound to the configuration of a signed-in (or anonymous) account. public struct GitHubAccount { let configuration: GitHubTokenConfiguration + /// Creates an account using the given token configuration; defaults to an unauthenticated `github.com` setup. public init(_ config: GitHubTokenConfiguration = GitHubTokenConfiguration()) { configuration = config } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift index 5be40e8e33..8c160afa6a 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift @@ -9,9 +9,11 @@ import Foundation // TODO: DOCS (Nanashi Li) +/// Entry point for GitLab API requests, bound to the configuration of a signed-in (or anonymous) account. public struct GitLabAccount { let configuration: GitRouterConfiguration + /// Creates an account using the given router configuration; defaults to an unauthenticated `gitlab.com` setup. public init(_ config: GitRouterConfiguration = GitLabTokenConfiguration()) { configuration = config } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift index 962dec7adf..51b54f7dc1 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift @@ -18,33 +18,45 @@ enum GitHTTPEncoding: Int { case url, form, json } +/// A single HTTP header (field name and value) to attach to git provider API requests. public struct GitHTTPHeader { var headerField: String var value: String } +/// Describes how to reach and authenticate against a git provider's REST API (GitHub, GitLab, Bitbucket). public protocol GitRouterConfiguration { + /// The base URL of the provider's API, e.g. `https://api.github.com`. var apiEndpoint: String? { get } + /// The token used to authenticate requests, if the account is signed in. var accessToken: String? { get } + /// The query-parameter name the provider expects the access token under. var accessTokenFieldName: String? { get } + /// The authorization scheme (e.g. `Bearer`) used to send the token in an `Authorization` header instead. var authorizationHeader: String? { get } + /// The domain used when constructing errors for failed requests. var errorDomain: String? { get } + /// Additional headers to attach to every request made with this configuration. var customHeaders: [GitHTTPHeader]? { get } } extension GitRouterConfiguration { + /// By default the access token is sent in the `access_token` query field. public var accessTokenFieldName: String? { "access_token" } + /// By default the token is sent as a query parameter instead of an `Authorization` header. public var authorizationHeader: String? { nil } + /// The default domain used for errors produced by failed account requests. public var errorDomain: String? { "com.codeedit.models.accounts.networking" } + /// By default no additional headers are attached to requests. public var customHeaders: [GitHTTPHeader]? { nil } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift index 015b981c1c..08bb5ebcb8 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift @@ -13,13 +13,16 @@ import FoundationNetworking #endif // TODO: DOCS (Nanashi Li) +/// Abstraction over `URLSession` for git account API requests, allowing the session to be mocked in tests. public protocol GitURLSession { + /// Creates a data task that fetches the given request and calls the handler with the response. func dataTask( with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void ) -> GitURLSessionDataTaskProtocol + /// Creates a task that uploads the given body data for the request and calls the handler with the response. func uploadTask( with request: URLRequest, fromData bodyData: Data?, @@ -27,12 +30,14 @@ public protocol GitURLSession { ) -> GitURLSessionDataTaskProtocol #if !canImport(FoundationNetworking) + /// Fetches the given request asynchronously, returning the response body and metadata. @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) func data( for request: URLRequest, delegate: URLSessionTaskDelegate? ) async throws -> (Data, URLResponse) + /// Uploads the given body data for the request asynchronously, returning the response body and metadata. @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) func upload( for request: URLRequest, @@ -42,7 +47,9 @@ public protocol GitURLSession { #endif } +/// Abstraction over `URLSessionDataTask` so tasks returned by a ``GitURLSession`` can be mocked in tests. public protocol GitURLSessionDataTaskProtocol { + /// Starts (or resumes) the network task. func resume() } diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift index 9272c56c2c..f74e18e48c 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift @@ -10,6 +10,7 @@ import Combine import CodeEditCore extension GitClient { + /// A snapshot of clone progress: the total percentage (0-100) and the phase git is currently in. public struct CloneProgress { let progress: Double let state: GitCloneProgressState diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift index e3f185a0ef..2973c15954 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift @@ -45,6 +45,9 @@ extension GitClient { return try parseUnsyncedCommitsOutput(from: output) } + /// Lists the files a commit changed, using `git diff-tree` to compare the commit against its parent. + /// - Parameter commitSHA: The hash of the commit to inspect. + /// - Returns: The changed files with their change status, or an empty array if the lookup fails. public func getCommitChangedFiles(commitSHA: String) async throws -> [GitChangedFile] { do { let output = try await run("diff-tree --no-commit-id --name-status -r \(commitSHA)") diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift index 1d23755b95..cd4b3505d7 100644 --- a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift @@ -26,6 +26,7 @@ import CodeEditCore /// information can be included in the same call. extension GitClient { + /// The parsed result of `git status`: ordinary changes, unmerged (conflicting) paths, and untracked files. public struct Status { var changedFiles: [GitChangedFile] var unmergedChanges: [GitChangedFile] diff --git a/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift index fbeb247abe..ce81b820b7 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift @@ -9,16 +9,18 @@ import SwiftUI /// Enum to represent a task's status public enum CETaskStatus { - // default state + /// The task has not been started yet. case notRunning - // User suspended the process + /// The user suspended the task's process. case stopped + /// The task's process is currently executing. case running - // Processes finished with an error + /// The task's process exited with an error. case failed - // Processes finished without an error + /// The task's process exited successfully. case finished + /// The color used to represent this status in task indicators throughout the UI. public var color: Color { switch self { case .notRunning: return Color.gray diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift index c43c05e36f..0dcd584c9d 100644 --- a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift +++ b/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift @@ -12,6 +12,7 @@ import SwiftTerm /// This allows terminal views to continue to receive data even when not in the view hierarchy. @MainActor public final class TerminalCache { + /// The single cache shared by all terminal views in the app. public static let shared: TerminalCache = TerminalCache() /// The cache of terminal views. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift index 9aa12ae1e3..0ba399d700 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift @@ -9,6 +9,7 @@ import Foundation /// A protocol defining the requirements for an object that can be searched using fuzzy matching. public protocol FuzzySearchable { + /// The string content that fuzzy searches are matched against. var searchableString: String { get } /// Performs a fuzzy search on the conforming object's searchable string. @@ -22,6 +23,8 @@ public protocol FuzzySearchable { } public extension FuzzySearchable { + /// Default implementation scoring consecutive character matches; returns a zero-weight result + /// when the query is not fully contained in the searchable string. func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult { let compareString = characters.characters diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift index 2929b20557..47e665f393 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift @@ -23,6 +23,7 @@ public extension String { escape(replacing: #"""#) } + /// Returns a new string, prefixing every occurrence of the given character with `\` unless already escaped. func escape(replacing: Character) -> String { var string = "" var lastChar: Character? diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift index 30c6f9dcbe..c65969fff3 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift @@ -8,6 +8,7 @@ import Foundation extension URL { + /// The last path component with surrounding whitespace and newlines trimmed. public var fileName: String { self.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift index 58ab492716..cca78a0b1c 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift @@ -13,14 +13,17 @@ public extension URL { try? self.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey, .contentTypeKey]) } + /// Whether this URL points to a directory. var isFolder: Bool { resourceValues?.isDirectory ?? false } + /// Whether this URL points to a symbolic link or a Finder alias. var isSymbolicLink: Bool { resourceValues?.isSymbolicLink ?? false || (resourceValues?.contentType ?? .item) == .aliasFile } + /// The uniform type of the item at this URL, or `nil` if the resource value cannot be read. var contentType: UTType? { resourceValues?.contentType } diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift index 1d3a7a521e..d663b7effd 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift @@ -10,10 +10,14 @@ import CodeEditTextView import CodeEditLanguages import AppKit +/// The per-document editor integrations supplied by a language service, such as an LSP client. public struct LanguageServices { + /// Keeps the document's text view in sync with the language tooling as the user edits. public let textCoordinator: TextViewCoordinator + /// Supplies highlight ranges (e.g. semantic tokens) for the document's text. public let highlightProvider: any HighlightProviding + /// Creates a bundle of language services from a text coordinator and a highlight provider. public init(textCoordinator: TextViewCoordinator, highlightProvider: any HighlightProviding) { self.textCoordinator = textCoordinator self.highlightProvider = highlightProvider diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift index ffc105e20d..4609d728db 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift @@ -10,6 +10,7 @@ import Foundation /// Loopable protocol implements a method that will return all child /// properties and their associated values of a `Type` public protocol Loopable { + /// Returns all child properties and their associated values of `self`, keyed by property name. func allProperties() throws -> [String: Any] } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift index c7a6144580..d74de8717e 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift @@ -8,6 +8,7 @@ import SwiftUI public extension Theme.Attributes { + /// The attribute's color as a SwiftUI `Color`; setting it stores the new value as a hex string. var swiftColor: Color { get { Color(hex: color) @@ -17,6 +18,7 @@ public extension Theme.Attributes { } } + /// The attribute's color as an AppKit `NSColor`; setting it stores the new value as a hex string. var nsColor: NSColor { get { NSColor(hex: color) diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift index fa06e4a843..70b639212c 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift @@ -44,6 +44,8 @@ public struct SettingsDataEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// The app-wide settings model. Views read individual settings through this value (usually via + /// the ``AppSettings`` property wrapper) so they update whenever a setting changes. var settings: SettingsDataEnvironmentKey.Value { get { self[SettingsDataEnvironmentKey.self] } set { self[SettingsDataEnvironmentKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift index 52f149822b..0c27156f9a 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift @@ -12,7 +12,9 @@ import Foundation /// Conform to this protocol to define a default value that will be used /// when decoding fails or the key is missing from the JSON. public protocol DefaultValueProvider { + /// The type of the value being defaulted; must round-trip through `Codable`. associatedtype Value: Codable & Hashable + /// The fallback value used when the key is missing from the JSON or its value fails to decode. static var defaultValue: Value { get } } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift index 1035008666..436c78d660 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift @@ -8,6 +8,7 @@ import SwiftUI public extension Color { + /// Creates a color from a hex string such as `#AABBCC`; surrounding non-alphanumeric characters are ignored. init(hex: String, alpha: Double = 1.0) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 @@ -15,6 +16,7 @@ public extension Color { self.init(hex: Int(int), alpha: alpha) } + /// Creates a color in the sRGB color space from a packed `0xRRGGBB` integer and an optional alpha. init(hex: Int, alpha: Double = 1.0) { let red = (hex >> 16) & 0xFF let green = (hex >> 8) & 0xFF @@ -22,6 +24,7 @@ public extension Color { self.init(.sRGB, red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255, opacity: alpha) } + /// The color's RGB components packed into a single `0xRRGGBB` integer; alpha is not included. var hex: Int { guard let components = cgColor?.components, components.count >= 3 else { return 0 } let red = lround((Double(components[0]) * 255.0)) << 16 @@ -30,16 +33,19 @@ public extension Color { return red | green | blue } + /// The color formatted as a lowercase web-style hex string, e.g. `#aabbcc`. var hexString: String { "#" + String(format: "%06x", hex) } + /// The color's alpha (opacity) component, in the range `0...1`. var alphaComponent: Double { NSColor(self).alphaComponent } } public extension NSColor { + /// Creates a color from a hex string such as `#AABBCC`; surrounding non-alphanumeric characters are ignored. convenience init(hex: String, alpha: Double = 1.0) { let hex = hex.trimmingCharacters(in: .alphanumerics.inverted) var int: UInt64 = 0 @@ -47,6 +53,7 @@ public extension NSColor { self.init(hex: Int(int), alpha: alpha) } + /// Creates a color in the sRGB color space from a packed `0xRRGGBB` integer and an optional alpha. convenience init(hex: Int, alpha: Double = 1.0) { let red = (hex >> 16) & 0xFF let green = (hex >> 8) & 0xFF @@ -54,6 +61,7 @@ public extension NSColor { self.init(srgbRed: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255, alpha: alpha) } + /// The color's RGB components packed into a single `0xRRGGBB` integer; alpha is not included. var hex: Int { guard let components = cgColor.components, components.count >= 3 else { return 0 } let red = lround((Double(components[0]) * 255.0)) << 16 @@ -62,6 +70,7 @@ public extension NSColor { return red | green | blue } + /// The color formatted as a lowercase web-style hex string, e.g. `#aabbcc`. var hexString: String { "#" + String(format: "%06x", hex) } diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift index ad00355eee..d28f05d43f 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift @@ -16,6 +16,8 @@ private struct CurrentDarkThemeKey: EnvironmentKey { } public extension EnvironmentValues { + /// The theme currently active in the editor, following the user's selection + /// and the app's light/dark appearance. var currentTheme: Theme? { get { self[CurrentThemeKey.self] } set { self[CurrentThemeKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift index ebde99b341..170a0a861c 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift @@ -12,6 +12,8 @@ private struct WorkspaceFullscreenStateEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// Whether the window hosting this view is in fullscreen, so views can adapt their layout (e.g. the + /// toolbar inset). var isFullscreen: Bool { get { self[WorkspaceFullscreenStateEnvironmentKey.self] } set { self[WorkspaceFullscreenStateEnvironmentKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift index 5aa9895442..468eba69fa 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift @@ -12,6 +12,8 @@ public struct EventModifierEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// The modifier keys (command, option, shift, ...) currently held down, for views that adapt while + /// a modifier is pressed. var modifierKeys: EventModifierEnvironmentKey.Value { get { self[EventModifierEnvironmentKey.self] } set { self[EventModifierEnvironmentKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift index 700d031ba2..50162aac96 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift @@ -7,8 +7,12 @@ import SwiftUI +/// Wraps a weak `NSWindow` reference so it can travel through the SwiftUI environment without the +/// environment retaining the window. public struct WindowBox { + /// The boxed window; nil once the window is gone or when none was injected. public weak var value: NSWindow? + /// Creates a box holding a weak reference to the given window. public init(value: NSWindow? = nil) { self.value = value } } @@ -18,6 +22,8 @@ public struct NSWindowEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// The `NSWindow` hosting this view hierarchy, boxed to avoid retaining it. Injected by the window + /// controller at the SwiftUI root; the box is empty in previews. var window: WindowBox { get { self[NSWindowEnvironmentKey.self] } set { self[NSWindowEnvironmentKey.self] = newValue } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift index 0b68692593..da144cf33e 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift @@ -7,6 +7,8 @@ import CoreGraphics +/// Shared layout constants that multiple features need to agree on. public enum LayoutMetrics { + /// The fixed height of the workspace window's status bar, in points. public static let statusBarHeight: CGFloat = 28.0 } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift index 4f363499cd..5721f7529a 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift @@ -8,7 +8,9 @@ import SwiftUI public extension ButtonStyle where Self == BlurButtonStyle { + /// A button style with a translucent, blurred material background, for buttons overlaid on content. static var blur: BlurButtonStyle { BlurButtonStyle() } + /// A more subdued variant of ``blur``, for the less prominent action next to a primary blur button. static var secondaryBlur: BlurButtonStyle { BlurButtonStyle(isSecondary: true) } } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift index bcc1c2ee4c..303ce2aaa5 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift @@ -96,6 +96,7 @@ public struct IconButtonStyle: ButtonStyle { } public extension ButtonStyle where Self == IconButtonStyle { + /// An icon button style with a custom font, active state, and fixed square frame of the given side length. static func icon( isActive: Bool? = false, font: Font? = Font.system(size: 14.5, weight: .regular, design: .default), @@ -103,6 +104,7 @@ public extension ButtonStyle where Self == IconButtonStyle { ) -> IconButtonStyle { return IconButtonStyle(isActive: isActive, font: font, size: size) } + /// An icon button style with a custom font, active state, and fixed frame of the given width and height. static func icon( isActive: Bool? = false, font: Font? = Font.system(size: 14.5, weight: .regular, design: .default), @@ -110,11 +112,13 @@ public extension ButtonStyle where Self == IconButtonStyle { ) -> IconButtonStyle { return IconButtonStyle(isActive: isActive, font: font, size: size) } + /// An icon button style with a custom font and active state, and no fixed frame. static func icon( isActive: Bool? = false, font: Font? = Font.system(size: 14.5, weight: .regular, design: .default) ) -> IconButtonStyle { return IconButtonStyle(isActive: isActive, font: font) } + /// An icon button style with the default font and no fixed frame. static var icon: IconButtonStyle { .init() } } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift index e734e8baf2..c0dcaf9149 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift @@ -38,22 +38,26 @@ public struct IconToggleStyle: ToggleStyle { } public extension ToggleStyle where Self == IconToggleStyle { + /// An icon toggle style with a custom font and a fixed square frame of the given side length. static func icon( font: Font? = Font.system(size: 14.5, weight: .regular, design: .default), size: CGFloat? = 24 ) -> IconToggleStyle { return IconToggleStyle(font: font, size: size) } + /// An icon toggle style with a custom font and a fixed frame of the given width and height. static func icon( font: Font? = Font.system(size: 14.5, weight: .regular, design: .default), size: CGSize? = CGSize(width: 24, height: 24) ) -> IconToggleStyle { return IconToggleStyle(font: font, size: size) } + /// An icon toggle style with a custom font and no fixed frame. static func icon( font: Font? = Font.system(size: 14.5, weight: .regular, design: .default) ) -> IconToggleStyle { return IconToggleStyle(font: font) } + /// An icon toggle style with the default font and no fixed frame. static var icon: IconToggleStyle { .init() } } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift index 208444cc16..198e0a00ee 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift @@ -8,6 +8,8 @@ import SwiftUI public extension View { + /// Overlays a 24pt bar along the view's bottom edge, showing `content` as small icon buttons. + /// Used under lists for add/remove-style controls, like the +/- bar in settings tables. func actionBar(@ViewBuilder content: () -> Content) -> some View { self .padding(.bottom, 24) diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift index 53c51e9a5e..84973c792c 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift @@ -13,6 +13,7 @@ public struct EdgeInsetsEnvironmentKey: EnvironmentKey { } public extension EnvironmentValues { + /// The insets a container applies around its content, e.g. to keep split view content clear of the toolbar. var edgeInsets: EdgeInsetsEnvironmentKey.Value { get { self[EdgeInsetsEnvironmentKey.self] } set { self[EdgeInsetsEnvironmentKey.self] = newValue } @@ -20,6 +21,7 @@ public extension EnvironmentValues { } public extension EdgeInsets { + /// The same insets converted to an AppKit `NSEdgeInsets`, mapping leading/trailing to left/right. var nsEdgeInsets: NSEdgeInsets { .init(top: top, left: leading, bottom: bottom, right: trailing) } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift index e73c2d3c11..2c9aff7dfe 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift @@ -28,6 +28,7 @@ public struct SplitViewItemCanAnimateViewTraitKey: _ViewTraitKey { } public extension View { + /// Binds the collapsed state of this split view item, so it can be collapsed and observed programmatically. func collapsed(_ value: Binding) -> some View { self ._trait(SplitViewItemCollapsedViewTraitKey.self, .init { @@ -37,16 +38,19 @@ public extension View { }) } + /// Allows the user to collapse this split view item by dragging its divider to the edge. func collapsable() -> some View { self ._trait(SplitViewItemCanCollapseViewTraitKey.self, true) } + /// Sets the holding priority of this split view item, deciding which item resizes first when space changes. func holdingPriority(_ priority: NSLayoutConstraint.Priority) -> some View { self ._trait(SplitViewHoldingPriorityTraitKey.self, priority) } + /// Controls whether collapsing or expanding this split view item is animated. func splitViewCanAnimate(_ enabled: Binding) -> some View { self._trait(SplitViewItemCanAnimateViewTraitKey.self, enabled.wrappedValue) } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift index 196be47446..ff85ec7a59 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift @@ -34,18 +34,23 @@ public struct SplitViewReader: View { } } +/// A handle to a `SplitView`, vended by ``SplitViewReader``, for imperatively moving dividers and +/// collapsing items. public struct SplitViewProxy { private var viewController: () -> SplitViewController? + /// Creates a proxy that resolves the underlying controller lazily, so it works before the split view exists. public init(viewController: @escaping () -> SplitViewController?) { self.viewController = viewController } + /// Moves the divider at `index` to the given position, in points from the split view's leading/top edge. @MainActor public func setPosition(of index: Int, position: CGFloat) { viewController()?.splitView.setPosition(position, ofDividerAt: index) } + /// Collapses or expands the split view item identified by `id`, animating the change. @MainActor public func collapseView(with id: AnyHashable, _ enabled: Bool) { viewController()?.collapse(for: id, enabled: enabled) diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift index 65b1cd9298..119c45cfb2 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift @@ -16,6 +16,8 @@ public struct Helper: _VariadicView_UnaryViewRoot { } public extension View { + /// Exposes this view's resolved children so `process` can rebuild the hierarchy from them, e.g. to + /// read per-child view traits. Used by ``SplitView`` to turn its content into individual split items. func variadic(@ViewBuilder process: @escaping (_VariadicView.Children) -> R) -> some View { _VariadicView.Tree(Helper(_body: process), content: { self }) } diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift index 71d0c23ed8..655f5152b0 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift @@ -8,6 +8,8 @@ import SwiftUI public extension View { + /// Applies `transform` to the view only when `condition` is true, returning the view unchanged otherwise. + /// Note the condition changing swaps the view's identity, resetting any state inside. @ViewBuilder func `if`(_ condition: Bool, @ViewBuilder transform: (Self) -> Content) -> some View { if condition { @@ -17,6 +19,8 @@ public extension View { } } + /// Applies `transform` to the view when `condition` is true, otherwise applies `elseTransform`. + /// Note the condition changing swaps the view's identity, resetting any state inside. @ViewBuilder func `if`( _ condition: Bool, @@ -32,6 +36,7 @@ public extension View { } public extension Bool { + /// Whether the app is running on macOS 26 (Tahoe) or later, for gating Tahoe-specific styling. static var tahoe: Bool { if #available(macOS 26, *) { return true From 81687a31f91b0da40e70fe41bc69017c2f2c4e67 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 11:27:00 +0200 Subject: [PATCH 177/335] Docs: Add contributor architecture guide with placement decision tree --- .github/pull_request_template.md | 1 + CONTRIBUTING.md | 11 ++ docs/ARCHITECTURE.md | 174 +++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d976c3fa5f..ee315c524c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,6 +22,7 @@ - [ ] My code builds and runs on my machine - [ ] My changes are all related to the related issue above - [ ] I documented my code +- [ ] New files follow the placement rules in [docs/ARCHITECTURE.md](https://github.com/CodeEditApp/CodeEdit/blob/main/docs/ARCHITECTURE.md) (features as packages, no cross-feature imports, purpose-first folders) ### Screenshots diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df215d282d..53a1cfd368 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,17 @@ We also have a [troubleshooting guide](https://github.com/CodeEditApp/CodeEdit/w Please read our guide on [Code Style](https://github.com/CodeEditApp/CodeEdit/wiki/Code-Style) in our wiki. +## Architecture + +CodeEdit is organized as a tiered workspace of local Swift packages (foundation, services, +features) plus a thin app target that composes them. Before adding files, please consult the +decision tree in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — it answers "where does my code +go?" in a few steps. The short version: new features start as `Packages/Features/CE` +packages, feature packages never import each other, and shared code moves to a foundation +package only when it has multiple consumers *and* passes that package's dependency charter. +CI enforces these rules (SwiftLint charter rules + a package import audit), so a misplaced +file will fail checks. + ## Pull Request Once you are happy with your changes, submit a `Pull Request`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000000..6ec9c09a8f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,174 @@ +# CodeEdit Architecture Guide + +This guide explains how the codebase is organized and — most importantly — **where new code +goes**. CI enforces the rules described here (see [Enforcement](#enforcement)), so reading this +before you add files will save you a failed check. + +## Package topology + +The workspace contains one app project and nine local Swift packages, grouped by tier: + +``` +CodeEdit.xcworkspace +├── CodeEdit.xcodeproj — app shell + composition UI +└── Packages/ + ├── Foundation/ + │ ├── CodeEditCore — pure types, EventBus, command interfaces (no UI/IO, zero deps) + │ ├── CodeEditUI — shared presentation atoms (→ CodeEditSymbols only) + │ ├── CodeEditDocument — CodeFileDocument + editor-framework bridging protocols + │ └── CodeEditSettings — settings model + store (UI pages stay app-side) + ├── Features/ — CEEditor, CESearch, CENotifications, CELSP, + │ CESourceControl, CETerminal (one package per feature) + └── Services/CodeEditServices — ShellClient, CEWorkspaceFileManager (one target each) +``` + +Naming: `CodeEdit*` = foundation substrate (peer-named with the external CodeEdit libraries), +`CE*` = feature packages (peer-named with the `CE*` domain types). Services are named after +their primary type. + +All local packages build with Swift 6 strict concurrency. The app target is still Swift 5 — +write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers +aren't isolated (it cascades). + +## Tier charters + +| Tier | Package(s) | May depend on | Must never contain | +|---|---|---|---| +| Foundation | CodeEditCore | *nothing* | UI or I/O framework imports (SwiftUI/AppKit), external deps | +| Foundation | CodeEditUI | CodeEditSymbols only | feature semantics, model/service imports | +| Foundation | CodeEditDocument | CodeEditCore + editor libraries | app-tier types | +| Foundation | CodeEditSettings | CodeEditCore | settings *pages* (those are app-side composition UI) | +| Services | CodeEditServices targets | CodeEditCore only | UI imports, sibling service targets | +| Features | `CE*` packages | Foundation tiers + external libraries | **other `CE*` feature packages** | +| App | CodeEdit target | everything | — (it's the composition layer, not the default dumping ground) | + +## Where does my code go? + +Work through these in order; the first match wins. + +1. **A new user-facing feature?** → A new `Packages/Features/CE` package (see the + [recipe](#creating-a-new-feature-package)). Features start as packages; the app target is + not the default. Exception: *shell chrome* that composes multiple features around the + concrete `Workspace` hub — navigator/inspector/utility areas, the status bar — stays + app-side, because its interface would effectively be "the whole app". +2. **A type, protocol, event, or command interface needed by two or more features?** → + `CodeEditCore`, *if* it passes the charter (no UI/IO imports, no external dependencies). + Events (facts, e.g. `TaskNotificationEvent`) and command interfaces (requests with exactly + one handler, e.g. `WorkspaceNavigator`) always live here. +3. **A reusable view, style, or view modifier with no feature semantics?** → `CodeEditUI`. +4. **A service that performs I/O and has no UI?** → A new target in `CodeEditServices` + (Core-only dependencies, its own library product, the app links it directly). +5. **Cross-service orchestration?** → A doer-style role-noun class in the feature that owns + the operation (`WorkspaceOpener`, `FileMover`, `RepositoryCloner` — the `NSFileCoordinator` + naming idiom). One doer per operation that touches more than one service; dependencies + arrive via the initializer. +6. **Everything else — composition, adapters, menu commands, settings pages, app lifecycle?** + → The app target, inside the owning feature folder. + +### The consumer-count litmus + +The most common placement mistake is moving something "up" because it *looks* generic. +Don't judge generic-ness — **count consumers**: + +- A helper with **one** consumer lives next to that consumer, even if it looks + general-purpose. +- It moves to `CodeEditCore` only when a **second consumer actually appears** *and* it passes + the zero-dependency charter. + +Worked example: when fuzzy search was consolidated, the pure parts (`FuzzySearchable`, +the string-matching primitives) moved to CodeEditCore — but `Collection+FuzzySearch` stayed +app-side because it depends on CollectionConcurrencyKit, which would breach Core's charter. +**Dependency honesty beats tidiness**: never add a dependency to a foundation package just to +make a move possible. Mirroring a one-line helper locally is the accepted alternative. + +## Folder conventions (app target) + +Grouping is **purpose-first**: + +- Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never + by kind — there are no `Models/`, `Views/`, `ViewModels/`, `Services/`, or `UseCases/` + folders. +- A feature with roughly ten files or fewer stays flat. +- Shell/entry views and the feature's primary models sit at the feature root. +- Single-consumer helpers live next to their consumer. +- `Utils/` is closed. Every file in it carries a justification (an app-wide platform patch, a + helper genuinely shared by multiple features with no better home). "It's generic" is not a + justification. + +## Communication rules + +- **Feature packages never import each other.** Cross-feature signaling is typed: + - **Facts** (something happened) → an event on the `EventBus` in CodeEditCore. + - **Requests** (do something, exactly one rightful handler) → a command interface in + CodeEditCore, implemented by an app-side adapter. +- No custom `Notification.Name`s. `NotificationCenter` is only used to observe platform + notifications (NSWindow, NSApplication, NSMenu). +- No singletons and no DI container. `AppDependencies` is the app-scope composition root; + objects receive dependencies through initializers, SwiftUI views through environment keys + (`appServices(_:)`). Only composition roots may hold the whole `AppDependencies`. +- No SwiftUI view observes a service directly — services expose a concrete view-state object + (the presentation-state split), and views issue commands through protocol-typed environment + keys. + +## Creating a new feature package + +1. Create `Packages/Features/CE/Package.swift`: + + ```swift + // swift-tools-version: 6.0 + + import PackageDescription + + let package = Package( + name: "CE", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CE", targets: ["CE"]) + ], + dependencies: [ + .package(path: "../../Foundation/CodeEditCore"), + .package(path: "../../Foundation/CodeEditUI") + ], + targets: [ + .target( + name: "CE", + dependencies: [ + .product(name: "CodeEditCore", package: "CodeEditCore"), + .product(name: "CodeEditUI", package: "CodeEditUI") + ] + ) + ] + ) + ``` + +2. Add the package to the workspace: in Xcode, drag the folder into the **Features** group of + the workspace navigator (or add a `FileRef` to `CodeEdit.xcworkspace/contents.xcworkspacedata`). +3. Link the product to the app: CodeEdit target → *General* → *Frameworks, Libraries, and + Embedded Content* → add `CE`. +4. Remember the package builds with **Swift 6 strict concurrency** — types crossing actor + boundaries need `Sendable`, and UI-bound classes are usually `@MainActor`. +5. Known quirk: packages that depend on `CodeEditSymbols` build via Xcode/xcodebuild only — + standalone `swift build` fails on its `Bundle.module` resolution. +6. Declare **every** module you import in the manifest. The workspace's shared build directory + makes undeclared imports of sibling packages compile by accident — CI will catch it + (see below). + +## Enforcement + +Two automated checks keep this document honest; both run on every PR: + +- **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`) — includes custom rules + that reject UI imports in CodeEditCore, feature→feature imports, and model/feature imports + in CodeEditUI. These fire inside Xcode while you type. +- **The package audit** (`.github/scripts/audit_package_imports.py`) — verifies every + `import` in every package is declared in that package's manifest, and that the tier rules + in the charter table hold. It exists because Xcode workspace builds share one build + directory, so an undeclared import of a sibling package compiles fine locally and the + violation stays invisible until a standalone build breaks. + +Run both locally from the repo root: + +```bash +swiftlint lint --quiet +python3 .github/scripts/audit_package_imports.py +``` From a864c82027b8d8df8ab0342e0508709e0996250a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 11:27:00 +0200 Subject: [PATCH 178/335] CI: Enforce package tier charters via import audit and SwiftLint rules --- .github/scripts/audit_package_imports.py | 105 +++++++++++++++++++++++ .github/workflows/lint.yml | 2 + .swiftlint.yml | 18 ++++ 3 files changed, 125 insertions(+) create mode 100755 .github/scripts/audit_package_imports.py diff --git a/.github/scripts/audit_package_imports.py b/.github/scripts/audit_package_imports.py new file mode 100755 index 0000000000..fd31c079c7 --- /dev/null +++ b/.github/scripts/audit_package_imports.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Audit local Swift packages: every `import` must be declared in the package +manifest, and the tier rules from docs/ARCHITECTURE.md must hold. + +Why: Xcode workspace builds share one build directory, so an undeclared import +of a sibling local package compiles fine ("leaky import") and only breaks a +standalone `swift build`. This script makes manifest honesty a PR gate. + +Usage: python3 .github/scripts/audit_package_imports.py (from anywhere) +""" +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +PACKAGES = REPO / "Packages" + +# Apple SDK modules used in this codebase; extend when a new system framework is adopted. +SYSTEM_MODULES = { + "Foundation", "FoundationNetworking", "SwiftUI", "AppKit", "Cocoa", "Combine", + "CoreGraphics", "OSLog", "os", "PDFKit", "QuickLookUI", "RegexBuilder", + "UniformTypeIdentifiers", "UserNotifications", "AVKit", "CryptoKit", "Security", + "Swift", "XCTest", "Testing", +} + +LOCAL_PRODUCTS = { + "CodeEditCore", "CodeEditUI", "CodeEditDocument", "CodeEditSettings", + "CEEditor", "CESearch", "CENotifications", "CELSP", "CESourceControl", + "CETerminal", "ShellClient", "CEWorkspaceFileManager", +} +FEATURE_PRODUCTS = {"CEEditor", "CESearch", "CENotifications", "CELSP", "CESourceControl", "CETerminal"} +UI_FRAMEWORKS = {"SwiftUI", "AppKit", "Cocoa"} + +IMPORT_RE = re.compile( + r"^\s*(?:@[\w()]+\s+)?import\s+(?:struct\s+|class\s+|enum\s+|func\s+|var\s+)?([A-Za-z_][A-Za-z0-9_]*)", + re.MULTILINE, +) +PRODUCT_DEP_RE = re.compile(r'\.product\(\s*name:\s*"([^"]+)"') +TARGET_RE = re.compile(r'\.(?:target|executableTarget|testTarget)\(\s*name:\s*"([^"]+)"') + + +def manifest_declared(manifest_text: str) -> set: + """Modules a target in this package may legitimately import.""" + declared = set(PRODUCT_DEP_RE.findall(manifest_text)) + declared |= set(TARGET_RE.findall(manifest_text)) # own targets + # bare-string dependencies inside dependencies: [...] arrays + for match in re.findall(r"dependencies:\s*\[([^\]]*)\]", manifest_text, re.DOTALL): + declared |= set(re.findall(r'"([A-Za-z][\w-]*)"', match)) + return declared + + +def main() -> int: + failures = [] + manifests = sorted(PACKAGES.glob("*/*/Package.swift")) + if not manifests: + print(f"Package audit FAILED: no manifests found under {PACKAGES}") + return 1 + + for manifest in manifests: + pkg_dir = manifest.parent + pkg_name = pkg_dir.name + tier = pkg_dir.parent.name # Foundation | Services | Features + text = manifest.read_text() + declared = manifest_declared(text) + own_targets = set(TARGET_RE.findall(text)) | {pkg_name} + # A package's own targets are not dependencies — exclude them from tier analysis. + local_deps = (declared - own_targets) & LOCAL_PRODUCTS + + # --- tier rules on the manifest itself --- + if pkg_name == "CodeEditCore" and PRODUCT_DEP_RE.search(text): + failures.append(f"{pkg_name}: CodeEditCore must have zero dependencies") + if pkg_name == "CodeEditUI" and local_deps: + failures.append(f"{pkg_name}: CodeEditUI may not depend on local packages (CodeEditSymbols only)") + if tier == "Services" and local_deps - {"CodeEditCore"}: + failures.append( + f"{pkg_name}: service targets may depend on CodeEditCore only " + f"(found {sorted(local_deps - {'CodeEditCore'})})" + ) + if tier == "Features" and (local_deps & FEATURE_PRODUCTS) - {pkg_name}: + failures.append( + f"{pkg_name}: feature packages may not depend on other feature packages " + f"(found {sorted((local_deps & FEATURE_PRODUCTS) - {pkg_name})})" + ) + + # --- import honesty per source file --- + allowed = declared | SYSTEM_MODULES | {pkg_name} + for swift in sorted((pkg_dir / "Sources").rglob("*.swift")): + rel = swift.relative_to(REPO) + for module in sorted(set(IMPORT_RE.findall(swift.read_text()))): + if module not in allowed: + failures.append(f"{rel}: import {module} is not declared in {pkg_name}/Package.swift") + if pkg_name == "CodeEditCore" and module in UI_FRAMEWORKS: + failures.append(f"{rel}: {module} import violates the CodeEditCore no-UI charter") + + if failures: + print(f"Package audit FAILED ({len(failures)} violations):") + for failure in failures: + print(f" {failure}") + return 1 + print(f"Package audit passed ({len(manifests)} packages).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6188582359..1a9b7ac65c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,3 +11,5 @@ jobs: - uses: actions/checkout@v3 - name: GitHub Action for SwiftLint run: swiftlint --reporter github-actions-logging --strict + - name: Audit package imports and tier rules + run: python3 .github/scripts/audit_package_imports.py diff --git a/.swiftlint.yml b/.swiftlint.yml index 6db51196ae..3e56fbf64c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -39,3 +39,21 @@ custom_rules: regex: "\t" message: "Prefer spaces for indents over tabs. See Xcode setting: 'Text Editing' -> 'Indentation'" severity: warning + no_ui_in_core: + included: "Packages/Foundation/CodeEditCore/.*\\.swift" + name: "CodeEditCore charter" + regex: "^import (SwiftUI|AppKit|Cocoa)$" + message: "CodeEditCore must stay UI-free — move UI-coupled code to CodeEditUI or the owning feature (see docs/ARCHITECTURE.md)" + severity: error + no_feature_to_feature: + included: "Packages/Features/.*\\.swift" + name: "No feature→feature imports" + regex: "^import (CEEditor|CESearch|CENotifications|CELSP|CESourceControl|CETerminal)$" + message: "Feature packages may not import each other — communicate via CodeEditCore events or command interfaces (see docs/ARCHITECTURE.md)" + severity: error + ui_package_purity: + included: "Packages/Foundation/CodeEditUI/.*\\.swift" + name: "CodeEditUI charter" + regex: "^import (CodeEditCore|CodeEditDocument|CodeEditSettings|CEEditor|CESearch|CENotifications|CELSP|CESourceControl|CETerminal|ShellClient|CEWorkspaceFileManager)$" + message: "CodeEditUI depends on CodeEditSymbols only — presentation atoms must not know about models or features (see docs/ARCHITECTURE.md)" + severity: error From 4eca68943c892f800e9a3c80ebe25a3aae5e7c52 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 11:45:01 +0200 Subject: [PATCH 179/335] Fix: Resolve main-thread hang when opening the Output utility area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UtilityAreaOutputSourcePicker rebuilt a fresh AnyPublisher on every body evaluation, so onReceive re-subscribed each render pass. The @Published publisher replays its current value on subscription, and the handler's unconditional updater = UUID() write invalidated the view again — an infinite render loop that beachballed the app as soon as the picker appeared. The picker now observes LanguageServerListState directly via a nested @ObservedObject view and derives the server list as plain data; the updater/.id machinery and both onReceive subscriptions are gone. The auto-select-first-server behavior is kept via onAppear + onChange over the filtered server IDs. --- .../UtilityAreaOutputSourcePicker.swift | 141 ++++++++++-------- 1 file changed, 80 insertions(+), 61 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift index 65c6d20968..757558ee1f 100644 --- a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift +++ b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift @@ -8,86 +8,105 @@ import CodeEditCore import CELSP import SwiftUI -import Combine import CodeEditSettings struct UtilityAreaOutputSourcePicker: View { typealias Sources = UtilityAreaOutputView.Sources - @Environment(\.workspaceFileURL) - private var workspaceFileURL - - @AppSettings(\.developerSettings.showInternalDevelopmentInspector) - var showInternalDevelopmentInspector + @Environment(\.languageServerListState) + private var languageServerListState @Binding var selectedSource: Sources? - @ObservedObject var extensionManager = ExtensionManager.shared + var body: some View { + if let languageServerListState { + ObservingContent(listState: languageServerListState, selectedSource: $selectedSource) + } else { + // Previews: the environment key is nil and the server list is legitimately empty. + Content(runningServers: [], selectedSource: $selectedSource) + } + } - @Environment(\.languageServerListState) - private var languageServerListState - @State private var updater: UUID = UUID() - @State private var languageServerClients: [RunningLanguageServer] = [] + /// Observes the server list and feeds it to ``Content`` as plain data. A separate view because + /// `@ObservedObject` cannot hold the optional environment value directly. + private struct ObservingContent: View { + @ObservedObject var listState: LanguageServerListState + @Binding var selectedSource: Sources? - var body: some View { - Picker("Output Source", selection: $selectedSource) { - if selectedSource == nil { - Text("No Selected Output Source") - .italic() - .tag(Sources?.none) - Divider() - } + var body: some View { + Content(runningServers: listState.runningServers, selectedSource: $selectedSource) + } + } - if languageServerClients.isEmpty { - Text("No Language Servers") - } else { - ForEach(languageServerClients, id: \.languageId) { server in - Text(Sources.languageServer(server.logContainer).title) - .tag(Sources.languageServer(server.logContainer)) - } - } + private struct Content: View { + @Environment(\.workspaceFileURL) + private var workspaceFileURL + + @AppSettings(\.developerSettings.showInternalDevelopmentInspector) + var showInternalDevelopmentInspector + + let runningServers: [RunningLanguageServer] - Divider() + @Binding var selectedSource: Sources? - if extensionManager.extensions.isEmpty { - Text("No Extensions") - } else { - ForEach(extensionManager.extensions) { extensionInfo in - Text(Sources.extensions(.init(extensionInfo: extensionInfo)).title) - .tag(Sources.extensions(.init(extensionInfo: extensionInfo))) + @ObservedObject var extensionManager = ExtensionManager.shared + + private var languageServerClients: [RunningLanguageServer] { + runningServers + .filter { $0.workspacePath == workspaceFileURL?.absolutePath } + .sorted(by: { $0.languageId.rawValue < $1.languageId.rawValue }) + } + + var body: some View { + Picker("Output Source", selection: $selectedSource) { + if selectedSource == nil { + Text("No Selected Output Source") + .italic() + .tag(Sources?.none) + Divider() + } + + if languageServerClients.isEmpty { + Text("No Language Servers") + } else { + ForEach(languageServerClients, id: \.languageId) { server in + Text(Sources.languageServer(server.logContainer).title) + .tag(Sources.languageServer(server.logContainer)) + } } - } - if showInternalDevelopmentInspector { Divider() - Text(Sources.devOutput.title) - .tag(Sources.devOutput) + + if extensionManager.extensions.isEmpty { + Text("No Extensions") + } else { + ForEach(extensionManager.extensions) { extensionInfo in + Text(Sources.extensions(.init(extensionInfo: extensionInfo)).title) + .tag(Sources.extensions(.init(extensionInfo: extensionInfo))) + } + } + + if showInternalDevelopmentInspector { + Divider() + Text(Sources.devOutput.title) + .tag(Sources.devOutput) + } + } + .buttonStyle(.borderless) + .labelsHidden() + .controlSize(.small) + .onAppear { + selectDefaultSourceIfNeeded() + } + .onChange(of: languageServerClients.map(\.id)) { _, _ in + selectDefaultSourceIfNeeded() } } - .id(updater) - .buttonStyle(.borderless) - .labelsHidden() - .controlSize(.small) - .onAppear { - updateLanguageServers(languageServerListState?.runningServers ?? []) - } - .onReceive( - languageServerListState?.$runningServers.eraseToAnyPublisher() ?? Just([]).eraseToAnyPublisher() - ) { servers in - updateLanguageServers(servers) - } - .onReceive(extensionManager.$extensions) { _ in - updater = UUID() - } - } - func updateLanguageServers(_ servers: [RunningLanguageServer]) { - languageServerClients = servers - .filter { $0.workspacePath == workspaceFileURL?.absolutePath } - .sorted(by: { $0.languageId.rawValue < $1.languageId.rawValue }) - if selectedSource == nil, let client = languageServerClients.first { - selectedSource = Sources.languageServer(client.logContainer) + private func selectDefaultSourceIfNeeded() { + if selectedSource == nil, let client = languageServerClients.first { + selectedSource = Sources.languageServer(client.logContainer) + } } - updater = UUID() } } From 63018e1aedc897fd132c54d99cfc6c2158ffa693 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 13:52:44 +0200 Subject: [PATCH 180/335] Refactor: Move fuzzy-search algorithm into CodeEditCore Rewrites Collection+FuzzySearch with withTaskGroup so CodeEditCore keeps its zero-dependency charter (drops CollectionConcurrencyKit at this call site). Adds Sendable to FuzzySearchMatchResult and Theme (+nested types) as required by strict concurrency. --- .../FuzzySearch/Collection+FuzzySearch.swift | 30 ------------- .../FuzzySearch/FuzzySearchUIModel.swift | 2 +- .../Search/FuzzySearch/FuzzySearchTests.swift | 23 ++++++++++ .../Search/Collection+FuzzySearch.swift | 42 +++++++++++++++++++ .../Domain/Search/FuzzySearchModels.swift | 2 +- .../Sources/CodeEditSettings/Theme.swift | 10 ++--- 6 files changed, 72 insertions(+), 37 deletions(-) delete mode 100644 CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift create mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift diff --git a/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift b/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift deleted file mode 100644 index 594a5ff932..0000000000 --- a/CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Collection+FuzzySearch.swift -// CodeEdit -// -// Created by Tommy Ludwig on 03.02.24. -// - -import Foundation -import CollectionConcurrencyKit -import CodeEditCore - -extension Collection where Iterator.Element: FuzzySearchable { - /// Asynchronously performs a fuzzy search on a collection of elements conforming to FuzzySearchable. - /// - /// - Parameter query: The query string to match against the elements. - /// - /// - Returns: An array of tuples containing FuzzySearchMatchResult and the corresponding element. - /// - /// - Note: Because this is an extension on Collection and not only array, - /// you can also use this on sets. - func fuzzySearch(query: String) async -> [(result: FuzzySearchMatchResult, item: Iterator.Element)] { - return await concurrentMap { - (result: $0.fuzzyMatch(query: query), item: $0) - }.filter { - $0.result.weight > 0 - }.sorted { - $0.result.weight > $1.result.weight - } - } -} diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift index 4c4ae42c6a..6fbc52d518 100644 --- a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift +++ b/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift @@ -11,7 +11,7 @@ import AsyncAlgorithms import CodeEditCore @MainActor -final class FuzzySearchUIModel: ObservableObject { +final class FuzzySearchUIModel: ObservableObject { @Published var items: [Element]? private var allItems: [Element] = [] diff --git a/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift b/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift index 6f8fad5190..5b8b395973 100644 --- a/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift +++ b/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift @@ -64,4 +64,27 @@ final class FuzzySearchTests: XCTestCase { } XCTAssertEqual(swiftResults.count, 3) } + + func testFuzzySearchExcludesNonMatches() async { + let urls = [ + URL(string: "ContentView.swift")!, + URL(string: "README.md")! + ] + + let results = await urls.fuzzySearch(query: "swift") + + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results[0].item.lastPathComponent, "ContentView.swift") + XCTAssertTrue(results.allSatisfy { $0.result.weight > 0 }) + } + + func testFuzzySearchPreservesInputOrderForEqualWeights() async { + // Identical file names produce identical weights; Swift's sort is stable, + // so the result order must match the input order. + let urls = (0..<50).map { URL(string: "Folder\($0)/SameName.swift")! } + + let results = await urls.fuzzySearch(query: "same").map(\.item) + + XCTAssertEqual(results, urls) + } } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift new file mode 100644 index 0000000000..5c3b043b6c --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift @@ -0,0 +1,42 @@ +// +// Collection+FuzzySearch.swift +// CodeEdit +// +// Created by Tommy Ludwig on 03.02.24. +// + +import Foundation + +public extension Collection where Element: FuzzySearchable & Sendable { + /// Concurrently performs a fuzzy search on a collection of elements conforming to FuzzySearchable. + /// + /// - Parameter query: The query string to match against the elements. + /// + /// - Returns: Matching elements (weight > 0) paired with their match results, sorted by + /// descending weight. Elements with equal weight keep their input order. + /// + /// - Note: Because this is an extension on Collection and not only array, + /// you can also use this on sets. + func fuzzySearch(query: String) async -> [(result: FuzzySearchMatchResult, item: Element)] { + let items = Array(self) + + let matches = await withTaskGroup(of: (Int, FuzzySearchMatchResult).self) { group in + for (index, item) in items.enumerated() { + group.addTask { + (index, item.fuzzyMatch(query: query)) + } + } + + var results = [FuzzySearchMatchResult?](repeating: nil, count: items.count) + for await (index, result) in group { + results[index] = result + } + return results + } + + return zip(matches, items) + .compactMap { match, item in match.map { (result: $0, item: item) } } + .filter { $0.result.weight > 0 } + .sorted { $0.result.weight > $1.result.weight } + } +} diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift index c69cc6e8a2..421bc395cd 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift @@ -33,7 +33,7 @@ public struct FuzzySearchString { } /// The result of a fuzzy match operation, containing a relevance weight and the matched ranges. -public struct FuzzySearchMatchResult { +public struct FuzzySearchMatchResult: Sendable { /// A score indicating how closely the input matched; higher values mean a better match. public let weight: Int /// The ranges within the original string that were matched. diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift index ba086ec162..f15963e876 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift @@ -12,7 +12,7 @@ import Foundation /// # Theme /// /// The model structure of themes for the editor & terminal emulator -public struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable { +public struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable, Sendable { enum CodingKeys: String, CodingKey { case author, license, distributionURL, name, displayName, editor, terminal, version case appearance = "type" @@ -95,7 +95,7 @@ extension Theme { /// The type of the theme /// - **dark**: this is a theme for dark system appearance /// - **light**: this is a theme for light system appearance - public enum ThemeType: String, Codable, Hashable { + public enum ThemeType: String, Codable, Hashable, Sendable { case dark case light } @@ -107,7 +107,7 @@ extension Theme { /// /// As of now it only includes the colors `hex` string and /// an accessor for a `SwiftUI` `Color`. - public struct Attributes: Codable, Equatable, Hashable, Loopable { + public struct Attributes: Codable, Equatable, Hashable, Loopable, Sendable { /// The 24-bit hex string of the color (e.g. #123456) public var color: String @@ -150,7 +150,7 @@ extension Theme { extension Theme { /// The editor colors of the theme - public struct EditorColors: Codable, Hashable, Loopable { + public struct EditorColors: Codable, Hashable, Loopable, Sendable { public var text: Attributes public var insertionPoint: Attributes public var invisibles: Attributes @@ -261,7 +261,7 @@ extension Theme { extension Theme { /// The terminal emulator colors of the theme - public struct TerminalColors: Codable, Hashable, Loopable { + public struct TerminalColors: Codable, Hashable, Loopable, Sendable { public var text: Attributes public var boldText: Attributes public var cursor: Attributes From 38171ac34c083506c1d0ab90cfa41edb84cf7e43 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 13:53:56 +0200 Subject: [PATCH 181/335] Refactor: Dissolve Features/Search into Utils/FuzzySearch Resolves the naming collision with the CESearch package. Utils entry justified: three consumers across two features (OpenQuickly, ThemeSettings, LanguageServers), no owning feature, protocol lives in CodeEditCore. --- .../Search => Utils}/FuzzySearch/FuzzySearchUIModel.swift | 0 .../{Features/Search => Utils}/FuzzySearch/FuzzySearchTests.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/{Features/Search => Utils}/FuzzySearch/FuzzySearchUIModel.swift (100%) rename CodeEditTests/{Features/Search => Utils}/FuzzySearch/FuzzySearchTests.swift (100%) diff --git a/CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift b/CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift similarity index 100% rename from CodeEdit/Features/Search/FuzzySearch/FuzzySearchUIModel.swift rename to CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift diff --git a/CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift b/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift similarity index 100% rename from CodeEditTests/Features/Search/FuzzySearch/FuzzySearchTests.swift rename to CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift From a3266afea068ad7c52ac7f20ffd6f26e859f8a30 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 13:55:01 +0200 Subject: [PATCH 182/335] Refactor: Group app-shell files under App/ WindowCommands becomes App/MenuBar (it is the menu bar); Keybindings becomes App/Commands (it holds CommandManager + KeybindingManager, not just keybindings). --- CodeEdit/{ => App}/AppDelegate.swift | 0 CodeEdit/{ => App}/AppDependencies.swift | 0 CodeEdit/{ => App}/CodeEditApp.swift | 0 .../{Features/Keybindings => App/Commands}/CommandManager.swift | 0 .../Keybindings => App/Commands}/CommandManaging.swift | 0 .../Keybindings => App/Commands}/KeybindingManager.swift | 0 .../Keybindings => App/Commands}/KeybindingManaging.swift | 0 .../Keybindings => App/Commands}/ModifierKeysObserver.swift | 0 .../Keybindings => App/Commands}/default_keybindings.json | 0 CodeEdit/{ => App}/Environment+AppCommands.swift | 0 .../WindowCommands => App/MenuBar}/CodeEditCommands.swift | 0 .../WindowCommands => App/MenuBar}/CommandsFixes.swift | 0 .../WindowCommands => App/MenuBar}/EditorCommands.swift | 0 .../WindowCommands => App/MenuBar}/ExtensionCommands.swift | 0 .../{Features/WindowCommands => App/MenuBar}/FileCommands.swift | 0 .../{Features/WindowCommands => App/MenuBar}/FindCommands.swift | 0 .../MenuBar}/FirstResponderPropertyWrapper.swift | 0 .../WindowCommands => App/MenuBar}/FocusedValues.swift | 0 .../{Features/WindowCommands => App/MenuBar}/HelpCommands.swift | 0 .../MenuBar}/KeyWindowControllerObserver.swift | 0 .../{Features/WindowCommands => App/MenuBar}/MainCommands.swift | 0 .../WindowCommands => App/MenuBar}/NavigateCommands.swift | 0 .../WindowCommands => App/MenuBar}/RecentProjectsMenu.swift | 0 .../WindowCommands => App/MenuBar}/SourceControlCommands.swift | 0 .../WindowCommands => App/MenuBar}/TasksCommands.swift | 0 .../{Features/WindowCommands => App/MenuBar}/ViewCommands.swift | 0 .../WindowCommands => App/MenuBar}/WindowCommands.swift | 0 .../MenuBar}/WindowControllerPropertyWrapper.swift | 0 CodeEdit/{ => App}/NSApp+openWindow.swift | 0 CodeEdit/{ => App}/SceneID.swift | 0 CodeEdit/{ => App}/SoftwareUpdater.swift | 0 CodeEdit/{ => App}/WindowObserver.swift | 0 CodeEdit/{ => App}/withTimeout.swift | 2 +- 33 files changed, 1 insertion(+), 1 deletion(-) rename CodeEdit/{ => App}/AppDelegate.swift (100%) rename CodeEdit/{ => App}/AppDependencies.swift (100%) rename CodeEdit/{ => App}/CodeEditApp.swift (100%) rename CodeEdit/{Features/Keybindings => App/Commands}/CommandManager.swift (100%) rename CodeEdit/{Features/Keybindings => App/Commands}/CommandManaging.swift (100%) rename CodeEdit/{Features/Keybindings => App/Commands}/KeybindingManager.swift (100%) rename CodeEdit/{Features/Keybindings => App/Commands}/KeybindingManaging.swift (100%) rename CodeEdit/{Features/Keybindings => App/Commands}/ModifierKeysObserver.swift (100%) rename CodeEdit/{Features/Keybindings => App/Commands}/default_keybindings.json (100%) rename CodeEdit/{ => App}/Environment+AppCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/CodeEditCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/CommandsFixes.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/EditorCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/ExtensionCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/FileCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/FindCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/FirstResponderPropertyWrapper.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/FocusedValues.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/HelpCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/KeyWindowControllerObserver.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/MainCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/NavigateCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/RecentProjectsMenu.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/SourceControlCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/TasksCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/ViewCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/WindowCommands.swift (100%) rename CodeEdit/{Features/WindowCommands => App/MenuBar}/WindowControllerPropertyWrapper.swift (100%) rename CodeEdit/{ => App}/NSApp+openWindow.swift (100%) rename CodeEdit/{ => App}/SceneID.swift (100%) rename CodeEdit/{ => App}/SoftwareUpdater.swift (100%) rename CodeEdit/{ => App}/WindowObserver.swift (100%) rename CodeEdit/{ => App}/withTimeout.swift (98%) diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/App/AppDelegate.swift similarity index 100% rename from CodeEdit/AppDelegate.swift rename to CodeEdit/App/AppDelegate.swift diff --git a/CodeEdit/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift similarity index 100% rename from CodeEdit/AppDependencies.swift rename to CodeEdit/App/AppDependencies.swift diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/App/CodeEditApp.swift similarity index 100% rename from CodeEdit/CodeEditApp.swift rename to CodeEdit/App/CodeEditApp.swift diff --git a/CodeEdit/Features/Keybindings/CommandManager.swift b/CodeEdit/App/Commands/CommandManager.swift similarity index 100% rename from CodeEdit/Features/Keybindings/CommandManager.swift rename to CodeEdit/App/Commands/CommandManager.swift diff --git a/CodeEdit/Features/Keybindings/CommandManaging.swift b/CodeEdit/App/Commands/CommandManaging.swift similarity index 100% rename from CodeEdit/Features/Keybindings/CommandManaging.swift rename to CodeEdit/App/Commands/CommandManaging.swift diff --git a/CodeEdit/Features/Keybindings/KeybindingManager.swift b/CodeEdit/App/Commands/KeybindingManager.swift similarity index 100% rename from CodeEdit/Features/Keybindings/KeybindingManager.swift rename to CodeEdit/App/Commands/KeybindingManager.swift diff --git a/CodeEdit/Features/Keybindings/KeybindingManaging.swift b/CodeEdit/App/Commands/KeybindingManaging.swift similarity index 100% rename from CodeEdit/Features/Keybindings/KeybindingManaging.swift rename to CodeEdit/App/Commands/KeybindingManaging.swift diff --git a/CodeEdit/Features/Keybindings/ModifierKeysObserver.swift b/CodeEdit/App/Commands/ModifierKeysObserver.swift similarity index 100% rename from CodeEdit/Features/Keybindings/ModifierKeysObserver.swift rename to CodeEdit/App/Commands/ModifierKeysObserver.swift diff --git a/CodeEdit/Features/Keybindings/default_keybindings.json b/CodeEdit/App/Commands/default_keybindings.json similarity index 100% rename from CodeEdit/Features/Keybindings/default_keybindings.json rename to CodeEdit/App/Commands/default_keybindings.json diff --git a/CodeEdit/Environment+AppCommands.swift b/CodeEdit/App/Environment+AppCommands.swift similarity index 100% rename from CodeEdit/Environment+AppCommands.swift rename to CodeEdit/App/Environment+AppCommands.swift diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/App/MenuBar/CodeEditCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/CodeEditCommands.swift rename to CodeEdit/App/MenuBar/CodeEditCommands.swift diff --git a/CodeEdit/Features/WindowCommands/CommandsFixes.swift b/CodeEdit/App/MenuBar/CommandsFixes.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/CommandsFixes.swift rename to CodeEdit/App/MenuBar/CommandsFixes.swift diff --git a/CodeEdit/Features/WindowCommands/EditorCommands.swift b/CodeEdit/App/MenuBar/EditorCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/EditorCommands.swift rename to CodeEdit/App/MenuBar/EditorCommands.swift diff --git a/CodeEdit/Features/WindowCommands/ExtensionCommands.swift b/CodeEdit/App/MenuBar/ExtensionCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/ExtensionCommands.swift rename to CodeEdit/App/MenuBar/ExtensionCommands.swift diff --git a/CodeEdit/Features/WindowCommands/FileCommands.swift b/CodeEdit/App/MenuBar/FileCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/FileCommands.swift rename to CodeEdit/App/MenuBar/FileCommands.swift diff --git a/CodeEdit/Features/WindowCommands/FindCommands.swift b/CodeEdit/App/MenuBar/FindCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/FindCommands.swift rename to CodeEdit/App/MenuBar/FindCommands.swift diff --git a/CodeEdit/Features/WindowCommands/FirstResponderPropertyWrapper.swift b/CodeEdit/App/MenuBar/FirstResponderPropertyWrapper.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/FirstResponderPropertyWrapper.swift rename to CodeEdit/App/MenuBar/FirstResponderPropertyWrapper.swift diff --git a/CodeEdit/Features/WindowCommands/FocusedValues.swift b/CodeEdit/App/MenuBar/FocusedValues.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/FocusedValues.swift rename to CodeEdit/App/MenuBar/FocusedValues.swift diff --git a/CodeEdit/Features/WindowCommands/HelpCommands.swift b/CodeEdit/App/MenuBar/HelpCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/HelpCommands.swift rename to CodeEdit/App/MenuBar/HelpCommands.swift diff --git a/CodeEdit/Features/WindowCommands/KeyWindowControllerObserver.swift b/CodeEdit/App/MenuBar/KeyWindowControllerObserver.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/KeyWindowControllerObserver.swift rename to CodeEdit/App/MenuBar/KeyWindowControllerObserver.swift diff --git a/CodeEdit/Features/WindowCommands/MainCommands.swift b/CodeEdit/App/MenuBar/MainCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/MainCommands.swift rename to CodeEdit/App/MenuBar/MainCommands.swift diff --git a/CodeEdit/Features/WindowCommands/NavigateCommands.swift b/CodeEdit/App/MenuBar/NavigateCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/NavigateCommands.swift rename to CodeEdit/App/MenuBar/NavigateCommands.swift diff --git a/CodeEdit/Features/WindowCommands/RecentProjectsMenu.swift b/CodeEdit/App/MenuBar/RecentProjectsMenu.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/RecentProjectsMenu.swift rename to CodeEdit/App/MenuBar/RecentProjectsMenu.swift diff --git a/CodeEdit/Features/WindowCommands/SourceControlCommands.swift b/CodeEdit/App/MenuBar/SourceControlCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/SourceControlCommands.swift rename to CodeEdit/App/MenuBar/SourceControlCommands.swift diff --git a/CodeEdit/Features/WindowCommands/TasksCommands.swift b/CodeEdit/App/MenuBar/TasksCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/TasksCommands.swift rename to CodeEdit/App/MenuBar/TasksCommands.swift diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/App/MenuBar/ViewCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/ViewCommands.swift rename to CodeEdit/App/MenuBar/ViewCommands.swift diff --git a/CodeEdit/Features/WindowCommands/WindowCommands.swift b/CodeEdit/App/MenuBar/WindowCommands.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/WindowCommands.swift rename to CodeEdit/App/MenuBar/WindowCommands.swift diff --git a/CodeEdit/Features/WindowCommands/WindowControllerPropertyWrapper.swift b/CodeEdit/App/MenuBar/WindowControllerPropertyWrapper.swift similarity index 100% rename from CodeEdit/Features/WindowCommands/WindowControllerPropertyWrapper.swift rename to CodeEdit/App/MenuBar/WindowControllerPropertyWrapper.swift diff --git a/CodeEdit/NSApp+openWindow.swift b/CodeEdit/App/NSApp+openWindow.swift similarity index 100% rename from CodeEdit/NSApp+openWindow.swift rename to CodeEdit/App/NSApp+openWindow.swift diff --git a/CodeEdit/SceneID.swift b/CodeEdit/App/SceneID.swift similarity index 100% rename from CodeEdit/SceneID.swift rename to CodeEdit/App/SceneID.swift diff --git a/CodeEdit/SoftwareUpdater.swift b/CodeEdit/App/SoftwareUpdater.swift similarity index 100% rename from CodeEdit/SoftwareUpdater.swift rename to CodeEdit/App/SoftwareUpdater.swift diff --git a/CodeEdit/WindowObserver.swift b/CodeEdit/App/WindowObserver.swift similarity index 100% rename from CodeEdit/WindowObserver.swift rename to CodeEdit/App/WindowObserver.swift diff --git a/CodeEdit/withTimeout.swift b/CodeEdit/App/withTimeout.swift similarity index 98% rename from CodeEdit/withTimeout.swift rename to CodeEdit/App/withTimeout.swift index 9db61b69b4..38f3b23430 100644 --- a/CodeEdit/withTimeout.swift +++ b/CodeEdit/App/withTimeout.swift @@ -1,5 +1,5 @@ // -// TimedOutError.swift +// withTimeout.swift // CodeEdit // // Created by Khan Winter on 7/8/25. From 4dc8cb40cc70d1df8ca234d993d1f48c9e2d7c9f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 13:56:21 +0200 Subject: [PATCH 183/335] Refactor: Group workspace-window features under WorkspaceWindow/ Window/ is promoted from Workspace/Window to a direct child of WorkspaceWindow. The Quick Actions palette folder is renamed from Commands to QuickActions (matching its types); CEWorkspaceSettings folder becomes WorkspaceSettings (the CE* folder prefix now signals feature packages; the type keeps its name). --- .../ActivityViewer/ActivityViewer.swift | 0 .../ActivityViewer/Notifications/TaskNotificationHandler.swift | 0 .../ActivityViewer/Notifications/TaskNotificationView.swift | 0 .../Notifications/TaskNotificationsDetailView.swift | 0 .../ActivityViewer/Tasks/ActiveTaskView.swift | 0 .../ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift | 0 .../ActivityViewer/Tasks/OptionMenuItemView.swift | 0 .../ActivityViewer/Tasks/SchemeDropDownView.swift | 0 .../ActivityViewer/Tasks/TaskDropDownView.swift | 0 .../ActivityViewer/Tasks/TaskView.swift | 0 .../ActivityViewer/Tasks/TasksPopoverMenuItem.swift | 0 .../ActivityViewer/Tasks/WorkspaceMenuItemView.swift | 0 .../InspectorArea/FileInspector/FileInspectorView.swift | 0 .../InspectorArea/HistoryInspector/HistoryInspectorItemView.swift | 0 .../InspectorArea/HistoryInspector/HistoryInspectorModel.swift | 0 .../InspectorArea/HistoryInspector/HistoryInspectorView.swift | 0 .../InspectorArea/HistoryInspector/HistoryPopoverView.swift | 0 .../InspectorArea/InspectorAreaView.swift | 0 .../InspectorArea/InspectorAreaViewModel.swift | 0 .../InspectorArea/InspectorField.swift | 0 .../InspectorArea/InspectorSection.swift | 0 .../InspectorArea/InspectorTab.swift | 0 .../InternalDevelopmentInspectorView.swift | 0 .../InternalDevelopmentNotificationsView.swift | 0 .../InternalDevelopmentOutputView.swift | 0 .../InspectorArea/NoSelectionInspectorView.swift | 0 .../NavigatorArea/FindNavigator/FindNavigatorTab.swift | 0 .../NavigatorArea/NavigatorAreaView.swift | 0 .../NavigatorArea/NavigatorAreaViewModel.swift | 0 .../NavigatorArea/NavigatorTab.swift | 0 .../NavigatorArea/OutlineView/FileSystemTableViewCell.swift | 0 .../NavigatorArea/OutlineView/StandardTableViewCell.swift | 0 .../NavigatorArea/OutlineView/TextTableViewCell.swift | 0 .../ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift | 0 .../OutlineView/ProjectNavigatorMenuActions.swift | 0 .../OutlineView/ProjectNavigatorNSOutlineView.swift | 0 .../OutlineView/ProjectNavigatorOutlineView.swift | 0 .../OutlineView/ProjectNavigatorTableViewCell.swift | 0 .../ProjectNavigatorViewController+NSMenuDelegate.swift | 0 .../ProjectNavigatorViewController+NSOutlineViewDataSource.swift | 0 .../ProjectNavigatorViewController+NSOutlineViewDelegate.swift | 0 ...jectNavigatorViewController+OutlineTableViewCellDelegate.swift | 0 .../OutlineView/ProjectNavigatorViewController.swift | 0 .../ProjectNavigator/ProjectNavigatorToolbarBottom.swift | 0 .../NavigatorArea/ProjectNavigator/ProjectNavigatorView.swift | 0 .../ProjectNavigator/ProjectNavigatorViewModel.swift | 0 .../Changes/SourceControlNavigatorChangesCommitView.swift | 0 .../Changes/SourceControlNavigatorChangesList.swift | 0 .../Changes/SourceControlNavigatorChangesView.swift | 0 .../Changes/SourceControlNavigatorNoRemotesView.swift | 0 .../Changes/SourceControlNavigatorSyncView.swift | 0 .../SourceControlNavigator/GitChangedFileLabel.swift | 0 .../SourceControlNavigator/GitChangedFileListView.swift | 0 .../SourceControlNavigator/History/CommitDetailsHeaderView.swift | 0 .../SourceControlNavigator/History/CommitDetailsView.swift | 0 .../SourceControlNavigator/History/CommitListItemView.swift | 0 .../History/SourceControlNavigatorHistoryView.swift | 0 .../NavigatorArea/SourceControlNavigator/History/String+MD5.swift | 0 .../SourceControlNavigator/Repository/RepoOutlineGroupItem.swift | 0 .../Repository/SourceControlNavigatorRepositoryItem.swift | 0 .../SourceControlNavigatorRepositoryView+contextMenu.swift | 0 .../SourceControlNavigatorRepositoryView+outlineGroupData.swift | 0 .../Repository/SourceControlNavigatorRepositoryView.swift | 0 .../SourceControlNavigatorToolbarBottom.swift | 0 .../SourceControlNavigator/SourceControlNavigatorView.swift | 0 .../OpenQuickly/OpenQuicklyListItemView.swift | 0 .../OpenQuickly/OpenQuicklyPreviewView.swift | 0 .../OpenQuickly/OpenQuicklyView.swift | 0 .../OpenQuickly/OpenQuicklyViewModel.swift | 0 .../OpenQuickly/URL+FuzzySearchable.swift | 0 .../OpenQuickly/URL+Identifiable.swift | 0 .../QuickActions}/QuickActionsView.swift | 0 .../QuickActions}/QuickActionsViewModel.swift | 0 .../{Features => WorkspaceWindow}/StatusBar/ImageDimensions.swift | 0 .../{Features => WorkspaceWindow}/StatusBar/StatusBarIcon.swift | 0 .../StatusBar/StatusBarItems/StatusBarBreakpointButton.swift | 0 .../StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift | 0 .../StatusBar/StatusBarItems/StatusBarEncodingSelector.swift | 0 .../StatusBar/StatusBarItems/StatusBarFileInfoView.swift | 0 .../StatusBar/StatusBarItems/StatusBarIndentSelector.swift | 0 .../StatusBar/StatusBarItems/StatusBarLineEndSelector.swift | 0 .../StatusBar/StatusBarItems/StatusBarMenuStyle.swift | 0 .../StatusBarItems/StatusBarToggleUtilityAreaButton.swift | 0 .../StatusBar/StatusBarItems/View+isHovering.swift | 0 .../{Features => WorkspaceWindow}/StatusBar/StatusBarView.swift | 0 .../StatusBar/StatusBarViewModel.swift | 0 .../UtilityArea/DebugUtility/TaskOutputActionsView.swift | 0 .../UtilityArea/DebugUtility/TaskOutputView.swift | 0 .../UtilityArea/DebugUtility/UtilityAreaDebugView.swift | 0 .../OutputUtility/ExtensionUtilityAreaOutputSource.swift | 0 .../OutputUtility/InternalDevelopmentOutputSource.swift | 0 .../OutputUtility/LanguageServerLogContainer+UtilityArea.swift | 0 .../UtilityArea/OutputUtility/UtilityAreaLogLevel.swift | 0 .../UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift | 0 .../UtilityArea/OutputUtility/UtilityAreaOutputSource.swift | 0 .../UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift | 0 .../UtilityArea/OutputUtility/UtilityAreaOutputView.swift | 0 .../{Features => WorkspaceWindow}/UtilityArea/PaneToolbar.swift | 0 .../UtilityArea/TerminalUtility/UtilityAreaTerminal.swift | 0 .../UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift | 0 .../UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift | 0 .../UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift | 0 .../UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift | 0 .../UtilityArea/Toolbar/UtilityAreaClearButton.swift | 0 .../UtilityArea/Toolbar/UtilityAreaFilterTextField.swift | 0 .../UtilityArea/Toolbar/UtilityAreaMaximizeButton.swift | 0 .../UtilityArea/Toolbar/UtilityAreaSplitTerminalButton.swift | 0 .../UtilityArea/UtilityAreaTab.swift | 0 .../UtilityArea/UtilityAreaTabView.swift | 0 .../UtilityArea/UtilityAreaTabViewModel.swift | 0 .../UtilityArea/UtilityAreaView.swift | 0 .../UtilityArea/UtilityAreaViewModel.swift | 0 .../UtilityArea/View+paneToolbar.swift | 0 .../Window/CodeEditSplitViewController.swift | 0 .../Window/CodeEditWindowController+Panels.swift | 0 .../Window/CodeEditWindowController+Toolbar.swift | 0 .../Window/CodeEditWindowController.swift | 0 .../Window/CodeEditWindowControllerExtensions.swift | 0 .../Window/NotificationPanelViewModel+Toolbar.swift | 0 .../Window/Toolbar/StartTaskToolbarButton.swift | 0 .../Window/Toolbar/StartTaskToolbarItem.swift | 0 .../Window/Toolbar/StopTaskToolbarButton.swift | 0 .../Window/Toolbar/StopTaskToolbarItem.swift | 0 .../Window/WorkspacePanel/WorkspacePanelTabBar.swift | 0 .../Window/WorkspacePanel/WorkspacePanelView.swift | 0 CodeEdit/{ => WorkspaceWindow/Window}/WorkspaceSheets.swift | 0 CodeEdit/{ => WorkspaceWindow/Window}/WorkspaceView.swift | 0 .../Workspace/Adapters/AppCodeFileDocumentDelegate.swift | 0 .../Workspace/Adapters/AppErrorNotifier.swift | 0 .../Workspace/Adapters/AppFileRelocator.swift | 0 .../Workspace/Adapters/AppWorkspaceFileOpener.swift | 0 .../Workspace/Adapters/AppWorkspaceNavigator.swift | 0 .../Workspace/Environment+Workspace.swift | 0 .../Workspace/Files/CEWorkspaceFile+Presentation.swift | 0 .../Workspace/Files/CEWorkspaceFileIcon.swift | 0 .../Workspace/Files/FileDropHandler.swift | 0 .../{Features => WorkspaceWindow}/Workspace/Files/FileMover.swift | 0 .../WindowManagement/ApplicationShutdownCoordinator.swift | 0 .../Workspace/WindowManagement/DocumentOpener.swift | 0 .../Workspace/WindowManagement/WorkspaceCloser.swift | 0 .../Workspace/WindowManagement/WorkspaceOpener.swift | 0 .../Workspace/WindowManagement/WorkspaceWindowManager.swift | 0 .../Workspace/WindowManagement/WorkspaceWindowManaging.swift | 0 CodeEdit/{Features => WorkspaceWindow}/Workspace/Workspace.swift | 0 .../Workspace/WorkspaceFactory.swift | 0 .../Workspace/WorkspaceNotificationModel.swift | 0 .../Workspace/WorkspaceStatePersistence.swift | 0 .../WorkspaceSettings}/AddCETaskView.swift | 0 .../WorkspaceSettings}/CETaskFormView.swift | 0 .../CEWorkspaceSettings+TasksConfigurationProviding.swift | 0 .../WorkspaceSettings}/CEWorkspaceSettings.swift | 0 .../WorkspaceSettings}/CEWorkspaceSettingsTaskListView.swift | 0 .../WorkspaceSettings}/CEWorkspaceSettingsView.swift | 0 .../WorkspaceSettings}/EditCETaskView.swift | 0 .../WorkspaceSettings}/EnvironmentVariableListItem.swift | 0 .../{Commands => QuickActions}/QuickActionsViewModelTests.swift | 0 156 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/ActivityViewer.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Notifications/TaskNotificationHandler.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Notifications/TaskNotificationView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Notifications/TaskNotificationsDetailView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/ActiveTaskView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/OptionMenuItemView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/SchemeDropDownView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/TaskDropDownView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/TaskView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/TasksPopoverMenuItem.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/ActivityViewer/Tasks/WorkspaceMenuItemView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/FileInspector/FileInspectorView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/HistoryInspector/HistoryInspectorModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/HistoryInspector/HistoryInspectorView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/HistoryInspector/HistoryPopoverView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InspectorAreaView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InspectorAreaViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InspectorField.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InspectorSection.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InspectorTab.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/InspectorArea/NoSelectionInspectorView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/FindNavigator/FindNavigatorTab.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/NavigatorAreaView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/NavigatorAreaViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/NavigatorTab.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/OutlineView/FileSystemTableViewCell.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/OutlineView/StandardTableViewCell.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/OutlineView/TextTableViewCell.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/ProjectNavigatorView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/History/String+MD5.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/OpenQuickly/OpenQuicklyListItemView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/OpenQuickly/OpenQuicklyPreviewView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/OpenQuickly/OpenQuicklyView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/OpenQuickly/OpenQuicklyViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/OpenQuickly/URL+FuzzySearchable.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/OpenQuickly/URL+Identifiable.swift (100%) rename CodeEdit/{Features/Commands => WorkspaceWindow/QuickActions}/QuickActionsView.swift (100%) rename CodeEdit/{Features/Commands => WorkspaceWindow/QuickActions}/QuickActionsViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/ImageDimensions.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarIcon.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarFileInfoView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarIndentSelector.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarMenuStyle.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarItems/View+isHovering.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/StatusBar/StatusBarViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/DebugUtility/TaskOutputActionsView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/DebugUtility/TaskOutputView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/DebugUtility/UtilityAreaDebugView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/OutputUtility/UtilityAreaOutputView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/PaneToolbar.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/Toolbar/UtilityAreaClearButton.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/Toolbar/UtilityAreaFilterTextField.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/Toolbar/UtilityAreaMaximizeButton.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/Toolbar/UtilityAreaSplitTerminalButton.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/UtilityAreaTab.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/UtilityAreaTabView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/UtilityAreaTabViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/UtilityAreaView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/UtilityAreaViewModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/UtilityArea/View+paneToolbar.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/CodeEditSplitViewController.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/CodeEditWindowController+Panels.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/CodeEditWindowController+Toolbar.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/CodeEditWindowController.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/CodeEditWindowControllerExtensions.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/NotificationPanelViewModel+Toolbar.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/Toolbar/StartTaskToolbarButton.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/Toolbar/StartTaskToolbarItem.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/Toolbar/StopTaskToolbarButton.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/Toolbar/StopTaskToolbarItem.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/WorkspacePanel/WorkspacePanelTabBar.swift (100%) rename CodeEdit/{Features/Workspace => WorkspaceWindow}/Window/WorkspacePanel/WorkspacePanelView.swift (100%) rename CodeEdit/{ => WorkspaceWindow/Window}/WorkspaceSheets.swift (100%) rename CodeEdit/{ => WorkspaceWindow/Window}/WorkspaceView.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Adapters/AppCodeFileDocumentDelegate.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Adapters/AppErrorNotifier.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Adapters/AppFileRelocator.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Adapters/AppWorkspaceFileOpener.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Adapters/AppWorkspaceNavigator.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Environment+Workspace.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Files/CEWorkspaceFile+Presentation.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Files/CEWorkspaceFileIcon.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Files/FileDropHandler.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Files/FileMover.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WindowManagement/DocumentOpener.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WindowManagement/WorkspaceCloser.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WindowManagement/WorkspaceOpener.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WindowManagement/WorkspaceWindowManager.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WindowManagement/WorkspaceWindowManaging.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/Workspace.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WorkspaceFactory.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WorkspaceNotificationModel.swift (100%) rename CodeEdit/{Features => WorkspaceWindow}/Workspace/WorkspaceStatePersistence.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/AddCETaskView.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/CETaskFormView.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/CEWorkspaceSettings+TasksConfigurationProviding.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/CEWorkspaceSettings.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/CEWorkspaceSettingsTaskListView.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/CEWorkspaceSettingsView.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/EditCETaskView.swift (100%) rename CodeEdit/{Features/CEWorkspaceSettings => WorkspaceWindow/WorkspaceSettings}/EnvironmentVariableListItem.swift (100%) rename CodeEditTests/Features/{Commands => QuickActions}/QuickActionsViewModelTests.swift (100%) diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/ActivityViewer.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/ActivityViewer.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/ActivityViewer.swift diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationHandler.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationHandler.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationHandler.swift diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationView.swift diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationsDetailView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationsDetailView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationsDetailView.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/ActiveTaskView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/ActiveTaskView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/ActiveTaskView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/ActiveTaskView.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/DropdownMenuItemStyleModifier.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/OptionMenuItemView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/OptionMenuItemView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/OptionMenuItemView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/OptionMenuItemView.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/SchemeDropDownView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/SchemeDropDownView.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/TaskDropDownView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/TaskDropDownView.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/TaskView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/TaskView.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/TasksPopoverMenuItem.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/TasksPopoverMenuItem.swift diff --git a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/WorkspaceMenuItemView.swift similarity index 100% rename from CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/Tasks/WorkspaceMenuItemView.swift diff --git a/CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/FileInspector/FileInspectorView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/FileInspector/FileInspectorView.swift diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorModel.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/HistoryInspector/HistoryInspectorView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift diff --git a/CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryPopoverView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/HistoryInspector/HistoryPopoverView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryPopoverView.swift diff --git a/CodeEdit/Features/InspectorArea/InspectorAreaView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InspectorAreaView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift diff --git a/CodeEdit/Features/InspectorArea/InspectorAreaViewModel.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InspectorAreaViewModel.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift diff --git a/CodeEdit/Features/InspectorArea/InspectorField.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorField.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InspectorField.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InspectorField.swift diff --git a/CodeEdit/Features/InspectorArea/InspectorSection.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorSection.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InspectorSection.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InspectorSection.swift diff --git a/CodeEdit/Features/InspectorArea/InspectorTab.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorTab.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InspectorTab.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InspectorTab.swift diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentNotificationsView.swift diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift diff --git a/CodeEdit/Features/InspectorArea/NoSelectionInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/NoSelectionInspectorView.swift similarity index 100% rename from CodeEdit/Features/InspectorArea/NoSelectionInspectorView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/NoSelectionInspectorView.swift diff --git a/CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigator/FindNavigatorTab.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/FindNavigator/FindNavigatorTab.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigator/FindNavigatorTab.swift diff --git a/CodeEdit/Features/NavigatorArea/NavigatorAreaView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/NavigatorAreaView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift diff --git a/CodeEdit/Features/NavigatorArea/NavigatorAreaViewModel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/NavigatorAreaViewModel.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift diff --git a/CodeEdit/Features/NavigatorArea/NavigatorTab.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorTab.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/NavigatorTab.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorTab.swift diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/FileSystemTableViewCell.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/OutlineView/FileSystemTableViewCell.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/FileSystemTableViewCell.swift diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/StandardTableViewCell.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/OutlineView/StandardTableViewCell.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/StandardTableViewCell.swift diff --git a/CodeEdit/Features/NavigatorArea/OutlineView/TextTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/TextTableViewCell.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/OutlineView/TextTableViewCell.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/TextTableViewCell.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorNSOutlineView.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSMenuDelegate.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDataSource.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorView.swift diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorViewModel.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/String+MD5.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/String+MD5.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/String+MD5.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/String+MD5.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift similarity index 100% rename from CodeEdit/Features/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift diff --git a/CodeEdit/Features/OpenQuickly/OpenQuicklyListItemView.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyListItemView.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/OpenQuicklyListItemView.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyListItemView.swift diff --git a/CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyPreviewView.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/OpenQuicklyPreviewView.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyPreviewView.swift diff --git a/CodeEdit/Features/OpenQuickly/OpenQuicklyView.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyView.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/OpenQuicklyView.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyView.swift diff --git a/CodeEdit/Features/OpenQuickly/OpenQuicklyViewModel.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyViewModel.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/OpenQuicklyViewModel.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyViewModel.swift diff --git a/CodeEdit/Features/OpenQuickly/URL+FuzzySearchable.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/URL+FuzzySearchable.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift diff --git a/CodeEdit/Features/OpenQuickly/URL+Identifiable.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/URL+Identifiable.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/URL+Identifiable.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/URL+Identifiable.swift diff --git a/CodeEdit/Features/Commands/QuickActionsView.swift b/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift similarity index 100% rename from CodeEdit/Features/Commands/QuickActionsView.swift rename to CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift diff --git a/CodeEdit/Features/Commands/QuickActionsViewModel.swift b/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsViewModel.swift similarity index 100% rename from CodeEdit/Features/Commands/QuickActionsViewModel.swift rename to CodeEdit/WorkspaceWindow/QuickActions/QuickActionsViewModel.swift diff --git a/CodeEdit/Features/StatusBar/ImageDimensions.swift b/CodeEdit/WorkspaceWindow/StatusBar/ImageDimensions.swift similarity index 100% rename from CodeEdit/Features/StatusBar/ImageDimensions.swift rename to CodeEdit/WorkspaceWindow/StatusBar/ImageDimensions.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarIcon.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarIcon.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarIcon.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarIcon.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarBreakpointButton.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarFileInfoView.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarFileInfoView.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarFileInfoView.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarIndentSelector.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarIndentSelector.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarIndentSelector.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarIndentSelector.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarMenuStyle.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarMenuStyle.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarMenuStyle.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarMenuStyle.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarItems/View+isHovering.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+isHovering.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarItems/View+isHovering.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+isHovering.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarView.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarView.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarView.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarView.swift diff --git a/CodeEdit/Features/StatusBar/StatusBarViewModel.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarViewModel.swift similarity index 100% rename from CodeEdit/Features/StatusBar/StatusBarViewModel.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarViewModel.swift diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/DebugUtility/TaskOutputActionsView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputActionsView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/DebugUtility/TaskOutputActionsView.swift diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/DebugUtility/TaskOutputView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/DebugUtility/TaskOutputView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/DebugUtility/TaskOutputView.swift diff --git a/CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/DebugUtility/UtilityAreaDebugView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/DebugUtility/UtilityAreaDebugView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/DebugUtility/UtilityAreaDebugView.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/ExtensionUtilityAreaOutputSource.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/InternalDevelopmentOutputSource.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/LanguageServerLogContainer+UtilityArea.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaLogLevel.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputLogList.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputSource.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputSourcePicker.swift diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/OutputUtility/UtilityAreaOutputView.swift diff --git a/CodeEdit/Features/UtilityArea/PaneToolbar.swift b/CodeEdit/WorkspaceWindow/UtilityArea/PaneToolbar.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/PaneToolbar.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/PaneToolbar.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminal.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalPicker.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift diff --git a/CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaClearButton.swift b/CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaClearButton.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaClearButton.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaClearButton.swift diff --git a/CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaFilterTextField.swift b/CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaFilterTextField.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaFilterTextField.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaFilterTextField.swift diff --git a/CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaMaximizeButton.swift b/CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaMaximizeButton.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaMaximizeButton.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaMaximizeButton.swift diff --git a/CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaSplitTerminalButton.swift b/CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaSplitTerminalButton.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Toolbar/UtilityAreaSplitTerminalButton.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/Toolbar/UtilityAreaSplitTerminalButton.swift diff --git a/CodeEdit/Features/UtilityArea/UtilityAreaTab.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTab.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/UtilityAreaTab.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTab.swift diff --git a/CodeEdit/Features/UtilityArea/UtilityAreaTabView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTabView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/UtilityAreaTabView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTabView.swift diff --git a/CodeEdit/Features/UtilityArea/UtilityAreaTabViewModel.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTabViewModel.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/UtilityAreaTabViewModel.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTabViewModel.swift diff --git a/CodeEdit/Features/UtilityArea/UtilityAreaView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/UtilityAreaView.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift diff --git a/CodeEdit/Features/UtilityArea/UtilityAreaViewModel.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/UtilityAreaViewModel.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift diff --git a/CodeEdit/Features/UtilityArea/View+paneToolbar.swift b/CodeEdit/WorkspaceWindow/UtilityArea/View+paneToolbar.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/View+paneToolbar.swift rename to CodeEdit/WorkspaceWindow/UtilityArea/View+paneToolbar.swift diff --git a/CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/Window/CodeEditSplitViewController.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/CodeEditSplitViewController.swift rename to CodeEdit/WorkspaceWindow/Window/CodeEditSplitViewController.swift diff --git a/CodeEdit/Features/Workspace/Window/CodeEditWindowController+Panels.swift b/CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Panels.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/CodeEditWindowController+Panels.swift rename to CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Panels.swift diff --git a/CodeEdit/Features/Workspace/Window/CodeEditWindowController+Toolbar.swift b/CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Toolbar.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/CodeEditWindowController+Toolbar.swift rename to CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Toolbar.swift diff --git a/CodeEdit/Features/Workspace/Window/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/Window/CodeEditWindowController.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/CodeEditWindowController.swift rename to CodeEdit/WorkspaceWindow/Window/CodeEditWindowController.swift diff --git a/CodeEdit/Features/Workspace/Window/CodeEditWindowControllerExtensions.swift b/CodeEdit/WorkspaceWindow/Window/CodeEditWindowControllerExtensions.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/CodeEditWindowControllerExtensions.swift rename to CodeEdit/WorkspaceWindow/Window/CodeEditWindowControllerExtensions.swift diff --git a/CodeEdit/Features/Workspace/Window/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/WorkspaceWindow/Window/NotificationPanelViewModel+Toolbar.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/NotificationPanelViewModel+Toolbar.swift rename to CodeEdit/WorkspaceWindow/Window/NotificationPanelViewModel+Toolbar.swift diff --git a/CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarButton.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarButton.swift rename to CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarButton.swift diff --git a/CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarItem.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Toolbar/StartTaskToolbarItem.swift rename to CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarItem.swift diff --git a/CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarButton.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarButton.swift rename to CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarButton.swift diff --git a/CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarItem.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/Toolbar/StopTaskToolbarItem.swift rename to CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarItem.swift diff --git a/CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelTabBar.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelTabBar.swift rename to CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelTabBar.swift diff --git a/CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelView.swift b/CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelView.swift similarity index 100% rename from CodeEdit/Features/Workspace/Window/WorkspacePanel/WorkspacePanelView.swift rename to CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelView.swift diff --git a/CodeEdit/WorkspaceSheets.swift b/CodeEdit/WorkspaceWindow/Window/WorkspaceSheets.swift similarity index 100% rename from CodeEdit/WorkspaceSheets.swift rename to CodeEdit/WorkspaceWindow/Window/WorkspaceSheets.swift diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceWindow/Window/WorkspaceView.swift similarity index 100% rename from CodeEdit/WorkspaceView.swift rename to CodeEdit/WorkspaceWindow/Window/WorkspaceView.swift diff --git a/CodeEdit/Features/Workspace/Adapters/AppCodeFileDocumentDelegate.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift similarity index 100% rename from CodeEdit/Features/Workspace/Adapters/AppCodeFileDocumentDelegate.swift rename to CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift diff --git a/CodeEdit/Features/Workspace/Adapters/AppErrorNotifier.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppErrorNotifier.swift similarity index 100% rename from CodeEdit/Features/Workspace/Adapters/AppErrorNotifier.swift rename to CodeEdit/WorkspaceWindow/Workspace/Adapters/AppErrorNotifier.swift diff --git a/CodeEdit/Features/Workspace/Adapters/AppFileRelocator.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppFileRelocator.swift similarity index 100% rename from CodeEdit/Features/Workspace/Adapters/AppFileRelocator.swift rename to CodeEdit/WorkspaceWindow/Workspace/Adapters/AppFileRelocator.swift diff --git a/CodeEdit/Features/Workspace/Adapters/AppWorkspaceFileOpener.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceFileOpener.swift similarity index 100% rename from CodeEdit/Features/Workspace/Adapters/AppWorkspaceFileOpener.swift rename to CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceFileOpener.swift diff --git a/CodeEdit/Features/Workspace/Adapters/AppWorkspaceNavigator.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift similarity index 100% rename from CodeEdit/Features/Workspace/Adapters/AppWorkspaceNavigator.swift rename to CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift diff --git a/CodeEdit/Features/Workspace/Environment+Workspace.swift b/CodeEdit/WorkspaceWindow/Workspace/Environment+Workspace.swift similarity index 100% rename from CodeEdit/Features/Workspace/Environment+Workspace.swift rename to CodeEdit/WorkspaceWindow/Workspace/Environment+Workspace.swift diff --git a/CodeEdit/Features/Workspace/Files/CEWorkspaceFile+Presentation.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift similarity index 100% rename from CodeEdit/Features/Workspace/Files/CEWorkspaceFile+Presentation.swift rename to CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift diff --git a/CodeEdit/Features/Workspace/Files/CEWorkspaceFileIcon.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFileIcon.swift similarity index 100% rename from CodeEdit/Features/Workspace/Files/CEWorkspaceFileIcon.swift rename to CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFileIcon.swift diff --git a/CodeEdit/Features/Workspace/Files/FileDropHandler.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift similarity index 100% rename from CodeEdit/Features/Workspace/Files/FileDropHandler.swift rename to CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift diff --git a/CodeEdit/Features/Workspace/Files/FileMover.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileMover.swift similarity index 100% rename from CodeEdit/Features/Workspace/Files/FileMover.swift rename to CodeEdit/WorkspaceWindow/Workspace/Files/FileMover.swift diff --git a/CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift b/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift similarity index 100% rename from CodeEdit/Features/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift rename to CodeEdit/WorkspaceWindow/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift diff --git a/CodeEdit/Features/Workspace/WindowManagement/DocumentOpener.swift b/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/DocumentOpener.swift similarity index 100% rename from CodeEdit/Features/Workspace/WindowManagement/DocumentOpener.swift rename to CodeEdit/WorkspaceWindow/Workspace/WindowManagement/DocumentOpener.swift diff --git a/CodeEdit/Features/Workspace/WindowManagement/WorkspaceCloser.swift b/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceCloser.swift similarity index 100% rename from CodeEdit/Features/Workspace/WindowManagement/WorkspaceCloser.swift rename to CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceCloser.swift diff --git a/CodeEdit/Features/Workspace/WindowManagement/WorkspaceOpener.swift b/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceOpener.swift similarity index 100% rename from CodeEdit/Features/Workspace/WindowManagement/WorkspaceOpener.swift rename to CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceOpener.swift diff --git a/CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManager.swift b/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManager.swift similarity index 100% rename from CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManager.swift rename to CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManager.swift diff --git a/CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManaging.swift b/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManaging.swift similarity index 100% rename from CodeEdit/Features/Workspace/WindowManagement/WorkspaceWindowManaging.swift rename to CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManaging.swift diff --git a/CodeEdit/Features/Workspace/Workspace.swift b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift similarity index 100% rename from CodeEdit/Features/Workspace/Workspace.swift rename to CodeEdit/WorkspaceWindow/Workspace/Workspace.swift diff --git a/CodeEdit/Features/Workspace/WorkspaceFactory.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift similarity index 100% rename from CodeEdit/Features/Workspace/WorkspaceFactory.swift rename to CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift diff --git a/CodeEdit/Features/Workspace/WorkspaceNotificationModel.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceNotificationModel.swift similarity index 100% rename from CodeEdit/Features/Workspace/WorkspaceNotificationModel.swift rename to CodeEdit/WorkspaceWindow/Workspace/WorkspaceNotificationModel.swift diff --git a/CodeEdit/Features/Workspace/WorkspaceStatePersistence.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceStatePersistence.swift similarity index 100% rename from CodeEdit/Features/Workspace/WorkspaceStatePersistence.swift rename to CodeEdit/WorkspaceWindow/Workspace/WorkspaceStatePersistence.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/AddCETaskView.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/AddCETaskView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/AddCETaskView.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/AddCETaskView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/CETaskFormView.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/CETaskFormView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/CETaskFormView.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/CETaskFormView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettings.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsTaskListView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsTaskListView.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsTaskListView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsView.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/CEWorkspaceSettingsView.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/EditCETaskView.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/EditCETaskView.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/EditCETaskView.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/EditCETaskView.swift diff --git a/CodeEdit/Features/CEWorkspaceSettings/EnvironmentVariableListItem.swift b/CodeEdit/WorkspaceWindow/WorkspaceSettings/EnvironmentVariableListItem.swift similarity index 100% rename from CodeEdit/Features/CEWorkspaceSettings/EnvironmentVariableListItem.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSettings/EnvironmentVariableListItem.swift diff --git a/CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift b/CodeEditTests/Features/QuickActions/QuickActionsViewModelTests.swift similarity index 100% rename from CodeEditTests/Features/Commands/QuickActionsViewModelTests.swift rename to CodeEditTests/Features/QuickActions/QuickActionsViewModelTests.swift From fe8c0b1533b79e04f9d96956cc0e431615e61b7c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 14:05:22 +0200 Subject: [PATCH 184/335] Refactor: Group auxiliary windows under Windows/ and subgroup Settings Features/ is now empty and removed; the app target's top level reads App / WorkspaceWindow / Windows / Utils. Settings root splits into Search/ and Controls/; the Extensions settings page is renamed ExtensionsSettings to match its siblings. Updates the extension-point embed path in project.pbxproj (the one path literal in the project file). --- CodeEdit.xcodeproj/project.pbxproj | 2 +- CodeEdit/{Features => Windows}/About/AboutFooterView.swift | 0 CodeEdit/{Features => Windows}/About/AboutSubtitleView.swift | 0 .../About/Acknowledgements/AcknowledgementRowView.swift | 0 .../About/Acknowledgements/AcknowledgementsView.swift | 0 .../About/Acknowledgements/AcknowledgementsViewModel.swift | 0 .../About/Acknowledgements/ParsePackagesResolved.swift | 0 .../{Features => Windows}/About/Contributors/Contributor.swift | 0 .../About/Contributors/ContributorRowView.swift | 0 .../About/Contributors/ContributorsView.swift | 0 .../About/OperatingSystemVersion+String.swift | 0 .../{Features => Windows}/Extensions/Commands+ForEach.swift | 0 .../Extensions/ExtensionActivatorView.swift | 0 .../{Features => Windows}/Extensions/ExtensionDetailView.swift | 0 .../{Features => Windows}/Extensions/ExtensionDiscovery.swift | 0 CodeEdit/{Features => Windows}/Extensions/ExtensionInfo.swift | 0 .../Extensions/ExtensionManagerWindow.swift | 0 .../{Features => Windows}/Extensions/ExtensionSceneView.swift | 0 .../{Features => Windows}/Extensions/ExtensionsListView.swift | 0 .../{Features => Windows}/Extensions/ExtensionsManager.swift | 0 .../Extensions/codeedit.extension.appextensionpoint | 0 CodeEdit/{Features => Windows}/Feedback/FeedbackIssueArea.swift | 0 CodeEdit/{Features => Windows}/Feedback/FeedbackModel.swift | 0 CodeEdit/{Features => Windows}/Feedback/FeedbackToolbar.swift | 0 CodeEdit/{Features => Windows}/Feedback/FeedbackType.swift | 0 CodeEdit/{Features => Windows}/Feedback/FeedbackView.swift | 0 .../Feedback/FeedbackWindowController.swift | 0 .../Settings => Windows/Settings/Controls}/ExternalLink.swift | 0 .../Settings/Controls}/FontWeightPicker.swift | 0 .../Settings/Controls}/GlobPatternList.swift | 0 .../Settings/Controls}/GlobPatternListItem.swift | 0 .../Settings => Windows/Settings/Controls}/Int+HexString.swift | 0 .../Settings/Controls}/InvisibleCharacterWarningList.swift | 0 .../Settings/Controls}/MonospacedFontPicker.swift | 0 .../Settings/Controls}/SettingsColorPicker.swift | 0 .../Settings/Controls}/WarningCharactersView.swift | 0 CodeEdit/{Features => Windows}/Settings/PageAndSettings.swift | 0 .../Settings/Pages/AccountsSettings/AccountSelectionView.swift | 0 .../Pages/AccountsSettings/AccountsSettingsAccountLink.swift | 0 .../Pages/AccountsSettings/AccountsSettingsDetailsView.swift | 0 .../Pages/AccountsSettings/AccountsSettingsProviderRow.swift | 0 .../Pages/AccountsSettings/AccountsSettingsSigninView.swift | 0 .../Settings/Pages/AccountsSettings/AccountsSettingsView.swift | 0 .../Settings/Pages/AccountsSettings/CreateSSHKeyView.swift | 0 .../Settings/Pages/AccountsSettings/Font+Caption3.swift | 0 .../Pages/AccountsSettings/SourceControlAccount+Icon.swift | 0 .../Pages/DeveloperSettings/DeveloperSettingsView.swift | 0 .../Pages/ExtensionsSettings}/LanguageServerInstallView.swift | 0 .../Pages/ExtensionsSettings}/LanguageServerRowView.swift | 0 .../Pages/ExtensionsSettings}/LanguageServersView.swift | 0 .../ExtensionsSettings}/RegistryItem+FuzzySearchable.swift | 0 .../Settings/Pages/GeneralSettings/GeneralSettingsView.swift | 0 .../Settings/Pages/LocationsSettings/LocationsSettings.swift | 0 .../Pages/LocationsSettings/LocationsSettingsView.swift | 0 .../Pages/NavigationSettings/NavigationSettingsView.swift | 0 .../SearchSettingsIgnoreGlobPatternItemView.swift | 0 .../Settings/Pages/SearchSettings/SearchSettingsModel.swift | 0 .../Settings/Pages/SearchSettings/SearchSettingsView.swift | 0 .../Pages/SourceControlSettings/IgnorePatternModel.swift | 0 .../Pages/SourceControlSettings/IgnoredFilesListView.swift | 0 .../Settings/Pages/SourceControlSettings/Limiter.swift | 0 .../Pages/SourceControlSettings/SourceControlGeneralView.swift | 0 .../Pages/SourceControlSettings/SourceControlGitView.swift | 0 .../Pages/SourceControlSettings/SourceControlSettingsView.swift | 0 .../Settings/Pages/TerminalSettings/TerminalSettingsView.swift | 0 .../Pages/TextEditingSettings/InvisiblesSettingsView.swift | 0 .../Pages/TextEditingSettings/TextEditingSettingsView.swift | 0 .../Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift | 0 .../Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift | 0 .../Settings/Pages/ThemeSettings/ThemeModel+Export.swift | 0 .../Settings/Pages/ThemeSettings/ThemeModel.swift | 0 .../Settings/Pages/ThemeSettings/ThemeRepository.swift | 0 .../Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift | 0 .../Pages/ThemeSettings/ThemeSettingsColorPreview.swift | 0 .../Pages/ThemeSettings/ThemeSettingsThemeDetails.swift | 0 .../Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift | 0 .../Settings/Pages/ThemeSettings/ThemeSettingsView.swift | 0 .../Settings/Search}/SearchableSettingsPage.swift | 0 .../Settings/Search}/SettingsData+Search.swift | 0 .../Settings/Search}/SettingsSearchResult.swift | 0 .../Settings/Search}/String+HighlightOccurrences.swift | 0 .../Settings/SettingsData+CommandRegistration.swift | 0 .../Settings/SettingsData+KeybindingReconcile.swift | 0 CodeEdit/{Features => Windows}/Settings/SettingsForm.swift | 0 CodeEdit/{Features => Windows}/Settings/SettingsInjector.swift | 0 CodeEdit/{Features => Windows}/Settings/SettingsPage.swift | 0 CodeEdit/{Features => Windows}/Settings/SettingsPageView.swift | 0 .../{Features => Windows}/Settings/SettingsSidebarFix.swift | 0 CodeEdit/{Features => Windows}/Settings/SettingsView.swift | 0 CodeEdit/{Features => Windows}/Settings/SettingsWindow.swift | 0 .../Settings/View+ConstrainHeightToWindow.swift | 0 .../{Features => Windows}/Settings/View+HideSidebarToggle.swift | 0 .../Settings/View+NavigationBarBackButtonVisible.swift | 0 CodeEdit/{Features => Windows}/Welcome/GitCloneButton.swift | 0 CodeEdit/{Features => Windows}/Welcome/NewFileButton.swift | 0 .../{Features => Windows}/Welcome/OpenFileOrFolderButton.swift | 0 .../{Features => Windows}/Welcome/WelcomeSubtitleView.swift | 0 97 files changed, 1 insertion(+), 1 deletion(-) rename CodeEdit/{Features => Windows}/About/AboutFooterView.swift (100%) rename CodeEdit/{Features => Windows}/About/AboutSubtitleView.swift (100%) rename CodeEdit/{Features => Windows}/About/Acknowledgements/AcknowledgementRowView.swift (100%) rename CodeEdit/{Features => Windows}/About/Acknowledgements/AcknowledgementsView.swift (100%) rename CodeEdit/{Features => Windows}/About/Acknowledgements/AcknowledgementsViewModel.swift (100%) rename CodeEdit/{Features => Windows}/About/Acknowledgements/ParsePackagesResolved.swift (100%) rename CodeEdit/{Features => Windows}/About/Contributors/Contributor.swift (100%) rename CodeEdit/{Features => Windows}/About/Contributors/ContributorRowView.swift (100%) rename CodeEdit/{Features => Windows}/About/Contributors/ContributorsView.swift (100%) rename CodeEdit/{Features => Windows}/About/OperatingSystemVersion+String.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/Commands+ForEach.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionActivatorView.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionDetailView.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionDiscovery.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionInfo.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionManagerWindow.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionSceneView.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionsListView.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/ExtensionsManager.swift (100%) rename CodeEdit/{Features => Windows}/Extensions/codeedit.extension.appextensionpoint (100%) rename CodeEdit/{Features => Windows}/Feedback/FeedbackIssueArea.swift (100%) rename CodeEdit/{Features => Windows}/Feedback/FeedbackModel.swift (100%) rename CodeEdit/{Features => Windows}/Feedback/FeedbackToolbar.swift (100%) rename CodeEdit/{Features => Windows}/Feedback/FeedbackType.swift (100%) rename CodeEdit/{Features => Windows}/Feedback/FeedbackView.swift (100%) rename CodeEdit/{Features => Windows}/Feedback/FeedbackWindowController.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/ExternalLink.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/FontWeightPicker.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/GlobPatternList.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/GlobPatternListItem.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/Int+HexString.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/InvisibleCharacterWarningList.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/MonospacedFontPicker.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/SettingsColorPicker.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Controls}/WarningCharactersView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/PageAndSettings.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/AccountSelectionView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/AccountsSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/Font+Caption3.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift (100%) rename CodeEdit/{Features/Settings/Pages/Extensions => Windows/Settings/Pages/ExtensionsSettings}/LanguageServerInstallView.swift (100%) rename CodeEdit/{Features/Settings/Pages/Extensions => Windows/Settings/Pages/ExtensionsSettings}/LanguageServerRowView.swift (100%) rename CodeEdit/{Features/Settings/Pages/Extensions => Windows/Settings/Pages/ExtensionsSettings}/LanguageServersView.swift (100%) rename CodeEdit/{Features/Settings/Pages/Extensions => Windows/Settings/Pages/ExtensionsSettings}/RegistryItem+FuzzySearchable.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/GeneralSettings/GeneralSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/LocationsSettings/LocationsSettings.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/LocationsSettings/LocationsSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/NavigationSettings/NavigationSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SearchSettings/SearchSettingsModel.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SearchSettings/SearchSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SourceControlSettings/Limiter.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SourceControlSettings/SourceControlGitView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/TerminalSettings/TerminalSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeModel+Export.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeModel.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeRepository.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift (100%) rename CodeEdit/{Features => Windows}/Settings/Pages/ThemeSettings/ThemeSettingsView.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Search}/SearchableSettingsPage.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Search}/SettingsData+Search.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Search}/SettingsSearchResult.swift (100%) rename CodeEdit/{Features/Settings => Windows/Settings/Search}/String+HighlightOccurrences.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsData+CommandRegistration.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsData+KeybindingReconcile.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsForm.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsInjector.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsPage.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsPageView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsSidebarFix.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsView.swift (100%) rename CodeEdit/{Features => Windows}/Settings/SettingsWindow.swift (100%) rename CodeEdit/{Features => Windows}/Settings/View+ConstrainHeightToWindow.swift (100%) rename CodeEdit/{Features => Windows}/Settings/View+HideSidebarToggle.swift (100%) rename CodeEdit/{Features => Windows}/Settings/View+NavigationBarBackButtonVisible.swift (100%) rename CodeEdit/{Features => Windows}/Welcome/GitCloneButton.swift (100%) rename CodeEdit/{Features => Windows}/Welcome/NewFileButton.swift (100%) rename CodeEdit/{Features => Windows}/Welcome/OpenFileOrFolderButton.swift (100%) rename CodeEdit/{Features => Windows}/Welcome/WelcomeSubtitleView.swift (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 2524f86aaf..66d92392e6 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; buildPhase = 6C6BD6FD29CD154900235D17 /* Embed ExtensionKit ExtensionPoint */; membershipExceptions = ( - Features/Extensions/codeedit.extension.appextensionpoint, + Windows/Extensions/codeedit.extension.appextensionpoint, ); }; /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ diff --git a/CodeEdit/Features/About/AboutFooterView.swift b/CodeEdit/Windows/About/AboutFooterView.swift similarity index 100% rename from CodeEdit/Features/About/AboutFooterView.swift rename to CodeEdit/Windows/About/AboutFooterView.swift diff --git a/CodeEdit/Features/About/AboutSubtitleView.swift b/CodeEdit/Windows/About/AboutSubtitleView.swift similarity index 100% rename from CodeEdit/Features/About/AboutSubtitleView.swift rename to CodeEdit/Windows/About/AboutSubtitleView.swift diff --git a/CodeEdit/Features/About/Acknowledgements/AcknowledgementRowView.swift b/CodeEdit/Windows/About/Acknowledgements/AcknowledgementRowView.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/AcknowledgementRowView.swift rename to CodeEdit/Windows/About/Acknowledgements/AcknowledgementRowView.swift diff --git a/CodeEdit/Features/About/Acknowledgements/AcknowledgementsView.swift b/CodeEdit/Windows/About/Acknowledgements/AcknowledgementsView.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/AcknowledgementsView.swift rename to CodeEdit/Windows/About/Acknowledgements/AcknowledgementsView.swift diff --git a/CodeEdit/Features/About/Acknowledgements/AcknowledgementsViewModel.swift b/CodeEdit/Windows/About/Acknowledgements/AcknowledgementsViewModel.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/AcknowledgementsViewModel.swift rename to CodeEdit/Windows/About/Acknowledgements/AcknowledgementsViewModel.swift diff --git a/CodeEdit/Features/About/Acknowledgements/ParsePackagesResolved.swift b/CodeEdit/Windows/About/Acknowledgements/ParsePackagesResolved.swift similarity index 100% rename from CodeEdit/Features/About/Acknowledgements/ParsePackagesResolved.swift rename to CodeEdit/Windows/About/Acknowledgements/ParsePackagesResolved.swift diff --git a/CodeEdit/Features/About/Contributors/Contributor.swift b/CodeEdit/Windows/About/Contributors/Contributor.swift similarity index 100% rename from CodeEdit/Features/About/Contributors/Contributor.swift rename to CodeEdit/Windows/About/Contributors/Contributor.swift diff --git a/CodeEdit/Features/About/Contributors/ContributorRowView.swift b/CodeEdit/Windows/About/Contributors/ContributorRowView.swift similarity index 100% rename from CodeEdit/Features/About/Contributors/ContributorRowView.swift rename to CodeEdit/Windows/About/Contributors/ContributorRowView.swift diff --git a/CodeEdit/Features/About/Contributors/ContributorsView.swift b/CodeEdit/Windows/About/Contributors/ContributorsView.swift similarity index 100% rename from CodeEdit/Features/About/Contributors/ContributorsView.swift rename to CodeEdit/Windows/About/Contributors/ContributorsView.swift diff --git a/CodeEdit/Features/About/OperatingSystemVersion+String.swift b/CodeEdit/Windows/About/OperatingSystemVersion+String.swift similarity index 100% rename from CodeEdit/Features/About/OperatingSystemVersion+String.swift rename to CodeEdit/Windows/About/OperatingSystemVersion+String.swift diff --git a/CodeEdit/Features/Extensions/Commands+ForEach.swift b/CodeEdit/Windows/Extensions/Commands+ForEach.swift similarity index 100% rename from CodeEdit/Features/Extensions/Commands+ForEach.swift rename to CodeEdit/Windows/Extensions/Commands+ForEach.swift diff --git a/CodeEdit/Features/Extensions/ExtensionActivatorView.swift b/CodeEdit/Windows/Extensions/ExtensionActivatorView.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionActivatorView.swift rename to CodeEdit/Windows/Extensions/ExtensionActivatorView.swift diff --git a/CodeEdit/Features/Extensions/ExtensionDetailView.swift b/CodeEdit/Windows/Extensions/ExtensionDetailView.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionDetailView.swift rename to CodeEdit/Windows/Extensions/ExtensionDetailView.swift diff --git a/CodeEdit/Features/Extensions/ExtensionDiscovery.swift b/CodeEdit/Windows/Extensions/ExtensionDiscovery.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionDiscovery.swift rename to CodeEdit/Windows/Extensions/ExtensionDiscovery.swift diff --git a/CodeEdit/Features/Extensions/ExtensionInfo.swift b/CodeEdit/Windows/Extensions/ExtensionInfo.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionInfo.swift rename to CodeEdit/Windows/Extensions/ExtensionInfo.swift diff --git a/CodeEdit/Features/Extensions/ExtensionManagerWindow.swift b/CodeEdit/Windows/Extensions/ExtensionManagerWindow.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionManagerWindow.swift rename to CodeEdit/Windows/Extensions/ExtensionManagerWindow.swift diff --git a/CodeEdit/Features/Extensions/ExtensionSceneView.swift b/CodeEdit/Windows/Extensions/ExtensionSceneView.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionSceneView.swift rename to CodeEdit/Windows/Extensions/ExtensionSceneView.swift diff --git a/CodeEdit/Features/Extensions/ExtensionsListView.swift b/CodeEdit/Windows/Extensions/ExtensionsListView.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionsListView.swift rename to CodeEdit/Windows/Extensions/ExtensionsListView.swift diff --git a/CodeEdit/Features/Extensions/ExtensionsManager.swift b/CodeEdit/Windows/Extensions/ExtensionsManager.swift similarity index 100% rename from CodeEdit/Features/Extensions/ExtensionsManager.swift rename to CodeEdit/Windows/Extensions/ExtensionsManager.swift diff --git a/CodeEdit/Features/Extensions/codeedit.extension.appextensionpoint b/CodeEdit/Windows/Extensions/codeedit.extension.appextensionpoint similarity index 100% rename from CodeEdit/Features/Extensions/codeedit.extension.appextensionpoint rename to CodeEdit/Windows/Extensions/codeedit.extension.appextensionpoint diff --git a/CodeEdit/Features/Feedback/FeedbackIssueArea.swift b/CodeEdit/Windows/Feedback/FeedbackIssueArea.swift similarity index 100% rename from CodeEdit/Features/Feedback/FeedbackIssueArea.swift rename to CodeEdit/Windows/Feedback/FeedbackIssueArea.swift diff --git a/CodeEdit/Features/Feedback/FeedbackModel.swift b/CodeEdit/Windows/Feedback/FeedbackModel.swift similarity index 100% rename from CodeEdit/Features/Feedback/FeedbackModel.swift rename to CodeEdit/Windows/Feedback/FeedbackModel.swift diff --git a/CodeEdit/Features/Feedback/FeedbackToolbar.swift b/CodeEdit/Windows/Feedback/FeedbackToolbar.swift similarity index 100% rename from CodeEdit/Features/Feedback/FeedbackToolbar.swift rename to CodeEdit/Windows/Feedback/FeedbackToolbar.swift diff --git a/CodeEdit/Features/Feedback/FeedbackType.swift b/CodeEdit/Windows/Feedback/FeedbackType.swift similarity index 100% rename from CodeEdit/Features/Feedback/FeedbackType.swift rename to CodeEdit/Windows/Feedback/FeedbackType.swift diff --git a/CodeEdit/Features/Feedback/FeedbackView.swift b/CodeEdit/Windows/Feedback/FeedbackView.swift similarity index 100% rename from CodeEdit/Features/Feedback/FeedbackView.swift rename to CodeEdit/Windows/Feedback/FeedbackView.swift diff --git a/CodeEdit/Features/Feedback/FeedbackWindowController.swift b/CodeEdit/Windows/Feedback/FeedbackWindowController.swift similarity index 100% rename from CodeEdit/Features/Feedback/FeedbackWindowController.swift rename to CodeEdit/Windows/Feedback/FeedbackWindowController.swift diff --git a/CodeEdit/Features/Settings/ExternalLink.swift b/CodeEdit/Windows/Settings/Controls/ExternalLink.swift similarity index 100% rename from CodeEdit/Features/Settings/ExternalLink.swift rename to CodeEdit/Windows/Settings/Controls/ExternalLink.swift diff --git a/CodeEdit/Features/Settings/FontWeightPicker.swift b/CodeEdit/Windows/Settings/Controls/FontWeightPicker.swift similarity index 100% rename from CodeEdit/Features/Settings/FontWeightPicker.swift rename to CodeEdit/Windows/Settings/Controls/FontWeightPicker.swift diff --git a/CodeEdit/Features/Settings/GlobPatternList.swift b/CodeEdit/Windows/Settings/Controls/GlobPatternList.swift similarity index 100% rename from CodeEdit/Features/Settings/GlobPatternList.swift rename to CodeEdit/Windows/Settings/Controls/GlobPatternList.swift diff --git a/CodeEdit/Features/Settings/GlobPatternListItem.swift b/CodeEdit/Windows/Settings/Controls/GlobPatternListItem.swift similarity index 100% rename from CodeEdit/Features/Settings/GlobPatternListItem.swift rename to CodeEdit/Windows/Settings/Controls/GlobPatternListItem.swift diff --git a/CodeEdit/Features/Settings/Int+HexString.swift b/CodeEdit/Windows/Settings/Controls/Int+HexString.swift similarity index 100% rename from CodeEdit/Features/Settings/Int+HexString.swift rename to CodeEdit/Windows/Settings/Controls/Int+HexString.swift diff --git a/CodeEdit/Features/Settings/InvisibleCharacterWarningList.swift b/CodeEdit/Windows/Settings/Controls/InvisibleCharacterWarningList.swift similarity index 100% rename from CodeEdit/Features/Settings/InvisibleCharacterWarningList.swift rename to CodeEdit/Windows/Settings/Controls/InvisibleCharacterWarningList.swift diff --git a/CodeEdit/Features/Settings/MonospacedFontPicker.swift b/CodeEdit/Windows/Settings/Controls/MonospacedFontPicker.swift similarity index 100% rename from CodeEdit/Features/Settings/MonospacedFontPicker.swift rename to CodeEdit/Windows/Settings/Controls/MonospacedFontPicker.swift diff --git a/CodeEdit/Features/Settings/SettingsColorPicker.swift b/CodeEdit/Windows/Settings/Controls/SettingsColorPicker.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsColorPicker.swift rename to CodeEdit/Windows/Settings/Controls/SettingsColorPicker.swift diff --git a/CodeEdit/Features/Settings/WarningCharactersView.swift b/CodeEdit/Windows/Settings/Controls/WarningCharactersView.swift similarity index 100% rename from CodeEdit/Features/Settings/WarningCharactersView.swift rename to CodeEdit/Windows/Settings/Controls/WarningCharactersView.swift diff --git a/CodeEdit/Features/Settings/PageAndSettings.swift b/CodeEdit/Windows/Settings/PageAndSettings.swift similarity index 100% rename from CodeEdit/Features/Settings/PageAndSettings.swift rename to CodeEdit/Windows/Settings/PageAndSettings.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountSelectionView.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountSelectionView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/AccountSelectionView.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountSelectionView.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsView.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/AccountsSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/Font+Caption3.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/Font+Caption3.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/Font+Caption3.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/Font+Caption3.swift diff --git a/CodeEdit/Features/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift b/CodeEdit/Windows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift rename to CodeEdit/Windows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift diff --git a/CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift b/CodeEdit/Windows/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/Extensions/LanguageServerInstallView.swift rename to CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/Extensions/LanguageServerRowView.swift rename to CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift diff --git a/CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/Extensions/LanguageServersView.swift rename to CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift diff --git a/CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/Extensions/RegistryItem+FuzzySearchable.swift rename to CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift diff --git a/CodeEdit/Features/Settings/Pages/GeneralSettings/GeneralSettingsView.swift b/CodeEdit/Windows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/GeneralSettings/GeneralSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettings.swift b/CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettings.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettings.swift rename to CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettings.swift diff --git a/CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettingsView.swift b/CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/LocationsSettings/LocationsSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/NavigationSettings/NavigationSettingsView.swift b/CodeEdit/Windows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/NavigationSettings/NavigationSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift b/CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift rename to CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsModel.swift b/CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsModel.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsModel.swift rename to CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsModel.swift diff --git a/CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsView.swift b/CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SearchSettings/SearchSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift b/CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift rename to CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift b/CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift rename to CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/Limiter.swift b/CodeEdit/Windows/Settings/Pages/SourceControlSettings/Limiter.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/Limiter.swift rename to CodeEdit/Windows/Settings/Pages/SourceControlSettings/Limiter.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift rename to CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGitView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift rename to CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGitView.swift diff --git a/CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift b/CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/Windows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift b/CodeEdit/Windows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift b/CodeEdit/Windows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+Export.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel+Export.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeModel.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeRepository.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeRepository.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeRepository.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeRepository.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift diff --git a/CodeEdit/Features/Settings/SearchableSettingsPage.swift b/CodeEdit/Windows/Settings/Search/SearchableSettingsPage.swift similarity index 100% rename from CodeEdit/Features/Settings/SearchableSettingsPage.swift rename to CodeEdit/Windows/Settings/Search/SearchableSettingsPage.swift diff --git a/CodeEdit/Features/Settings/SettingsData+Search.swift b/CodeEdit/Windows/Settings/Search/SettingsData+Search.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsData+Search.swift rename to CodeEdit/Windows/Settings/Search/SettingsData+Search.swift diff --git a/CodeEdit/Features/Settings/SettingsSearchResult.swift b/CodeEdit/Windows/Settings/Search/SettingsSearchResult.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsSearchResult.swift rename to CodeEdit/Windows/Settings/Search/SettingsSearchResult.swift diff --git a/CodeEdit/Features/Settings/String+HighlightOccurrences.swift b/CodeEdit/Windows/Settings/Search/String+HighlightOccurrences.swift similarity index 100% rename from CodeEdit/Features/Settings/String+HighlightOccurrences.swift rename to CodeEdit/Windows/Settings/Search/String+HighlightOccurrences.swift diff --git a/CodeEdit/Features/Settings/SettingsData+CommandRegistration.swift b/CodeEdit/Windows/Settings/SettingsData+CommandRegistration.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsData+CommandRegistration.swift rename to CodeEdit/Windows/Settings/SettingsData+CommandRegistration.swift diff --git a/CodeEdit/Features/Settings/SettingsData+KeybindingReconcile.swift b/CodeEdit/Windows/Settings/SettingsData+KeybindingReconcile.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsData+KeybindingReconcile.swift rename to CodeEdit/Windows/Settings/SettingsData+KeybindingReconcile.swift diff --git a/CodeEdit/Features/Settings/SettingsForm.swift b/CodeEdit/Windows/Settings/SettingsForm.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsForm.swift rename to CodeEdit/Windows/Settings/SettingsForm.swift diff --git a/CodeEdit/Features/Settings/SettingsInjector.swift b/CodeEdit/Windows/Settings/SettingsInjector.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsInjector.swift rename to CodeEdit/Windows/Settings/SettingsInjector.swift diff --git a/CodeEdit/Features/Settings/SettingsPage.swift b/CodeEdit/Windows/Settings/SettingsPage.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsPage.swift rename to CodeEdit/Windows/Settings/SettingsPage.swift diff --git a/CodeEdit/Features/Settings/SettingsPageView.swift b/CodeEdit/Windows/Settings/SettingsPageView.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsPageView.swift rename to CodeEdit/Windows/Settings/SettingsPageView.swift diff --git a/CodeEdit/Features/Settings/SettingsSidebarFix.swift b/CodeEdit/Windows/Settings/SettingsSidebarFix.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsSidebarFix.swift rename to CodeEdit/Windows/Settings/SettingsSidebarFix.swift diff --git a/CodeEdit/Features/Settings/SettingsView.swift b/CodeEdit/Windows/Settings/SettingsView.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsView.swift rename to CodeEdit/Windows/Settings/SettingsView.swift diff --git a/CodeEdit/Features/Settings/SettingsWindow.swift b/CodeEdit/Windows/Settings/SettingsWindow.swift similarity index 100% rename from CodeEdit/Features/Settings/SettingsWindow.swift rename to CodeEdit/Windows/Settings/SettingsWindow.swift diff --git a/CodeEdit/Features/Settings/View+ConstrainHeightToWindow.swift b/CodeEdit/Windows/Settings/View+ConstrainHeightToWindow.swift similarity index 100% rename from CodeEdit/Features/Settings/View+ConstrainHeightToWindow.swift rename to CodeEdit/Windows/Settings/View+ConstrainHeightToWindow.swift diff --git a/CodeEdit/Features/Settings/View+HideSidebarToggle.swift b/CodeEdit/Windows/Settings/View+HideSidebarToggle.swift similarity index 100% rename from CodeEdit/Features/Settings/View+HideSidebarToggle.swift rename to CodeEdit/Windows/Settings/View+HideSidebarToggle.swift diff --git a/CodeEdit/Features/Settings/View+NavigationBarBackButtonVisible.swift b/CodeEdit/Windows/Settings/View+NavigationBarBackButtonVisible.swift similarity index 100% rename from CodeEdit/Features/Settings/View+NavigationBarBackButtonVisible.swift rename to CodeEdit/Windows/Settings/View+NavigationBarBackButtonVisible.swift diff --git a/CodeEdit/Features/Welcome/GitCloneButton.swift b/CodeEdit/Windows/Welcome/GitCloneButton.swift similarity index 100% rename from CodeEdit/Features/Welcome/GitCloneButton.swift rename to CodeEdit/Windows/Welcome/GitCloneButton.swift diff --git a/CodeEdit/Features/Welcome/NewFileButton.swift b/CodeEdit/Windows/Welcome/NewFileButton.swift similarity index 100% rename from CodeEdit/Features/Welcome/NewFileButton.swift rename to CodeEdit/Windows/Welcome/NewFileButton.swift diff --git a/CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift b/CodeEdit/Windows/Welcome/OpenFileOrFolderButton.swift similarity index 100% rename from CodeEdit/Features/Welcome/OpenFileOrFolderButton.swift rename to CodeEdit/Windows/Welcome/OpenFileOrFolderButton.swift diff --git a/CodeEdit/Features/Welcome/WelcomeSubtitleView.swift b/CodeEdit/Windows/Welcome/WelcomeSubtitleView.swift similarity index 100% rename from CodeEdit/Features/Welcome/WelcomeSubtitleView.swift rename to CodeEdit/Windows/Welcome/WelcomeSubtitleView.swift From 89065d78f6c4258c0b3a02609dd045b34f94c969 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 15:14:37 +0200 Subject: [PATCH 185/335] Tests: Add CodeEditCore test target and move Core-pure tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CEWorkspaceFileCoreTests, WorkspaceEventsTests, and WorkspaceSettingsValueTypeTests move as-is; the fuzzy-search algorithm tests move to Core against a local FuzzySearchable conformer, leaving the app-side suite to cover only the URL conformance. The target is wired into CodeEditTestPlan directly — headless xcodebuild's autogenerated package schemes have no test action, so the plan (the CI path) is the package-test entry point. --- CodeEditTestPlan.xctestplan | 7 ++ .../Utils/FuzzySearch/FuzzySearchTests.swift | 63 ++--------------- .../Foundation/CodeEditCore/Package.swift | 4 ++ .../Tests/CodeEditCoreTests/.gitkeep | 0 .../CEWorkspaceFileCoreTests.swift | 0 .../CodeEditCoreTests/FuzzySearchTests.swift | 70 +++++++++++++++++++ .../WorkspaceEventsTests.swift | 0 .../WorkspaceSettingsValueTypeTests.swift | 0 8 files changed, 86 insertions(+), 58 deletions(-) delete mode 100644 Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep rename {CodeEditTests/Features/CEWorkspace => Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests}/CEWorkspaceFileCoreTests.swift (100%) create mode 100644 Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift rename {CodeEditTests/Features/CEWorkspace => Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests}/WorkspaceEventsTests.swift (100%) rename {CodeEditTests/Features/WorkspaceSettings => Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests}/WorkspaceSettingsValueTypeTests.swift (100%) diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 1b4e456b21..61a28b7d29 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -52,6 +52,13 @@ "identifier" : "B658FB4627DA9E1000EA4DBD", "name" : "CodeEditUITests" } + }, + { + "target" : { + "containerPath" : "container:Packages\/Foundation\/CodeEditCore", + "identifier" : "CodeEditCoreTests", + "name" : "CodeEditCoreTests" + } } ], "version" : 1 diff --git a/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift b/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift index 5b8b395973..beccf594af 100644 --- a/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift +++ b/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift @@ -9,14 +9,10 @@ import XCTest import CodeEditCore @testable import CodeEdit +/// Tests the app's `URL: FuzzySearchable` conformance (OpenQuickly). The fuzzy-match +/// algorithm itself is covered in CodeEditCore's `CodeEditCoreTests/FuzzySearchTests`. final class FuzzySearchTests: XCTestCase { - func testNormalisation() { - XCTAssertEqual("ü".normalise()[0].normalisedContent, "u") - XCTAssertEqual("ñ".normalise()[0].normalisedContent, "n") - XCTAssertEqual("é".normalise()[0].normalisedContent, "e") - } - - func testFuzzyMatchWeight() { + func testFuzzyMatchWeightUsesFileNameOnly() { guard let url = URL(string: "path/ContentView.swift") else { XCTFail("URL could not be created") return @@ -24,10 +20,11 @@ final class FuzzySearchTests: XCTestCase { XCTAssert(url.fuzzyMatch(query: "CV").weight > 0) XCTAssert(url.fuzzyMatch(query: "conv").weight > 0) XCTAssert(url.fuzzyMatch(query: "sw").weight > 0) + // Directory components must not match — only the file name is searchable. XCTAssert(url.fuzzyMatch(query: "path").weight == 0) } - func testFuzzyMatchRange() { + func testFuzzyMatchRangesIndexIntoFileName() { guard let url = URL(string: "path/ContentView.swift") else { XCTFail("URL could not be created") return @@ -37,54 +34,4 @@ final class FuzzySearchTests: XCTestCase { XCTAssertEqual(url.lastPathComponent[range[0]], "Con") XCTAssertEqual(url.lastPathComponent[range[1]], "Vie") } - - func testFuzzySearch() async { - let urls = [ - URL(string: "FuzzySearchable.swift")!, - URL(string: "ContentView.swift")!, - URL(string: "FuzzyMatch.swift")! - ] - let fuzzyMatchResult = await urls.fuzzySearch(query: "mch").map { - $0.item - } - XCTAssertEqual(fuzzyMatchResult[0].lastPathComponent, "FuzzyMatch.swift") - - let contentViewResult = await urls.fuzzySearch(query: "CV").map { - $0.item - } - XCTAssertEqual(contentViewResult[0].lastPathComponent, "ContentView.swift") - - let fuzzySearchableResult = await urls.fuzzySearch(query: "seable").map { - $0.item - } - XCTAssertEqual(fuzzySearchableResult[0].lastPathComponent, "FuzzySearchable.swift") - - let swiftResults = await urls.fuzzySearch(query: "swif").map { - $0.item - } - XCTAssertEqual(swiftResults.count, 3) - } - - func testFuzzySearchExcludesNonMatches() async { - let urls = [ - URL(string: "ContentView.swift")!, - URL(string: "README.md")! - ] - - let results = await urls.fuzzySearch(query: "swift") - - XCTAssertEqual(results.count, 1) - XCTAssertEqual(results[0].item.lastPathComponent, "ContentView.swift") - XCTAssertTrue(results.allSatisfy { $0.result.weight > 0 }) - } - - func testFuzzySearchPreservesInputOrderForEqualWeights() async { - // Identical file names produce identical weights; Swift's sort is stable, - // so the result order must match the input order. - let urls = (0..<50).map { URL(string: "Folder\($0)/SameName.swift")! } - - let results = await urls.fuzzySearch(query: "same").map(\.item) - - XCTAssertEqual(results, urls) - } } diff --git a/Packages/Foundation/CodeEditCore/Package.swift b/Packages/Foundation/CodeEditCore/Package.swift index 28735ccec3..d9ace66786 100644 --- a/Packages/Foundation/CodeEditCore/Package.swift +++ b/Packages/Foundation/CodeEditCore/Package.swift @@ -13,6 +13,10 @@ let package = Package( .target( name: "CodeEditCore", dependencies: [] + ), + .testTarget( + name: "CodeEditCoreTests", + dependencies: ["CodeEditCore"] ) ] ) diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/CodeEditTests/Features/CEWorkspace/CEWorkspaceFileCoreTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift similarity index 100% rename from CodeEditTests/Features/CEWorkspace/CEWorkspaceFileCoreTests.swift rename to Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift new file mode 100644 index 0000000000..cf8da1f10c --- /dev/null +++ b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift @@ -0,0 +1,70 @@ +// +// FuzzySearchTests.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/07/26. +// + +import Testing +import Foundation +import CodeEditCore + +private struct TestSearchable: FuzzySearchable, Sendable, Equatable { + let id: Int + let searchableString: String + + init(_ id: Int = 0, _ searchableString: String) { + self.id = id + self.searchableString = searchableString + } +} + +struct FuzzySearchTests { + @Test func normalisation() { + #expect("ü".normalise()[0].normalisedContent == "u") + #expect("ñ".normalise()[0].normalisedContent == "n") + #expect("é".normalise()[0].normalisedContent == "e") + } + + @Test func matchWeightReflectsContainment() { + let item = TestSearchable(0, "ContentView.swift") + #expect(item.fuzzyMatch(query: "CV").weight > 0) + #expect(item.fuzzyMatch(query: "conv").weight > 0) + #expect(item.fuzzyMatch(query: "xyz").weight == 0) + } + + @Test func matchedPartsCoverTheQuery() { + let item = TestSearchable(0, "ContentView.swift") + let string = item.searchableString + + let substrings = item.fuzzyMatch(query: "ConVie").matchedParts.compactMap { part in + Range(part, in: string).map { String(string[$0]) } + } + + #expect(substrings == ["Con", "Vie"]) + } + + @Test func searchSortsByDescendingWeightAndDropsNonMatches() async { + let items = [ + TestSearchable(0, "FuzzySearchable.swift"), + TestSearchable(1, "README.md"), + TestSearchable(2, "FuzzyMatch.swift") + ] + + let results = await items.fuzzySearch(query: "fuzzy") + + #expect(results.count == 2) + #expect(results.allSatisfy { $0.result.weight > 0 }) + #expect(results.map(\.result.weight) == results.map(\.result.weight).sorted(by: >)) + } + + @Test func searchPreservesInputOrderForEqualWeights() async { + // Identical searchable strings produce identical weights; the sort is stable, + // so the result order must match the input order. + let items = (0..<50).map { TestSearchable($0, "SameName.swift") } + + let results = await items.fuzzySearch(query: "same").map(\.item) + + #expect(results == items) + } +} diff --git a/CodeEditTests/Features/CEWorkspace/WorkspaceEventsTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift similarity index 100% rename from CodeEditTests/Features/CEWorkspace/WorkspaceEventsTests.swift rename to Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift diff --git a/CodeEditTests/Features/WorkspaceSettings/WorkspaceSettingsValueTypeTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift similarity index 100% rename from CodeEditTests/Features/WorkspaceSettings/WorkspaceSettingsValueTypeTests.swift rename to Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift From be55ccae55e5501b9bed54c34744a10934326007 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 15:17:01 +0200 Subject: [PATCH 186/335] Tests: Add CESearch test target and move FindReplaceQueryTests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed from WorkspaceDocument+SearchState+FindReplaceQueryTests — the WorkspaceDocument type no longer exists. The no_feature_to_feature lint rule now excludes package Tests/ dirs: a package's own test target importing it is not a feature→feature edge. --- .swiftlint.yml | 1 + CodeEditTestPlan.xctestplan | 7 +++++++ Packages/Features/CESearch/Package.swift | 7 +++++++ .../Tests/CESearchTests/FindReplaceQueryTests.swift | 4 ++-- 4 files changed, 17 insertions(+), 2 deletions(-) rename CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift => Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift (97%) diff --git a/.swiftlint.yml b/.swiftlint.yml index 3e56fbf64c..8dac768e36 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -47,6 +47,7 @@ custom_rules: severity: error no_feature_to_feature: included: "Packages/Features/.*\\.swift" + excluded: "Packages/Features/[^/]*/Tests/.*\\.swift" name: "No feature→feature imports" regex: "^import (CEEditor|CESearch|CENotifications|CELSP|CESourceControl|CETerminal)$" message: "Feature packages may not import each other — communicate via CodeEditCore events or command interfaces (see docs/ARCHITECTURE.md)" diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 61a28b7d29..2b884faba3 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -59,6 +59,13 @@ "identifier" : "CodeEditCoreTests", "name" : "CodeEditCoreTests" } + }, + { + "target" : { + "containerPath" : "container:Packages\/Features\/CESearch", + "identifier" : "CESearchTests", + "name" : "CESearchTests" + } } ], "version" : 1 diff --git a/Packages/Features/CESearch/Package.swift b/Packages/Features/CESearch/Package.swift index 4ebfa41286..688743ce4f 100644 --- a/Packages/Features/CESearch/Package.swift +++ b/Packages/Features/CESearch/Package.swift @@ -19,6 +19,13 @@ let package = Package( .product(name: "CodeEditCore", package: "CodeEditCore"), .product(name: "CodeEditUI", package: "CodeEditUI") ] + ), + .testTarget( + name: "CESearchTests", + dependencies: [ + "CESearch", + .product(name: "CodeEditCore", package: "CodeEditCore") + ] ) ] ) diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift b/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift similarity index 97% rename from CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift rename to Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift index c6eaaf1148..cdc3826173 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindReplaceQueryTests.swift +++ b/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift @@ -1,6 +1,6 @@ // -// WorkspaceDocument+SearchState+FindReplaceQueryTests.swift -// CodeEditTests +// FindReplaceQueryTests.swift +// CESearchTests // // Created by Matthijs Eikelenboom. // From 59c5a71abff4e51f68fd94492f18fafbbd980748 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 15:19:39 +0200 Subject: [PATCH 187/335] Tests: Pilot CodeEditUI package test target; delete dead snapshot suite The pixel-snapshot tests were plan-skipped, keyed to a pre-rename file name, and recorded on an older macOS. Replaced by construction/behavior tests in the new CodeEditUIUnitTests package target (named to avoid the app's CodeEditUITests XCUITest bundle). Removes swift-snapshot-testing (no remaining consumer) and an orphaned test bridging header. --- CodeEdit.xcodeproj/project.pbxproj | 17 --- .../xcshareddata/swiftpm/Package.resolved | 20 +--- CodeEditTestPlan.xctestplan | 7 ++ .../CodeEditUITests-Bridging-Header.h | 12 -- .../Features/CodeEditUI/CodeEditUITests.swift | 111 ------------------ .../UnitTests/testBranchPickerDark.1.png | Bin 4465 -> 0 bytes .../UnitTests/testBranchPickerLight.1.png | Bin 4329 -> 0 bytes .../UnitTests/testEffectViewDark.1.png | Bin 1344 -> 0 bytes .../UnitTests/testEffectViewLight.1.png | Bin 1340 -> 0 bytes .../UnitTests/testFontPickerViewDark.1.png | Bin 5125 -> 0 bytes .../UnitTests/testFontPickerViewLight.1.png | Bin 5626 -> 0 bytes .../UnitTests/testHelpButtonDark.1.png | Bin 3691 -> 0 bytes .../UnitTests/testHelpButtonLight.1.png | Bin 3406 -> 0 bytes .../UnitTests/testSegmentedControlDark.1.png | Bin 4842 -> 0 bytes .../UnitTests/testSegmentedControlLight.1.png | Bin 4392 -> 0 bytes .../testSegmentedControlProminentDark.1.png | Bin 4336 -> 0 bytes .../testSegmentedControlProminentLight.1.png | Bin 4175 -> 0 bytes Packages/Foundation/CodeEditUI/Package.swift | 4 + .../AtomConstructionTests.swift | 58 +++++++++ 19 files changed, 70 insertions(+), 159 deletions(-) delete mode 100644 CodeEditTests/Features/CodeEditUI/CodeEditUITests-Bridging-Header.h delete mode 100644 CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testBranchPickerDark.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testBranchPickerLight.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testEffectViewDark.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testEffectViewLight.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testFontPickerViewDark.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testFontPickerViewLight.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testHelpButtonDark.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testHelpButtonLight.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testSegmentedControlDark.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testSegmentedControlLight.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testSegmentedControlProminentDark.1.png delete mode 100644 CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testSegmentedControlProminentLight.1.png create mode 100644 Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 66d92392e6..aa3e5dc9f5 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 5800E2F82FF843390085ECF1 /* CodeEditUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5800E2F72FF843390085ECF1 /* CodeEditUI */; }; - 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 583E529B29361BAB001AB554 /* SnapshotTesting */; }; 588950C52FFA5C05004BE116 /* CESearch in Frameworks */ = {isa = PBXBuildFile; productRef = 588950C42FFA5C05004BE116 /* CESearch */; }; 588957132FFA679E004BE116 /* ShellClient in Frameworks */ = {isa = PBXBuildFile; productRef = 588957122FFA679E004BE116 /* ShellClient */; }; 5889571530CE12AB004BE116 /* CEWorkspaceFileManager in Frameworks */ = {isa = PBXBuildFile; productRef = 5889571430CE12AB004BE116 /* CEWorkspaceFileManager */; }; @@ -227,7 +226,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 583E529C29361BAB001AB554 /* SnapshotTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -393,7 +391,6 @@ ); name = CodeEditTests; packageProductDependencies = ( - 583E529B29361BAB001AB554 /* SnapshotTesting */, ); productName = CodeEditTests; productReference = B658FB3D27DA9E1000EA4DBD /* CodeEditTests.xctest */; @@ -461,7 +458,6 @@ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */, 287136B1292A407E00E9F5F4 /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */, 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */, - 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */, 6C6BD6F229CD142C00235D17 /* XCRemoteSwiftPackageReference "collectionconcurrencykit" */, 6C66C31129D05CC800DE9ED2 /* XCRemoteSwiftPackageReference "GRDB.swift" */, @@ -1750,14 +1746,6 @@ minimumVersion = 0.8.0; }; }; - 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 1.14.2; - }; - }; 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; @@ -1884,11 +1872,6 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditUI; }; - 583E529B29361BAB001AB554 /* SnapshotTesting */ = { - isa = XCSwiftPackageProductDependency; - package = 583E529A29361BAB001AB554 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; - productName = SnapshotTesting; - }; 588950C42FFA5C05004BE116 /* CESearch */ = { isa = XCSwiftPackageProductDependency; productName = CESearch; diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved index beda0fa474..c2774e4fff 100644 --- a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "284326793962ba3e88f2e400e6ddd71d0f84fc0dffae3cc190272c17b6b8d095", + "originHash" : "9f3ffaf512dafa48c6a63229ac8dfbf9fe483823402bc3f215cc353a3a127fb2", "pins" : [ { "identity" : "aboutwindow", @@ -208,24 +208,6 @@ "version" : "0.2.0" } }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", - "state" : { - "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", - "version" : "1.14.2" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - }, { "identity" : "swiftlintplugin", "kind" : "remoteSourceControl", diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 2b884faba3..676a84dd4c 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -66,6 +66,13 @@ "identifier" : "CESearchTests", "name" : "CESearchTests" } + }, + { + "target" : { + "containerPath" : "container:Packages\/Foundation\/CodeEditUI", + "identifier" : "CodeEditUIUnitTests", + "name" : "CodeEditUIUnitTests" + } } ], "version" : 1 diff --git a/CodeEditTests/Features/CodeEditUI/CodeEditUITests-Bridging-Header.h b/CodeEditTests/Features/CodeEditUI/CodeEditUITests-Bridging-Header.h deleted file mode 100644 index d37372e10d..0000000000 --- a/CodeEditTests/Features/CodeEditUI/CodeEditUITests-Bridging-Header.h +++ /dev/null @@ -1,12 +0,0 @@ -// -// CodeEditUITests-Bridging-Header.h -// CodeEditUITests -// -// Created by Matthijs Eikelenboom on 29/11/2022. -// - -#ifndef CodeEditUITests_Bridging_Header_h -#define CodeEditUITests_Bridging_Header_h - - -#endif /* CodeEditUITests_Bridging_Header_h */ diff --git a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift b/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift deleted file mode 100644 index 5eac606e3e..0000000000 --- a/CodeEditTests/Features/CodeEditUI/CodeEditUITests.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// UnitTests.swift -// CodeEditModules/CodeEditUITests -// -// Created by Lukas Pistrol on 19.04.22. -// - -@testable import CodeEdit -import CESourceControl -import CodeEditUI -import Foundation -import SnapshotTesting -import SwiftUI -import XCTest - -final class CodeEditUIUnitTests: XCTestCase { - - // MARK: Help Button - - func testHelpButtonLight() throws { - let view = HelpButton(action: {}) - let hosting = NSHostingView(rootView: view) - hosting.frame = CGRect(origin: .zero, size: .init(width: 40, height: 40)) - hosting.appearance = .init(named: .aqua) - assertSnapshot(matching: hosting, as: .image(size: .init(width: 40, height: 40))) - } - - func testHelpButtonDark() throws { - let view = HelpButton(action: {}) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .darkAqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 40, height: 40)) - assertSnapshot(matching: hosting, as: .image) - } - - // MARK: Segmented Control - - func testSegmentedControlLight() throws { - let view = SegmentedControl(.constant(0), options: ["Opt1", "Opt2"]) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .aqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 100, height: 30)) - assertSnapshot(matching: hosting, as: .image) - } - - func testSegmentedControlDark() throws { - let view = SegmentedControl(.constant(0), options: ["Opt1", "Opt2"]) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .darkAqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 100, height: 30)) - assertSnapshot(matching: hosting, as: .image) - } - - func testSegmentedControlProminentLight() throws { - let view = SegmentedControl(.constant(0), options: ["Opt1", "Opt2"], prominent: true) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .aqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 100, height: 30)) - assertSnapshot(matching: hosting, as: .image) - } - - func testSegmentedControlProminentDark() throws { - let view = SegmentedControl(.constant(0), options: ["Opt1", "Opt2"], prominent: true) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .darkAqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 100, height: 30)) - assertSnapshot(matching: hosting, as: .image) - } - - // MARK: EffectView - - func testEffectViewLight() throws { - let view = EffectView() - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .aqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 20, height: 20)) - assertSnapshot(matching: hosting, as: .image) - } - - func testEffectViewDark() throws { - let view = EffectView() - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .darkAqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 20, height: 20)) - assertSnapshot(matching: hosting, as: .image) - } - - // MARK: ToolbarBranchPicker - - func testBranchPickerLight() throws { - let view = ToolbarBranchPicker( - fallbackTitle: "Empty", - sourceControlManager: nil - ) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .aqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 100, height: 50)) - assertSnapshot(matching: hosting, as: .image) - } - - func testBranchPickerDark() throws { - let view = ToolbarBranchPicker( - fallbackTitle: "Empty", - sourceControlManager: nil - ) - let hosting = NSHostingView(rootView: view) - hosting.appearance = .init(named: .darkAqua) - hosting.frame = CGRect(origin: .zero, size: .init(width: 100, height: 50)) - assertSnapshot(matching: hosting, as: .image) - } -} diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testBranchPickerDark.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testBranchPickerDark.1.png deleted file mode 100644 index 33caaf708daaa12ae5e91110460103bb8c694697..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4465 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 TT)ocUAA^>fo@#}%UBv$YJ}Mm> diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testEffectViewDark.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testEffectViewDark.1.png deleted file mode 100644 index f564d2318a8530fc54af74143677076b144836f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1344 zcmYL}dpOez7{`CRge{kygPfKpvWBqI#U(MhwN{5Dm*Qw*V`DCDbdZ_TC?~|RqB=As z%H>3j&?si^J(*hZm`vufnrl6wXghPxc{=a&eBbZ)d7sbwy#Kwwdwb!u;7B+C09yFN z?mj9vUzdiOs%I-2qX7V_O>uMcj&j4f#ZqHwC*sb8k#M9K5{(k(gL4G{`d>%^o@L(Dw@6 z3Mv>ygRPJcFq5ywxm{BNfkS*7L#V|NVl1y*v$$po+9KsY8=jpv?i?LlYFJiA$RBv) zK$&kdhN|s*O~y%pLcilta2nKQwHr1Vx&jj%pcWaA|JYs+1pIVMXW>!*jX(Be5ai;n-YAFz^PG1mTakRPr*AA0Dj~?5qn_j3_Ev!}MV~@8{0x|1YF4vl&CxJ9B z6C%O@V zkgyUH+8ld3=iJxocN2=>38_9sno~Mkfw6H5Ol|TqYHO`0#UisWCxC3*^)Yj$acWaI zm@$Wmpg0T)_m_oVcCnhvl79Tb{~lHlP;sl!^&0`xpx23Zx_G)sGrrq8VeRwRa{R!1M1E0A_$M;nYrU5-JW80FNpJugPinc8?k49!3y-R=ky$G zOHZ27w+_*D&okDcqy&YO-h(4$1%|jxP$J!8cOTEtd^gyOt|a3&KkN zJlPM`teKibu1XNX!sAe)-S}PjeAbGg346kN^R`mse6yYt#BAb#!x3$pt^yM}@l(@) zC<%n};n-z0XUTqb=~P4zuaDLBcO2o^A%^;g`lEWU4aS^yV8jGu@ecWOcyH+M1snANjHX1MMub|>+Qf2?bAD9Ma=wu%Q#v|fVi(z=7kcG3tm!dW9a z2Nx+yn%@^SS5x{BK1C@!KEiPg9Ktx76EmFn?R?_VV>11V>98}qlF!*y>E;*&^VGU( zV~JIa>NJB2`b2R@4VS0Bbt|-KKr@+T5Ejy4WTK7lzmt|XMP-Ovn1>3@tH*t?fs)Z?eT0%ab5k*50~3}CZwV8nmrbqG$&h2 z%;isHJi~!}< zpc-4Axa5;)CJ2>)?O(In+X9Xu`>TxPpMw%((4)XmO3P{G>bGRZCx}P_G?pkKN_67J zj<7dQsJe~g#P*DFt@ z^__nmTmt@1n3q8Q&+91Sd2Xf3U~z{}(E(r+dR-vA&-Mk?Fa+=J;(HP{`|_bytF;gt6sk?+taa yELdgQQna;;zVNA*>3#5ULmSlZ;HvF9tOSX3L08EY{;jH80N%sP{ef%9g}(vQYFn59 diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testEffectViewLight.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testEffectViewLight.1.png deleted file mode 100644 index 73d8797f580cbf0c54c5c4d07e81ded55dc13898..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1340 zcmYL}dpOez7{`CRxNN!XoLo-J6Im|FO35WL&84-9B$pyhY;11RMh6*Aqnr?j6V;)K zP$*6#G^)*9*U8k12bs)evD@5V2CG70XM z=GED9{&1$+G-^2jszzTNzUpXm_x^TcfqRc_%}r0#t2X9p>Ld5Jk^Euiv0SzlLq`m0 zUM{kr6~4QSVEImr#AVj)q_8^>3Q`LNwuAzLIIS5p_!5R6>U*KpKpJa`{R?H|cbmI9)nZtP$636~FfR%X$Bj z8|vfv`}8*;vrTsSH7>byr}{UV-9Zo@#iizsk92zs7xaQyPg%%GuL8q@xziZF?)99W z1Kjk)SzW8(jc$2H+LYwL;PNUsQcAuptsvi)y&jtt^S1vb2bz2$~}bXmK|cSZ?G@2_u4?r83#s;e-`JUAB*#b{yuJ7 zI5Gkq*F7XEq4gDLOvFatRnjJDL+SoOta#;*n!g-An1Pv%8Yc+tqB$(GTLN zcGXjfHH_Lc{d@G;m|FImcg{Ws3sudMD&fxiB@ylYLxI+KHl?JY@#cps?L8Bc5P01l3q6g= zXQk#dkEI-g{z|RJBJw0qe_@Y3kBXOn)gMl*AkkT>_G@PQR{|dG(;lmZ2v^Ah+q7^= z+X7IFsZ3b*PB7z#h{5)+neANvW5~W5!KiZX$e!hkk^%LhT`0OX%J7yvF)0O)^=r_$Fe zPpS2te;r&3{zjOWLjKR|DB^i;wbEd*hfdJ}K+j}dAiVdsMdk1`-VN(>5;pges|g@< zRIq=Ft;;f6>}yv|oh5%T#4_3aTZQ^^Wv;_&m=DmST6#yXtRI1Q_jG&U5`6J*fo@ye diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testFontPickerViewDark.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testFontPickerViewDark.1.png deleted file mode 100644 index add2588172bd3e8420cd55a981bfdaa1d288d5db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5125 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 A&;S4c diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testFontPickerViewLight.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testFontPickerViewLight.1.png deleted file mode 100644 index 1648835b64fda49e7ac4fc04ab7fd29db786fffb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5626 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 diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testHelpButtonDark.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testHelpButtonDark.1.png deleted file mode 100644 index 7ca2a455a5e4292464cbab69b145cd1ad80f3b4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3691 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ⅅmZq=!@%f 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 CjqH2? diff --git a/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testSegmentedControlProminentLight.1.png b/CodeEditTests/Features/CodeEditUI/__Snapshots__/UnitTests/testSegmentedControlProminentLight.1.png deleted file mode 100644 index bc57bbbbce1943e25dd02e503c8669b7d4eb2420..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4175 zcmb7`WmME%+r|GgL(Py9f`D{)D&0s(r{vJXNH<8Q;C?nwf6b#b*+7UJL{|+r=z7p2&0Ap06?gws-$o zU!R0`ckm=(@FXucDxE8x4Mc

0CTKW*AtR|J!(Wi`f69qXLRuj$Wt~8)n+D2QU=< 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 diff --git a/Packages/Foundation/CodeEditUI/Package.swift b/Packages/Foundation/CodeEditUI/Package.swift index 3726d54003..197487b61c 100644 --- a/Packages/Foundation/CodeEditUI/Package.swift +++ b/Packages/Foundation/CodeEditUI/Package.swift @@ -16,6 +16,10 @@ let package = Package( .target( name: "CodeEditUI", dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")] + ), + .testTarget( + name: "CodeEditUIUnitTests", + dependencies: ["CodeEditUI"] ) ] ) diff --git a/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift b/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift new file mode 100644 index 0000000000..2c4d048a0e --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift @@ -0,0 +1,58 @@ +// +// AtomConstructionTests.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/07/26. +// + +import Testing +import SwiftUI +import AppKit +import CodeEditUI + +/// Construction/behavior smoke tests for the shared atoms formerly covered by the +/// (plan-skipped, deleted) pixel-snapshot suite. Views are materialized in an +/// offscreen window; no reference images. +@MainActor +struct AtomConstructionTests { + private func materialize(_ view: some View, appearance: NSAppearance.Name) -> NSWindow { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 200, height: 100), + styleMask: .borderless, + backing: .buffered, + defer: false + ) + window.appearance = NSAppearance(named: appearance) + window.contentView = NSHostingView(rootView: view) + window.layoutIfNeeded() + return window + } + + @Test(arguments: [NSAppearance.Name.aqua, .darkAqua]) + func helpButtonHasIntrinsicSize(appearance: NSAppearance.Name) throws { + let window = materialize(HelpButton(action: {}), appearance: appearance) + let hosting = try #require(window.contentView as? NSHostingView) + #expect(hosting.fittingSize.width > 0) + #expect(hosting.fittingSize.height > 0) + } + + @Test(arguments: [false, true]) + func segmentedControlHasIntrinsicSize(prominent: Bool) throws { + let view = SegmentedControl(.constant(0), options: ["One", "Two"], prominent: prominent) + let window = materialize(view, appearance: .aqua) + let hosting = try #require(window.contentView as? NSHostingView) + #expect(hosting.fittingSize.width > 0) + #expect(hosting.fittingSize.height > 0) + } + + @Test func effectViewMaterializesAVisualEffectView() throws { + let window = materialize(EffectView(), appearance: .aqua) + + func containsVisualEffectView(_ view: NSView) -> Bool { + if view is NSVisualEffectView { return true } + return view.subviews.contains(where: containsVisualEffectView) + } + + #expect(containsVisualEffectView(try #require(window.contentView))) + } +} From 58ffd5fa9252791054fdc5a2ec2b6aff8daf592c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 15:21:39 +0200 Subject: [PATCH 188/335] Tests: Clean stale skips from CodeEditTestPlan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes skip entries for the deleted snapshot suite and the phantom WelcomeModuleUnitTests; RegistryTests stays skipped (network-bound). The three package test targets were wired into the plan by their own commits — the plan is the package-test entry point since headless xcodebuild package schemes lack a test action. --- CodeEditTestPlan.xctestplan | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 676a84dd4c..98b15c1249 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -18,27 +18,7 @@ "testTargets" : [ { "skippedTests" : [ - "CodeEditUIUnitTests", - "CodeEditUIUnitTests\/testBranchPickerDark()", - "CodeEditUIUnitTests\/testBranchPickerLight()", - "CodeEditUIUnitTests\/testEffectViewDark()", - "CodeEditUIUnitTests\/testEffectViewLight()", - "CodeEditUIUnitTests\/testFontPickerViewDark()", - "CodeEditUIUnitTests\/testFontPickerViewLight()", - "CodeEditUIUnitTests\/testHelpButtonDark()", - "CodeEditUIUnitTests\/testHelpButtonLight()", - "CodeEditUIUnitTests\/testSegmentedControlDark()", - "CodeEditUIUnitTests\/testSegmentedControlLight()", - "CodeEditUIUnitTests\/testSegmentedControlProminentDark()", - "CodeEditUIUnitTests\/testSegmentedControlProminentLight()", - "RegistryTests", - "WelcomeModuleUnitTests", - "WelcomeModuleUnitTests\/testRecentJSFileDarkSnapshot()", - "WelcomeModuleUnitTests\/testRecentJSFileLightSnapshot()", - "WelcomeModuleUnitTests\/testRecentProjectItemDarkSnapshot()", - "WelcomeModuleUnitTests\/testRecentProjectItemLightSnapshot()", - "WelcomeModuleUnitTests\/testWelcomeActionViewDarkSnapshot()", - "WelcomeModuleUnitTests\/testWelcomeActionViewLightSnapshot()" + "RegistryTests" ], "target" : { "containerPath" : "container:CodeEdit.xcodeproj", From 8dfd4fde166e0402c7c5a3ed1cef08252f7e7676 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 16:24:44 +0200 Subject: [PATCH 189/335] Tests: Move Indexer tests into CESearchTests Their @testable import CodeEdit was vestigial (no app symbols used). TemporaryFile helper moves with its only consumers. The sibling FindReplaceQuery tests' waitUntil timeout is raised 2s -> 10s: the RunLoop.main-scheduled sync occasionally exceeded 2s under the load the indexing suites add to the target (passing runs still exit on the first successful poll). --- .../CESearch/Tests/CESearchTests}/AsyncIndexingTests.swift | 1 - .../CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift | 2 +- .../CESearch/Tests/CESearchTests}/MemoryIndexingTests.swift | 1 - .../CESearch/Tests/CESearchTests}/MemorySearchTests.swift | 1 - .../Features/CESearch/Tests/CESearchTests}/TemporaryFile.swift | 0 5 files changed, 1 insertion(+), 4 deletions(-) rename {CodeEditTests/Features/Documents/Indexer => Packages/Features/CESearch/Tests/CESearchTests}/AsyncIndexingTests.swift (98%) rename {CodeEditTests/Features/Documents/Indexer => Packages/Features/CESearch/Tests/CESearchTests}/MemoryIndexingTests.swift (99%) rename {CodeEditTests/Features/Documents/Indexer => Packages/Features/CESearch/Tests/CESearchTests}/MemorySearchTests.swift (99%) rename {CodeEditTests/Features/Documents/Indexer => Packages/Features/CESearch/Tests/CESearchTests}/TemporaryFile.swift (100%) diff --git a/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift b/Packages/Features/CESearch/Tests/CESearchTests/AsyncIndexingTests.swift similarity index 98% rename from CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift rename to Packages/Features/CESearch/Tests/CESearchTests/AsyncIndexingTests.swift index 2e8fdc5c99..ae565fa1e9 100644 --- a/CodeEditTests/Features/Documents/Indexer/AsyncIndexingTests.swift +++ b/Packages/Features/CESearch/Tests/CESearchTests/AsyncIndexingTests.swift @@ -7,7 +7,6 @@ import XCTest import CESearch -@testable import CodeEdit final class AsyncIndexingTests: XCTestCase { func testAddDocuments() { diff --git a/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift b/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift index cdc3826173..6982422bde 100644 --- a/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift +++ b/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift @@ -76,7 +76,7 @@ final class FindReplaceQueryBridgeTests: XCTestCase { /// Polls `condition` until it's true or 2 seconds elapse, yielding to the run loop between checks so /// `.receive(on: RunLoop.main)`-scheduled Combine work actually gets a chance to run. - private func waitUntil(timeout: TimeInterval = 2, _ condition: @escaping () -> Bool) async { + private func waitUntil(timeout: TimeInterval = 10, _ condition: @escaping () -> Bool) async { let startTime = Date() while !condition() { try? await Task.sleep(nanoseconds: 20_000_000) diff --git a/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift b/Packages/Features/CESearch/Tests/CESearchTests/MemoryIndexingTests.swift similarity index 99% rename from CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift rename to Packages/Features/CESearch/Tests/CESearchTests/MemoryIndexingTests.swift index b1cc513363..e7d97709f0 100644 --- a/CodeEditTests/Features/Documents/Indexer/MemoryIndexingTests.swift +++ b/Packages/Features/CESearch/Tests/CESearchTests/MemoryIndexingTests.swift @@ -7,7 +7,6 @@ import XCTest import CESearch -@testable import CodeEdit final class MemoryIndexingTests: XCTestCase { func testIndexFile() { diff --git a/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift b/Packages/Features/CESearch/Tests/CESearchTests/MemorySearchTests.swift similarity index 99% rename from CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift rename to Packages/Features/CESearch/Tests/CESearchTests/MemorySearchTests.swift index 35a446ae7d..d02b420545 100644 --- a/CodeEditTests/Features/Documents/Indexer/MemorySearchTests.swift +++ b/Packages/Features/CESearch/Tests/CESearchTests/MemorySearchTests.swift @@ -7,7 +7,6 @@ import XCTest import CESearch -@testable import CodeEdit final class MemoryIndexSearchTests: XCTestCase { func testIndexFileSearch() { diff --git a/CodeEditTests/Features/Documents/Indexer/TemporaryFile.swift b/Packages/Features/CESearch/Tests/CESearchTests/TemporaryFile.swift similarity index 100% rename from CodeEditTests/Features/Documents/Indexer/TemporaryFile.swift rename to Packages/Features/CESearch/Tests/CESearchTests/TemporaryFile.swift From da6638781299ffeb546d8725b7c3c432f0d5e8e4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 16:25:58 +0200 Subject: [PATCH 190/335] Tests: Add CELSP test target with the SemanticTokens tests Their @testable import CodeEdit was vestigial. --- CodeEditTestPlan.xctestplan | 7 +++++++ Packages/Features/CELSP/Package.swift | 8 ++++++++ .../CELSP/Tests/CELSPTests}/SemanticTokenMapTests.swift | 1 - .../Tests/CELSPTests}/SemanticTokenStorageTests.swift | 1 - 4 files changed, 15 insertions(+), 2 deletions(-) rename {CodeEditTests/Features/LSP/SemanticTokens => Packages/Features/CELSP/Tests/CELSPTests}/SemanticTokenMapTests.swift (99%) rename {CodeEditTests/Features/LSP/SemanticTokens => Packages/Features/CELSP/Tests/CELSPTests}/SemanticTokenStorageTests.swift (99%) diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 98b15c1249..51c3c9791d 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -53,6 +53,13 @@ "identifier" : "CodeEditUIUnitTests", "name" : "CodeEditUIUnitTests" } + }, + { + "target" : { + "containerPath" : "container:Packages\/Features\/CELSP", + "identifier" : "CELSPTests", + "name" : "CELSPTests" + } } ], "version" : 1 diff --git a/Packages/Features/CELSP/Package.swift b/Packages/Features/CELSP/Package.swift index c4a8994009..eff7e0684c 100644 --- a/Packages/Features/CELSP/Package.swift +++ b/Packages/Features/CELSP/Package.swift @@ -37,6 +37,14 @@ let package = Package( .product(name: "ZIPFoundation", package: "ZIPFoundation"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") ] + ), + .testTarget( + name: "CELSPTests", + dependencies: [ + "CELSP", + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol") + ] ) ] ) diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift b/Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenMapTests.swift similarity index 99% rename from CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift rename to Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenMapTests.swift index b2aaf755c2..f168edf002 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift +++ b/Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenMapTests.swift @@ -9,7 +9,6 @@ import XCTest import CodeEditSourceEditor import LanguageServerProtocol -@testable import CodeEdit final class SemanticTokenMapTests: XCTestCase { // Ignores the line parameter and just returns a range from the char and length for testing diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift b/Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenStorageTests.swift similarity index 99% rename from CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift rename to Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenStorageTests.swift index e5c459cea1..2aebe230f9 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift +++ b/Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenStorageTests.swift @@ -10,7 +10,6 @@ import Foundation import Testing import CodeEditSourceEditor import LanguageServerProtocol -@testable import CodeEdit // For easier comparison while setting semantic tokens extension SemanticToken: @retroactive Equatable { From 95195a61a8718ad20da1ee5c4c4f1120bdd52c33 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 15 Jul 2026 16:30:04 +0200 Subject: [PATCH 191/335] Tests: Add CESourceControl test target with the pure SCM tests GitRefreshActionsTests and SourceControlViewModelTests carried vestigial @testable import CodeEdit lines; GitClientTests stays app-side (needs the withTempDir app-test helper and ShellClient). --- CodeEditTestPlan.xctestplan | 7 +++++++ Packages/Features/CESourceControl/Package.swift | 4 ++++ .../CESourceControlTests}/GitRefreshActionsTests.swift | 1 - .../SourceControlViewModelTests.swift | 1 - 4 files changed, 11 insertions(+), 2 deletions(-) rename {CodeEditTests/Features/SourceControl => Packages/Features/CESourceControl/Tests/CESourceControlTests}/GitRefreshActionsTests.swift (98%) rename {CodeEditTests/Features/SourceControl => Packages/Features/CESourceControl/Tests/CESourceControlTests}/SourceControlViewModelTests.swift (98%) diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 51c3c9791d..cdd0f06607 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -60,6 +60,13 @@ "identifier" : "CELSPTests", "name" : "CELSPTests" } + }, + { + "target" : { + "containerPath" : "container:Packages\/Features\/CESourceControl", + "identifier" : "CESourceControlTests", + "name" : "CESourceControlTests" + } } ], "version" : 1 diff --git a/Packages/Features/CESourceControl/Package.swift b/Packages/Features/CESourceControl/Package.swift index 933708fff5..1fada2d040 100644 --- a/Packages/Features/CESourceControl/Package.swift +++ b/Packages/Features/CESourceControl/Package.swift @@ -23,6 +23,10 @@ let package = Package( .product(name: "CodeEditUI", package: "CodeEditUI"), .product(name: "CodeEditSymbols", package: "CodeEditSymbols") ] + ), + .testTarget( + name: "CESourceControlTests", + dependencies: ["CESourceControl"] ) ] ) diff --git a/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift b/Packages/Features/CESourceControl/Tests/CESourceControlTests/GitRefreshActionsTests.swift similarity index 98% rename from CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift rename to Packages/Features/CESourceControl/Tests/CESourceControlTests/GitRefreshActionsTests.swift index 3ea19292b6..81e865b522 100644 --- a/CodeEditTests/Features/SourceControl/GitRefreshActionsTests.swift +++ b/Packages/Features/CESourceControl/Tests/CESourceControlTests/GitRefreshActionsTests.swift @@ -7,7 +7,6 @@ @testable import CESourceControl import XCTest -@testable import CodeEdit final class GitRefreshActionsTests: XCTestCase { private let root = "MyWorkspace" diff --git a/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift b/Packages/Features/CESourceControl/Tests/CESourceControlTests/SourceControlViewModelTests.swift similarity index 98% rename from CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift rename to Packages/Features/CESourceControl/Tests/CESourceControlTests/SourceControlViewModelTests.swift index 6084fc6a36..dd29fe5daf 100644 --- a/CodeEditTests/Features/SourceControl/SourceControlViewModelTests.swift +++ b/Packages/Features/CESourceControl/Tests/CESourceControlTests/SourceControlViewModelTests.swift @@ -7,7 +7,6 @@ @testable import CESourceControl import XCTest -@testable import CodeEdit @MainActor final class SourceControlViewModelTests: XCTestCase { From f4cfa2009f6717df4b716e09fc888ce44fd0c7c0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 25 Jul 2026 15:36:16 +0200 Subject: [PATCH 192/335] Refactor: Move project-search model types into CESearch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SearchResultFile and SearchModeModel were public in CodeEditCore but consumed only by CESearch (verified: 5 and 3 consumers, all in-package). They are the Search context's own model, not shared-kernel interchange, so the context now owns them. Both stay public — they appear in CESearch's public API (SearchResult*.file, SearchState.selectedMode). --- .../CESearch/Sources/CESearch/Model}/SearchModeModel.swift | 0 .../CESearch/Sources/CESearch/Model}/SearchResultFile.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search => Features/CESearch/Sources/CESearch/Model}/SearchModeModel.swift (100%) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search => Features/CESearch/Sources/CESearch/Model}/SearchResultFile.swift (100%) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift b/Packages/Features/CESearch/Sources/CESearch/Model/SearchModeModel.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchModeModel.swift rename to Packages/Features/CESearch/Sources/CESearch/Model/SearchModeModel.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift b/Packages/Features/CESearch/Sources/CESearch/Model/SearchResultFile.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/SearchResultFile.swift rename to Packages/Features/CESearch/Sources/CESearch/Model/SearchResultFile.swift From 7ff33b2710e37a04f072d78159902716a87bf9e6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 25 Jul 2026 15:38:52 +0200 Subject: [PATCH 193/335] Refactor: Rename FuzzySearch* symbols to FuzzyMatch* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic subdomain ranks candidates by match quality; it does not search. Freeing the word 'search' means it now unambiguously refers to project search (CESearch). fuzzySearch(query:) becomes fuzzyMatches(query:). searchableString keeps its name — it names the string being matched. --- .../FuzzySearch/FuzzySearchUIModel.swift | 6 ++-- .../LanguageServersView.swift | 2 +- .../RegistryItem+FuzzySearchable.swift | 4 +-- .../ThemeSettings/Theme+FuzzySearchable.swift | 4 +-- .../ThemeSettings/ThemeSettingsView.swift | 2 +- .../OpenQuickly/OpenQuicklyViewModel.swift | 4 +-- .../OpenQuickly/URL+FuzzySearchable.swift | 4 +-- .../Utils/FuzzySearch/FuzzySearchTests.swift | 8 ++--- .../Search/Collection+FuzzySearch.swift | 10 +++--- .../Domain/Search/FuzzySearchModels.swift | 20 +++++------ .../Domain/Search/FuzzySearchable.swift | 34 +++++++++---------- .../String+LengthOfMatchingPrefix.swift | 4 +-- .../Domain/Search/String+Normalise.swift | 10 +++--- .../CodeEditCoreTests/FuzzySearchTests.swift | 12 +++---- 14 files changed, 62 insertions(+), 62 deletions(-) diff --git a/CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift b/CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift index 6fbc52d518..40316f7495 100644 --- a/CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift +++ b/CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift @@ -1,5 +1,5 @@ // -// FuzzySearchUIModel.swift +// FuzzyMatchUIModel.swift // CodeEdit // // Created by Khan Winter on 8/14/25. @@ -11,7 +11,7 @@ import AsyncAlgorithms import CodeEditCore @MainActor -final class FuzzySearchUIModel: ObservableObject { +final class FuzzyMatchUIModel: ObservableObject { @Published var items: [Element]? private var allItems: [Element] = [] @@ -42,7 +42,7 @@ final class FuzzySearchUIModel: ObservableO return } - let results = await allItems.fuzzySearch(query: query) + let results = await allItems.fuzzyMatches(query: query) items = results.map { $0.item } } diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift index 3cae27c66f..57dc778623 100644 --- a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift +++ b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift @@ -13,7 +13,7 @@ import CodeEditCore struct LanguageServersView: View { let registryManager: any RegistryManaging @ObservedObject var registryState: RegistryViewState - @StateObject private var searchModel = FuzzySearchUIModel() + @StateObject private var searchModel = FuzzyMatchUIModel() @State private var searchText: String = "" @State private var selectedInstall: PackageManagerInstallOperation? diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift index 5a979db6e3..71f17bbd33 100644 --- a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift +++ b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift @@ -1,5 +1,5 @@ // -// RegistryItem+FuzzySearchable.swift +// RegistryItem+FuzzyMatchable.swift // CodeEdit // // Created by Matthijs Eikelenboom on 12/07/2026. @@ -8,6 +8,6 @@ import CELSP import CodeEditCore -extension RegistryItem: FuzzySearchable { +extension RegistryItem: FuzzyMatchable { public var searchableString: String { name } } diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift index 999f3f6dd5..501081147b 100644 --- a/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift +++ b/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift @@ -1,5 +1,5 @@ // -// Theme+FuzzySearchable.swift +// Theme+FuzzyMatchable.swift // CodeEdit // // Created by Tommy Ludwig on 14.08.24. @@ -9,7 +9,7 @@ import Foundation import CodeEditSettings import CodeEditCore -extension Theme: FuzzySearchable { +extension Theme: FuzzyMatchable { public var searchableString: String { return id } diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 09049fa89a..9fa7fb1ebd 100644 --- a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -149,7 +149,7 @@ struct ThemeSettingsView: View { } private func filterAndSortThemes(_ themes: [Theme]) async -> [Theme] { - return await themes.fuzzySearch(query: themeSearchQuery).map { $1 } + return await themes.fuzzyMatches(query: themeSearchQuery).map { $1 } } } diff --git a/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyViewModel.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyViewModel.swift index 9f54bb58e8..7b60382a73 100644 --- a/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyViewModel.swift +++ b/CodeEdit/WorkspaceWindow/OpenQuickly/OpenQuicklyViewModel.swift @@ -71,7 +71,7 @@ final class OpenQuicklyViewModel: ObservableObject { } } - let fuzzySearchResults = await filteredFiles.fuzzySearch( + let fuzzyMatchResults = await filteredFiles.fuzzyMatches( query: self.query.trimmingCharacters(in: .whitespaces) ).concurrentMap { SearchResult( @@ -82,7 +82,7 @@ final class OpenQuicklyViewModel: ObservableObject { guard !Task.isCancelled else { return } await MainActor.run { - self.searchResults = fuzzySearchResults + self.searchResults = fuzzyMatchResults print("Duration: \(Date().timeIntervalSince(startTime))") } } diff --git a/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift index 8759431a6f..5282cd0ba9 100644 --- a/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift +++ b/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift @@ -1,5 +1,5 @@ // -// URL+FuzzySearchable.swift +// URL+FuzzyMatchable.swift // CodeEdit // // Created by Tommy Ludwig on 03.02.24. @@ -8,7 +8,7 @@ import Foundation import CodeEditCore -extension URL: FuzzySearchable { +extension URL: FuzzyMatchable { public var searchableString: String { return self.lastPathComponent } diff --git a/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift b/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift index beccf594af..ac4dd1665a 100644 --- a/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift +++ b/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift @@ -1,5 +1,5 @@ // -// FuzzySearchTests.swift +// FuzzyMatchTests.swift // CodeEditTests // // Created by Tommy Ludwig on 03.02.24. @@ -9,9 +9,9 @@ import XCTest import CodeEditCore @testable import CodeEdit -/// Tests the app's `URL: FuzzySearchable` conformance (OpenQuickly). The fuzzy-match -/// algorithm itself is covered in CodeEditCore's `CodeEditCoreTests/FuzzySearchTests`. -final class FuzzySearchTests: XCTestCase { +/// Tests the app's `URL: FuzzyMatchable` conformance (OpenQuickly). The fuzzy-match +/// algorithm itself is covered in CodeEditCore's `CodeEditCoreTests/FuzzyMatchTests`. +final class FuzzyMatchTests: XCTestCase { func testFuzzyMatchWeightUsesFileNameOnly() { guard let url = URL(string: "path/ContentView.swift") else { XCTFail("URL could not be created") diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift index 5c3b043b6c..330365182f 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift @@ -7,8 +7,8 @@ import Foundation -public extension Collection where Element: FuzzySearchable & Sendable { - /// Concurrently performs a fuzzy search on a collection of elements conforming to FuzzySearchable. +public extension Collection where Element: FuzzyMatchable & Sendable { + /// Concurrently fuzzy-matches a collection of elements conforming to FuzzyMatchable. /// /// - Parameter query: The query string to match against the elements. /// @@ -17,17 +17,17 @@ public extension Collection where Element: FuzzySearchable & Sendable { /// /// - Note: Because this is an extension on Collection and not only array, /// you can also use this on sets. - func fuzzySearch(query: String) async -> [(result: FuzzySearchMatchResult, item: Element)] { + func fuzzyMatches(query: String) async -> [(result: FuzzyMatchResult, item: Element)] { let items = Array(self) - let matches = await withTaskGroup(of: (Int, FuzzySearchMatchResult).self) { group in + let matches = await withTaskGroup(of: (Int, FuzzyMatchResult).self) { group in for (index, item) in items.enumerated() { group.addTask { (index, item.fuzzyMatch(query: query)) } } - var results = [FuzzySearchMatchResult?](repeating: nil, count: items.count) + var results = [FuzzyMatchResult?](repeating: nil, count: items.count) for await (index, result) in group { results[index] = result } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift index 421bc395cd..73333beb82 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift @@ -7,39 +7,39 @@ import Foundation -/// A single character in a fuzzy search string, storing both original and normalised forms. -public struct FuzzySearchCharacter { +/// A single character in a fuzzy match string, storing both original and normalised forms. +public struct FuzzyMatchCharacter { /// The original character content. public let content: String /// The case- and accent-insensitive form of ``content``. public let normalisedContent: String - /// Creates a ``FuzzySearchCharacter`` with the given original and normalised content. + /// Creates a ``FuzzyMatchCharacter`` with the given original and normalised content. public init(content: String, normalisedContent: String) { self.content = content self.normalisedContent = normalisedContent } } -/// A sequence of ``FuzzySearchCharacter`` values representing a string prepared for fuzzy matching. -public struct FuzzySearchString { +/// A sequence of ``FuzzyMatchCharacter`` values representing a string prepared for fuzzy matching. +public struct FuzzyMatchString { /// The individual characters that make up this string. - public var characters: [FuzzySearchCharacter] + public var characters: [FuzzyMatchCharacter] - /// Creates a ``FuzzySearchString`` from an array of ``FuzzySearchCharacter`` values. - public init(characters: [FuzzySearchCharacter]) { + /// Creates a ``FuzzyMatchString`` from an array of ``FuzzyMatchCharacter`` values. + public init(characters: [FuzzyMatchCharacter]) { self.characters = characters } } /// The result of a fuzzy match operation, containing a relevance weight and the matched ranges. -public struct FuzzySearchMatchResult: Sendable { +public struct FuzzyMatchResult: Sendable { /// A score indicating how closely the input matched; higher values mean a better match. public let weight: Int /// The ranges within the original string that were matched. public let matchedParts: [NSRange] - /// Creates a ``FuzzySearchMatchResult`` with the given weight and matched ranges. + /// Creates a ``FuzzyMatchResult`` with the given weight and matched ranges. public init(weight: Int, matchedParts: [NSRange]) { self.weight = weight self.matchedParts = matchedParts diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift index 0ba399d700..7233d7776e 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift @@ -1,5 +1,5 @@ // -// FuzzySearchable.swift +// FuzzyMatchable.swift // CodeEdit // // Created by Tommy Ludwig on 03.02.24. @@ -8,24 +8,24 @@ import Foundation /// A protocol defining the requirements for an object that can be searched using fuzzy matching. -public protocol FuzzySearchable { - /// The string content that fuzzy searches are matched against. +public protocol FuzzyMatchable { + /// The string content that fuzzy matches are made against. var searchableString: String { get } - /// Performs a fuzzy search on the conforming object's searchable string. + /// Performs a fuzzy match against the conforming object's searchable string. /// /// - Parameters: /// - query: The query string to match against the searchable content. /// - characters: The set of characters used for fuzzy matching. /// - /// - Returns: A FuzzySearchMatchResult indicating the result of the fuzzy search. - func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult + /// - Returns: A FuzzyMatchResult indicating the result of the fuzzy match. + func fuzzyMatch(query: String, characters: FuzzyMatchString) -> FuzzyMatchResult } -public extension FuzzySearchable { +public extension FuzzyMatchable { /// Default implementation scoring consecutive character matches; returns a zero-weight result /// when the query is not fully contained in the searchable string. - func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult { + func fuzzyMatch(query: String, characters: FuzzyMatchString) -> FuzzyMatchResult { let compareString = characters.characters let searchString = query.lowercased() @@ -60,26 +60,26 @@ public extension FuzzySearchable { if searchString.count == matchedParts.reduce(0, { partialResult, range in range.length + partialResult }) { - return FuzzySearchMatchResult(weight: totalScore, matchedParts: matchedParts) + return FuzzyMatchResult(weight: totalScore, matchedParts: matchedParts) } else { - return FuzzySearchMatchResult(weight: 0, matchedParts: []) + return FuzzyMatchResult(weight: 0, matchedParts: []) } } /// Normalises the searchable string of the conforming object by converting its characters to ASCII representation. - /// The resulting FuzzySearchString contains both the original and normalised content of each character. + /// The resulting FuzzyMatchString contains both the original and normalised content of each character. /// - /// - Returns: A FuzzySearchString - func normaliseString() -> FuzzySearchString { - return FuzzySearchString(characters: searchableString.normalise()) + /// - Returns: A FuzzyMatchString + func normaliseString() -> FuzzyMatchString { + return FuzzyMatchString(characters: searchableString.normalise()) } - /// Performs a fuzzy search on the normalised content of the conforming object's searchable string. + /// Performs a fuzzy match against the normalised content of the conforming object's searchable string. /// /// - Parameter query: The query string to match against the normalised searchable content. /// - /// - Returns: A FuzzySearchMatchResult indicating the result of the fuzzy search. - func fuzzyMatch(query: String) -> FuzzySearchMatchResult { + /// - Returns: A FuzzyMatchResult indicating the result of the fuzzy match. + func fuzzyMatch(query: String) -> FuzzyMatchResult { let characters = normaliseString() return fuzzyMatch(query: query, characters: characters) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift index d710b97e4e..e4ceb0bb2c 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift @@ -12,11 +12,11 @@ extension String { /// Returns the length of the matching prefix content or normalised content at the specified index. /// /// - Parameters: - /// - prefix: The FuzzySearchCharacter whose content or normalised content to check for a prefix match. + /// - prefix: The FuzzyMatchCharacter whose content or normalised content to check for a prefix match. /// - index: The index from which to start searching for the prefix. /// /// - Returns: The length of the matching prefix, or nil if no match is found. - func lengthOfMatchingPrefix(prefix: FuzzySearchCharacter, startingAt index: Int) -> Int? { + func lengthOfMatchingPrefix(prefix: FuzzyMatchCharacter, startingAt index: Int) -> Int? { guard let stringIndex = self.index(self.startIndex, offsetBy: index, limitedBy: self.endIndex) else { return nil } diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift index d2672abf72..b104a6fbeb 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift @@ -10,18 +10,18 @@ import Foundation public extension String { /// Normalises the characters of the string by converting them to ASCII representation. /// Each character is transformed into its ASCII equivalent, and the resulting array - /// of FuzzySearchCharacter objects contains both the original and normalised content. + /// of FuzzyMatchCharacter objects contains both the original and normalised content. /// - /// - Returns: An array of FuzzySearchCharacter objects representing the original and + /// - Returns: An array of FuzzyMatchCharacter objects representing the original and /// normalised content of each character in the string. - func normalise() -> [FuzzySearchCharacter] { + func normalise() -> [FuzzyMatchCharacter] { return self.lowercased().map { char in guard let data = String(char).data(using: .ascii, allowLossyConversion: true), let normalisedCharacter = String(data: data, encoding: .ascii) else { - return FuzzySearchCharacter(content: String(char), normalisedContent: String(char)) + return FuzzyMatchCharacter(content: String(char), normalisedContent: String(char)) } - return FuzzySearchCharacter(content: String(char), normalisedContent: normalisedCharacter) + return FuzzyMatchCharacter(content: String(char), normalisedContent: normalisedCharacter) } } } diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift index cf8da1f10c..f74ce689ad 100644 --- a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift +++ b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift @@ -1,5 +1,5 @@ // -// FuzzySearchTests.swift +// FuzzyMatchTests.swift // CodeEdit // // Created by Matthijs Eikelenboom on 15/07/26. @@ -9,7 +9,7 @@ import Testing import Foundation import CodeEditCore -private struct TestSearchable: FuzzySearchable, Sendable, Equatable { +private struct TestSearchable: FuzzyMatchable, Sendable, Equatable { let id: Int let searchableString: String @@ -19,7 +19,7 @@ private struct TestSearchable: FuzzySearchable, Sendable, Equatable { } } -struct FuzzySearchTests { +struct FuzzyMatchTests { @Test func normalisation() { #expect("ü".normalise()[0].normalisedContent == "u") #expect("ñ".normalise()[0].normalisedContent == "n") @@ -46,12 +46,12 @@ struct FuzzySearchTests { @Test func searchSortsByDescendingWeightAndDropsNonMatches() async { let items = [ - TestSearchable(0, "FuzzySearchable.swift"), + TestSearchable(0, "FuzzyMatchable.swift"), TestSearchable(1, "README.md"), TestSearchable(2, "FuzzyMatch.swift") ] - let results = await items.fuzzySearch(query: "fuzzy") + let results = await items.fuzzyMatches(query: "fuzzy") #expect(results.count == 2) #expect(results.allSatisfy { $0.result.weight > 0 }) @@ -63,7 +63,7 @@ struct FuzzySearchTests { // so the result order must match the input order. let items = (0..<50).map { TestSearchable($0, "SameName.swift") } - let results = await items.fuzzySearch(query: "same").map(\.item) + let results = await items.fuzzyMatches(query: "same").map(\.item) #expect(results == items) } From ba6aff2cf15ffc18778d40d5e67ccfb205d19957 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 25 Jul 2026 15:40:03 +0200 Subject: [PATCH 194/335] Style: Put @Test attributes on their own lines Clears 6 SwiftLint 'attributes' warnings introduced by the package test-target slices (the repo's rule wants attributes on their own line in functions). --- .../CodeEditCoreTests/FuzzySearchTests.swift | 15 ++++++++++----- .../AtomConstructionTests.swift | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift index f74ce689ad..7ff9f255b2 100644 --- a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift +++ b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift @@ -20,20 +20,23 @@ private struct TestSearchable: FuzzyMatchable, Sendable, Equatable { } struct FuzzyMatchTests { - @Test func normalisation() { + @Test + func normalisation() { #expect("ü".normalise()[0].normalisedContent == "u") #expect("ñ".normalise()[0].normalisedContent == "n") #expect("é".normalise()[0].normalisedContent == "e") } - @Test func matchWeightReflectsContainment() { + @Test + func matchWeightReflectsContainment() { let item = TestSearchable(0, "ContentView.swift") #expect(item.fuzzyMatch(query: "CV").weight > 0) #expect(item.fuzzyMatch(query: "conv").weight > 0) #expect(item.fuzzyMatch(query: "xyz").weight == 0) } - @Test func matchedPartsCoverTheQuery() { + @Test + func matchedPartsCoverTheQuery() { let item = TestSearchable(0, "ContentView.swift") let string = item.searchableString @@ -44,7 +47,8 @@ struct FuzzyMatchTests { #expect(substrings == ["Con", "Vie"]) } - @Test func searchSortsByDescendingWeightAndDropsNonMatches() async { + @Test + func searchSortsByDescendingWeightAndDropsNonMatches() async { let items = [ TestSearchable(0, "FuzzyMatchable.swift"), TestSearchable(1, "README.md"), @@ -58,7 +62,8 @@ struct FuzzyMatchTests { #expect(results.map(\.result.weight) == results.map(\.result.weight).sorted(by: >)) } - @Test func searchPreservesInputOrderForEqualWeights() async { + @Test + func searchPreservesInputOrderForEqualWeights() async { // Identical searchable strings produce identical weights; the sort is stable, // so the result order must match the input order. let items = (0..<50).map { TestSearchable($0, "SameName.swift") } diff --git a/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift b/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift index 2c4d048a0e..9b51be53b3 100644 --- a/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift +++ b/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift @@ -45,7 +45,8 @@ struct AtomConstructionTests { #expect(hosting.fittingSize.height > 0) } - @Test func effectViewMaterializesAVisualEffectView() throws { + @Test + func effectViewMaterializesAVisualEffectView() throws { let window = materialize(EffectView(), appearance: .aqua) func containsVisualEffectView(_ view: NSView) -> Bool { From 68e290acf189491d510086599f740b2a0052c15f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 25 Jul 2026 15:41:51 +0200 Subject: [PATCH 195/335] Refactor: Rename fuzzy-matching files and folders to match CodeEditCore's Domain/Search becomes Domain/FuzzyMatching (now that the project-search types have moved to CESearch, the folder is purely the generic matching subdomain); app-side Utils/FuzzySearch becomes Utils/FuzzyMatching. --- .../FuzzyMatchUIModel.swift} | 0 ...+FuzzySearchable.swift => RegistryItem+FuzzyMatchable.swift} | 0 .../{Theme+FuzzySearchable.swift => Theme+FuzzyMatchable.swift} | 0 .../{URL+FuzzySearchable.swift => URL+FuzzyMatchable.swift} | 0 .../FuzzyMatchTests.swift} | 0 .../Collection+FuzzyMatches.swift} | 2 +- .../FuzzyMatchModels.swift} | 2 +- .../FuzzyMatchable.swift} | 0 .../String+LengthOfMatchingPrefix.swift | 0 .../Domain/{Search => FuzzyMatching}/String+Normalise.swift | 0 .../{FuzzySearchTests.swift => FuzzyMatchTests.swift} | 0 11 files changed, 2 insertions(+), 2 deletions(-) rename CodeEdit/Utils/{FuzzySearch/FuzzySearchUIModel.swift => FuzzyMatching/FuzzyMatchUIModel.swift} (100%) rename CodeEdit/Windows/Settings/Pages/ExtensionsSettings/{RegistryItem+FuzzySearchable.swift => RegistryItem+FuzzyMatchable.swift} (100%) rename CodeEdit/Windows/Settings/Pages/ThemeSettings/{Theme+FuzzySearchable.swift => Theme+FuzzyMatchable.swift} (100%) rename CodeEdit/WorkspaceWindow/OpenQuickly/{URL+FuzzySearchable.swift => URL+FuzzyMatchable.swift} (100%) rename CodeEditTests/Utils/{FuzzySearch/FuzzySearchTests.swift => FuzzyMatching/FuzzyMatchTests.swift} (100%) rename Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/{Search/Collection+FuzzySearch.swift => FuzzyMatching/Collection+FuzzyMatches.swift} (97%) rename Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/{Search/FuzzySearchModels.swift => FuzzyMatching/FuzzyMatchModels.swift} (98%) rename Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/{Search/FuzzySearchable.swift => FuzzyMatching/FuzzyMatchable.swift} (100%) rename Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/{Search => FuzzyMatching}/String+LengthOfMatchingPrefix.swift (100%) rename Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/{Search => FuzzyMatching}/String+Normalise.swift (100%) rename Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/{FuzzySearchTests.swift => FuzzyMatchTests.swift} (100%) diff --git a/CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift b/CodeEdit/Utils/FuzzyMatching/FuzzyMatchUIModel.swift similarity index 100% rename from CodeEdit/Utils/FuzzySearch/FuzzySearchUIModel.swift rename to CodeEdit/Utils/FuzzyMatching/FuzzyMatchUIModel.swift diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift b/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzySearchable.swift rename to CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift b/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzySearchable.swift rename to CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift diff --git a/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift b/CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzyMatchable.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzySearchable.swift rename to CodeEdit/WorkspaceWindow/OpenQuickly/URL+FuzzyMatchable.swift diff --git a/CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift b/CodeEditTests/Utils/FuzzyMatching/FuzzyMatchTests.swift similarity index 100% rename from CodeEditTests/Utils/FuzzySearch/FuzzySearchTests.swift rename to CodeEditTests/Utils/FuzzyMatching/FuzzyMatchTests.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift similarity index 97% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift index 330365182f..a5b16319a4 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/Collection+FuzzySearch.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift @@ -1,5 +1,5 @@ // -// Collection+FuzzySearch.swift +// Collection+FuzzyMatches.swift // CodeEdit // // Created by Tommy Ludwig on 03.02.24. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift similarity index 98% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift index 73333beb82..5c2bce6aac 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchModels.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift @@ -1,5 +1,5 @@ // -// FuzzySearchModels.swift +// FuzzyMatchModels.swift // CodeEdit // // Created by Tommy Ludwig on 03.02.24. diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/FuzzySearchable.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+LengthOfMatchingPrefix.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Search/String+Normalise.swift rename to Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzyMatchTests.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzySearchTests.swift rename to Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzyMatchTests.swift From f13c77bc27e9f0f877c337ebc73508f425c5262b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 11:33:06 +0200 Subject: [PATCH 196/335] Refactor: Move the registry install cluster into CELSP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InstallationMethod, PackageSource, PackageManagerType, and RegistryManagerError were public in CodeEditCore but consumed only by CELSP. They join the InstallationMethod+PackageManager extension and PackageManagerError that already live there. RegistryItem stays in Core — the app's Language Servers settings page consumes it too. --- .../Sources/CELSP/Registry/Errors}/RegistryManagerError.swift | 0 .../CELSP/Sources/CELSP/Registry/Model}/InstallationMethod.swift | 0 .../CELSP/Sources/CELSP/Registry/Model}/PackageManagerType.swift | 0 .../CELSP/Sources/CELSP/Registry/Model}/PackageSource.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry => Features/CELSP/Sources/CELSP/Registry/Errors}/RegistryManagerError.swift (100%) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry => Features/CELSP/Sources/CELSP/Registry/Model}/InstallationMethod.swift (100%) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry => Features/CELSP/Sources/CELSP/Registry/Model}/PackageManagerType.swift (100%) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry => Features/CELSP/Sources/CELSP/Registry/Model}/PackageSource.swift (100%) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Errors/RegistryManagerError.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryManagerError.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Errors/RegistryManagerError.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/InstallationMethod.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageManagerType.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageManagerType.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageManagerType.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift b/Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageSource.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/PackageSource.swift rename to Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageSource.swift From a567622f6beeefe052a9d3e49a5d279e7bf06eaf Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 11:34:30 +0200 Subject: [PATCH 197/335] Refactor: Move GitBranchesGroup into CESourceControl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was public in CodeEditCore with exactly one consumer, ToolbarBranchPicker, so it now sits next to it. GitBranch stays in Core — both the app and CESourceControl consume it. --- .../Sources/CESourceControl/Views}/GitBranchesGroup.swift | 1 + 1 file changed, 1 insertion(+) rename Packages/{Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git => Features/CESourceControl/Sources/CESourceControl/Views}/GitBranchesGroup.swift (95%) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/GitBranchesGroup.swift similarity index 95% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift rename to Packages/Features/CESourceControl/Sources/CESourceControl/Views/GitBranchesGroup.swift index b5f6994720..def147968b 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranchesGroup.swift +++ b/Packages/Features/CESourceControl/Sources/CESourceControl/Views/GitBranchesGroup.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore public struct GitBranchesGroup: Hashable, Sendable { public let name: String From 821a267a6e939bee98b19f9301dca44c1b70dcad Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 11:35:30 +0200 Subject: [PATCH 198/335] Docs: Sharpen the Core admission rule and add a glossary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The consumer-count litmus now runs both ways: a type already in CodeEditCore with a single consumer moves out, with an explicit carve-out for deliberate contracts (events, command interfaces, read-models). Fixes a worked example that no longer matched the code — the fuzzy-matching helper was rewritten over withTaskGroup and now lives in Core. Adds a glossary for the overloaded terms (Editor's five meanings, project search vs fuzzy matching, Workspace vs workspace window, Document, doer). --- docs/ARCHITECTURE.md | 45 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6ec9c09a8f..c3be09950a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -74,12 +74,26 @@ Don't judge generic-ness — **count consumers**: general-purpose. - It moves to `CodeEditCore` only when a **second consumer actually appears** *and* it passes the zero-dependency charter. - -Worked example: when fuzzy search was consolidated, the pure parts (`FuzzySearchable`, -the string-matching primitives) moved to CodeEditCore — but `Collection+FuzzySearch` stayed -app-side because it depends on CollectionConcurrencyKit, which would breach Core's charter. -**Dependency honesty beats tidiness**: never add a dependency to a foundation package just to -make a move possible. Mirroring a one-line helper locally is the accepted alternative. +- The rule runs **both ways**. A type already in `CodeEditCore` that turns out to have a + single consumer moves *out*, into that consumer's package. Framework-freedom is necessary + but not sufficient: Core is a shared kernel, and every type in it that isn't actually + shared is coupling every context pays for and nobody uses. +- The exception is **deliberate contracts**: events, command interfaces, and read-models stay + in Core even with one publisher or one implementor today, because being a seam is their + entire purpose. `FileEditorOverrideValues` stays for the same reason — it is the payload of + Core's `FileEditorOverrides` protocol. + +Applying this in July 2026 evicted five types: the registry install cluster +(`InstallationMethod`, `PackageSource`, `PackageManagerType`, `RegistryManagerError`) to +CELSP, and `GitBranchesGroup` to CESourceControl. + +Worked example: fuzzy matching earned its place in CodeEditCore — three consumers across two +features (Open Quickly, Theme settings, Language Servers) — but its concurrency helper +depended on CollectionConcurrencyKit, which Core's zero-dependency charter forbids. The fix +was to rewrite the helper over `withTaskGroup` (about ten lines) rather than admit the +dependency, so `Domain/FuzzyMatching/` is now dependency-free. +**Dependency honesty beats tidiness**: never add a dependency to a foundation package to make +a move possible. Rewrite the helper, or mirror it locally, instead. ## Folder conventions (app target) @@ -172,3 +186,22 @@ Run both locally from the repo root: swiftlint lint --quiet python3 .github/scripts/audit_package_imports.py ``` + +## Glossary + +Several words are overloaded in this codebase. These are the intended meanings; prefer the +qualified term whenever the bare one could be read two ways. + +| Term | Means | +| --- | --- | +| **Editor** (`Editor`) | One tab group inside a workspace window — a split pane with its own tab bar and selection. | +| **Editor instance** (`EditorInstance`) | One open file within an editor, holding that file's editing state. | +| **`EditorManager`** | The per-workspace owner of the editor layout (splits, the active editor). | +| **CEEditor** | The package containing the editor feature. | +| **CodeEditSourceEditor** | The external text-editing widget (a separate repository), not part of this codebase. | +| **Search** | *Project* search: find/replace across files, the index, query modes, the Find navigator. Lives in `CESearch`, which owns its whole model. | +| **Fuzzy matching** | Ranking candidates by match quality for typeahead (Open Quickly, theme and language-server pickers). A generic capability in CodeEditCore `Domain/FuzzyMatching/`. It does no searching; nothing here is named `*Search*`. | +| **Workspace** (`Workspace`) | The session aggregate for one open project: the project-scoped services and their lifetime. It owns lifecycle, *not* mutation routing — features mutate the sub-models they are handed. | +| **Workspace window** (`WorkspaceWindow/`) | The window and its chrome around a workspace: navigator, inspector, utility area, status bar. Window-UI state lives on `CodeEditWindowController`, not on `Workspace`. | +| **Document** (`CodeFileDocument`) | An open, editable file backed by NSDocument. Distinct from `CEWorkspaceFile` (a node in the file tree) and from the file on disk. | +| **Doer** | A role-noun class performing one operation that spans services (`WorkspaceOpener`, `FileMover`, `RepositoryCloner`), following the `NSFileCoordinator` naming idiom. Formerly called UseCases. | From 4fd5a6b212f7c0f0bce9af7d946623cd9229bfc6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 21:25:52 +0200 Subject: [PATCH 199/335] Fix: Remove unnecessary nonisolated(unsafe) annotations in CodeEditSettings --- .../CodeEditSettings/Store/CodableDefault+Providers.swift | 8 ++++---- .../CodeEditSettings/Store/Environment+Theme.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift index f537ea1ac0..37db943076 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift @@ -10,11 +10,11 @@ import AppKit // MARK: - Bool Defaults public enum DefaultTrue: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = true + public static let defaultValue = true } public enum DefaultFalse: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = false + public static let defaultValue = false } // MARK: - Terminal Defaults @@ -44,7 +44,7 @@ public enum DefaultEmptyGlobPatterns: DefaultValueProvider { } public enum DefaultEmptyStringDictionary: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue: [String: String] = [:] + public static let defaultValue: [String: String] = [:] } public enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { @@ -62,7 +62,7 @@ public enum DefaultEmptySourceControlAccounts: DefaultValueProvider { } public enum DefaultEmptyString: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = "" + public static let defaultValue = "" } // MARK: - General Settings Defaults diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift index d28f05d43f..9bcf47f46c 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift @@ -8,11 +8,11 @@ import SwiftUI private struct CurrentThemeKey: EnvironmentKey { - nonisolated(unsafe) static let defaultValue: Theme? = nil + static let defaultValue: Theme? = nil } private struct CurrentDarkThemeKey: EnvironmentKey { - nonisolated(unsafe) static let defaultValue: Theme? = nil + static let defaultValue: Theme? = nil } public extension EnvironmentValues { From c298e8ec31a35e02aa3e062a1b0114e4f0b1e1d6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 21:36:50 +0200 Subject: [PATCH 200/335] Fix: Clear strict-concurrency warnings in CodeEditCore and CodeEditUI --- .../FuzzyMatching/String+LengthOfMatchingPrefix.swift | 1 - .../EnvironmentKeys/Environment+IsFullscreen.swift | 2 +- .../CodeEditUI/Views/SplitView/SplitViewItem.swift | 10 +++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift index e4ceb0bb2c..04f1c1abce 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift @@ -6,7 +6,6 @@ // import Foundation -import CodeEditCore extension String { /// Returns the length of the matching prefix content or normalised content at the specified index. diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift index 170a0a861c..2e3473231c 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift @@ -8,7 +8,7 @@ import SwiftUI private struct WorkspaceFullscreenStateEnvironmentKey: EnvironmentKey { - nonisolated(unsafe) static let defaultValue: Bool = false + static let defaultValue: Bool = false } public extension EnvironmentValues { diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift index 62155924cd..c7ed9df838 100644 --- a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift @@ -35,7 +35,15 @@ public class SplitViewItem: ObservableObject { private func createObservers() -> [NSKeyValueObservation] { [ item.observe(\.isCollapsed) { [weak self] item, _ in - self?.collapsed.wrappedValue = item.isCollapsed + // Read the value out here so only a `Bool` crosses into the main actor + // region — `item` is non-Sendable and cannot be sent. + let isCollapsed = item.isCollapsed + // AppKit mutates `isCollapsed` on the main thread, so this KVO callback is + // always delivered there. Assert that rather than hopping asynchronously — + // a `Task { @MainActor }` would delay the binding update by a runloop turn. + MainActor.assumeIsolated { + self?.collapsed.wrappedValue = isCollapsed + } } ] } From a8c2805d193077ad421a0ba45db9ccbd537dfaa5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 22:21:20 +0200 Subject: [PATCH 201/335] Fix: Isolate applyAppearance to the main actor --- .../Sources/CodeEditSettings/Models/GeneralSettings.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift index 37f21ab0c3..bb374eedd8 100644 --- a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift +++ b/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift @@ -84,6 +84,11 @@ extension SettingsData { case dark /// Applies the selected appearance + /// + /// Main-actor isolated because it mutates `NSApp.appearance`, and AppKit is + /// main-thread-only. Both callers (`AppDelegate` and `GeneralSettingsView`) are + /// already main-actor contexts. + @MainActor public func applyAppearance() { switch self { case .system: From 508a02a5e998aa3c04ac6764ee714fa1addb9692 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 26 Jul 2026 22:49:01 +0200 Subject: [PATCH 202/335] Fix: Express CodeFileDocument's static and autosave isolation --- .../CodeEditDocument/CodeFileDocument.swift | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift index 1ad721d655..94e7896ce6 100644 --- a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift @@ -33,7 +33,8 @@ public final class CodeFileDocument: NSDocument, ObservableObject { } } - static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") + /// `nonisolated` so the nonisolated overrides can log; `Logger` is `Sendable`. + nonisolated static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") /// Vends the app-registered delegate (see ``CodeFileDocumentDelegate``). A static provider — /// not a per-instance property — because framework-created documents fire `documentDidOpen` @@ -105,9 +106,13 @@ public final class CodeFileDocument: NSDocument, ObservableObject { } /// A lock that ensures autosave scheduling happens correctly. - private var autosaveTimerLock: NSLock = NSLock() + /// `nonisolated` for the nonisolated `scheduleAutosaving()` override; `NSLock` is `Sendable`. + nonisolated private let autosaveTimerLock: NSLock = NSLock() + /// Timer used to schedule autosave intervals. - private var autosaveTimer: Timer? + /// `nonisolated(unsafe)` because every access happens with ``autosaveTimerLock`` held — the + /// lock is the synchronisation, which the compiler cannot see. Never touch this outside it. + nonisolated(unsafe) private var autosaveTimer: Timer? /// Provides the current "autosave enabled" preference without coupling this type to the /// Settings feature. Wired by the app at launch (see `AppDelegate`). Defaults to `false` @@ -276,7 +281,11 @@ public final class CodeFileDocument: NSDocument, ObservableObject { self?.autosaveTimerLock.withLock { guard timer.isValid else { return } self?.autosaveTimer = nil - self?.autosave(withDelegate: nil, didAutosave: nil, contextInfo: nil) + // Delivered on the main runloop the timer was scheduled on; assert that + // rather than hopping, which would fire outside the lock. + MainActor.assumeIsolated { + self?.autosave(withDelegate: nil, didAutosave: nil, contextInfo: nil) + } } } } else { @@ -382,7 +391,9 @@ public final class CodeFileDocument: NSDocument, ObservableObject { private extension CodeFileDocument { - static let fileTypeExtension: [String: String?] = [ + /// `nonisolated` so `fileNameExtension(forType:saveOperation:)` can read it off the main actor. + /// `[String: String?]` is `Sendable`, so this is safe rather than merely asserted. + nonisolated static let fileTypeExtension: [String: String?] = [ "public.make-source": nil ] } From f93873e98274ce7b8dfdbc0132d0b0fb607cbdd4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 09:15:39 +0200 Subject: [PATCH 203/335] Refactor: Move workspace lifecycle doers to App scope WorkspaceWindowManager, its protocol, the open/close/document doers and ApplicationShutdownCoordinator are app-scope services owned by AppDependencies; ApplicationShutdownCoordinator is about NSApplication. They were nested two levels inside WorkspaceWindow/Workspace/, putting parent-scope services under a child scope. App/ now holds everything AppDependencies owns, and WorkspaceWindow/ means single-window scope only. --- .../WorkspaceLifecycle}/ApplicationShutdownCoordinator.swift | 0 .../WorkspaceLifecycle}/DocumentOpener.swift | 0 .../WorkspaceLifecycle}/WorkspaceCloser.swift | 0 .../WorkspaceLifecycle}/WorkspaceOpener.swift | 0 .../WorkspaceLifecycle}/WorkspaceWindowManager.swift | 0 .../WorkspaceLifecycle}/WorkspaceWindowManaging.swift | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/{WorkspaceWindow/Workspace/WindowManagement => App/WorkspaceLifecycle}/ApplicationShutdownCoordinator.swift (100%) rename CodeEdit/{WorkspaceWindow/Workspace/WindowManagement => App/WorkspaceLifecycle}/DocumentOpener.swift (100%) rename CodeEdit/{WorkspaceWindow/Workspace/WindowManagement => App/WorkspaceLifecycle}/WorkspaceCloser.swift (100%) rename CodeEdit/{WorkspaceWindow/Workspace/WindowManagement => App/WorkspaceLifecycle}/WorkspaceOpener.swift (100%) rename CodeEdit/{WorkspaceWindow/Workspace/WindowManagement => App/WorkspaceLifecycle}/WorkspaceWindowManager.swift (100%) rename CodeEdit/{WorkspaceWindow/Workspace/WindowManagement => App/WorkspaceLifecycle}/WorkspaceWindowManaging.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift b/CodeEdit/App/WorkspaceLifecycle/ApplicationShutdownCoordinator.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/WindowManagement/ApplicationShutdownCoordinator.swift rename to CodeEdit/App/WorkspaceLifecycle/ApplicationShutdownCoordinator.swift diff --git a/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/DocumentOpener.swift b/CodeEdit/App/WorkspaceLifecycle/DocumentOpener.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/WindowManagement/DocumentOpener.swift rename to CodeEdit/App/WorkspaceLifecycle/DocumentOpener.swift diff --git a/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceCloser.swift b/CodeEdit/App/WorkspaceLifecycle/WorkspaceCloser.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceCloser.swift rename to CodeEdit/App/WorkspaceLifecycle/WorkspaceCloser.swift diff --git a/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceOpener.swift b/CodeEdit/App/WorkspaceLifecycle/WorkspaceOpener.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceOpener.swift rename to CodeEdit/App/WorkspaceLifecycle/WorkspaceOpener.swift diff --git a/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManager.swift b/CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManager.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManager.swift rename to CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManager.swift diff --git a/CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManaging.swift b/CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManaging.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/WindowManagement/WorkspaceWindowManaging.swift rename to CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManaging.swift From d67b1cbedf42c0847d670db01d27e2ede05310e5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 09:16:35 +0200 Subject: [PATCH 204/335] Refactor: Dissolve WorkspaceWindow/Window into the group root 'WorkspaceWindow/Window' read as 'window window'. Its contents are this scope's AppKit shell and primary view, which the folder convention places at the feature root. Toolbar/ and WorkspacePanel/ are promoted unchanged. --- .../{Window => }/CodeEditSplitViewController.swift | 0 .../{Window => }/CodeEditWindowController+Panels.swift | 0 .../{Window => }/CodeEditWindowController+Toolbar.swift | 0 .../WorkspaceWindow/{Window => }/CodeEditWindowController.swift | 0 .../{Window => }/CodeEditWindowControllerExtensions.swift | 0 .../{Window => }/NotificationPanelViewModel+Toolbar.swift | 0 .../{Window => }/Toolbar/StartTaskToolbarButton.swift | 0 .../{Window => }/Toolbar/StartTaskToolbarItem.swift | 0 .../{Window => }/Toolbar/StopTaskToolbarButton.swift | 0 .../{Window => }/Toolbar/StopTaskToolbarItem.swift | 0 .../{Window => }/WorkspacePanel/WorkspacePanelTabBar.swift | 0 .../{Window => }/WorkspacePanel/WorkspacePanelView.swift | 0 CodeEdit/WorkspaceWindow/{Window => }/WorkspaceSheets.swift | 0 CodeEdit/WorkspaceWindow/{Window => }/WorkspaceView.swift | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/{Window => }/CodeEditSplitViewController.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/CodeEditWindowController+Panels.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/CodeEditWindowController+Toolbar.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/CodeEditWindowController.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/CodeEditWindowControllerExtensions.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/NotificationPanelViewModel+Toolbar.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/Toolbar/StartTaskToolbarButton.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/Toolbar/StartTaskToolbarItem.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/Toolbar/StopTaskToolbarButton.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/Toolbar/StopTaskToolbarItem.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/WorkspacePanel/WorkspacePanelTabBar.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/WorkspacePanel/WorkspacePanelView.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/WorkspaceSheets.swift (100%) rename CodeEdit/WorkspaceWindow/{Window => }/WorkspaceView.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/Window/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/CodeEditSplitViewController.swift rename to CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift diff --git a/CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Panels.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Panels.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Panels.swift rename to CodeEdit/WorkspaceWindow/CodeEditWindowController+Panels.swift diff --git a/CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Toolbar.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/CodeEditWindowController+Toolbar.swift rename to CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift diff --git a/CodeEdit/WorkspaceWindow/Window/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/CodeEditWindowController.swift rename to CodeEdit/WorkspaceWindow/CodeEditWindowController.swift diff --git a/CodeEdit/WorkspaceWindow/Window/CodeEditWindowControllerExtensions.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowControllerExtensions.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/CodeEditWindowControllerExtensions.swift rename to CodeEdit/WorkspaceWindow/CodeEditWindowControllerExtensions.swift diff --git a/CodeEdit/WorkspaceWindow/Window/NotificationPanelViewModel+Toolbar.swift b/CodeEdit/WorkspaceWindow/NotificationPanelViewModel+Toolbar.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/NotificationPanelViewModel+Toolbar.swift rename to CodeEdit/WorkspaceWindow/NotificationPanelViewModel+Toolbar.swift diff --git a/CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarButton.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarButton.swift rename to CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarButton.swift diff --git a/CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarItem.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/Toolbar/StartTaskToolbarItem.swift rename to CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarItem.swift diff --git a/CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarButton.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarButton.swift rename to CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarButton.swift diff --git a/CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarItem.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/Toolbar/StopTaskToolbarItem.swift rename to CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarItem.swift diff --git a/CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelTabBar.swift rename to CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift diff --git a/CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelView.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/WorkspacePanel/WorkspacePanelView.swift rename to CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift diff --git a/CodeEdit/WorkspaceWindow/Window/WorkspaceSheets.swift b/CodeEdit/WorkspaceWindow/WorkspaceSheets.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/WorkspaceSheets.swift rename to CodeEdit/WorkspaceWindow/WorkspaceSheets.swift diff --git a/CodeEdit/WorkspaceWindow/Window/WorkspaceView.swift b/CodeEdit/WorkspaceWindow/WorkspaceView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Window/WorkspaceView.swift rename to CodeEdit/WorkspaceWindow/WorkspaceView.swift From c685e3a845412697bc34edc96148fda6e911aa59 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 09:17:25 +0200 Subject: [PATCH 205/335] Refactor: Drop redundant prefix from workspace Settings group WorkspaceSettings/ repeated its parent's word. Scope distinguishes WorkspaceWindow/Settings/ from the Settings window's group, the same way the product presents User vs Workspace settings. The CEWorkspaceSettings type keeps its name. --- .../{WorkspaceSettings => Settings}/AddCETaskView.swift | 0 .../{WorkspaceSettings => Settings}/CETaskFormView.swift | 0 .../CEWorkspaceSettings+TasksConfigurationProviding.swift | 0 .../{WorkspaceSettings => Settings}/CEWorkspaceSettings.swift | 0 .../CEWorkspaceSettingsTaskListView.swift | 0 .../{WorkspaceSettings => Settings}/CEWorkspaceSettingsView.swift | 0 .../{WorkspaceSettings => Settings}/EditCETaskView.swift | 0 .../EnvironmentVariableListItem.swift | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/AddCETaskView.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/CETaskFormView.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/CEWorkspaceSettings+TasksConfigurationProviding.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/CEWorkspaceSettings.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/CEWorkspaceSettingsTaskListView.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/CEWorkspaceSettingsView.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/EditCETaskView.swift (100%) rename CodeEdit/WorkspaceWindow/{WorkspaceSettings => Settings}/EnvironmentVariableListItem.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/AddCETaskView.swift b/CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/AddCETaskView.swift rename to CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/CETaskFormView.swift b/CodeEdit/WorkspaceWindow/Settings/CETaskFormView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/CETaskFormView.swift rename to CodeEdit/WorkspaceWindow/Settings/CETaskFormView.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift b/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings+TasksConfigurationProviding.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings+TasksConfigurationProviding.swift rename to CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings+TasksConfigurationProviding.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings.swift b/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettings.swift rename to CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsTaskListView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsTaskListView.swift rename to CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsTaskListView.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsView.swift b/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/CEWorkspaceSettingsView.swift rename to CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsView.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/EditCETaskView.swift b/CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/EditCETaskView.swift rename to CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift diff --git a/CodeEdit/WorkspaceWindow/WorkspaceSettings/EnvironmentVariableListItem.swift b/CodeEdit/WorkspaceWindow/Settings/EnvironmentVariableListItem.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/WorkspaceSettings/EnvironmentVariableListItem.swift rename to CodeEdit/WorkspaceWindow/Settings/EnvironmentVariableListItem.swift From b61e3c820c6224d9364dadb7237ae71e5560f718 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 09:18:53 +0200 Subject: [PATCH 206/335] Refactor: Rename Windows group to AuxiliaryWindows Nothing in 'Windows' explained why the primary window lives outside it. The name now does. Also updates the Embed ExtensionKit ExtensionPoint build-phase membership exception, which hardcodes the group path and would otherwise silently stop embedding the extension point. --- CodeEdit.xcodeproj/project.pbxproj | 2 +- .../{Windows => AuxiliaryWindows}/About/AboutFooterView.swift | 0 .../{Windows => AuxiliaryWindows}/About/AboutSubtitleView.swift | 0 .../About/Acknowledgements/AcknowledgementRowView.swift | 0 .../About/Acknowledgements/AcknowledgementsView.swift | 0 .../About/Acknowledgements/AcknowledgementsViewModel.swift | 0 .../About/Acknowledgements/ParsePackagesResolved.swift | 0 .../About/Contributors/Contributor.swift | 0 .../About/Contributors/ContributorRowView.swift | 0 .../About/Contributors/ContributorsView.swift | 0 .../About/OperatingSystemVersion+String.swift | 0 .../Extensions/Commands+ForEach.swift | 0 .../Extensions/ExtensionActivatorView.swift | 0 .../Extensions/ExtensionDetailView.swift | 0 .../Extensions/ExtensionDiscovery.swift | 0 .../Extensions/ExtensionInfo.swift | 0 .../Extensions/ExtensionManagerWindow.swift | 0 .../Extensions/ExtensionSceneView.swift | 0 .../Extensions/ExtensionsListView.swift | 0 .../Extensions/ExtensionsManager.swift | 0 .../Extensions/codeedit.extension.appextensionpoint | 0 .../Feedback/FeedbackIssueArea.swift | 0 .../{Windows => AuxiliaryWindows}/Feedback/FeedbackModel.swift | 0 .../Feedback/FeedbackToolbar.swift | 0 .../{Windows => AuxiliaryWindows}/Feedback/FeedbackType.swift | 0 .../{Windows => AuxiliaryWindows}/Feedback/FeedbackView.swift | 0 .../Feedback/FeedbackWindowController.swift | 0 .../Settings/Controls/ExternalLink.swift | 0 .../Settings/Controls/FontWeightPicker.swift | 0 .../Settings/Controls/GlobPatternList.swift | 0 .../Settings/Controls/GlobPatternListItem.swift | 0 .../Settings/Controls/Int+HexString.swift | 0 .../Settings/Controls/InvisibleCharacterWarningList.swift | 0 .../Settings/Controls/MonospacedFontPicker.swift | 0 .../Settings/Controls/SettingsColorPicker.swift | 0 .../Settings/Controls/WarningCharactersView.swift | 0 .../Settings/PageAndSettings.swift | 0 .../Settings/Pages/AccountsSettings/AccountSelectionView.swift | 0 .../Pages/AccountsSettings/AccountsSettingsAccountLink.swift | 0 .../Pages/AccountsSettings/AccountsSettingsDetailsView.swift | 0 .../Pages/AccountsSettings/AccountsSettingsProviderRow.swift | 0 .../Pages/AccountsSettings/AccountsSettingsSigninView.swift | 0 .../Settings/Pages/AccountsSettings/AccountsSettingsView.swift | 0 .../Settings/Pages/AccountsSettings/CreateSSHKeyView.swift | 0 .../Settings/Pages/AccountsSettings/Font+Caption3.swift | 0 .../Pages/AccountsSettings/SourceControlAccount+Icon.swift | 0 .../Pages/DeveloperSettings/DeveloperSettingsView.swift | 0 .../Pages/ExtensionsSettings/LanguageServerInstallView.swift | 0 .../Pages/ExtensionsSettings/LanguageServerRowView.swift | 0 .../Settings/Pages/ExtensionsSettings/LanguageServersView.swift | 0 .../Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift | 0 .../Settings/Pages/GeneralSettings/GeneralSettingsView.swift | 0 .../Settings/Pages/LocationsSettings/LocationsSettings.swift | 0 .../Pages/LocationsSettings/LocationsSettingsView.swift | 0 .../Pages/NavigationSettings/NavigationSettingsView.swift | 0 .../SearchSettingsIgnoreGlobPatternItemView.swift | 0 .../Settings/Pages/SearchSettings/SearchSettingsModel.swift | 0 .../Settings/Pages/SearchSettings/SearchSettingsView.swift | 0 .../Pages/SourceControlSettings/IgnorePatternModel.swift | 0 .../Pages/SourceControlSettings/IgnoredFilesListView.swift | 0 .../Settings/Pages/SourceControlSettings/Limiter.swift | 0 .../Pages/SourceControlSettings/SourceControlGeneralView.swift | 0 .../Pages/SourceControlSettings/SourceControlGitView.swift | 0 .../Pages/SourceControlSettings/SourceControlSettingsView.swift | 0 .../Settings/Pages/TerminalSettings/TerminalSettingsView.swift | 0 .../Pages/TextEditingSettings/InvisiblesSettingsView.swift | 0 .../Pages/TextEditingSettings/TextEditingSettingsView.swift | 0 .../Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift | 0 .../Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift | 0 .../Settings/Pages/ThemeSettings/ThemeModel+Export.swift | 0 .../Settings/Pages/ThemeSettings/ThemeModel.swift | 0 .../Settings/Pages/ThemeSettings/ThemeRepository.swift | 0 .../Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift | 0 .../Pages/ThemeSettings/ThemeSettingsColorPreview.swift | 0 .../Pages/ThemeSettings/ThemeSettingsThemeDetails.swift | 0 .../Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift | 0 .../Settings/Pages/ThemeSettings/ThemeSettingsView.swift | 0 .../Settings/Search/SearchableSettingsPage.swift | 0 .../Settings/Search/SettingsData+Search.swift | 0 .../Settings/Search/SettingsSearchResult.swift | 0 .../Settings/Search/String+HighlightOccurrences.swift | 0 .../Settings/SettingsData+CommandRegistration.swift | 0 .../Settings/SettingsData+KeybindingReconcile.swift | 0 .../{Windows => AuxiliaryWindows}/Settings/SettingsForm.swift | 0 .../Settings/SettingsInjector.swift | 0 .../{Windows => AuxiliaryWindows}/Settings/SettingsPage.swift | 0 .../Settings/SettingsPageView.swift | 0 .../Settings/SettingsSidebarFix.swift | 0 .../{Windows => AuxiliaryWindows}/Settings/SettingsView.swift | 0 .../{Windows => AuxiliaryWindows}/Settings/SettingsWindow.swift | 0 .../Settings/View+ConstrainHeightToWindow.swift | 0 .../Settings/View+HideSidebarToggle.swift | 0 .../Settings/View+NavigationBarBackButtonVisible.swift | 0 .../{Windows => AuxiliaryWindows}/Welcome/GitCloneButton.swift | 0 .../{Windows => AuxiliaryWindows}/Welcome/NewFileButton.swift | 0 .../Welcome/OpenFileOrFolderButton.swift | 0 .../Welcome/WelcomeSubtitleView.swift | 0 97 files changed, 1 insertion(+), 1 deletion(-) rename CodeEdit/{Windows => AuxiliaryWindows}/About/AboutFooterView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/AboutSubtitleView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Acknowledgements/AcknowledgementRowView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Acknowledgements/AcknowledgementsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Acknowledgements/AcknowledgementsViewModel.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Acknowledgements/ParsePackagesResolved.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Contributors/Contributor.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Contributors/ContributorRowView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/Contributors/ContributorsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/About/OperatingSystemVersion+String.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/Commands+ForEach.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionActivatorView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionDetailView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionDiscovery.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionInfo.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionManagerWindow.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionSceneView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionsListView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/ExtensionsManager.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Extensions/codeedit.extension.appextensionpoint (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Feedback/FeedbackIssueArea.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Feedback/FeedbackModel.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Feedback/FeedbackToolbar.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Feedback/FeedbackType.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Feedback/FeedbackView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Feedback/FeedbackWindowController.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/ExternalLink.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/FontWeightPicker.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/GlobPatternList.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/GlobPatternListItem.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/Int+HexString.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/InvisibleCharacterWarningList.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/MonospacedFontPicker.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/SettingsColorPicker.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Controls/WarningCharactersView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/PageAndSettings.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/AccountSelectionView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/AccountsSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/Font+Caption3.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ExtensionsSettings/LanguageServersView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/GeneralSettings/GeneralSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/LocationsSettings/LocationsSettings.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/LocationsSettings/LocationsSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/NavigationSettings/NavigationSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SearchSettings/SearchSettingsModel.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SearchSettings/SearchSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SourceControlSettings/Limiter.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SourceControlSettings/SourceControlGitView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/TerminalSettings/TerminalSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeModel+Export.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeModel.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeRepository.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Pages/ThemeSettings/ThemeSettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Search/SearchableSettingsPage.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Search/SettingsData+Search.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Search/SettingsSearchResult.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/Search/String+HighlightOccurrences.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsData+CommandRegistration.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsData+KeybindingReconcile.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsForm.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsInjector.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsPage.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsPageView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsSidebarFix.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsView.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/SettingsWindow.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/View+ConstrainHeightToWindow.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/View+HideSidebarToggle.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Settings/View+NavigationBarBackButtonVisible.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Welcome/GitCloneButton.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Welcome/NewFileButton.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Welcome/OpenFileOrFolderButton.swift (100%) rename CodeEdit/{Windows => AuxiliaryWindows}/Welcome/WelcomeSubtitleView.swift (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index aa3e5dc9f5..98f3478898 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; buildPhase = 6C6BD6FD29CD154900235D17 /* Embed ExtensionKit ExtensionPoint */; membershipExceptions = ( - Windows/Extensions/codeedit.extension.appextensionpoint, + AuxiliaryWindows/Extensions/codeedit.extension.appextensionpoint, ); }; /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ diff --git a/CodeEdit/Windows/About/AboutFooterView.swift b/CodeEdit/AuxiliaryWindows/About/AboutFooterView.swift similarity index 100% rename from CodeEdit/Windows/About/AboutFooterView.swift rename to CodeEdit/AuxiliaryWindows/About/AboutFooterView.swift diff --git a/CodeEdit/Windows/About/AboutSubtitleView.swift b/CodeEdit/AuxiliaryWindows/About/AboutSubtitleView.swift similarity index 100% rename from CodeEdit/Windows/About/AboutSubtitleView.swift rename to CodeEdit/AuxiliaryWindows/About/AboutSubtitleView.swift diff --git a/CodeEdit/Windows/About/Acknowledgements/AcknowledgementRowView.swift b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementRowView.swift similarity index 100% rename from CodeEdit/Windows/About/Acknowledgements/AcknowledgementRowView.swift rename to CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementRowView.swift diff --git a/CodeEdit/Windows/About/Acknowledgements/AcknowledgementsView.swift b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsView.swift similarity index 100% rename from CodeEdit/Windows/About/Acknowledgements/AcknowledgementsView.swift rename to CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsView.swift diff --git a/CodeEdit/Windows/About/Acknowledgements/AcknowledgementsViewModel.swift b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsViewModel.swift similarity index 100% rename from CodeEdit/Windows/About/Acknowledgements/AcknowledgementsViewModel.swift rename to CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsViewModel.swift diff --git a/CodeEdit/Windows/About/Acknowledgements/ParsePackagesResolved.swift b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/ParsePackagesResolved.swift similarity index 100% rename from CodeEdit/Windows/About/Acknowledgements/ParsePackagesResolved.swift rename to CodeEdit/AuxiliaryWindows/About/Acknowledgements/ParsePackagesResolved.swift diff --git a/CodeEdit/Windows/About/Contributors/Contributor.swift b/CodeEdit/AuxiliaryWindows/About/Contributors/Contributor.swift similarity index 100% rename from CodeEdit/Windows/About/Contributors/Contributor.swift rename to CodeEdit/AuxiliaryWindows/About/Contributors/Contributor.swift diff --git a/CodeEdit/Windows/About/Contributors/ContributorRowView.swift b/CodeEdit/AuxiliaryWindows/About/Contributors/ContributorRowView.swift similarity index 100% rename from CodeEdit/Windows/About/Contributors/ContributorRowView.swift rename to CodeEdit/AuxiliaryWindows/About/Contributors/ContributorRowView.swift diff --git a/CodeEdit/Windows/About/Contributors/ContributorsView.swift b/CodeEdit/AuxiliaryWindows/About/Contributors/ContributorsView.swift similarity index 100% rename from CodeEdit/Windows/About/Contributors/ContributorsView.swift rename to CodeEdit/AuxiliaryWindows/About/Contributors/ContributorsView.swift diff --git a/CodeEdit/Windows/About/OperatingSystemVersion+String.swift b/CodeEdit/AuxiliaryWindows/About/OperatingSystemVersion+String.swift similarity index 100% rename from CodeEdit/Windows/About/OperatingSystemVersion+String.swift rename to CodeEdit/AuxiliaryWindows/About/OperatingSystemVersion+String.swift diff --git a/CodeEdit/Windows/Extensions/Commands+ForEach.swift b/CodeEdit/AuxiliaryWindows/Extensions/Commands+ForEach.swift similarity index 100% rename from CodeEdit/Windows/Extensions/Commands+ForEach.swift rename to CodeEdit/AuxiliaryWindows/Extensions/Commands+ForEach.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionActivatorView.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionActivatorView.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionActivatorView.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionActivatorView.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionDetailView.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionDetailView.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionDetailView.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionDetailView.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionDiscovery.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionDiscovery.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionDiscovery.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionDiscovery.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionInfo.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionInfo.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionInfo.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionInfo.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionManagerWindow.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionManagerWindow.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionManagerWindow.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionManagerWindow.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionSceneView.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionSceneView.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionSceneView.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionSceneView.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionsListView.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionsListView.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionsListView.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionsListView.swift diff --git a/CodeEdit/Windows/Extensions/ExtensionsManager.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift similarity index 100% rename from CodeEdit/Windows/Extensions/ExtensionsManager.swift rename to CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift diff --git a/CodeEdit/Windows/Extensions/codeedit.extension.appextensionpoint b/CodeEdit/AuxiliaryWindows/Extensions/codeedit.extension.appextensionpoint similarity index 100% rename from CodeEdit/Windows/Extensions/codeedit.extension.appextensionpoint rename to CodeEdit/AuxiliaryWindows/Extensions/codeedit.extension.appextensionpoint diff --git a/CodeEdit/Windows/Feedback/FeedbackIssueArea.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackIssueArea.swift similarity index 100% rename from CodeEdit/Windows/Feedback/FeedbackIssueArea.swift rename to CodeEdit/AuxiliaryWindows/Feedback/FeedbackIssueArea.swift diff --git a/CodeEdit/Windows/Feedback/FeedbackModel.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackModel.swift similarity index 100% rename from CodeEdit/Windows/Feedback/FeedbackModel.swift rename to CodeEdit/AuxiliaryWindows/Feedback/FeedbackModel.swift diff --git a/CodeEdit/Windows/Feedback/FeedbackToolbar.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackToolbar.swift similarity index 100% rename from CodeEdit/Windows/Feedback/FeedbackToolbar.swift rename to CodeEdit/AuxiliaryWindows/Feedback/FeedbackToolbar.swift diff --git a/CodeEdit/Windows/Feedback/FeedbackType.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackType.swift similarity index 100% rename from CodeEdit/Windows/Feedback/FeedbackType.swift rename to CodeEdit/AuxiliaryWindows/Feedback/FeedbackType.swift diff --git a/CodeEdit/Windows/Feedback/FeedbackView.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift similarity index 100% rename from CodeEdit/Windows/Feedback/FeedbackView.swift rename to CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift diff --git a/CodeEdit/Windows/Feedback/FeedbackWindowController.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift similarity index 100% rename from CodeEdit/Windows/Feedback/FeedbackWindowController.swift rename to CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift diff --git a/CodeEdit/Windows/Settings/Controls/ExternalLink.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/ExternalLink.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/ExternalLink.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/ExternalLink.swift diff --git a/CodeEdit/Windows/Settings/Controls/FontWeightPicker.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/FontWeightPicker.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/FontWeightPicker.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/FontWeightPicker.swift diff --git a/CodeEdit/Windows/Settings/Controls/GlobPatternList.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/GlobPatternList.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/GlobPatternList.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/GlobPatternList.swift diff --git a/CodeEdit/Windows/Settings/Controls/GlobPatternListItem.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/GlobPatternListItem.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/GlobPatternListItem.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/GlobPatternListItem.swift diff --git a/CodeEdit/Windows/Settings/Controls/Int+HexString.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/Int+HexString.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/Int+HexString.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/Int+HexString.swift diff --git a/CodeEdit/Windows/Settings/Controls/InvisibleCharacterWarningList.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/InvisibleCharacterWarningList.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/InvisibleCharacterWarningList.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/InvisibleCharacterWarningList.swift diff --git a/CodeEdit/Windows/Settings/Controls/MonospacedFontPicker.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/MonospacedFontPicker.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/MonospacedFontPicker.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/MonospacedFontPicker.swift diff --git a/CodeEdit/Windows/Settings/Controls/SettingsColorPicker.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/SettingsColorPicker.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/SettingsColorPicker.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/SettingsColorPicker.swift diff --git a/CodeEdit/Windows/Settings/Controls/WarningCharactersView.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/WarningCharactersView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Controls/WarningCharactersView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Controls/WarningCharactersView.swift diff --git a/CodeEdit/Windows/Settings/PageAndSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/PageAndSettings.swift similarity index 100% rename from CodeEdit/Windows/Settings/PageAndSettings.swift rename to CodeEdit/AuxiliaryWindows/Settings/PageAndSettings.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountSelectionView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountSelectionView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsSigninView.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/CreateSSHKeyView.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/Font+Caption3.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/Font+Caption3.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/Font+Caption3.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/Font+Caption3.swift diff --git a/CodeEdit/Windows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift diff --git a/CodeEdit/Windows/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/DeveloperSettings/DeveloperSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServerInstallView.swift diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServerRowView.swift diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift diff --git a/CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/RegistryItem+FuzzyMatchable.swift diff --git a/CodeEdit/Windows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettings.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettings.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettings.swift diff --git a/CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsIgnoreGlobPatternItemView.swift diff --git a/CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsModel.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsModel.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsModel.swift diff --git a/CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SearchSettings/SearchSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/IgnorePatternModel.swift diff --git a/CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/IgnoredFilesListView.swift diff --git a/CodeEdit/Windows/Settings/Pages/SourceControlSettings/Limiter.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/Limiter.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SourceControlSettings/Limiter.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/Limiter.swift diff --git a/CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift diff --git a/CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGitView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGitView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlGitView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGitView.swift diff --git a/CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/Theme+FuzzyMatchable.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeModel.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeRepository.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeRepository.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeRepository.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeRepository.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift diff --git a/CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift diff --git a/CodeEdit/Windows/Settings/Search/SearchableSettingsPage.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SearchableSettingsPage.swift similarity index 100% rename from CodeEdit/Windows/Settings/Search/SearchableSettingsPage.swift rename to CodeEdit/AuxiliaryWindows/Settings/Search/SearchableSettingsPage.swift diff --git a/CodeEdit/Windows/Settings/Search/SettingsData+Search.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift similarity index 100% rename from CodeEdit/Windows/Settings/Search/SettingsData+Search.swift rename to CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift diff --git a/CodeEdit/Windows/Settings/Search/SettingsSearchResult.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchResult.swift similarity index 100% rename from CodeEdit/Windows/Settings/Search/SettingsSearchResult.swift rename to CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchResult.swift diff --git a/CodeEdit/Windows/Settings/Search/String+HighlightOccurrences.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/String+HighlightOccurrences.swift similarity index 100% rename from CodeEdit/Windows/Settings/Search/String+HighlightOccurrences.swift rename to CodeEdit/AuxiliaryWindows/Settings/Search/String+HighlightOccurrences.swift diff --git a/CodeEdit/Windows/Settings/SettingsData+CommandRegistration.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsData+CommandRegistration.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift diff --git a/CodeEdit/Windows/Settings/SettingsData+KeybindingReconcile.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData+KeybindingReconcile.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsData+KeybindingReconcile.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsData+KeybindingReconcile.swift diff --git a/CodeEdit/Windows/Settings/SettingsForm.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsForm.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsForm.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsForm.swift diff --git a/CodeEdit/Windows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsInjector.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift diff --git a/CodeEdit/Windows/Settings/SettingsPage.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsPage.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsPage.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsPage.swift diff --git a/CodeEdit/Windows/Settings/SettingsPageView.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsPageView.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift diff --git a/CodeEdit/Windows/Settings/SettingsSidebarFix.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsSidebarFix.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsSidebarFix.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsSidebarFix.swift diff --git a/CodeEdit/Windows/Settings/SettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsView.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsView.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsView.swift diff --git a/CodeEdit/Windows/Settings/SettingsWindow.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsWindow.swift similarity index 100% rename from CodeEdit/Windows/Settings/SettingsWindow.swift rename to CodeEdit/AuxiliaryWindows/Settings/SettingsWindow.swift diff --git a/CodeEdit/Windows/Settings/View+ConstrainHeightToWindow.swift b/CodeEdit/AuxiliaryWindows/Settings/View+ConstrainHeightToWindow.swift similarity index 100% rename from CodeEdit/Windows/Settings/View+ConstrainHeightToWindow.swift rename to CodeEdit/AuxiliaryWindows/Settings/View+ConstrainHeightToWindow.swift diff --git a/CodeEdit/Windows/Settings/View+HideSidebarToggle.swift b/CodeEdit/AuxiliaryWindows/Settings/View+HideSidebarToggle.swift similarity index 100% rename from CodeEdit/Windows/Settings/View+HideSidebarToggle.swift rename to CodeEdit/AuxiliaryWindows/Settings/View+HideSidebarToggle.swift diff --git a/CodeEdit/Windows/Settings/View+NavigationBarBackButtonVisible.swift b/CodeEdit/AuxiliaryWindows/Settings/View+NavigationBarBackButtonVisible.swift similarity index 100% rename from CodeEdit/Windows/Settings/View+NavigationBarBackButtonVisible.swift rename to CodeEdit/AuxiliaryWindows/Settings/View+NavigationBarBackButtonVisible.swift diff --git a/CodeEdit/Windows/Welcome/GitCloneButton.swift b/CodeEdit/AuxiliaryWindows/Welcome/GitCloneButton.swift similarity index 100% rename from CodeEdit/Windows/Welcome/GitCloneButton.swift rename to CodeEdit/AuxiliaryWindows/Welcome/GitCloneButton.swift diff --git a/CodeEdit/Windows/Welcome/NewFileButton.swift b/CodeEdit/AuxiliaryWindows/Welcome/NewFileButton.swift similarity index 100% rename from CodeEdit/Windows/Welcome/NewFileButton.swift rename to CodeEdit/AuxiliaryWindows/Welcome/NewFileButton.swift diff --git a/CodeEdit/Windows/Welcome/OpenFileOrFolderButton.swift b/CodeEdit/AuxiliaryWindows/Welcome/OpenFileOrFolderButton.swift similarity index 100% rename from CodeEdit/Windows/Welcome/OpenFileOrFolderButton.swift rename to CodeEdit/AuxiliaryWindows/Welcome/OpenFileOrFolderButton.swift diff --git a/CodeEdit/Windows/Welcome/WelcomeSubtitleView.swift b/CodeEdit/AuxiliaryWindows/Welcome/WelcomeSubtitleView.swift similarity index 100% rename from CodeEdit/Windows/Welcome/WelcomeSubtitleView.swift rename to CodeEdit/AuxiliaryWindows/Welcome/WelcomeSubtitleView.swift From 31cb0cbe0f4f3901bc5f875bb26bf41bffd2b98a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 15:09:58 +0200 Subject: [PATCH 207/335] Fix: Build CI via the workspace, not the bare project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both CI paths invoked xcodebuild with neither -workspace nor -project, which resolves to CodeEdit.xcodeproj. That project has no local package references — the packages are members of CodeEdit.xcworkspace only — so once the app target gained 11 local package products the bare build fails with "Missing package product" x11. Names the workspace explicitly in the PR test gate and the pre-release archive, matching the documented rule to always build via the workspace. Side effect: the PR gate now also runs the package test targets, which the bare project could not see. --- .github/scripts/test_app.sh | 1 + .github/workflows/pre-release.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/test_app.sh b/.github/scripts/test_app.sh index cd5354c4a1..7ca60d576c 100755 --- a/.github/scripts/test_app.sh +++ b/.github/scripts/test_app.sh @@ -20,6 +20,7 @@ export LC_CTYPE=en_US.UTF-8 # - is-ci: include test results in output set -o pipefail && arch -"${ARCH}" xcodebuild \ + -workspace CodeEdit.xcworkspace \ -scheme CodeEdit \ -destination "platform=OS X,arch=${ARCH}" \ -skipPackagePluginValidation \ diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 2bfce92e9a..e335f7ff0e 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -48,7 +48,7 @@ jobs: - name: Build CodeEdit env: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: xcodebuild -scheme CodeEdit -configuration Pre -derivedDataPath "$RUNNER_TEMP/DerivedData" -archivePath "$RUNNER_TEMP/CodeEdit.xcarchive" -skipPackagePluginValidation DEVELOPMENT_TEAM=$APPLE_TEAM_ID archive | xcpretty + run: xcodebuild -workspace CodeEdit.xcworkspace -scheme CodeEdit -configuration Pre -derivedDataPath "$RUNNER_TEMP/DerivedData" -archivePath "$RUNNER_TEMP/CodeEdit.xcarchive" -skipPackagePluginValidation DEVELOPMENT_TEAM=$APPLE_TEAM_ID archive | xcpretty ############################ # Sign From bbfa435cbf1651f6bdb84aaabc0d610fc683fc77 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 15:24:55 +0200 Subject: [PATCH 208/335] Fix: Bundle the authoritative Package.resolved for Acknowledgements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app ships Package.resolved as a resource and About > Acknowledgements is built from it (AcknowledgementsViewModel). It pointed at the .xcodeproj's implicit-workspace lockfile, which went stale when this branch removed Factory and the snapshot-testing suite — so the About screen credited three packages CodeEdit no longer uses: factory, swift-snapshot-testing and swift-syntax. Repoints the resource at CodeEdit.xcworkspace's lockfile, the one that matches what actually builds, and removes the now-unreferenced duplicate. Local packages never appear in a lockfile (nothing to resolve), so the list is unchanged apart from dropping those three: 35 pins becomes 32. Format is identical (version 3), and swiftterm was already branch-pinned with no version, which the view model already renders as "-". --- .gitignore | 5 + CodeEdit.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 321 ------------------ 3 files changed, 6 insertions(+), 322 deletions(-) delete mode 100644 CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/.gitignore b/.gitignore index 83cf70a4eb..0b354eb762 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,8 @@ iOSInjectionProject/ .codeedit .idea .vscode + +# The .xcodeproj's implicit workspace is not a build entry point — CodeEdit.xcworkspace is. +# Xcode regenerates this lockfile whenever the bare project is opened; it drifts and misleads. +# The app bundles the workspace's lockfile as its Acknowledgements data source. +CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 98f3478898..d9fdfd8d7d 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -118,7 +118,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 283BDCBC2972EEBD002AFF81 /* Package.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; name = Package.resolved; path = CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved; sourceTree = ""; }; + 283BDCBC2972EEBD002AFF81 /* Package.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; name = Package.resolved; path = CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved; sourceTree = ""; }; 284DC8502978BA2600BF2770 /* .all-contributorsrc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ".all-contributorsrc"; sourceTree = ""; }; 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenWithCodeEdit.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 589F3E342936185400E1A4DA /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 63b20a4ac0..0000000000 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,321 +0,0 @@ -{ - "originHash" : "c4368adf5cf7353593e131deab35e74464b98a4f7755aea8cdd711a421976b22", - "pins" : [ - { - "identity" : "aboutwindow", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/AboutWindow", - "state" : { - "revision" : "79c7c01fb739d024a3ca07fe153a068339213baf", - "version" : "1.0.0" - } - }, - { - "identity" : "anycodable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Flight-School/AnyCodable", - "state" : { - "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", - "version" : "0.6.7" - } - }, - { - "identity" : "codeeditkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditKit.git", - "state" : { - "revision" : "ad28213a968586abb0cb21a8a56a3587227895f1", - "version" : "0.1.2" - } - }, - { - "identity" : "codeeditlanguages", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", - "state" : { - "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", - "version" : "0.1.20" - } - }, - { - "identity" : "codeeditsourceeditor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", - "state" : { - "revision" : "ee0c00a2343903df9d6ef45ce53228aca8637369", - "version" : "0.15.1" - } - }, - { - "identity" : "codeeditsymbols", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSymbols", - "state" : { - "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", - "version" : "0.2.3" - } - }, - { - "identity" : "codeedittextview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", - "state" : { - "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", - "version" : "0.12.1" - } - }, - { - "identity" : "collectionconcurrencykit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/johnsundell/collectionconcurrencykit", - "state" : { - "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", - "version" : "0.2.0" - } - }, - { - "identity" : "concurrencyplus", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/ConcurrencyPlus", - "state" : { - "revision" : "8dc56499412a373d617d50d059116bccf44b9874", - "version" : "0.4.2" - } - }, - { - "identity" : "factory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hmlongco/Factory", - "state" : { - "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", - "version" : "2.5.3" - } - }, - { - "identity" : "fseventswrapper", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Frizlab/FSEventsWrapper", - "state" : { - "revision" : "70bbea4b108221fcabfce8dbced8502831c0ae04", - "version" : "2.1.0" - } - }, - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift.git", - "state" : { - "revision" : "2cf6c756e1e5ef6901ebae16576a7e4e4b834622", - "version" : "6.29.3" - } - }, - { - "identity" : "jsonrpc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/JSONRPC", - "state" : { - "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", - "version" : "0.9.0" - } - }, - { - "identity" : "languageclient", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/LanguageClient", - "state" : { - "revision" : "4f28cc3cad7512470275f65ca2048359553a86f5", - "version" : "0.8.2" - } - }, - { - "identity" : "languageserverprotocol", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", - "state" : { - "revision" : "f7879c782c0845af9c576de7b8baedd946237286", - "version" : "0.14.0" - } - }, - { - "identity" : "logstream", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Wouter01/LogStream", - "state" : { - "revision" : "6f83694b2675dcf3b1cea0a52546ff4469c18282", - "version" : "1.3.0" - } - }, - { - "identity" : "processenv", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/ProcessEnv", - "state" : { - "revision" : "83f1ebc9dd6fb1db0bd89a3fcae00488a0f3fdd9", - "version" : "1.0.0" - } - }, - { - "identity" : "queue", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattmassicotte/Queue", - "state" : { - "revision" : "8d6f936097888f97011610ced40313655dc5948d", - "version" : "0.1.4" - } - }, - { - "identity" : "rearrange", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/Rearrange", - "state" : { - "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3", - "version" : "2.0.0" - } - }, - { - "identity" : "semaphore", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/Semaphore", - "state" : { - "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", - "version" : "0.1.0" - } - }, - { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle.git", - "state" : { - "revision" : "2a98381dfe72e24bf593c5c06d2c4fc1763c3f19", - "version" : "2.3.0" - } - }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", - "version" : "1.0.1" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" - } - }, - { - "identity" : "swift-glob", - "kind" : "remoteSourceControl", - "location" : "https://github.com/davbeck/swift-glob", - "state" : { - "revision" : "07ba6f47d903a0b1b59f12ca70d6de9949b975d6", - "version" : "0.2.0" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", - "state" : { - "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", - "version" : "1.14.2" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - }, - { - "identity" : "swiftlintplugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/lukepistrol/SwiftLintPlugin", - "state" : { - "revision" : "3780efccceaa87f17ec39638a9d263d0e742b71c", - "version" : "0.59.1" - } - }, - { - "identity" : "swiftterm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/thecoolwinter/SwiftTerm", - "state" : { - "branch" : "codeedit", - "revision" : "2f36f54742d3882e69ff009d084e8675b80934bd" - } - }, - { - "identity" : "swifttreesitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", - "state" : { - "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", - "version" : "0.25.0" - } - }, - { - "identity" : "swiftui-introspect", - "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect.git", - "state" : { - "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", - "version" : "1.3.0" - } - }, - { - "identity" : "textformation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/TextFormation", - "state" : { - "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", - "version" : "0.9.0" - } - }, - { - "identity" : "textstory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/TextStory", - "state" : { - "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", - "version" : "0.9.1" - } - }, - { - "identity" : "tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter", - "state" : { - "revision" : "f2f197b6b27ce75c280c20f131d4f71e906b86f7", - "version" : "0.25.8" - } - }, - { - "identity" : "welcomewindow", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/WelcomeWindow", - "state" : { - "revision" : "cbd5c0d6f432449e2a8618e2b24e4691acbfcc98", - "version" : "1.1.0" - } - }, - { - "identity" : "zipfoundation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/weichsel/ZIPFoundation", - "state" : { - "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", - "version" : "0.9.19" - } - } - ], - "version" : 3 -} From abfcef80d41be8a7eda986d7fb79d3301c48adcb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 15:26:19 +0200 Subject: [PATCH 209/335] Refactor: Untrack per-package resolved files .gitignore already declares Packages/**/Package.resolved as Xcode-generated noise, but two of the nine were committed before that rule existed and gitignore does not untrack existing files. A package's own lockfile is ignored by SwiftPM when it is consumed from a root, so these affect nothing. Keeps the working copies; the workspace lockfile stays the single source of truth. --- Packages/Features/CESearch/Package.resolved | 15 --------------- .../Services/CodeEditServices/Package.resolved | 15 --------------- 2 files changed, 30 deletions(-) delete mode 100644 Packages/Features/CESearch/Package.resolved delete mode 100644 Packages/Services/CodeEditServices/Package.resolved diff --git a/Packages/Features/CESearch/Package.resolved b/Packages/Features/CESearch/Package.resolved deleted file mode 100644 index 36b9b98a1a..0000000000 --- a/Packages/Features/CESearch/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "8ea09dcc49375fb167b233dc483c2613886ed8cdda256195fc8b29196333173a", - "pins" : [ - { - "identity" : "codeeditsymbols", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", - "state" : { - "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", - "version" : "0.2.3" - } - } - ], - "version" : 3 -} diff --git a/Packages/Services/CodeEditServices/Package.resolved b/Packages/Services/CodeEditServices/Package.resolved deleted file mode 100644 index 23cb55baf6..0000000000 --- a/Packages/Services/CodeEditServices/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "7162d49eba1cbb48351105ade4ed394fdf37929e9d5153f2f0711d2b1102d4c0", - "pins" : [ - { - "identity" : "factory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hmlongco/Factory", - "state" : { - "revision" : "ccc898f21992ebc130bc04cc197460a5ae230bcf", - "version" : "2.5.3" - } - } - ], - "version" : 3 -} From 7e6eef569c73e184b76c00b31a8c5fb55455604b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:07:24 +0200 Subject: [PATCH 210/335] Fix: Target the right file when renaming from the navigator menu renameFile() read highlightedFileItem, so it renamed the last-revealed file rather than the right-clicked one, and did nothing when nothing had been revealed. Split the operation out of the action so each caller passes its own target: the menu action uses the presented item, and newFileFromClipboard passes the file it just created (where the menu item is the parent folder). Also fixes the row guard: row(forItem:) returns -1 when not found, so the 'row > 0' test rejected row 0, silently failing to rename the topmost row. --- .../ProjectNavigatorMenuActions.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index b48824be28..d65b8af5ab 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -100,12 +100,10 @@ extension ProjectNavigatorMenu { } } - /// Opens the rename file dialogue on the cell this was presented from. - @objc - func renameFile() { - guard let newFile = workspace?.listenerModel.highlightedFileItem else { return } - let row = sender.outlineView.row(forItem: newFile) - guard row > 0, + /// Puts the cell for `file` into edit mode, if that row is visible. + private func beginRenaming(_ file: CEWorkspaceFile) { + let row = sender.outlineView.row(forItem: file) + guard row >= 0, let cell = sender.outlineView.view( atColumn: 0, row: row, @@ -116,6 +114,13 @@ extension ProjectNavigatorMenu { sender.outlineView.window?.makeFirstResponder(cell.textField) } + /// Opens the rename dialogue on the cell this menu was presented from. + @objc + func renameFile() { + guard let item else { return } + beginRenaming(item) + } + // TODO: Automatically identified the file type /// Action that creates a new file with clipboard content @objc @@ -132,7 +137,7 @@ extension ProjectNavigatorMenu { ) { workspace?.listenerModel.highlightedFileItem = newFile sender.workspaceNavigator.open(file: newFile, asTemporary: false) - renameFile() + beginRenaming(newFile) } } catch { let alert = NSAlert(error: error) From 27d33d4e0b4ac295ffb3c1db01982d7ab2dcfeb7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:09:42 +0200 Subject: [PATCH 211/335] Refactor: Replace the workspace notification model with a reveal signal WorkspaceNotificationModel was an ObservableObject wrapping a single @Published CEWorkspaceFile? that no view rendered from: its only subscriber was a Combine sink in the navigator's outline coordinator, which already held the workspace. It also modelled a one-shot reveal request as retained state, overloading nil as both 'no request' and 'nothing revealed', and one of its two environment injections had no consumers at all. Replace it with a PassthroughSubject on Workspace. Writers that already hold a Workspace send on it directly; ProjectNavigatorToolbarBottom instead calls WorkspaceNavigator.reveal(file:), the command interface it already has injected and whose adapter was itself implemented as a write to the deleted model. --- .../CodeEditSplitViewController.swift | 2 -- .../ProjectNavigatorMenuActions.swift | 6 +++--- .../ProjectNavigatorOutlineView.swift | 9 +++------ .../ProjectNavigatorToolbarBottom.swift | 5 ++--- .../Adapters/AppWorkspaceNavigator.swift | 2 +- .../Workspace/Files/FileMover.swift | 2 +- .../WorkspaceWindow/Workspace/Workspace.swift | 11 ++++++---- .../Workspace/WorkspaceFactory.swift | 1 - .../WorkspaceNotificationModel.swift | 20 ------------------- 9 files changed, 17 insertions(+), 41 deletions(-) delete mode 100644 CodeEdit/WorkspaceWindow/Workspace/WorkspaceNotificationModel.swift diff --git a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift index f584ffc33f..d54533295b 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift @@ -120,7 +120,6 @@ final class CodeEditSplitViewController: NSSplitViewController { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) .environmentObject(workspace) .environmentObject(workspace.editorManager) - .environmentObject(workspace.listenerModel) .environmentObject(workspace.projectNavigatorViewModel) .environmentObject(workspace.sourceControlManager) .environmentObject(workspace.sourceControlViewModel) @@ -148,7 +147,6 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(workspace.taskManager) .environmentObject(workspace.sourceControlManager) .environmentObject(workspace.sourceControlViewModel) - .environmentObject(workspace.listenerModel) .environmentObject(workspace.undoRegistration) .environmentObject(notificationPanel) .environment(\.workspaceFileManager, workspace.workspaceFileManager) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift index d65b8af5ab..7ad109333b 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenuActions.swift @@ -90,7 +90,7 @@ extension ProjectNavigatorMenu { guard let item else { return } do { if let newFile = try workspace?.workspaceFileManager.addFile(fileName: "untitled", toFile: item) { - workspace?.listenerModel.highlightedFileItem = newFile + workspace?.revealRequests.send(newFile) sender.workspaceNavigator.open(file: newFile, asTemporary: false) } } catch { @@ -135,7 +135,7 @@ extension ProjectNavigatorMenu { toFile: item, contents: clipBoardContent ) { - workspace?.listenerModel.highlightedFileItem = newFile + workspace?.revealRequests.send(newFile) sender.workspaceNavigator.open(file: newFile, asTemporary: false) beginRenaming(newFile) } @@ -153,7 +153,7 @@ extension ProjectNavigatorMenu { guard let item else { return } do { if let newFolder = try workspace?.workspaceFileManager.addFolder(folderName: "untitled", toFile: item) { - workspace?.listenerModel.highlightedFileItem = newFolder + workspace?.revealRequests.send(newFolder) } } catch { let alert = NSAlert(error: error) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 7e7f71a16a..a6e6cc7e62 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -63,12 +63,9 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { self.fileManager = workspace.workspaceFileManager super.init() - workspace.listenerModel.$highlightedFileItem - .sink(receiveValue: { [weak self] fileItem in - guard let fileItem else { - return - } - self?.controller?.reveal(fileItem) + workspace.revealRequests + .sink(receiveValue: { [weak self] file in + self?.controller?.reveal(file) }) .store(in: &cancellables) do { diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 4722b34f28..d8b1b9099d 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -21,7 +21,6 @@ struct ProjectNavigatorToolbarBottom: View { private var activeEditorState @Environment(\.workspaceNavigator) private var workspaceNavigator - @EnvironmentObject var listenerModel: WorkspaceNotificationModel @EnvironmentObject var projectNavigatorViewModel: ProjectNavigatorViewModel @Environment(\.workspaceFileManager) @@ -118,7 +117,7 @@ struct ProjectNavigatorToolbarBottom: View { fileName: "untitled", toFile: rootFile ) { - listenerModel.highlightedFileItem = newFile + workspaceNavigator.reveal(file: newFile) workspaceNavigator.open(file: newFile, asTemporary: false) } } catch { @@ -136,7 +135,7 @@ struct ProjectNavigatorToolbarBottom: View { folderName: "untitled", toFile: rootFile ) { - listenerModel.highlightedFileItem = newFolder + workspaceNavigator.reveal(file: newFolder) } } catch { let alert = NSAlert(error: error) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift index dc226e2290..9e2d174f54 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift @@ -27,7 +27,7 @@ final class AppWorkspaceNavigator: WorkspaceNavigator { @MainActor func reveal(file: CEWorkspaceFile) { - windowManager.workspace(containing: file.url)?.listenerModel.highlightedFileItem = file + windowManager.workspace(containing: file.url)?.revealRequests.send(file) } @MainActor diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/FileMover.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileMover.swift index 1160c80028..e81b7948ff 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/FileMover.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Files/FileMover.swift @@ -27,7 +27,7 @@ final class FileMover { if !file.isFolder { workspace.editorManager.editorLayout.closeAllTabs(of: file) } - workspace.listenerModel.highlightedFileItem = newFile + workspace.revealRequests.send(newFile) workspace.editorManager.openTab(item: newFile) return newFile diff --git a/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift index 97d89a8b98..680bf2ba29 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift @@ -7,6 +7,7 @@ import CESourceControl import AppKit +import Combine import CEWorkspaceFileManager import CodeEditCore import CEEditor @@ -34,11 +35,15 @@ final class Workspace: ObservableObject { let statePersistence: WorkspaceStatePersistence let undoRegistration: UndoManagerRegistration - // Navigator-coupled — stay until the Navigator feature is packaged + // Navigator-coupled — stays until the Navigator feature is packaged // (consumed by the ProjectNavigator AppKit cluster and by-workspace command paths). - let listenerModel: WorkspaceNotificationModel let projectNavigatorViewModel: ProjectNavigatorViewModel + /// Requests to reveal a file in the Project Navigator. The navigator's outline view subscribes + /// and scrolls to each file. A request is a one-shot event, not retained state — which is why + /// this is a subject rather than a `@Published` property. + let revealRequests = PassthroughSubject() + /// The original (possibly bookmark-derived) security-scoped URL whose access is held for this /// workspace's lifetime. Set by `WorkspaceFactory` when the URL is security-scoped (e.g. opened /// from recents in the sandbox); released in ``tearDown()``. @@ -56,7 +61,6 @@ final class Workspace: ObservableObject { workspaceSettingsManager: CEWorkspaceSettings, statePersistence: WorkspaceStatePersistence, undoRegistration: UndoManagerRegistration, - listenerModel: WorkspaceNotificationModel, projectNavigatorViewModel: ProjectNavigatorViewModel, securityScopedURL: URL? ) { @@ -71,7 +75,6 @@ final class Workspace: ObservableObject { self.workspaceSettingsManager = workspaceSettingsManager self.statePersistence = statePersistence self.undoRegistration = undoRegistration - self.listenerModel = listenerModel self.projectNavigatorViewModel = projectNavigatorViewModel self.securityScopedURL = securityScopedURL } diff --git a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift index eca21a2505..6dbdb043a4 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift @@ -64,7 +64,6 @@ enum WorkspaceFactory { workspaceSettingsManager: workspaceSettingsManager, statePersistence: statePersistence, undoRegistration: undoRegistration, - listenerModel: WorkspaceNotificationModel(), projectNavigatorViewModel: ProjectNavigatorViewModel(), securityScopedURL: securityScopedURL ) diff --git a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceNotificationModel.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceNotificationModel.swift deleted file mode 100644 index 23598dc37c..0000000000 --- a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceNotificationModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// WorkspaceNotificationModel.swift -// CodeEdit -// -// Created by Khan Winter on 6/5/22. -// - -import Foundation -import CodeEditCore -import Combine - -class WorkspaceNotificationModel: ObservableObject { - - @Published var highlightedFileItem: CEWorkspaceFile? - - init() { - highlightedFileItem = nil - } - -} From 2b0a3959906b56aeec971593f18394717a9a6d73 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:10:25 +0200 Subject: [PATCH 212/335] Refactor: Remove unused navigator table cell TextTableViewCell had no references anywhere in the project, including Interface Builder files. --- .../OutlineView/TextTableViewCell.swift | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/TextTableViewCell.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/TextTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/TextTableViewCell.swift deleted file mode 100644 index d18e1cb1b2..0000000000 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/TextTableViewCell.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// TextTableViewCell.swift -// CodeEdit -// -// Created by TAY KAI QUAN on 11/9/22. -// - -import SwiftUI - -class TextTableViewCell: NSTableCellView { - - var label: NSTextField! - - init(frame frameRect: NSRect, isEditable: Bool = true, startingText: String = "") { - super.init(frame: frameRect) - setupViews(frame: frameRect, isEditable: isEditable) - self.label.stringValue = startingText - } - - // Default init, assumes isEditable to be false - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - setupViews(frame: frameRect, isEditable: false) - } - - private func setupViews(frame frameRect: NSRect, isEditable: Bool) { - // Create the label - label = createLabel() - configLabel(label: self.label, isEditable: isEditable) - self.textField = label - - addSubview(label) - createConstraints(frame: frameRect) - } - - // MARK: Create and config stuff - func createLabel() -> NSTextField { - return NSTextField(frame: .zero) - } - - func configLabel(label: NSTextField, isEditable: Bool) { - label.translatesAutoresizingMaskIntoConstraints = false - label.drawsBackground = false - label.isBordered = false - label.isEditable = isEditable - label.isSelectable = isEditable - label.layer?.cornerRadius = 10.0 - label.font = .boldSystemFont(ofSize: fontSize) - label.lineBreakMode = .byTruncatingMiddle - label.textColor = NSColor.textColor - label.alphaValue = 0.7 - } - - func createConstraints(frame frameRect: NSRect) { - resizeSubviews(withOldSize: .zero) - } - - override func resizeSubviews(withOldSize oldSize: NSSize) { - super.resizeSubviews(withOldSize: oldSize) - label.frame = NSRect( - x: 2, - y: 2.5, - width: frame.width - 4, - height: 25 - ) - } - - /// Returns the font size for the current row height. Defaults to `13.0` - private var fontSize: Double { - switch self.frame.height { - case 20: return 11 - case 22: return 13 - case 24: return 14 - default: return 13 - } - } - - /// *Not Implemented* - required init(coder: NSCoder) { - fatalError(""" - init?(coder: NSCoder) isn't implemented on `TextTableViewCell`. - Please use `.init(frame: NSRect, isEditable: Bool) - """) - } -} From e87c36f623bd22e85ba64642ebda2e0c29b4b6db Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:11:08 +0200 Subject: [PATCH 213/335] Refactor: Fold navigator cells into ProjectNavigator StandardTableViewCell and FileSystemTableViewCell are used only by ProjectNavigatorTableViewCell, so a sibling OutlineView group implied sharing that does not exist and left two identically named folders in the tree. --- .../OutlineView/FileSystemTableViewCell.swift | 0 .../OutlineView/StandardTableViewCell.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/NavigatorArea/{ => ProjectNavigator}/OutlineView/FileSystemTableViewCell.swift (100%) rename CodeEdit/WorkspaceWindow/NavigatorArea/{ => ProjectNavigator}/OutlineView/StandardTableViewCell.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/FileSystemTableViewCell.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/StandardTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/StandardTableViewCell.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/OutlineView/StandardTableViewCell.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/StandardTableViewCell.swift From 9a965a0e5f287b4eb01b9f6048ac0dae9cce786b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:12:41 +0200 Subject: [PATCH 214/335] Refactor: Split CodeEditWindowController extensions by concern CodeEditWindowControllerExtensions.swift held four unrelated concerns under a filename that described none of them, and read as if it were about the ExtensionKit feature. Each part moves to a file that already owns that responsibility: command registration and document-edited tracking get their own extensions, the workspace-settings action joins the other @IBAction presenters in the main file, and the toolbar identifiers join +Toolbar. --- .../CodeEditWindowController+Commands.swift | 35 +++++ ...WindowController+DocumentEditedState.swift | 59 ++++++++ .../CodeEditWindowController+Toolbar.swift | 13 ++ .../CodeEditWindowController.swift | 31 +++++ .../CodeEditWindowControllerExtensions.swift | 128 ------------------ 5 files changed, 138 insertions(+), 128 deletions(-) create mode 100644 CodeEdit/WorkspaceWindow/CodeEditWindowController+Commands.swift create mode 100644 CodeEdit/WorkspaceWindow/CodeEditWindowController+DocumentEditedState.swift delete mode 100644 CodeEdit/WorkspaceWindow/CodeEditWindowControllerExtensions.swift diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Commands.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Commands.swift new file mode 100644 index 0000000000..7871760f8a --- /dev/null +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Commands.swift @@ -0,0 +1,35 @@ +// +// CodeEditWindowController+Commands.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 31/07/26. +// + +import SwiftUI + +extension CodeEditWindowController { + /// These are example items that added as commands to command palette + func registerCommands() { + let commandManager = dependencies.commandManager + commandManager.addCommand( + name: "Quick Open", + title: "Quick Open", + id: "quick_open", + command: { [weak self] in self?.openQuickly(nil) } + ) + + commandManager.addCommand( + name: "Toggle Navigator", + title: "Toggle Navigator", + id: "toggle_left_sidebar", + command: { [weak self] in self?.toggleFirstPanel() } + ) + + commandManager.addCommand( + name: "Toggle Inspector", + title: "Toggle Inspector", + id: "toggle_right_sidebar", + command: { [weak self] in self?.toggleLastPanel() } + ) + } +} diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController+DocumentEditedState.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+DocumentEditedState.swift new file mode 100644 index 0000000000..2515311ade --- /dev/null +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController+DocumentEditedState.swift @@ -0,0 +1,59 @@ +// +// CodeEditWindowController+DocumentEditedState.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 31/07/26. +// + +import SwiftUI +import Combine + +extension CodeEditWindowController { + // Listen to changes in all tabs/files + internal func listenToDocumentEdited(workspace: Workspace) { + let editorManager = workspace.editorManager + editorManager.$activeEditor + .flatMap({ editor in + editor.$tabs + }) + .compactMap({ tab in + Publishers.MergeMany(tab.elements.map({ editorManager.documentPublisher(for: $0.file) })) + }) + .switchToLatest() + .compactMap({ fileDocument in + fileDocument?.isDocumentEditedPublisher + }) + .flatMap({ $0 }) + .sink { isDocumentEdited in + if isDocumentEdited { + self.setDocumentEdited(true) + return + } + + self.updateDocumentEdited(workspace: workspace) + } + .store(in: &cancellables) + + // Listen to change of tabs, if closed tab without saving content, + // we also need to recalculate isDocumentEdited + editorManager.$activeEditor + .flatMap({ editor in + editor.$tabs + }) + .sink { _ in + self.updateDocumentEdited(workspace: workspace) + } + .store(in: &cancellables) + } + + // Recalculate documentEdited by checking if any tab/file is edited + private func updateDocumentEdited(workspace: Workspace) { + let editorManager = workspace.editorManager + let hasEditedDocuments = !editorManager + .editorLayout + .gatherOpenFiles() + .filter({ editorManager.document(for: $0)?.isDocumentEdited == true }) + .isEmpty + self.setDocumentEdited(hasEditedDocuments) + } +} diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift index 3a72d25c7b..336d3af711 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift @@ -272,3 +272,16 @@ extension CodeEditWindowController { return toolbarItem } } + +extension NSToolbarItem.Identifier { + static let toggleFirstSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleFirstSidebarItem") + static let toggleLastSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleLastSidebarItem") + static let stopTaskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("StopTaskSidebarItem") + static let startTaskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("StartTaskSidebarItem") + static let itemListTrackingSeparator = NSToolbarItem.Identifier("ItemListTrackingSeparator") + static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker") + static let activityViewer: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ActivityViewer") + static let notificationItem = NSToolbarItem.Identifier("notificationItem") + + static let taskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("TaskSidebarItem") +} diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index c1ad0790ac..9ac1d3bf6d 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -248,6 +248,37 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } } + @IBAction func openWorkspaceSettings(_ sender: Any) { + guard let window = window, + let workspace = workspace + else { return } + let workspaceSettingsManager = workspace.workspaceSettingsManager + let taskManager = workspace.taskManager + + if let workspaceSettingsWindow, workspaceSettingsWindow.isVisible { + workspaceSettingsWindow.makeKeyAndOrderFront(self) + } else { + let settingsWindow = NSWindow() + self.workspaceSettingsWindow = settingsWindow + let contentView = CEWorkspaceSettingsView( + dismiss: { [weak self, weak settingsWindow] in + guard let settingsWindow else { return } + self?.window?.endSheet(settingsWindow) + } + ) + .environmentObject(workspaceSettingsManager) + .environmentObject(workspace) + .environmentObject(taskManager) + + settingsWindow.contentView = NSHostingView(rootView: contentView) + settingsWindow.titlebarAppearsTransparent = true + settingsWindow.setContentSize(NSSize(width: 515, height: 515)) + settingsWindow.setAccessibilityTitle("Workspace Settings") + + window.beginSheet(settingsWindow, completionHandler: nil) + } + } + func windowShouldClose(_ sender: NSWindow) -> Bool { // Check for unsaved changes before closing if let workspace, workspace.hasUnsavedChanges() { diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowControllerExtensions.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowControllerExtensions.swift deleted file mode 100644 index 609019bab8..0000000000 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowControllerExtensions.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// CodeEditWindowControllerExtensions.swift -// CodeEdit -// -// Created by Austin Condiff on 10/14/23. -// - -import SwiftUI -import Combine - -extension CodeEditWindowController { - /// These are example items that added as commands to command palette - func registerCommands() { - let commandManager = dependencies.commandManager - commandManager.addCommand( - name: "Quick Open", - title: "Quick Open", - id: "quick_open", - command: { [weak self] in self?.openQuickly(nil) } - ) - - commandManager.addCommand( - name: "Toggle Navigator", - title: "Toggle Navigator", - id: "toggle_left_sidebar", - command: { [weak self] in self?.toggleFirstPanel() } - ) - - commandManager.addCommand( - name: "Toggle Inspector", - title: "Toggle Inspector", - id: "toggle_right_sidebar", - command: { [weak self] in self?.toggleLastPanel() } - ) - } - - // Listen to changes in all tabs/files - internal func listenToDocumentEdited(workspace: Workspace) { - let editorManager = workspace.editorManager - editorManager.$activeEditor - .flatMap({ editor in - editor.$tabs - }) - .compactMap({ tab in - Publishers.MergeMany(tab.elements.map({ editorManager.documentPublisher(for: $0.file) })) - }) - .switchToLatest() - .compactMap({ fileDocument in - fileDocument?.isDocumentEditedPublisher - }) - .flatMap({ $0 }) - .sink { isDocumentEdited in - if isDocumentEdited { - self.setDocumentEdited(true) - return - } - - self.updateDocumentEdited(workspace: workspace) - } - .store(in: &cancellables) - - // Listen to change of tabs, if closed tab without saving content, - // we also need to recalculate isDocumentEdited - editorManager.$activeEditor - .flatMap({ editor in - editor.$tabs - }) - .sink { _ in - self.updateDocumentEdited(workspace: workspace) - } - .store(in: &cancellables) - } - - // Recalculate documentEdited by checking if any tab/file is edited - private func updateDocumentEdited(workspace: Workspace) { - let editorManager = workspace.editorManager - let hasEditedDocuments = !editorManager - .editorLayout - .gatherOpenFiles() - .filter({ editorManager.document(for: $0)?.isDocumentEdited == true }) - .isEmpty - self.setDocumentEdited(hasEditedDocuments) - } - - @IBAction func openWorkspaceSettings(_ sender: Any) { - guard let window = window, - let workspace = workspace - else { return } - let workspaceSettingsManager = workspace.workspaceSettingsManager - let taskManager = workspace.taskManager - - if let workspaceSettingsWindow, workspaceSettingsWindow.isVisible { - workspaceSettingsWindow.makeKeyAndOrderFront(self) - } else { - let settingsWindow = NSWindow() - self.workspaceSettingsWindow = settingsWindow - let contentView = CEWorkspaceSettingsView( - dismiss: { [weak self, weak settingsWindow] in - guard let settingsWindow else { return } - self?.window?.endSheet(settingsWindow) - } - ) - .environmentObject(workspaceSettingsManager) - .environmentObject(workspace) - .environmentObject(taskManager) - - settingsWindow.contentView = NSHostingView(rootView: contentView) - settingsWindow.titlebarAppearsTransparent = true - settingsWindow.setContentSize(NSSize(width: 515, height: 515)) - settingsWindow.setAccessibilityTitle("Workspace Settings") - - window.beginSheet(settingsWindow, completionHandler: nil) - } - } -} - -extension NSToolbarItem.Identifier { - static let toggleFirstSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleFirstSidebarItem") - static let toggleLastSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ToggleLastSidebarItem") - static let stopTaskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("StopTaskSidebarItem") - static let startTaskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("StartTaskSidebarItem") - static let itemListTrackingSeparator = NSToolbarItem.Identifier("ItemListTrackingSeparator") - static let branchPicker: NSToolbarItem.Identifier = NSToolbarItem.Identifier("BranchPicker") - static let activityViewer: NSToolbarItem.Identifier = NSToolbarItem.Identifier("ActivityViewer") - static let notificationItem = NSToolbarItem.Identifier("notificationItem") - - static let taskSidebarItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("TaskSidebarItem") -} From 3b3b85ecc8b46c11fae37b11a79d7a0a91e2a958 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:13:21 +0200 Subject: [PATCH 215/335] Refactor: Flatten vestigial navigator and status bar files FindNavigator/ held a single tab view left behind when the feature moved to the CESearch package, and ImageDimensions sat a level above its only consumer in StatusBarItems/. --- .../NavigatorArea/{FindNavigator => }/FindNavigatorTab.swift | 0 .../StatusBar/{ => StatusBarItems}/ImageDimensions.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/NavigatorArea/{FindNavigator => }/FindNavigatorTab.swift (100%) rename CodeEdit/WorkspaceWindow/StatusBar/{ => StatusBarItems}/ImageDimensions.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigator/FindNavigatorTab.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigatorTab.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigator/FindNavigatorTab.swift rename to CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigatorTab.swift diff --git a/CodeEdit/WorkspaceWindow/StatusBar/ImageDimensions.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/ImageDimensions.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/StatusBar/ImageDimensions.swift rename to CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/ImageDimensions.swift From 4d8a15c80afeda3c7b610c3db153bdf04ae99928 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 31 Jul 2026 18:15:33 +0200 Subject: [PATCH 216/335] Test: Assert reveal requests are sent, not stored Follow-up to replacing WorkspaceNotificationModel with a PassthroughSubject. AppWorkspaceNavigatorTests asserted the reveal target by reading it back off the workspace, which a one-shot signal cannot support; the test now subscribes and captures what was sent. This should have been part of the reveal-signal commit. It was missed because `xcodebuild build` does not compile test targets, so only the later `test` run surfaced it. --- .../Workspace/AppWorkspaceNavigatorTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index 4d44a5bc90..0608913209 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -7,6 +7,7 @@ import Foundation import Testing +import Combine import CodeEditCore @testable import CodeEdit @testable import CEEditor @@ -52,16 +53,21 @@ struct AppWorkspaceNavigatorTests { @MainActor @Test - func revealSetsHighlightedFileItemOnCorrectWorkspace() throws { + func revealSendsRevealRequestOnCorrectWorkspace() throws { let workspace = try TestWorkspaceFactory.make() let mock = MockWindowManager() mock.stubbedWorkspace = workspace let navigator = AppWorkspaceNavigator(windowManager: mock) let file = CEWorkspaceFile(url: URL(fileURLWithPath: "/tmp/example.swift")) + var revealed: [CEWorkspaceFile] = [] + let cancellable = workspace.revealRequests.sink { revealed.append($0) } + defer { cancellable.cancel() } + navigator.reveal(file: file) - #expect(workspace.listenerModel.highlightedFileItem === file) + #expect(revealed.count == 1) + #expect(revealed.first === file) } @MainActor From 8ce1e466e2b4f30e0ef91417ae07a33763a43896 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 2 Aug 2026 20:48:44 +0200 Subject: [PATCH 217/335] Refactor: Inject Workspace as an environment value Workspace has no @Published members, so @EnvironmentObject installed change tracking on an object that never emits. Inject it through an EnvironmentKey instead, alongside the workspace-scope keys that already exist for injection-without-observation. The key is optional because Workspace has no cheap no-op instance, so the one consumer guards with an assertionFailure: a missing workspace is a developer wiring error worth trapping in debug, but not worth crashing a release build over a navigator pane. The downstream AppKit cluster already declared workspace as optional, so only Coordinator.init needed widening. --- .../CodeEditSplitViewController.swift | 2 +- .../ProjectNavigatorOutlineView.swift | 18 +++++++++++++----- .../Workspace/Environment+Workspace.swift | 11 +++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift index d54533295b..fe56358951 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift @@ -118,7 +118,7 @@ final class CodeEditSplitViewController: NSSplitViewController { ) -> NSSplitViewItem { makeNavigator(view: SettingsInjector { NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) - .environmentObject(workspace) + .environment(\.workspace, workspace) .environmentObject(workspace.editorManager) .environmentObject(workspace.projectNavigatorViewModel) .environmentObject(workspace.sourceControlManager) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index a6e6cc7e62..03f832a373 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -15,7 +15,8 @@ import Combine /// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable` struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { - @EnvironmentObject var workspace: Workspace + @Environment(\.workspace) + private var workspace @EnvironmentObject var editorManager: EditorManager @Environment(\.activeEditorState) @@ -29,15 +30,20 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { func makeNSViewController(context: Context) -> ProjectNavigatorViewController { let controller = ProjectNavigatorViewController() - controller.workspace = workspace controller.iconColor = prefs.preferences.general.fileIconStyle controller.activeEditorState = activeEditorState controller.workspaceNavigator = workspaceNavigator - workspace.workspaceFileManager.addObserver(context.coordinator) context.coordinator.controller = controller context.coordinator.observeActiveFile(activeEditorState) + guard let workspace else { + assertionFailure("ProjectNavigatorOutlineView built with no workspace in the environment") + return controller + } + controller.workspace = workspace + workspace.workspaceFileManager.addObserver(context.coordinator) + return controller } @@ -58,11 +64,13 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @MainActor class Coordinator: NSObject, WorkspaceFileObserver { - init(_ workspace: Workspace) { + init(_ workspace: Workspace?) { self.workspace = workspace - self.fileManager = workspace.workspaceFileManager + self.fileManager = workspace?.workspaceFileManager super.init() + guard let workspace else { return } + workspace.revealRequests .sink(receiveValue: { [weak self] file in self?.controller?.reveal(file) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Environment+Workspace.swift b/CodeEdit/WorkspaceWindow/Workspace/Environment+Workspace.swift index eefc414af5..47c76af986 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Environment+Workspace.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Environment+Workspace.swift @@ -33,11 +33,22 @@ private struct WorkspaceStatePersistenceKey: EnvironmentKey { static let defaultValue: (any WorkspaceStatePersisting)? = nil } +private struct WorkspaceKey: EnvironmentKey { + nonisolated(unsafe) static let defaultValue: Workspace? = nil +} + private struct FilePreviewFactoryKey: EnvironmentKey { static let defaultValue: (CEWorkspaceFile) -> AnyView = { _ in AnyView(EmptyView()) } } extension EnvironmentValues { + /// The workspace owning this view tree. `nil` only in previews or a mis-wired tree — + /// `CodeEditSplitViewController` populates it for every real workspace window. + var workspace: Workspace? { + get { self[WorkspaceKey.self] } + set { self[WorkspaceKey.self] = newValue } + } + /// The concrete workspace file manager, for app-shell views that need the mutating /// API (create/rename). Feature packages use `\.workspaceFileProvider` (CEEditor) instead. var workspaceFileManager: CEWorkspaceFileManager? { From 088445c3594a4766b0e4af53e0d1bbf1932861cf Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 2 Aug 2026 20:49:43 +0200 Subject: [PATCH 218/335] Refactor: Drop redundant Workspace injections NavigatorAreaView held a Workspace for the sole purpose of re-injecting it into a subtree the split view controller's injection already covered, so it loses the dependency entirely. The workspace-settings sheet injected a Workspace that nothing in it reads. --- CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift | 2 +- CodeEdit/WorkspaceWindow/CodeEditWindowController.swift | 1 - .../WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift | 5 +---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift index fe56358951..c6a373883c 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift @@ -117,7 +117,7 @@ final class CodeEditSplitViewController: NSSplitViewController { activeEditorState: AppActiveEditorState ) -> NSSplitViewItem { makeNavigator(view: SettingsInjector { - NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) + NavigatorAreaView(viewModel: navigatorViewModel) .environment(\.workspace, workspace) .environmentObject(workspace.editorManager) .environmentObject(workspace.projectNavigatorViewModel) diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index 9ac1d3bf6d..f001baeef7 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -267,7 +267,6 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } ) .environmentObject(workspaceSettingsManager) - .environmentObject(workspace) .environmentObject(taskManager) settingsWindow.contentView = NSHostingView(rootView: contentView) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift index 3d41f3e717..4a1d7b1e20 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift @@ -9,15 +9,13 @@ import SwiftUI import CodeEditSettings struct NavigatorAreaView: View { - @ObservedObject private var workspace: Workspace @ObservedObject private var extensionManager = ExtensionManager.shared @ObservedObject public var viewModel: NavigatorAreaViewModel @AppSettings(\.general.navigatorTabBarPosition) var sidebarPosition: SettingsData.SidebarTabBarPosition - init(workspace: Workspace, viewModel: NavigatorAreaViewModel) { - self.workspace = workspace + init(viewModel: NavigatorAreaViewModel) { self.viewModel = viewModel viewModel.tabItems = [.project, .sourceControl, .search] + @@ -41,7 +39,6 @@ struct NavigatorAreaView: View { tabItems: $viewModel.tabItems, sidebarPosition: sidebarPosition ) - .environmentObject(workspace) .accessibilityElement(children: .contain) .accessibilityLabel("navigator") } From 706751d137d3688baee7ff10a72c8d3f094ade32 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 2 Aug 2026 20:51:14 +0200 Subject: [PATCH 219/335] Refactor: Drop Workspace's unused ObservableObject conformance Workspace never had a @Published member and nothing subscribed to it; the conformance existed only because @EnvironmentObject and @ObservedObject refuse non-ObservableObject types. With injection moved to an EnvironmentKey it serves no purpose, and removing it means any future observer has to be a deliberate choice rather than an accident. --- CodeEdit/WorkspaceWindow/Workspace/Workspace.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift index 680bf2ba29..38e3c9fdd8 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift @@ -21,7 +21,7 @@ import Foundation /// Constructed complete by ``WorkspaceFactory/make(url:dependencies:)`` — every /// manager is non-optional for the workspace's lifetime. @MainActor -final class Workspace: ObservableObject { +final class Workspace { let fileURL: URL let displayName: String From b42feb8fac931db6b95b0aaefecb92b9fb8f2516 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 4 Aug 2026 14:22:41 +0200 Subject: [PATCH 220/335] Refactor: Rename ActivityViewer Notifications group to TaskNotifications --- .../TaskNotificationHandler.swift | 0 .../TaskNotificationView.swift | 0 .../TaskNotificationsDetailView.swift | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/ActivityViewer/{Notifications => TaskNotifications}/TaskNotificationHandler.swift (100%) rename CodeEdit/WorkspaceWindow/ActivityViewer/{Notifications => TaskNotifications}/TaskNotificationView.swift (100%) rename CodeEdit/WorkspaceWindow/ActivityViewer/{Notifications => TaskNotifications}/TaskNotificationsDetailView.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationHandler.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/TaskNotifications/TaskNotificationHandler.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationHandler.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/TaskNotifications/TaskNotificationHandler.swift diff --git a/CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/TaskNotifications/TaskNotificationView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/TaskNotifications/TaskNotificationView.swift diff --git a/CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationsDetailView.swift b/CodeEdit/WorkspaceWindow/ActivityViewer/TaskNotifications/TaskNotificationsDetailView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/ActivityViewer/Notifications/TaskNotificationsDetailView.swift rename to CodeEdit/WorkspaceWindow/ActivityViewer/TaskNotifications/TaskNotificationsDetailView.swift From 7808546f01252c6d76c35e065ac065c99692fe99 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 4 Aug 2026 14:22:54 +0200 Subject: [PATCH 221/335] Refactor: Rename WorkspaceWindow Toolbar group to TaskToolbarItems --- .../{Toolbar => TaskToolbarItems}/StartTaskToolbarButton.swift | 0 .../{Toolbar => TaskToolbarItems}/StartTaskToolbarItem.swift | 0 .../{Toolbar => TaskToolbarItems}/StopTaskToolbarButton.swift | 0 .../{Toolbar => TaskToolbarItems}/StopTaskToolbarItem.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/{Toolbar => TaskToolbarItems}/StartTaskToolbarButton.swift (100%) rename CodeEdit/WorkspaceWindow/{Toolbar => TaskToolbarItems}/StartTaskToolbarItem.swift (100%) rename CodeEdit/WorkspaceWindow/{Toolbar => TaskToolbarItems}/StopTaskToolbarButton.swift (100%) rename CodeEdit/WorkspaceWindow/{Toolbar => TaskToolbarItems}/StopTaskToolbarItem.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarButton.swift rename to CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift diff --git a/CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Toolbar/StartTaskToolbarItem.swift rename to CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift diff --git a/CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StopTaskToolbarButton.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarButton.swift rename to CodeEdit/WorkspaceWindow/TaskToolbarItems/StopTaskToolbarButton.swift diff --git a/CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StopTaskToolbarItem.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Toolbar/StopTaskToolbarItem.swift rename to CodeEdit/WorkspaceWindow/TaskToolbarItems/StopTaskToolbarItem.swift From 2988eee711374fe92e3e46a70bc021cc06719d89 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 4 Aug 2026 14:23:05 +0200 Subject: [PATCH 222/335] Refactor: Flatten single-file FileInspector group --- .../InspectorArea/{FileInspector => }/FileInspectorView.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/InspectorArea/{FileInspector => }/FileInspectorView.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/FileInspector/FileInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/InspectorArea/FileInspector/FileInspectorView.swift rename to CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift From 769c13f439a158382fb2ba42af4da0f6a3feaa0a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 4 Aug 2026 14:25:08 +0200 Subject: [PATCH 223/335] Refactor: Rename isHovering helper to setHoverCursor --- .../StatusBarCursorPositionLabel.swift | 2 +- .../StatusBarEncodingSelector.swift | 2 +- .../StatusBarIndentSelector.swift | 2 +- .../StatusBarLineEndSelector.swift | 2 +- .../StatusBarToggleUtilityAreaButton.swift | 2 +- .../StatusBarItems/View+HoverCursor.swift | 29 +++++++++++++++++++ .../StatusBarItems/View+isHovering.swift | 25 ---------------- 7 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+HoverCursor.swift delete mode 100644 CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+isHovering.swift diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift index ded23882a6..3a6e37da4b 100644 --- a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift +++ b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarCursorPositionLabel.swift @@ -36,7 +36,7 @@ struct StatusBarCursorPositionLabel: View { .fixedSize() .accessibilityIdentifier("CursorPositionLabel") .accessibilityAddTraits(.updatesFrequently) - .onHover { isHovering($0) } + .onHover { setHoverCursor($0) } .onReceive(activeCursorState.cursorPositionsPublisher) { newValue in self.cursorPositions = newValue } diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift index c64212461f..11e1c08410 100644 --- a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift +++ b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarEncodingSelector.swift @@ -16,6 +16,6 @@ struct StatusBarEncodingSelector: View { Text("UTF 8") } .menuStyle(StatusBarMenuStyle()) - .onHover { isHovering($0) } + .onHover { setHoverCursor($0) } } } diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarIndentSelector.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarIndentSelector.swift index a5d4537d2a..ac86a92df5 100644 --- a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarIndentSelector.swift +++ b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarIndentSelector.swift @@ -34,6 +34,6 @@ struct StatusBarIndentSelector: View { Text("\(defaultTabWidth) Spaces") } .menuStyle(StatusBarMenuStyle()) - .onHover { isHovering($0) } + .onHover { setHoverCursor($0) } } } diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift index bedcf1bb8a..911b82e3a3 100644 --- a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift +++ b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarLineEndSelector.swift @@ -16,6 +16,6 @@ struct StatusBarLineEndSelector: View { Text("LF") } .menuStyle(StatusBarMenuStyle()) - .onHover { isHovering($0) } + .onHover { setHoverCursor($0) } } } diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift index 3e100d33a8..7ab8441ecb 100644 --- a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift +++ b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/StatusBarToggleUtilityAreaButton.swift @@ -26,7 +26,7 @@ internal struct StatusBarToggleUtilityAreaButton: View { .buttonStyle(.icon) .keyboardShortcut("Y", modifiers: [.command, .shift]) .help(utilityAreaViewModel.isCollapsed ? "Show the Utility area" : "Hide the Utility area") - .onHover { isHovering($0) } + .onHover { setHoverCursor($0) } .onChange(of: controlActiveState) { _, newValue in if newValue == .key { commandManager?.addCommand( diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+HoverCursor.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+HoverCursor.swift new file mode 100644 index 0000000000..b54ae98feb --- /dev/null +++ b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+HoverCursor.swift @@ -0,0 +1,29 @@ +// +// View+HoverCursor.swift +// CodeEdit +// +// Created by Lukas Pistrol on 22.03.22. +// + +import SwiftUI + +extension View { + + /// Pushes or pops a cursor to match a hover state. Call from `onHover`. + /// + /// This is not a view modifier — it returns `Void` and mutates the cursor stack + /// as a side effect, so it must be called inside the `onHover` closure rather + /// than chained onto a view. + /// - Parameters: + /// - isHovering: The `onHover()` value. + /// - isDragging: Indicates that dragging is happening. If true, the cursor is left alone. + /// - cursor: The cursor to display while hovering. + func setHoverCursor(_ isHovering: Bool, isDragging: Bool = false, cursor: NSCursor = .arrow) { + if isDragging { return } + if isHovering { + cursor.push() + } else { + NSCursor.pop() + } + } +} diff --git a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+isHovering.swift b/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+isHovering.swift deleted file mode 100644 index 570f574c31..0000000000 --- a/CodeEdit/WorkspaceWindow/StatusBar/StatusBarItems/View+isHovering.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// View+isHovering.swift -// CodeEditModules/StatusBar -// -// Created by Lukas Pistrol on 22.03.22. -// - -import SwiftUI - -extension View { - - /// Changes the cursor appearance when hovering attached View - /// - Parameters: - /// - active: onHover() value - /// - isDragging: indicate that dragging is happening. If true this will not change the cursor. - /// - cursor: the cursor to display on hover - func isHovering(_ active: Bool, isDragging: Bool = false, cursor: NSCursor = .arrow) { - if isDragging { return } - if active { - cursor.push() - } else { - NSCursor.pop() - } - } -} From 8a4dd12a3086f922e0a65f2ca8a05e38877952f3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 4 Aug 2026 14:25:49 +0200 Subject: [PATCH 224/335] Refactor: Name FileIcon's file after the type it declares --- .../Workspace/Files/{CEWorkspaceFileIcon.swift => FileIcon.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/WorkspaceWindow/Workspace/Files/{CEWorkspaceFileIcon.swift => FileIcon.swift} (100%) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFileIcon.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileIcon.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFileIcon.swift rename to CodeEdit/WorkspaceWindow/Workspace/Files/FileIcon.swift From 271988a64b520a2ba8640e3f84b57027d73040df Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Tue, 4 Aug 2026 14:27:22 +0200 Subject: [PATCH 225/335] Refactor: Drop inherited CE prefix from workspace Settings views --- CodeEdit/WorkspaceWindow/CodeEditWindowController.swift | 2 +- CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift | 2 +- CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift | 2 +- .../Settings/{CETaskFormView.swift => TaskFormView.swift} | 4 ++-- ...ListView.swift => WorkspaceSettingsTaskListView.swift} | 4 ++-- ...paceSettingsView.swift => WorkspaceSettingsView.swift} | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) rename CodeEdit/WorkspaceWindow/Settings/{CETaskFormView.swift => TaskFormView.swift} (98%) rename CodeEdit/WorkspaceWindow/Settings/{CEWorkspaceSettingsTaskListView.swift => WorkspaceSettingsTaskListView.swift} (95%) rename CodeEdit/WorkspaceWindow/Settings/{CEWorkspaceSettingsView.swift => WorkspaceSettingsView.swift} (92%) diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index f001baeef7..0d80ecba83 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -260,7 +260,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } else { let settingsWindow = NSWindow() self.workspaceSettingsWindow = settingsWindow - let contentView = CEWorkspaceSettingsView( + let contentView = WorkspaceSettingsView( dismiss: { [weak self, weak settingsWindow] in guard let settingsWindow else { return } self?.window?.endSheet(settingsWindow) diff --git a/CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift b/CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift index e00d4a4ab9..8c055d9e86 100644 --- a/CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift +++ b/CodeEdit/WorkspaceWindow/Settings/AddCETaskView.swift @@ -17,7 +17,7 @@ struct AddCETaskView: View { var body: some View { VStack(spacing: 0) { - CETaskFormView(task: $newTask) + TaskFormView(task: $newTask) Divider() HStack { Button { diff --git a/CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift b/CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift index 4cae363894..888b6e6ad7 100644 --- a/CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift +++ b/CodeEdit/WorkspaceWindow/Settings/EditCETaskView.swift @@ -29,7 +29,7 @@ struct EditCETaskView: View { var body: some View { VStack(spacing: 0) { - CETaskFormView(task: $task) + TaskFormView(task: $task) Divider() HStack { Button(role: .destructive) { diff --git a/CodeEdit/WorkspaceWindow/Settings/CETaskFormView.swift b/CodeEdit/WorkspaceWindow/Settings/TaskFormView.swift similarity index 98% rename from CodeEdit/WorkspaceWindow/Settings/CETaskFormView.swift rename to CodeEdit/WorkspaceWindow/Settings/TaskFormView.swift index e405a4b0b5..c3f7b6cba3 100644 --- a/CodeEdit/WorkspaceWindow/Settings/CETaskFormView.swift +++ b/CodeEdit/WorkspaceWindow/Settings/TaskFormView.swift @@ -1,5 +1,5 @@ // -// CETaskFormView.swift +// TaskFormView.swift // CodeEdit // // Created by Tommy Ludwig on 01.07.24. @@ -9,7 +9,7 @@ import SwiftUI import CodeEditUI import CodeEditCore -struct CETaskFormView: View { +struct TaskFormView: View { @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings @Binding var task: CETask @State private var selectedEnvID: UUID? diff --git a/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/WorkspaceWindow/Settings/WorkspaceSettingsTaskListView.swift similarity index 95% rename from CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsTaskListView.swift rename to CodeEdit/WorkspaceWindow/Settings/WorkspaceSettingsTaskListView.swift index c07add8fcd..0bef09eaec 100644 --- a/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsTaskListView.swift +++ b/CodeEdit/WorkspaceWindow/Settings/WorkspaceSettingsTaskListView.swift @@ -1,5 +1,5 @@ // -// CEWorkspaceSettingsTaskListView.swift +// WorkspaceSettingsTaskListView.swift // CodeEdit // // Created by Tommy Ludwig on 01.07.24. @@ -9,7 +9,7 @@ import SwiftUI import CETerminal import CodeEditCore -struct CEWorkspaceSettingsTaskListView: View { +struct WorkspaceSettingsTaskListView: View { @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings @EnvironmentObject var taskManager: TaskManager diff --git a/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsView.swift b/CodeEdit/WorkspaceWindow/Settings/WorkspaceSettingsView.swift similarity index 92% rename from CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsView.swift rename to CodeEdit/WorkspaceWindow/Settings/WorkspaceSettingsView.swift index 1c3f5843f6..212038b783 100644 --- a/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettingsView.swift +++ b/CodeEdit/WorkspaceWindow/Settings/WorkspaceSettingsView.swift @@ -1,5 +1,5 @@ // -// CEWorkspaceSettingsView.swift +// WorkspaceSettingsView.swift // CodeEdit // // Created by Tommy Ludwig on 01.07.24. @@ -8,7 +8,7 @@ import SwiftUI import CodeEditCore -struct CEWorkspaceSettingsView: View { +struct WorkspaceSettingsView: View { var dismiss: () -> Void @EnvironmentObject var workspaceSettingsManager: CEWorkspaceSettings @@ -31,7 +31,7 @@ struct CEWorkspaceSettingsView: View { } Section { - CEWorkspaceSettingsTaskListView( + WorkspaceSettingsTaskListView( selectedTaskID: $selectedTaskID, showAddTaskSheet: $showAddTaskSheet ) @@ -80,5 +80,5 @@ struct CEWorkspaceSettingsView: View { } #Preview { - CEWorkspaceSettingsView(dismiss: { print("Dismiss") }) + WorkspaceSettingsView(dismiss: { print("Dismiss") }) } From ba5beee2c1f9023cd287245648345a14ab013370 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 5 Aug 2026 13:33:11 +0200 Subject: [PATCH 226/335] Refactor: Add URL-keyed FileIcon to CodeEditUI --- .../FileIcon/FileIconParityTests.swift | 96 ++++++++++ Packages/Foundation/CodeEditUI/Package.swift | 3 +- .../Sources/CodeEditUI/FileIcon.swift | 181 ++++++++++++++++++ .../Amber.colorset/Contents.json | 38 ++++ .../Resources/Colors.xcassets/Contents.json | 6 + .../Scarlet.colorset/Contents.json | 38 ++++ .../Steel.colorset/Contents.json | 38 ++++ 7 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 CodeEditTests/Features/FileIcon/FileIconParityTests.swift create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/FileIcon.swift create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json create mode 100644 Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json diff --git a/CodeEditTests/Features/FileIcon/FileIconParityTests.swift b/CodeEditTests/Features/FileIcon/FileIconParityTests.swift new file mode 100644 index 0000000000..5f8041dc9b --- /dev/null +++ b/CodeEditTests/Features/FileIcon/FileIconParityTests.swift @@ -0,0 +1,96 @@ +// +// FileIconParityTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 05/08/2026. +// + +import XCTest +import SwiftUI +import CodeEditCore +import CodeEditUI +@testable import CodeEdit + +/// Temporary: proves the new `CodeEditUI.FileIcon` table reproduces the old +/// mapping. Deleted together with `FileType` in the final commit of this slice. +/// +/// Both sides are qualified explicitly — `FileIcon` alone is ambiguous while the +/// app target still declares its own copy. +final class FileIconParityTests: XCTestCase { + + /// Every raw value declared by `FileType`, in declaration order. + private let rawValues = [ + "adb", "aif", "avi", "bash", "c", "cetheme", "clj", "cls", "cs", "css", "d", + "dart", "elm", "entitlements", "env", "ex", "example", "f95", "fs", + "gitignore", "go", "gs", "h", "hs", "html", "ico", "java", "jl", "jpeg", + "jpg", "js", "json", "jsx", "kt", "LICENSE", "lock", "lsp", "lua", "m", + "Makefile", "md", "mid", "mjs", "mk", "mod", "mov", "mp3", "mp4", "pas", + "pdf", "pl", "plist", "png", "py", "resolved", "rb", "rs", "rtf", "scm", + "scpt", "sh", "ss", "strings", "sum", "svg", "swift", "text", "ts", "tsx", + "vue", "wav", "xcconfig", "yml", "zsh" + ] + + /// The deliberate fixes: raw value → expected new symbol. + private let symbolExceptions = ["ts": "t.square", "c": "c.square"] + /// Raw values whose colour deliberately changes (to `jpg`'s blue). + private let colorExceptions: Set = ["jpeg", "ico"] + + private func url(for rawValue: String) -> URL { + // LICENSE and Makefile match on the whole filename; everything else is an extension. + ["LICENSE", "Makefile"].contains(rawValue) + ? URL(fileURLWithPath: "/tmp/\(rawValue)") + : URL(fileURLWithPath: "/tmp/file.\(rawValue)") + } + + private func rgba(_ color: Color) -> [CGFloat] { + guard let converted = NSColor(color).usingColorSpace(.sRGB) else { + return [] + } + return [ + converted.redComponent, converted.greenComponent, + converted.blueComponent, converted.alphaComponent + ] + } + + func testSymbolsMatchLegacyMapping() { + for rawValue in rawValues { + guard let type = FileType(rawValue: rawValue) else { + XCTFail("FileType(rawValue: \(rawValue)) is nil — raw-value list is stale") + continue + } + let new = CodeEditUI.FileIcon.spec(for: url(for: rawValue)).symbol + let expected = symbolExceptions[rawValue] ?? CodeEdit.FileIcon.fileIcon(fileType: type) + XCTAssertEqual(new, expected, "symbol mismatch for \(rawValue)") + } + } + + func testColorsMatchLegacyMapping() { + for rawValue in rawValues where !colorExceptions.contains(rawValue) { + guard let type = FileType(rawValue: rawValue) else { continue } + let new = CodeEditUI.FileIcon.spec(for: url(for: rawValue)).color + XCTAssertEqual( + rgba(new), rgba(CodeEdit.FileIcon.iconColor(fileType: type)), + "color mismatch for \(rawValue)" + ) + } + } + + func testDeliberateExceptions() { + // jpeg/ico now share jpg's blue. + let jpg = CodeEditUI.FileIcon.spec(for: url(for: "jpg")).color + for rawValue in colorExceptions { + XCTAssertEqual( + rgba(CodeEditUI.FileIcon.spec(for: url(for: rawValue)).color), rgba(jpg), + "\(rawValue) should share jpg's color" + ) + } + // Unidentifiable files get bare `doc`, not `doc.plaintext`. + let unknown = CodeEditUI.FileIcon.spec(for: URL(fileURLWithPath: "/tmp/file.qqzz")) + XCTAssertEqual(unknown.symbol, "doc") + // The generic (no-file) spec is unchanged. + XCTAssertEqual( + CodeEditUI.FileIcon.generic.symbol, + CodeEdit.FileIcon.fileIcon(fileType: nil) + ) + } +} diff --git a/Packages/Foundation/CodeEditUI/Package.swift b/Packages/Foundation/CodeEditUI/Package.swift index 197487b61c..f40a3d0d90 100644 --- a/Packages/Foundation/CodeEditUI/Package.swift +++ b/Packages/Foundation/CodeEditUI/Package.swift @@ -15,7 +15,8 @@ let package = Package( targets: [ .target( name: "CodeEditUI", - dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")] + dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")], + resources: [.process("Resources")] ), .testTarget( name: "CodeEditUIUnitTests", diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/FileIcon.swift b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/FileIcon.swift new file mode 100644 index 0000000000..713a18af20 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/FileIcon.swift @@ -0,0 +1,181 @@ +// +// FileIcon.swift +// CodeEditUI +// +// Created by Matthijs Eikelenboom on 05/08/2026. +// + +import SwiftUI +import UniformTypeIdentifiers +import CodeEditSymbols + +/// The symbol and tint used to represent a file or folder. +public struct FileIconSpec: Sendable, Equatable { + public let symbol: String + public let color: Color + + public init(symbol: String, color: Color) { + self.symbol = symbol + self.color = color + } +} + +public extension FileIconSpec { + /// SwiftUI image, preferring a `CodeEditSymbols` custom symbol. + var image: Image { + if let custom = NSImage.symbol(named: symbol) { + return Image(nsImage: custom) + } + return Image(systemName: symbol) + } + + /// AppKit image, preferring a `CodeEditSymbols` custom symbol. + var nsImage: NSImage { + if let custom = NSImage.symbol(named: symbol) { + return custom + } + return NSImage(systemSymbolName: symbol, accessibilityDescription: symbol) + ?? NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")! + } +} + +/// Maps files to a symbol and tint. +/// +/// Keyed on the file's *name*, not on a domain type, so this stays free of model +/// dependencies and can live beside the other presentation atoms. +public enum FileIcon { + + // Resolved from this package's own asset catalog, so the package is + // self-contained and its tests can assert colours without an app host. + private static let amber = Color("Amber", bundle: .module) + private static let scarlet = Color("Scarlet", bundle: .module) + private static let steel = Color("Steel", bundle: .module) + + /// Used when no file is known. Matches the old `fileIcon(fileType: nil)`. + public static let generic = FileIconSpec(symbol: "doc", color: Color("Steel", bundle: .module)) + + /// Whole-filename matches, checked before any extension. + private static let byFilename: [String: FileIconSpec] = [ + "LICENSE": .init(symbol: "key.fill", color: amber), + "Makefile": .init(symbol: "terminal", color: Color(red: 0.937, green: 0.325, blue: 0.314)) + ] + + private static let byExtension: [String: FileIconSpec] = [ + // structured data + "json": .init(symbol: "doc.json", color: scarlet), + "yml": .init(symbol: "doc.json", color: scarlet), + "resolved": .init(symbol: "doc.json", color: scarlet), + "strings": .init(symbol: "text.quote", color: scarlet), + "plist": .init(symbol: "tablecells", color: steel), + "lock": .init(symbol: "lock.doc", color: steel), + // web + "css": .init(symbol: "curlybraces", color: .teal), + "html": .init(symbol: "chevron.left.forwardslash.chevron.right", color: .orange), + "js": .init(symbol: "doc.javascript", color: amber), + "mjs": .init(symbol: "doc.javascript", color: amber), + "ts": .init(symbol: "t.square", color: .blue), + "jsx": .init(symbol: "atom", color: .cyan), + "tsx": .init(symbol: "atom", color: .cyan), + "vue": .init(symbol: "v.square", color: Color(red: 0.255, green: 0.722, blue: 0.514)), + // languages + "swift": .init(symbol: "swift", color: .orange), + "java": .init(symbol: "cup.and.saucer", color: .blue), + "py": .init(symbol: "doc.python", color: amber), + "rb": .init(symbol: "doc.ruby", color: scarlet), + "c": .init(symbol: "c.square", color: .purple), + "h": .init(symbol: "h.square", color: Color(red: 0.667, green: 0.031, blue: 0.133)), + "m": .init(symbol: "m.square", color: Color(red: 0.271, green: 0.106, blue: 0.525)), + "go": .init(symbol: "g.square", color: Color(red: 0.02, green: 0.675, blue: 0.757)), + "sum": .init(symbol: "s.square", color: Color(red: 0.925, green: 0.251, blue: 0.478)), + "mod": .init(symbol: "m.square", color: Color(red: 0.925, green: 0.251, blue: 0.478)), + "rs": .init(symbol: "r.square", color: .orange), + // shells + "bash": .init(symbol: "terminal", color: steel), + "sh": .init(symbol: "terminal", color: steel), + "zsh": .init(symbol: "terminal", color: steel), + "scpt": .init(symbol: "applescript", color: steel), + // config + "env": .init(symbol: "gearshape.fill", color: steel), + "example": .init(symbol: "gearshape.fill", color: steel), + "gitignore": .init(symbol: "arrow.triangle.branch", color: steel), + "entitlements": .init(symbol: "checkmark.seal", color: amber), + "xcconfig": .init(symbol: "gearshape.2", color: steel), + "cetheme": .init(symbol: "paintbrush", color: .purple), + // media + "png": .init(symbol: "photo", color: .blue), + "jpg": .init(symbol: "photo", color: .blue), + "jpeg": .init(symbol: "photo", color: .blue), + "ico": .init(symbol: "photo", color: .blue), + "svg": .init(symbol: "square.fill.on.circle.fill", color: .blue), + "pdf": .init(symbol: "photo", color: steel), + "wav": .init(symbol: "speaker.wave.2", color: steel), + "mp3": .init(symbol: "speaker.wave.2", color: steel), + "aif": .init(symbol: "speaker.wave.2", color: steel), + "mid": .init(symbol: "speaker.wave.2", color: steel), + "avi": .init(symbol: "film", color: steel), + "mp4": .init(symbol: "film", color: steel), + "mov": .init(symbol: "film", color: steel), + // documents + "rtf": .init(symbol: "doc.richtext", color: steel), + "md": .init(symbol: "doc.plaintext", color: steel), + "txt": .init(symbol: "doc.plaintext", color: steel), + "text": .init(symbol: "doc.plaintext", color: steel) + ].merging( + // Known plain-text languages. Explicit entries, not fallbacks — without + // them these would regress to bare `doc`. + [ + "adb", "clj", "cls", "cs", "d", "dart", "elm", "ex", "f95", "fs", "gs", + "hs", "jl", "kt", "l", "lsp", "lua", "mk", "pas", "pl", "scm", "ss" + ].reduce(into: [:]) { dict, ext in + dict[ext] = FileIconSpec(symbol: "doc.plaintext", color: steel) + }, + uniquingKeysWith: { current, _ in current } + ) + + /// Spec for the file at `url`. + public static func spec(for url: URL) -> FileIconSpec { + let filename = url.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) + + if let match = byFilename[filename] { + return match + } + + // Leading-dot names (.gitignore, .env.example) — drop the dot, then treat + // the remainder as an extension chain. + let searchable = filename.hasPrefix(".") ? String(filename.dropFirst()) : filename + + // Last extension wins: `file.d.ts` resolves `ts` before `d`. + for component in searchable.components(separatedBy: ".").reversed() { + if let match = byExtension[component] { + return match + } + } + + return systemSpec(for: url) ?? FileIconSpec(symbol: "doc", color: steel) + } + + /// Spec for a folder. + public static func folderSpec( + isEmpty: Bool, + isRoot: Bool, + isCodeEditDirectory: Bool + ) -> FileIconSpec { + if isRoot || isCodeEditDirectory { + return .init(symbol: "folder.fill.badge.gearshape", color: steel) + } + return .init(symbol: isEmpty ? "folder" : "folder.fill", color: steel) + } + + /// Fallback for extensions the table does not cover, using the system's + /// declared type. Keeps unrecognised-but-identifiable files meaningful. + private static func systemSpec(for url: URL) -> FileIconSpec? { + guard let type = UTType(filenameExtension: url.pathExtension) else { return nil } + if type.conforms(to: .image) { return .init(symbol: "photo", color: steel) } + if type.conforms(to: .audio) { return .init(symbol: "speaker.wave.2", color: steel) } + if type.conforms(to: .audiovisualContent) { return .init(symbol: "film", color: steel) } + if type.conforms(to: .sourceCode) || type.conforms(to: .text) { + return .init(symbol: "doc.plaintext", color: steel) + } + return nil + } +} diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json new file mode 100644 index 0000000000..db17ef8758 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.635", + "red" : "0.784" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.302", + "green" : "0.812", + "red" : "0.961" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json new file mode 100644 index 0000000000..4eb6678a66 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.176", + "green" : "0.303", + "red" : "0.956" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.376", + "green" : "0.475", + "red" : "0.960" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json new file mode 100644 index 0000000000..03876e18e0 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.522", + "green" : "0.463", + "red" : "0.373" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.749", + "green" : "0.690", + "red" : "0.585" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From 69b79043f4f1577de089cd9be75bbb715b71d528 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 5 Aug 2026 20:35:08 +0200 Subject: [PATCH 227/335] Refactor: Route file icons through CodeEditUI Deletes both duplicated FileIcon copies (app target and CEEditor) in favour of the URL-keyed table added in ba5beee2, and moves the three custom colorsets into CodeEditUI so the package owns what it draws. The table was proven exhaustively before either copy was deleted: a temporary parity test in the app target compared all 73 legacy file types, symbol and colour, against the old mapping and passed. Because the two sides read from different asset catalogs, that run also verified the colorset move preserved the exact display-P3 values. The permanent tests now live in CodeEditUIUnitTests, beside the code, where Bundle.module resolves the assets. Four deliberate fixes to gaps in the old mapping: ts doc -> t.square (had a colour but no glyph) c doc -> c.square (same gap) jpeg/ico steel -> blue (absent from the colour switch, unlike jpg) unidentifiable files doc.plaintext -> doc --- .../Amber.colorset/Contents.json | 38 ----- .../Scarlet.colorset/Contents.json | 38 ----- .../Steel.colorset/Contents.json | 38 ----- .../GitChangedFileLabel.swift | 3 +- .../GitChangedFileListView.swift | 3 +- .../Files/CEWorkspaceFile+Presentation.swift | 42 ++---- .../Workspace/Files/FileIcon.swift | 130 ------------------ .../FileIcon/FileIconParityTests.swift | 96 ------------- .../CEEditor/CEWorkspaceFile+Editor.swift | 42 ++---- .../Sources/CEEditor/Models/FileIcon.swift | 126 ----------------- .../CodeEditUIUnitTests/FileIconTests.swift | 118 ++++++++++++++++ 11 files changed, 148 insertions(+), 526 deletions(-) delete mode 100644 CodeEdit/Assets.xcassets/Custom Colors/Amber.colorset/Contents.json delete mode 100644 CodeEdit/Assets.xcassets/Custom Colors/Scarlet.colorset/Contents.json delete mode 100644 CodeEdit/Assets.xcassets/Custom Colors/Steel.colorset/Contents.json delete mode 100644 CodeEdit/WorkspaceWindow/Workspace/Files/FileIcon.swift delete mode 100644 CodeEditTests/Features/FileIcon/FileIconParityTests.swift delete mode 100644 Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift create mode 100644 Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/FileIconTests.swift diff --git a/CodeEdit/Assets.xcassets/Custom Colors/Amber.colorset/Contents.json b/CodeEdit/Assets.xcassets/Custom Colors/Amber.colorset/Contents.json deleted file mode 100644 index db17ef8758..0000000000 --- a/CodeEdit/Assets.xcassets/Custom Colors/Amber.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.133", - "green" : "0.635", - "red" : "0.784" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.302", - "green" : "0.812", - "red" : "0.961" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CodeEdit/Assets.xcassets/Custom Colors/Scarlet.colorset/Contents.json b/CodeEdit/Assets.xcassets/Custom Colors/Scarlet.colorset/Contents.json deleted file mode 100644 index 4eb6678a66..0000000000 --- a/CodeEdit/Assets.xcassets/Custom Colors/Scarlet.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.176", - "green" : "0.303", - "red" : "0.956" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.376", - "green" : "0.475", - "red" : "0.960" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CodeEdit/Assets.xcassets/Custom Colors/Steel.colorset/Contents.json b/CodeEdit/Assets.xcassets/Custom Colors/Steel.colorset/Contents.json deleted file mode 100644 index 03876e18e0..0000000000 --- a/CodeEdit/Assets.xcassets/Custom Colors/Steel.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.522", - "green" : "0.463", - "red" : "0.373" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.749", - "green" : "0.690", - "red" : "0.585" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift index 8fc4b594c1..d8a0ccfc99 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift @@ -10,6 +10,7 @@ import SwiftUI import ShellClient import CEWorkspaceFileManager import CodeEditCore +import CodeEditUI struct GitChangedFileLabel: View { @EnvironmentObject private var sourceControlManager: SourceControlManager @@ -29,7 +30,7 @@ struct GitChangedFileLabel: View { Image(nsImage: ceFile.nsIcon) .renderingMode(.template) } else { - Image(systemName: FileIcon.fileIcon(fileType: nil)) + FileIcon.generic.image .renderingMode(.template) } } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift index b0922c15b2..0ed134fdf2 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift @@ -10,6 +10,7 @@ import SwiftUI import CodeEditSettings import CEWorkspaceFileManager import CodeEditCore +import CodeEditUI /// A view to display a changed file's information in a list view. Optionally displays the staged status. struct GitChangedFileListView: View { @@ -77,7 +78,7 @@ struct GitChangedFileListView: View { if let file { return file.iconColor } else { - return FileIcon.iconColor(fileType: nil) + return FileIcon.generic.color } case .monochrome: return Color("CoolGray") diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift index fdbce35f46..eb09f814e0 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift @@ -8,41 +8,25 @@ import SwiftUI import CodeEditCore import CodeEditSettings -import CodeEditSymbols +import CodeEditUI extension CEWorkspaceFile { // MARK: Icons - var icon: Image { - if let customImage = NSImage.symbol(named: systemImage) { - return Image(nsImage: customImage) - } else { - return Image(systemName: systemImage) - } - } - - var nsIcon: NSImage { - if let customImage = NSImage.symbol(named: systemImage) { - return customImage - } else { - return NSImage(systemSymbolName: systemImage, accessibilityDescription: systemImage) - ?? NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")! - } + /// Symbol + tint for this file or folder, from ``CodeEditUI/FileIcon``. + var iconSpec: FileIconSpec { + isFolder + ? FileIcon.folderSpec( + isEmpty: isEmptyFolder, + isRoot: parent == nil, + isCodeEditDirectory: name == ".codeedit" + ) + : FileIcon.spec(for: url) } - var iconColor: Color { - FileIcon.iconColor(fileType: type) - } - - var systemImage: String { - if isFolder { - if self.parent == nil { return "folder.fill.badge.gearshape" } - if self.name == ".codeedit" { return "folder.fill.badge.gearshape" } - return isEmptyFolder ? "folder" : "folder.fill" - } else { - return FileIcon.fileIcon(fileType: type) - } - } + var icon: Image { iconSpec.image } + var nsIcon: NSImage { iconSpec.nsImage } + var iconColor: Color { iconSpec.color } // MARK: Intents diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/FileIcon.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileIcon.swift deleted file mode 100644 index 1611a20f37..0000000000 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/FileIcon.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// FileIcon.swift -// -// -// Created by Nanashi Li on 2022/05/20. -// - -import SwiftUI -import CodeEditCore - -// TODO: DOCS (Nanashi Li) -enum FileIcon { - - /// Returns a string describing a SFSymbol for files - /// If not specified otherwise this will return `"doc"` - static func fileIcon(fileType: FileType?) -> String { // swiftlint:disable:this cyclomatic_complexity function_body_length line_length - switch fileType { - case .json, .yml, .resolved: - return "doc.json" - case .lock: - return "lock.doc" - case .css: - return "curlybraces" - case .js, .mjs: - return "doc.javascript" - case .jsx, .tsx: - return "atom" - case .swift: - return "swift" - case .env, .example: - return "gearshape.fill" - case .gitignore: - return "arrow.triangle.branch" - case .pdf, .png, .jpg, .jpeg, .ico: - return "photo" - case .svg: - return "square.fill.on.circle.fill" - case .entitlements: - return "checkmark.seal" - case .plist: - return "tablecells" - case .md, .txt: - return "doc.plaintext" - case .rtf: - return "doc.richtext" - case .html: - return "chevron.left.forwardslash.chevron.right" - case .LICENSE: - return "key.fill" - case .java: - return "cup.and.saucer" - case .py: - return "doc.python" - case .rb: - return "doc.ruby" - case .strings: - return "text.quote" - case .h: - return "h.square" - case .m: - return "m.square" - case .vue: - return "v.square" - case .go: - return "g.square" - case .sum: - return "s.square" - case .mod: - return "m.square" - case .bash, .sh, .Makefile, .zsh: - return "terminal" - case .rs: - return "r.square" - case .wav, .mp3, .aif, .mid: - return "speaker.wave.2" - case .avi, .mp4, .mov: - return "film" - case .scpt: - return "applescript" - case .xcconfig: - return "gearshape.2" - case .cetheme: - return "paintbrush" - case .adb, .clj, .cls, .cs, .d, .dart, .elm, .ex, .f95, .fs, .gs, .hs, - .jl, .kt, .l, .lsp, .lua, .mk, .pas, .pl, .scm, .ss: - return "doc.plaintext" - default: - return "doc" - } - } - - /// Returns a `Color` for a specific `fileType` - /// If not specified otherwise this will return `Color.accentColor` - static func iconColor(fileType: FileType?) -> Color { // swiftlint:disable:this cyclomatic_complexity - switch fileType { - case .swift, .html: - return .orange - case .java, .jpg, .png, .svg, .ts: - return .blue - case .css: - return .teal - case .js, .mjs, .py, .entitlements, .LICENSE: - return Color.amber - case .json, .resolved, .rb, .strings, .yml: - return Color.scarlet - case .jsx, .tsx: - return .cyan - case .plist, .xcconfig, .sh: - return Color.steel - case .c, .cetheme: - return .purple - case .vue: - return Color(red: 0.255, green: 0.722, blue: 0.514, opacity: 1.0) - case .h: - return Color(red: 0.667, green: 0.031, blue: 0.133, opacity: 1.0) - case .m: - return Color(red: 0.271, green: 0.106, blue: 0.525, opacity: 1.0) - case .go: - return Color(red: 0.02, green: 0.675, blue: 0.757, opacity: 1.0) - case .sum, .mod: - return Color(red: 0.925, green: 0.251, blue: 0.478, opacity: 1.0) - case .Makefile: - return Color(red: 0.937, green: 0.325, blue: 0.314, opacity: 1.0) - case .rs: - return .orange - default: - return Color.steel - } - } -} diff --git a/CodeEditTests/Features/FileIcon/FileIconParityTests.swift b/CodeEditTests/Features/FileIcon/FileIconParityTests.swift deleted file mode 100644 index 5f8041dc9b..0000000000 --- a/CodeEditTests/Features/FileIcon/FileIconParityTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// FileIconParityTests.swift -// CodeEditTests -// -// Created by Matthijs Eikelenboom on 05/08/2026. -// - -import XCTest -import SwiftUI -import CodeEditCore -import CodeEditUI -@testable import CodeEdit - -/// Temporary: proves the new `CodeEditUI.FileIcon` table reproduces the old -/// mapping. Deleted together with `FileType` in the final commit of this slice. -/// -/// Both sides are qualified explicitly — `FileIcon` alone is ambiguous while the -/// app target still declares its own copy. -final class FileIconParityTests: XCTestCase { - - /// Every raw value declared by `FileType`, in declaration order. - private let rawValues = [ - "adb", "aif", "avi", "bash", "c", "cetheme", "clj", "cls", "cs", "css", "d", - "dart", "elm", "entitlements", "env", "ex", "example", "f95", "fs", - "gitignore", "go", "gs", "h", "hs", "html", "ico", "java", "jl", "jpeg", - "jpg", "js", "json", "jsx", "kt", "LICENSE", "lock", "lsp", "lua", "m", - "Makefile", "md", "mid", "mjs", "mk", "mod", "mov", "mp3", "mp4", "pas", - "pdf", "pl", "plist", "png", "py", "resolved", "rb", "rs", "rtf", "scm", - "scpt", "sh", "ss", "strings", "sum", "svg", "swift", "text", "ts", "tsx", - "vue", "wav", "xcconfig", "yml", "zsh" - ] - - /// The deliberate fixes: raw value → expected new symbol. - private let symbolExceptions = ["ts": "t.square", "c": "c.square"] - /// Raw values whose colour deliberately changes (to `jpg`'s blue). - private let colorExceptions: Set = ["jpeg", "ico"] - - private func url(for rawValue: String) -> URL { - // LICENSE and Makefile match on the whole filename; everything else is an extension. - ["LICENSE", "Makefile"].contains(rawValue) - ? URL(fileURLWithPath: "/tmp/\(rawValue)") - : URL(fileURLWithPath: "/tmp/file.\(rawValue)") - } - - private func rgba(_ color: Color) -> [CGFloat] { - guard let converted = NSColor(color).usingColorSpace(.sRGB) else { - return [] - } - return [ - converted.redComponent, converted.greenComponent, - converted.blueComponent, converted.alphaComponent - ] - } - - func testSymbolsMatchLegacyMapping() { - for rawValue in rawValues { - guard let type = FileType(rawValue: rawValue) else { - XCTFail("FileType(rawValue: \(rawValue)) is nil — raw-value list is stale") - continue - } - let new = CodeEditUI.FileIcon.spec(for: url(for: rawValue)).symbol - let expected = symbolExceptions[rawValue] ?? CodeEdit.FileIcon.fileIcon(fileType: type) - XCTAssertEqual(new, expected, "symbol mismatch for \(rawValue)") - } - } - - func testColorsMatchLegacyMapping() { - for rawValue in rawValues where !colorExceptions.contains(rawValue) { - guard let type = FileType(rawValue: rawValue) else { continue } - let new = CodeEditUI.FileIcon.spec(for: url(for: rawValue)).color - XCTAssertEqual( - rgba(new), rgba(CodeEdit.FileIcon.iconColor(fileType: type)), - "color mismatch for \(rawValue)" - ) - } - } - - func testDeliberateExceptions() { - // jpeg/ico now share jpg's blue. - let jpg = CodeEditUI.FileIcon.spec(for: url(for: "jpg")).color - for rawValue in colorExceptions { - XCTAssertEqual( - rgba(CodeEditUI.FileIcon.spec(for: url(for: rawValue)).color), rgba(jpg), - "\(rawValue) should share jpg's color" - ) - } - // Unidentifiable files get bare `doc`, not `doc.plaintext`. - let unknown = CodeEditUI.FileIcon.spec(for: URL(fileURLWithPath: "/tmp/file.qqzz")) - XCTAssertEqual(unknown.symbol, "doc") - // The generic (no-file) spec is unchanged. - XCTAssertEqual( - CodeEditUI.FileIcon.generic.symbol, - CodeEdit.FileIcon.fileIcon(fileType: nil) - ) - } -} diff --git a/Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift b/Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift index 1abca160d1..885988c416 100644 --- a/Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift +++ b/Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift @@ -7,39 +7,23 @@ import SwiftUI import CodeEditCore -import CodeEditSymbols +import CodeEditUI extension CEWorkspaceFile: EditorTabRepresentable { public var tabID: EditorTabID { .codeEditor(id) } - var icon: Image { - if let customImage = NSImage.symbol(named: systemImage) { - return Image(nsImage: customImage) - } else { - return Image(systemName: systemImage) - } + /// Symbol + tint for this file or folder, from ``CodeEditUI/FileIcon``. + var iconSpec: FileIconSpec { + isFolder + ? FileIcon.folderSpec( + isEmpty: isEmptyFolder, + isRoot: parent == nil, + isCodeEditDirectory: name == ".codeedit" + ) + : FileIcon.spec(for: url) } - var nsIcon: NSImage { - if let customImage = NSImage.symbol(named: systemImage) { - return customImage - } else { - return NSImage(systemSymbolName: systemImage, accessibilityDescription: systemImage) - ?? NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")! - } - } - - var iconColor: Color { - FileIcon.iconColor(fileType: type) - } - - var systemImage: String { - if isFolder { - if self.parent == nil { return "folder.fill.badge.gearshape" } - if self.name == ".codeedit" { return "folder.fill.badge.gearshape" } - return isEmptyFolder ? "folder" : "folder.fill" - } else { - return FileIcon.fileIcon(fileType: type) - } - } + var icon: Image { iconSpec.image } + var nsIcon: NSImage { iconSpec.nsImage } + var iconColor: Color { iconSpec.color } } diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift b/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift deleted file mode 100644 index ab8fb6bf93..0000000000 --- a/Packages/Features/CEEditor/Sources/CEEditor/Models/FileIcon.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// FileIcon.swift -// Editor -// -// Created by Nanashi Li on 2022/05/20. -// - -import SwiftUI -import CodeEditCore - -enum FileIcon { - - // swiftlint:disable:next cyclomatic_complexity function_body_length - static func fileIcon(fileType: FileType?) -> String { - switch fileType { - case .json, .yml, .resolved: - return "doc.json" - case .lock: - return "lock.doc" - case .css: - return "curlybraces" - case .js, .mjs: - return "doc.javascript" - case .jsx, .tsx: - return "atom" - case .swift: - return "swift" - case .env, .example: - return "gearshape.fill" - case .gitignore: - return "arrow.triangle.branch" - case .pdf, .png, .jpg, .jpeg, .ico: - return "photo" - case .svg: - return "square.fill.on.circle.fill" - case .entitlements: - return "checkmark.seal" - case .plist: - return "tablecells" - case .md, .txt: - return "doc.plaintext" - case .rtf: - return "doc.richtext" - case .html: - return "chevron.left.forwardslash.chevron.right" - case .LICENSE: - return "key.fill" - case .java: - return "cup.and.saucer" - case .py: - return "doc.python" - case .rb: - return "doc.ruby" - case .strings: - return "text.quote" - case .h: - return "h.square" - case .m: - return "m.square" - case .vue: - return "v.square" - case .go: - return "g.square" - case .sum: - return "s.square" - case .mod: - return "m.square" - case .bash, .sh, .Makefile, .zsh: - return "terminal" - case .rs: - return "r.square" - case .wav, .mp3, .aif, .mid: - return "speaker.wave.2" - case .avi, .mp4, .mov: - return "film" - case .scpt: - return "applescript" - case .xcconfig: - return "gearshape.2" - case .cetheme: - return "paintbrush" - case .adb, .clj, .cls, .cs, .d, .dart, .elm, .ex, .f95, .fs, .gs, .hs, - .jl, .kt, .l, .lsp, .lua, .mk, .pas, .pl, .scm, .ss: - return "doc.plaintext" - default: - return "doc" - } - } - - static func iconColor(fileType: FileType?) -> Color { // swiftlint:disable:this cyclomatic_complexity - switch fileType { - case .swift, .html: - return .orange - case .java, .jpg, .png, .svg, .ts: - return .blue - case .css: - return .teal - case .js, .mjs, .py, .entitlements, .LICENSE: - return Color("Amber", bundle: .main) - case .json, .resolved, .rb, .strings, .yml: - return Color("Scarlet", bundle: .main) - case .jsx, .tsx: - return .cyan - case .plist, .xcconfig, .sh: - return Color("Steel", bundle: .main) - case .c, .cetheme: - return .purple - case .vue: - return Color(red: 0.255, green: 0.722, blue: 0.514, opacity: 1.0) - case .h: - return Color(red: 0.667, green: 0.031, blue: 0.133, opacity: 1.0) - case .m: - return Color(red: 0.271, green: 0.106, blue: 0.525, opacity: 1.0) - case .go: - return Color(red: 0.02, green: 0.675, blue: 0.757, opacity: 1.0) - case .sum, .mod: - return Color(red: 0.925, green: 0.251, blue: 0.478, opacity: 1.0) - case .Makefile: - return Color(red: 0.937, green: 0.325, blue: 0.314, opacity: 1.0) - case .rs: - return .orange - default: - return Color("Steel", bundle: .main) - } - } -} diff --git a/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/FileIconTests.swift b/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/FileIconTests.swift new file mode 100644 index 0000000000..8e9430da39 --- /dev/null +++ b/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/FileIconTests.swift @@ -0,0 +1,118 @@ +// +// FileIconTests.swift +// CodeEditUIUnitTests +// +// Created by Matthijs Eikelenboom on 05/08/2026. +// + +import XCTest +import SwiftUI +@testable import CodeEditUI + +/// The table itself was proven exhaustively against the pre-existing mapping by a +/// temporary parity test in the app target — see commit `ba5beee2`, which compared +/// every one of the 73 legacy file types for both symbol and colour. These tests +/// guard the behaviour that survives it. +final class FileIconTests: XCTestCase { + + private func spec(_ filename: String) -> FileIconSpec { + FileIcon.spec(for: URL(fileURLWithPath: "/tmp/\(filename)")) + } + + func testRepresentativeExtensions() { + XCTAssertEqual(spec("main.swift").symbol, "swift") + XCTAssertEqual(spec("Package.resolved").symbol, "doc.json") + XCTAssertEqual(spec("script.py").symbol, "doc.python") + XCTAssertEqual(spec("photo.jpg").symbol, "photo") + } + + func testWholeFilenameAndDotfileMatches() { + XCTAssertEqual(spec("LICENSE").symbol, "key.fill") + XCTAssertEqual(spec("Makefile").symbol, "terminal") + XCTAssertEqual(spec(".gitignore").symbol, "arrow.triangle.branch") + XCTAssertEqual(spec(".env").symbol, "gearshape.fill") + XCTAssertEqual(spec(".env.example").symbol, "gearshape.fill") + } + + func testLastExtensionWins() { + XCTAssertEqual(spec("types.d.ts").symbol, "t.square") + } + + func testTheFourDeliberateFixes() { + XCTAssertEqual(spec("app.ts").symbol, "t.square") + XCTAssertEqual(spec("main.c").symbol, "c.square") + XCTAssertEqual(NSColor(spec("a.jpeg").color), NSColor(spec("a.jpg").color)) + XCTAssertEqual(spec("mystery.qqzz").symbol, "doc") + } + + func testKnownPlainTextLanguagesDoNotRegressToBareDoc() { + for ext in ["md", "txt", "kt", "lua", "hs", "dart"] { + XCTAssertEqual(spec("file.\(ext)").symbol, "doc.plaintext", "\(ext) regressed") + } + } + + /// Extensions absent from the table but declared to the system still get a + /// meaningful icon. Only types macOS actually declares are asserted here — + /// `.mkv`, for instance, is undeclared on a stock install (see the test below). + func testSystemFallbackIdentifiesDeclaredTypes() { + XCTAssertEqual(spec("art.heic").symbol, "photo") + XCTAssertEqual(spec("art.webp").symbol, "photo") + XCTAssertEqual(spec("clip.webm").symbol, "film") + XCTAssertEqual(spec("song.flac").symbol, "speaker.wave.2") + XCTAssertEqual(spec("Config.toml").symbol, "doc.plaintext") + } + + /// `UTType(filenameExtension:)` does **not** return nil for unknown extensions — + /// it synthesises a dynamic `dyn.…` type that conforms to nothing. Undeclared + /// extensions therefore fall through the conformance checks to bare `doc`. + func testUndeclaredExtensionsFallThroughToBareDoc() { + for ext in ["mkv", "zig", "qqzz"] { + XCTAssertEqual(spec("file.\(ext)").symbol, "doc", "\(ext) should fall through") + } + } + + func testFolderSpecs() { + XCTAssertEqual( + FileIcon.folderSpec(isEmpty: false, isRoot: true, isCodeEditDirectory: false).symbol, + "folder.fill.badge.gearshape" + ) + XCTAssertEqual( + FileIcon.folderSpec(isEmpty: false, isRoot: false, isCodeEditDirectory: true).symbol, + "folder.fill.badge.gearshape" + ) + XCTAssertEqual( + FileIcon.folderSpec(isEmpty: true, isRoot: false, isCodeEditDirectory: false).symbol, + "folder" + ) + XCTAssertEqual( + FileIcon.folderSpec(isEmpty: false, isRoot: false, isCodeEditDirectory: false).symbol, + "folder.fill" + ) + } + + func testGenericSpec() { + XCTAssertEqual(FileIcon.generic.symbol, "doc") + } + + /// Guards the package resource wiring: if `Resources` is ever dropped from the + /// manifest, every colour silently becomes unresolved rather than failing to build. + func testCustomColorAssetsResolveFromThePackageBundle() { + for name in ["Amber", "Scarlet", "Steel"] { + XCTAssertNotNil( + NSColor(named: name, bundle: .module), + "\(name) missing from the CodeEditUI asset catalog" + ) + } + } + + /// The three custom colours must differ from each other — a failed asset lookup + /// would collapse them to the same fallback and still pass a nil-check. + func testCustomColorsAreDistinct() { + let amber = NSColor(spec("app.js").color) + let scarlet = NSColor(spec("data.json").color) + let steel = NSColor(spec("Info.plist").color) + XCTAssertNotEqual(amber, scarlet) + XCTAssertNotEqual(scarlet, steel) + XCTAssertNotEqual(amber, steel) + } +} From 2715e319968af318c8039050176a747dd3a9d68d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 5 Aug 2026 20:36:15 +0200 Subject: [PATCH 228/335] Fix: Match extension-visibility preferences on the real file extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit labelFileName() compared FileType.rawValue against the user's extension lists, which failed silently twice: the raw value for .txt was "text", so entering "txt" never matched, and any extension absent from the enum fell back to .txt and reported itself as "text" — so the preference did nothing for .toml, .zig, .kt and every other unenumerated type. Extensionless names (LICENSE, Makefile, .gitignore) now compare as "" rather than matching their enum case, which has no visible effect: fileName(typeHidden:) strips via deletingPathExtension, a no-op for those names either way. --- .../Workspace/Files/CEWorkspaceFile+Presentation.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift index eb09f814e0..33877fb19d 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift @@ -43,6 +43,11 @@ extension CEWorkspaceFile { // MARK: Display name (user preference driven) /// A display name honoring the user's file-extension-visibility preference. + /// + /// Matches on the file's real extension. It used to compare `type.rawValue`, which + /// silently failed twice over: the raw value for `.txt` was `"text"`, so a user + /// entering `txt` never matched, and any extension absent from the `FileType` enum + /// fell back to `.txt` and so reported itself as `"text"`. func labelFileName() -> String { let prefs = Settings.shared.preferences.general switch prefs.fileExtensionsVisibility { @@ -51,9 +56,9 @@ extension CEWorkspaceFile { case .showAll: return self.fileName(typeHidden: false) case .showOnly: - return self.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(self.type.rawValue)) + return self.fileName(typeHidden: !prefs.shownFileExtensions.extensions.contains(url.pathExtension)) case .hideOnly: - return self.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(self.type.rawValue)) + return self.fileName(typeHidden: prefs.hiddenFileExtensions.extensions.contains(url.pathExtension)) } } From c16d87485aaae4ee57fdc80e12cad0fa4a39426a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 5 Aug 2026 20:38:41 +0200 Subject: [PATCH 229/335] Refactor: Retire the FileType enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FileType was an 80-case enum of file extensions living in CodeEditCore, the framework-free domain package. Its only consumers were FileIcon and labelFileName() — both presentation. That misplacement is what made the icon duplication hard to fix: the icon code needed Core solely for a type that was never domain, and CodeEditUI is barred from importing Core. With icons keyed on URL and preferences matching url.pathExtension, nothing needs it. CEWorkspaceFile.type goes too, along with the per-access filename string parsing it did on every read. testTypeDefaultsToTxt covered the enum's fallback; the behaviour it guarded is now covered by FileIconTests.testUndeclaredExtensionsFallThroughToBareDoc in CodeEditUIUnitTests. testNameAndType keeps its name assertion as testName. --- .../Domain/Workspace/CEWorkspaceFile.swift | 11 ---------- .../Domain/Workspace/FileType.swift | 21 ------------------- .../CEWorkspaceFileCoreTests.swift | 11 +++++----- 3 files changed, 5 insertions(+), 38 deletions(-) delete mode 100644 Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift index c3e58fd1b2..d1d0f1c3cc 100644 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift +++ b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift @@ -18,17 +18,6 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable /// Returns the file name (e.g.: `Package.swift`) public var name: String { url.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) } - /// The file's ``FileType`` derived from its extension (defaults to `.txt`). - public var type: FileType { - let filename = url.lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) - if let type = FileType(rawValue: filename) { - return type - } else { - let extensions = filename.dropFirst().components(separatedBy: ".").reversed() - return extensions.compactMap { FileType(rawValue: $0) }.first ?? .txt - } - } - /// Returns the URL of the ``CEWorkspaceFile`` public let url: URL diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift b/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift deleted file mode 100644 index 05e92dd555..0000000000 --- a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/FileType.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FileType.swift -// CodeEditCore -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -import Foundation - -// swiftlint:disable identifier_name -/// File-type discriminator derived from a file's extension. -public enum FileType: String { - case adb, aif, avi, bash, c, cetheme, clj, cls, cs, css, d, dart, elm, entitlements - case env, ex, example, f95, fs, gitignore, go, gs, h, hs, html, ico, java, jl, jpeg - case jpg, js, json, jsx, kt, l, LICENSE, lock, lsp, lua, m, Makefile, md, mid, mjs - case mk, mod, mov, mp3, mp4, pas, pdf, pl, plist, png, py, resolved, rb, rs, rtf, scm - case scpt, sh, ss, strings, sum, svg, swift, ts, tsx - case txt = "text" - case vue, wav, xcconfig, yml, zsh -} -// swiftlint:enable identifier_name diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift index dd114434f5..70a1613cf1 100644 --- a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift +++ b/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift @@ -10,16 +10,15 @@ import CodeEditCore final class CEWorkspaceFileCoreTests: XCTestCase { - func testNameAndType() { + func testName() { let file = CEWorkspaceFile(url: URL(filePath: "/tmp/Package.swift")) XCTAssertEqual(file.name, "Package.swift") - XCTAssertEqual(file.type, .swift) } - func testTypeDefaultsToTxt() { - let file = CEWorkspaceFile(url: URL(filePath: "/tmp/no-extension-here")) - XCTAssertEqual(file.type, .txt) - } + // `testTypeDefaultsToTxt` lived here to cover the `FileType` fallback. That enum was + // presentation, not domain, and is gone; the behaviour it guarded — an unrecognised + // extension still resolving to something sensible — is covered by + // `FileIconTests.testUndeclaredExtensionsFallThroughToBareDoc` in CodeEditUIUnitTests. func testFileNameTypeHidden() { let file = CEWorkspaceFile(url: URL(filePath: "/tmp/Model.swift")) From 5cace8de92bca270a2e85622a54301b9b247efbd Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 5 Aug 2026 20:40:07 +0200 Subject: [PATCH 230/335] Docs: Record the CodeEditUI charter's local-package-only weakness Both guardrails constrain local packages only, so CodeEditUI is barred from the zero-dependency CodeEditCore while an arbitrary external dependency would pass. Found while deduplicating FileIcon, which sidesteps the loophole by keying on URL rather than a domain type. Flags the asymmetry so the next reader does not treat it as a licence. --- docs/ARCHITECTURE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c3be09950a..a89cff4ecf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -187,6 +187,22 @@ swiftlint lint --quiet python3 .github/scripts/audit_package_imports.py ``` +### Known weakness in the CodeEditUI charter (2026-08-05) + +Both checks constrain **local** packages only. `ui_package_purity` lists sibling module names in +a regex, and the audit script inspects `local_deps`. So `CodeEditUI` is barred from importing +`CodeEditCore` — a zero-dependency, pure-types package — while nothing stops it taking an +arbitrary *external* dependency, up to and including a tree-sitter grammar bundle. The rule as +written is narrower than its own stated intent ("presentation atoms must not know about models +or features") in one direction and far wider in the other. + +This surfaced while deduplicating `FileIcon`, which is presentation keyed by a file's identity. +It was designed to take a `URL` rather than a domain type — so it needs neither `CodeEditCore` +nor the loophole — and its three custom colorsets moved into the package as resources, making +`CodeEditUI` self-contained and letting its tests assert colours without an app host. Treat the +asymmetry as a known weakness, not a licence: adding an external dependency to `CodeEditUI` to +sidestep the local-package rule would satisfy the letter of the charter and defeat its purpose. + ## Glossary Several words are overloaded in this codebase. These are the intended meanings; prefer the From 69962796df64a81572c17dad35e24a71d5f20019 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 5 Aug 2026 21:11:49 +0200 Subject: [PATCH 231/335] Fix: Reload the Project Navigator when file-extension preferences change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fileExtensionsVisibility, shownFileExtensions and hiddenFileExtensions were pushed into ProjectNavigatorViewController on every settings change and read by nothing — write-only properties with no observers. Row labels come from labelFileName(), called only when a cell is built in outlineView(_:viewFor:), so without a reload every visible label kept the text it was born with and the preference appeared to do nothing at all, including "Hide all". Adds the willSet/reloadData observers that iconColor and rowHeight already have three lines above, which is why those two refresh live and these did not. Also adds the regression coverage labelFileName() never had: all four visibility modes, plus the two cases fixed in 2715e319 — "txt" being matchable at all, and extensions absent from the old FileType enum (toml) being matchable. --- .../ProjectNavigatorViewController.swift | 30 ++++++- .../FileExtensionVisibilityTests.swift | 86 +++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index ba11cdbe98..192a9268af 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -53,9 +53,33 @@ final class ProjectNavigatorViewController: NSViewController { } } - var fileExtensionsVisibility: SettingsData.FileExtensionsVisibility = .showAll - var shownFileExtensions: SettingsData.FileExtensions = .default - var hiddenFileExtensions: SettingsData.FileExtensions = .default + // These three drive `CEWorkspaceFile.labelFileName()`, which is read when a cell is built. + // Cells are only built by `outlineView(_:viewFor:)`, so without a reload a preference change + // leaves every visible label showing the text it was born with — the same reason `iconColor` + // and `rowHeight` reload below. + var fileExtensionsVisibility: SettingsData.FileExtensionsVisibility = .showAll { + willSet { + if newValue != fileExtensionsVisibility { + outlineView?.reloadData() + } + } + } + + var shownFileExtensions: SettingsData.FileExtensions = .default { + willSet { + if newValue != shownFileExtensions { + outlineView?.reloadData() + } + } + } + + var hiddenFileExtensions: SettingsData.FileExtensions = .default { + willSet { + if newValue != hiddenFileExtensions { + outlineView?.reloadData() + } + } + } var rowHeight: Double = 22 { willSet { diff --git a/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift b/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift new file mode 100644 index 0000000000..2a69ef1167 --- /dev/null +++ b/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift @@ -0,0 +1,86 @@ +// +// FileExtensionVisibilityTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 05/08/2026. +// + +import XCTest +import CodeEditCore +import CodeEditSettings +@testable import CodeEdit + +/// Covers `CEWorkspaceFile.labelFileName()`, which had no tests despite driving every +/// Project Navigator row label. Mutates the `Settings.shared` singleton, so the original +/// general settings are restored in `tearDown`. +final class FileExtensionVisibilityTests: XCTestCase { + + private var original: SettingsData.GeneralSettings! + + override func setUp() { + super.setUp() + original = Settings.shared.preferences.general + } + + override func tearDown() { + Settings.shared.preferences.general = original + original = nil + super.tearDown() + } + + private func label(_ filename: String) -> String { + CEWorkspaceFile(url: URL(filePath: "/tmp/\(filename)")).labelFileName() + } + + func testShowAllKeepsEveryExtension() { + Settings.shared.preferences.general.fileExtensionsVisibility = .showAll + XCTAssertEqual(label("notes.txt"), "notes.txt") + XCTAssertEqual(label("Model.swift"), "Model.swift") + } + + func testHideAllStripsEveryExtension() { + Settings.shared.preferences.general.fileExtensionsVisibility = .hideAll + XCTAssertEqual(label("notes.txt"), "notes") + XCTAssertEqual(label("Model.swift"), "Model") + } + + func testShowOnlyKeepsListedAndStripsTheRest() { + Settings.shared.preferences.general.fileExtensionsVisibility = .showOnly + Settings.shared.preferences.general.shownFileExtensions.extensions = ["swift"] + XCTAssertEqual(label("Model.swift"), "Model.swift") + XCTAssertEqual(label("notes.txt"), "notes") + } + + func testHideOnlyStripsListedAndKeepsTheRest() { + Settings.shared.preferences.general.fileExtensionsVisibility = .hideOnly + Settings.shared.preferences.general.hiddenFileExtensions.extensions = ["swift"] + XCTAssertEqual(label("Model.swift"), "Model") + XCTAssertEqual(label("notes.txt"), "notes.txt") + } + + /// Regression for 2715e319. Matching used to compare `FileType.rawValue`, whose value for + /// `.txt` was the string `"text"` — so entering `txt` never matched anything. + func testTxtIsMatchableByItsRealExtension() { + Settings.shared.preferences.general.fileExtensionsVisibility = .showOnly + Settings.shared.preferences.general.shownFileExtensions.extensions = ["txt"] + XCTAssertEqual(label("notes.txt"), "notes.txt") + XCTAssertEqual(label("Model.swift"), "Model") + } + + /// Regression for 2715e319. Extensions absent from the old `FileType` enum all fell back to + /// `.txt` and reported themselves as `"text"`, so the preference could never match them. + func testExtensionsAbsentFromTheOldEnumAreMatchable() { + Settings.shared.preferences.general.fileExtensionsVisibility = .hideOnly + Settings.shared.preferences.general.hiddenFileExtensions.extensions = ["toml"] + XCTAssertEqual(label("Config.toml"), "Config") + XCTAssertEqual(label("notes.txt"), "notes.txt") + } + + func testExtensionlessNamesAreUnaffected() { + for mode in [SettingsData.FileExtensionsVisibility.hideAll, .showAll] { + Settings.shared.preferences.general.fileExtensionsVisibility = mode + XCTAssertEqual(label("LICENSE"), "LICENSE", "mode \(mode)") + XCTAssertEqual(label("Makefile"), "Makefile", "mode \(mode)") + } + } +} From 6f8d90fd596cb057548ecdc5428b7529b0f190c7 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 7 Aug 2026 17:15:40 +0200 Subject: [PATCH 232/335] Docs: Record why the 2022 CodeEditModules split failed --- docs/ARCHITECTURE.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a89cff4ecf..dd2f9b4b95 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -30,6 +30,48 @@ All local packages build with Swift 6 strict concurrency. The app target is stil write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't isolated (it cascades). +## History: why the 2022 module split failed + +The project already tried a single multi-target `CodeEditModules` package. It was deleted on +2022-12-03 (commit `4858de16`) after repeated cyclic-dependency problems, and everything moved into +the app target. **The cycles were not caused by the packaging shape.** The 2022 manifest's own edges +explain them: + +| Cause | Evidence in the 2022 manifest | +|---|---| +| **No kernel existed** | No framework-free contracts target. Shared types lived in whichever module happened to own them, so "A and B both need X" was only expressible as a feature→feature edge. | +| **Domain depended on UI** | `WorkspaceClient → TabBar` | +| **Shared UI depended on domain** | `CodeEditUI → WorkspaceClient, Git` | +| **A god-module hub** | `AppPreferences → CodeEditUI, Git, Keybindings, CodeEditUtils, Sparkle, CodeEditTextView`, itself depended on by half the tree | + +With the bottom of the graph pointing up into the top, cycles were the steady state rather than an +accident. The same manifest split across eleven separate packages fails identically — SwiftPM refuses +to resolve a cyclic graph either way. + +**`CodeEditCore` is the fix, and it now exists.** Zero dependencies, framework-free, holding domain +values, the typed `EventBus`, and the cross-feature command interfaces. Every "A and B both need X" +now resolves *downward*. Both 2022 killers are structurally impossible today: the domain lives in +`CodeEditCore`, which may import nothing, and `CodeEditUI → Git` is blocked by the `CodeEditUI` +purity rule. + +### Cycle-resolution playbook + +Detection was never the problem — SwiftPM refuses a cyclic target graph as a hard error. The 2022 +failure was that detection had no accompanying *resolution* technique, so the exit taken was to +collapse everything into the app target. When you hit a cycle, the legal moves, in preference order: + +1. **Push the shared thing down to `CodeEditCore`** — a protocol, an event, or a value type. This is + what `EventBus` and the command interfaces (`WorkspaceNavigator`, `TasksConfigurationProviding`, …) + are for. The default answer. +2. **Push the coordination up to the app target** — the app may depend on everything. Two leaf + features never need to know each other if a doer wires them (`WorkspaceOpener`, `DocumentOpener`). +3. **Merge the two targets** — if A and B genuinely will not separate, the boundary was drawn wrong. + Merging is a correct outcome, not a defeat; in one package it is a folder move plus a three-line + manifest edit. + +Never resolve a cycle by moving code back into the app target. That is what happened in 2022 and it +cost four years of enforced boundaries. + ## Tier charters | Tier | Package(s) | May depend on | Must never contain | From 6b8f7544da28e8be71ed908c9154af57bf0f8cc6 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 7 Aug 2026 17:21:36 +0200 Subject: [PATCH 233/335] Docs: Reduce package rules to three enforced checks and two norms --- .swiftlint.yml | 7 --- docs/ARCHITECTURE.md | 126 +++++++++++++++++++++++-------------------- 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 8dac768e36..c03f609b85 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -45,13 +45,6 @@ custom_rules: regex: "^import (SwiftUI|AppKit|Cocoa)$" message: "CodeEditCore must stay UI-free — move UI-coupled code to CodeEditUI or the owning feature (see docs/ARCHITECTURE.md)" severity: error - no_feature_to_feature: - included: "Packages/Features/.*\\.swift" - excluded: "Packages/Features/[^/]*/Tests/.*\\.swift" - name: "No feature→feature imports" - regex: "^import (CEEditor|CESearch|CENotifications|CELSP|CESourceControl|CETerminal)$" - message: "Feature packages may not import each other — communicate via CodeEditCore events or command interfaces (see docs/ARCHITECTURE.md)" - severity: error ui_package_purity: included: "Packages/Foundation/CodeEditUI/.*\\.swift" name: "CodeEditUI charter" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index dd2f9b4b95..534f6fe28c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -72,34 +72,55 @@ collapse everything into the app target. When you hit a cycle, the legal moves, Never resolve a cycle by moving code back into the app target. That is what happened in 2022 and it cost four years of enforced boundaries. -## Tier charters - -| Tier | Package(s) | May depend on | Must never contain | -|---|---|---|---| -| Foundation | CodeEditCore | *nothing* | UI or I/O framework imports (SwiftUI/AppKit), external deps | -| Foundation | CodeEditUI | CodeEditSymbols only | feature semantics, model/service imports | -| Foundation | CodeEditDocument | CodeEditCore + editor libraries | app-tier types | -| Foundation | CodeEditSettings | CodeEditCore | settings *pages* (those are app-side composition UI) | -| Services | CodeEditServices targets | CodeEditCore only | UI imports, sibling service targets | -| Features | `CE*` packages | Foundation tiers + external libraries | **other `CE*` feature packages** | -| App | CodeEdit target | everything | — (it's the composition layer, not the default dumping ground) | +## Rules + +Three checks are enforced in CI. Each one blocks a specific failure documented in +[History](#history-why-the-2022-module-split-failed) — none is enforced on principle alone. + +1. **`CodeEditCore` purity.** Zero dependencies, local or external. No `SwiftUI`, `AppKit`, or + `Cocoa` import. Blocks 2022's `WorkspaceClient → TabBar`. Keep it platform-free too: it is the one + target that would port to iPadOS unchanged. +2. **`CodeEditUI` purity.** No local target dependencies; external `CodeEditSymbols` only. Blocks + 2022's `CodeEditUI → Git`. This is why `FileIcon` is keyed on `URL` rather than on a domain type — + a deliberate consequence, not an accident. +3. **Import honesty.** Every `import` in a target's sources must be declared in that target's + manifest dependencies. Xcode workspace builds share one build directory, so an undeclared import + of a sibling compiles fine and only breaks a standalone `swift build`. + +Plus one assertion: **only `CEEditor` may declare `.swiftLanguageMode(.v5)`.** Every other target +inherits Swift 6 from the package's tools version. A target silently dropping to Swift 5 would lose +strict-concurrency enforcement without anything failing. + +### Norms (review-time, not gates) + +**Prefer features to be leaves.** Nothing should depend on a feature target. When an edge between two +features is genuinely needed, try the three +[cycle-resolution moves](#cycle-resolution-playbook) first, then declare the edge in the manifest +where it is visible to everyone. Acyclicity itself needs no rule — SwiftPM enforces it. + +**Hub heuristic.** Any target both depended on by three or more others *and* itself depending on +three or more is a hub under review. 2022's `AppPreferences` was exactly this and would have been +flagged years before it became fatal. `CodeEditSettings` is the current watch item: four dependents, +and it imports `AppKit` in 3 files and `SwiftUI` in 9. ## Where does my code go? Work through these in order; the first match wins. -1. **A new user-facing feature?** → A new `Packages/Features/CE` package (see the - [recipe](#creating-a-new-feature-package)). Features start as packages; the app target is +1. **A new user-facing feature?** → A new target at `CodeEditModules/Sources/CE` (see the + [recipe](#creating-a-new-feature-target)). Features start as targets; the app target is not the default. Exception: *shell chrome* that composes multiple features around the concrete `Workspace` hub — navigator/inspector/utility areas, the status bar — stays app-side, because its interface would effectively be "the whole app". 2. **A type, protocol, event, or command interface needed by two or more features?** → - `CodeEditCore`, *if* it passes the charter (no UI/IO imports, no external dependencies). - Events (facts, e.g. `TaskNotificationEvent`) and command interfaces (requests with exactly - one handler, e.g. `WorkspaceNavigator`) always live here. -3. **A reusable view, style, or view modifier with no feature semantics?** → `CodeEditUI`. -4. **A service that performs I/O and has no UI?** → A new target in `CodeEditServices` - (Core-only dependencies, its own library product, the app links it directly). + `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI/IO imports, no + external dependencies). Events (facts, e.g. `TaskNotificationEvent`) and command interfaces + (requests with exactly one handler, e.g. `WorkspaceNavigator`) always live here. +3. **A reusable view, style, or view modifier with no feature semantics?** → + `CodeEditModules/Sources/CodeEditUI`. +4. **A service that performs I/O and has no UI?** → A new target at + `CodeEditModules/Sources/` (Core-only dependencies, its own library product, the + app links it directly). 5. **Cross-service orchestration?** → A doer-style role-noun class in the feature that owns the operation (`WorkspaceOpener`, `FileMover`, `RepositoryCloner` — the `NSFileCoordinator` naming idiom). One doer per operation that touches more than one service; dependencies @@ -166,47 +187,34 @@ Grouping is **purpose-first**: (the presentation-state split), and views issue commands through protocol-typed environment keys. -## Creating a new feature package +## Creating a new feature target -1. Create `Packages/Features/CE/Package.swift`: +1. Create the folder `CodeEditModules/Sources/CE/` and add a target and product for it in + `CodeEditModules/Package.swift`: ```swift - // swift-tools-version: 6.0 - - import PackageDescription + .library(name: "CE", targets: ["CE"]), + ``` - let package = Package( + ```swift + .target( name: "CE", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CE", targets: ["CE"]) - ], dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditUI") - ], - targets: [ - .target( - name: "CE", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditUI", package: "CodeEditUI") - ] - ) + "CodeEditCore", + "CodeEditUI" ] - ) + ), ``` -2. Add the package to the workspace: in Xcode, drag the folder into the **Features** group of - the workspace navigator (or add a `FileRef` to `CodeEdit.xcworkspace/contents.xcworkspacedata`). -3. Link the product to the app: CodeEdit target → *General* → *Frameworks, Libraries, and +2. Link the product to the app: CodeEdit target → *General* → *Frameworks, Libraries, and Embedded Content* → add `CE`. -4. Remember the package builds with **Swift 6 strict concurrency** — types crossing actor - boundaries need `Sendable`, and UI-bound classes are usually `@MainActor`. -5. Known quirk: packages that depend on `CodeEditSymbols` build via Xcode/xcodebuild only — +3. Remember the target builds with **Swift 6 strict concurrency** — types crossing actor + boundaries need `Sendable`, and UI-bound classes are usually `@MainActor`. (`CEEditor` is the + sole exception — see [Rules](#rules).) +4. Known quirk: targets that depend on `CodeEditSymbols` build via Xcode/xcodebuild only — standalone `swift build` fails on its `Bundle.module` resolution. -6. Declare **every** module you import in the manifest. The workspace's shared build directory - makes undeclared imports of sibling packages compile by accident — CI will catch it +5. Declare **every** module you import in the manifest. The workspace's shared build directory + makes undeclared imports of sibling targets compile by accident — CI will catch it (see below). ## Enforcement @@ -214,13 +222,14 @@ Grouping is **purpose-first**: Two automated checks keep this document honest; both run on every PR: - **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`) — includes custom rules - that reject UI imports in CodeEditCore, feature→feature imports, and model/feature imports - in CodeEditUI. These fire inside Xcode while you type. -- **The package audit** (`.github/scripts/audit_package_imports.py`) — verifies every - `import` in every package is declared in that package's manifest, and that the tier rules - in the charter table hold. It exists because Xcode workspace builds share one build - directory, so an undeclared import of a sibling package compiles fine locally and the - violation stays invisible until a standalone build breaks. + that reject UI imports in CodeEditCore and model/feature imports in CodeEditUI. These fire + inside Xcode while you type. +- **The package audit** (`.github/scripts/audit_package_imports.py`) — verifies every `import` + in every target is declared in that target's manifest dependencies, and that the three + [Rules](#rules) plus the language-mode assertion hold. It exists because Xcode workspace + builds share one build directory, so an undeclared import of a sibling target compiles fine + locally and the violation stays invisible until a standalone build breaks. Every target is + declared in `CodeEditModules/Package.swift`. Run both locally from the repo root: @@ -231,8 +240,9 @@ python3 .github/scripts/audit_package_imports.py ### Known weakness in the CodeEditUI charter (2026-08-05) -Both checks constrain **local** packages only. `ui_package_purity` lists sibling module names in -a regex, and the audit script inspects `local_deps`. So `CodeEditUI` is barred from importing +Both checks constrain **local** targets only. `ui_package_purity` lists sibling module names in +a regex, and the audit script intersects the target's declared dependencies with the package's +library targets. So `CodeEditUI` is barred from importing `CodeEditCore` — a zero-dependency, pure-types package — while nothing stops it taking an arbitrary *external* dependency, up to and including a tree-sitter grammar bundle. The rule as written is narrower than its own stated intent ("presentation atoms must not know about models From 38b386fd54631697f958010d156c92528ad3c322 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 7 Aug 2026 17:53:40 +0200 Subject: [PATCH 234/335] Refactor: Consolidate eleven local packages into CodeEditModules --- .swiftlint.yml | 6 +- CodeEdit.xcworkspace/contents.xcworkspacedata | 48 +--- CodeEditModules/Package.resolved | 213 ++++++++++++++++++ CodeEditModules/Package.swift | 134 +++++++++++ .../CEEditor/CEWorkspaceFile+Editor.swift | 0 .../CEEditor/Environment+SplitEditor.swift | 0 .../Views/EditorJumpBarComponent.swift | 0 .../JumpBar/Views/EditorJumpBarMenu.swift | 0 .../JumpBar/Views/EditorJumpBarView.swift | 0 .../Models/AppActiveCursorState.swift | 0 .../Models/AppActiveEditorState.swift | 0 .../Models/AppFileEditorOverrides.swift | 0 .../CEEditor/Models/DocumentRegistry.swift | 0 .../Models/Editor/Editor+History.swift | 0 .../Models/Editor/Editor+TabSwitch.swift | 0 .../CEEditor/Models/Editor/Editor.swift | 0 .../CEEditor/Models/EditorInstance.swift | 0 .../EditorLayout+StateRestoration.swift | 0 .../Models/EditorLayout/EditorLayout.swift | 0 .../CEEditor/Models/EditorManager.swift | 0 .../Models/Environment+ActiveEditor.swift | 0 .../Environment+WorkspaceFileProvider.swift | 0 .../Environment+WorkspaceNavigator.swift | 0 .../Restoration/EditorStateRestoration.swift | 0 .../Restoration/UndoManagerRegistration.swift | 0 .../CEEditor/Models/Theme+EditorTheme.swift | 0 .../Sources/CEEditor/SplitViewData.swift | 0 .../Tabs/Tab/EditorFileTabCloseButton.swift | 0 .../TabBar/Tabs/Tab/EditorTabBackground.swift | 0 .../Tabs/Tab/EditorTabButtonStyle.swift | 0 .../Tabs/Tab/EditorTabCloseButton.swift | 0 .../TabBar/Tabs/Tab/EditorTabView.swift | 0 .../Tab/Models/EditorTabFileObserver.swift | 0 .../Tab/Models/EditorTabRepresentable.swift | 0 .../Tabs/Views/EditorTabOnDropDelegate.swift | 0 .../Tabs/Views/EditorTabs+DragGesture.swift | 0 .../TabBar/Tabs/Views/EditorTabs.swift | 0 .../Tabs/Views/EditorTabsOverflowShadow.swift | 0 .../TabBar/Views/EditorHistoryMenus.swift | 0 .../TabBar/Views/EditorTabBarAccessory.swift | 0 .../Views/EditorTabBarContextMenu.swift | 0 .../TabBar/Views/EditorTabBarDivider.swift | 0 .../EditorTabBarLeadingAccessories.swift | 0 .../EditorTabBarTrailingAccessories.swift | 0 .../TabBar/Views/EditorTabBarView.swift | 0 .../CEEditor/UseCases/EditorRestorer.swift | 0 .../Sources/CEEditor/Views/AnyFileView.swift | 0 .../Sources/CEEditor/Views/CodeFileView.swift | 0 .../CEEditor/Views/EditorAreaFileView.swift | 0 .../CEEditor/Views/EditorAreaView.swift | 0 .../CEEditor/Views/EditorLayoutView.swift | 0 .../Views/Environment+LanguageServices.swift | 0 .../CEEditor/Views/FilePreviewView.swift | 0 .../CEEditor/Views/ImageFileView.swift | 0 .../CEEditor/Views/LoadingFileView.swift | 0 .../CEEditor/Views/NonTextFileView.swift | 0 .../Sources/CEEditor/Views/PDFFileView.swift | 0 .../CEEditor/Views/WindowCodeFileView.swift | 0 ...eFileDocument+LanguageServerDocument.swift | 0 .../DocumentSync/LSPContentCoordinator.swift | 0 .../SemanticTokenHighlightProvider.swift | 0 .../SemanticTokens/SemanticTokenMap.swift | 0 .../SemanticTokenMapRangeProvider.swift | 0 .../GenericSemanticTokenStorage.swift | 0 .../SemanticTokenRange.swift | 0 .../SemanticTokenStorage.swift | 0 .../Sources/CELSP/LSPUtil.swift | 0 .../LanguageServer+CallHierarchy.swift | 0 .../LanguageServer+ColorPresentation.swift | 0 .../LanguageServer+Completion.swift | 0 .../LanguageServer+Declaration.swift | 0 .../LanguageServer+Definition.swift | 0 .../LanguageServer+Diagnostics.swift | 0 .../LanguageServer+DocumentColor.swift | 0 .../LanguageServer+DocumentHighlight.swift | 0 .../LanguageServer+DocumentLink.swift | 0 .../LanguageServer+DocumentSymbol.swift | 0 .../LanguageServer+DocumentSync.swift | 0 .../LanguageServer+FoldingRange.swift | 0 .../LanguageServer+Formatting.swift | 0 .../Capabilities/LanguageServer+Hover.swift | 0 .../LanguageServer+Implementation.swift | 0 .../LanguageServer+InlayHint.swift | 0 .../LanguageServer+References.swift | 0 .../Capabilities/LanguageServer+Rename.swift | 0 .../LanguageServer+SelectionRange.swift | 0 .../LanguageServer+SemanticTokens.swift | 0 .../LanguageServer+SignatureHelp.swift | 0 .../LanguageServer+TypeDefinition.swift | 0 .../CELSP/LanguageServer/LSPCache+Data.swift | 0 .../CELSP/LanguageServer/LSPCache.swift | 0 .../CELSP/LanguageServer/LanguageServer.swift | 0 .../LanguageServerFileMap.swift | 0 .../CELSP/LanguageServerDocument.swift | 0 .../Registry/Errors/PackageManagerError.swift | 0 .../Errors/RegistryManagerError.swift | 0 .../InstallationMethod+PackageManager.swift | 0 .../Registry/Model/InstallationMethod.swift | 0 .../Registry/Model/PackageManagerType.swift | 0 .../CELSP/Registry/Model/PackageSource.swift | 0 .../Model/RegistryItem+InstallMethod.swift | 0 .../Registry/PackageManagerProtocol.swift | 0 .../FileManager+MakeExecutable.swift | 0 .../Install/InstallStepConfirmation.swift | 0 .../PackageManagerInstallOperation.swift | 0 .../Install/PackageManagerInstallStep.swift | 0 .../Install/PackageManagerProgressModel.swift | 0 .../Sources/CargoPackageManager.swift | 0 .../Sources/GithubPackageManager.swift | 0 .../Sources/GolangPackageManager.swift | 0 .../Sources/NPMPackageManager.swift | 0 .../Sources/PipPackageManager.swift | 0 .../PackageSourceParser+Cargo.swift | 0 .../PackageSourceParser+Gem.swift | 0 .../PackageSourceParser+Golang.swift | 0 .../PackageSourceParser+NPM.swift | 0 .../PackageSourceParser+PYPI.swift | 0 .../PackageSourceParser.swift | 0 .../Registry/Protocols/RegistryManaging.swift | 0 .../Registry/RegistryItemTemplateParser.swift | 0 .../RegistryManager+HandleRegistryFile.swift | 0 .../CELSP/Registry/RegistryManager.swift | 0 .../CELSP/Registry/RegistryViewState.swift | 0 .../Service/AppLanguageServicesProvider.swift | 0 .../CELSP/Service/LSPService+Events.swift | 0 .../Sources/CELSP/Service/LSPService.swift | 0 .../CELSP/Service/LSPServiceError.swift | 0 .../CELSP/Service/LSPServiceProtocol.swift | 0 .../Service/LanguageServerListState.swift | 0 .../Service/LanguageServerLogContainer.swift | 0 .../LanguageIdentifier+CodeLanguage.swift | 0 .../CELSP/Utils/SemanticToken+Position.swift | 0 .../CELSP/Utils/TextView+LSPRange.swift | 0 .../TextView+SemanticTokenRangeProvider.swift | 0 .../Sources/CELSP/Utils/URL+LSPURI.swift | 0 .../Environment+NotificationManager.swift | 0 .../Models/CENotification.swift | 0 .../NotificationManager+Delegate.swift | 0 .../NotificationManager+System.swift | 0 .../CENotifications/NotificationManager.swift | 0 .../Protocols/NotificationManaging.swift | 0 ...nPanelViewModel+NotificationHandling.swift | 0 ...cationPanelViewModel+TimerManagement.swift | 0 ...otificationPanelViewModel+Visibility.swift | 0 .../NotificationPanelViewModel.swift | 0 .../Views/NotificationBannerView.swift | 0 .../Views/NotificationPanelView.swift | 0 .../Views/NotificationToolbarItem.swift | 0 .../Environment+WorkspaceFileOpener.swift | 0 .../CESearch/Extensions/Array+Index.swift | 0 .../FindNavigator/FindModePicker.swift | 0 .../FindNavigatorConfiguration.swift | 0 .../FindNavigator/FindNavigatorForm.swift | 0 .../FindNavigator/FindNavigatorIndexBar.swift | 0 .../FindNavigatorListViewController.swift | 0 .../FindNavigatorMatchListCell.swift | 0 .../FindNavigatorResultList.swift | 0 .../SearchResultFileCell.swift | 0 .../FindNavigatorToolbarBottom.swift | 0 .../FindNavigator/FindNavigatorView.swift | 0 .../CESearch/Indexer/AsyncFileIterator.swift | 0 .../Sources/CESearch/Indexer/FileHelper.swift | 0 .../CESearch/Indexer/SearchIndexer+Add.swift | 0 .../SearchIndexer+AsyncController.swift | 0 .../CESearch/Indexer/SearchIndexer+File.swift | 0 .../SearchIndexer+InternalMethods.swift | 0 .../Indexer/SearchIndexer+Memory.swift | 0 .../SearchIndexer+ProgressiveSearch.swift | 0 .../Indexer/SearchIndexer+Search.swift | 0 .../Indexer/SearchIndexer+Terms.swift | 0 .../CESearch/Indexer/SearchIndexer.swift | 0 .../CESearch/Model/SearchModeModel.swift | 0 .../CESearch/Model/SearchResultFile.swift | 0 .../Model/SearchResultMatchModel.swift | 0 .../CESearch/Model/SearchResultModel.swift | 0 .../SearchState/SearchState+Find.swift | 0 .../SearchState+FindAndReplace.swift | 0 .../SearchState/SearchState+Index.swift | 0 .../SearchState+MatchExtraction.swift | 0 .../SearchState+QueryProcessing.swift | 0 .../CESearch/SearchState/SearchState.swift | 0 .../Bitbucket/BitBucketAccount+Token.swift | 0 .../Accounts/Bitbucket/BitBucketAccount.swift | 0 .../BitBucketOAuthConfiguration.swift | 0 .../BitBucketTokenConfiguration.swift | 0 .../Model/BitBucketRepositories.swift | 0 .../Bitbucket/Model/BitBucketUser.swift | 0 .../Routers/BitBucketOAuthRouter.swift | 0 .../Routers/BitBucketRepositoryRouter.swift | 0 .../Routers/BitBucketTokenRouter.swift | 0 .../Routers/BitBucketUserRouter.swift | 0 .../Accounts/GitHub/GitHubAccount.swift | 0 .../Accounts/GitHub/GitHubConfiguration.swift | 0 .../Accounts/GitHub/GitHubOpenness.swift | 0 .../Accounts/GitHub/GitHubPreviewHeader.swift | 0 .../Model/GitHubAccount+deleteReference.swift | 0 .../Accounts/GitHub/Model/GitHubComment.swift | 0 .../Accounts/GitHub/Model/GitHubFiles.swift | 0 .../Accounts/GitHub/Model/GitHubGist.swift | 0 .../Accounts/GitHub/Model/GitHubIssue.swift | 0 .../GitHub/Model/GitHubPullRequest.swift | 0 .../GitHub/Model/GitHubRepositories.swift | 0 .../Accounts/GitHub/Model/GitHubReview.swift | 0 .../Accounts/GitHub/Model/GitHubUser.swift | 0 .../Accounts/GitHub/PublicKey.swift | 0 .../GitHub/Routers/GitHubGistRouter.swift | 0 .../GitHub/Routers/GitHubIssueRouter.swift | 0 .../Routers/GitHubPullRequestRouter.swift | 0 .../Routers/GitHubRepositoryRouter.swift | 0 .../GitHub/Routers/GitHubReviewsRouter.swift | 0 .../GitHub/Routers/GitHubRouter.swift | 0 .../GitHub/Routers/GitHubUserRouter.swift | 0 .../Accounts/GitLab/GitLabAccount.swift | 0 .../Accounts/GitLab/GitLabConfiguration.swift | 0 .../GitLab/GitLabOAuthConfiguration.swift | 0 .../GitLab/Model/GitLabAccountModel.swift | 0 .../GitLab/Model/GitLabAvatarURL.swift | 0 .../Accounts/GitLab/Model/GitLabCommit.swift | 0 .../Accounts/GitLab/Model/GitLabEvent.swift | 0 .../GitLab/Model/GitLabEventData.swift | 0 .../GitLab/Model/GitLabEventNote.swift | 0 .../GitLab/Model/GitLabGroupAccess.swift | 0 .../GitLab/Model/GitLabNamespace.swift | 0 .../GitLab/Model/GitLabPermissions.swift | 0 .../Accounts/GitLab/Model/GitLabProject.swift | 0 .../GitLab/Model/GitLabProjectAccess.swift | 0 .../GitLab/Model/GitLabProjectHook.swift | 0 .../Accounts/GitLab/Model/GitLabUser.swift | 0 .../GitLab/Routers/GitLabCommitRouter.swift | 0 .../GitLab/Routers/GitLabOAuthRouter.swift | 0 .../GitLab/Routers/GitLabProjectRouter.swift | 0 .../GitLab/Routers/GitLabUserRouter.swift | 0 .../Networking/GitJSONPostRouter.swift | 0 .../Accounts/Networking/GitRouter.swift | 0 .../Accounts/Networking/GitURLSession.swift | 0 .../CESourceControl/Accounts/Parameters.swift | 0 .../Accounts/Utils/GitTime.swift | 0 .../Utils/String+PercentEncoding.swift | 0 .../Utils/String+QueryParameters.swift | 0 .../Accounts/Utils/URL+URLParameters.swift | 0 .../CESourceControl/CESourceControl.swift | 0 .../Client/GitClient+Branches.swift | 0 .../Client/GitClient+Clone.swift | 0 .../Client/GitClient+Commit.swift | 0 .../Client/GitClient+CommitHistory.swift | 0 .../Client/GitClient+Fetch.swift | 0 .../Client/GitClient+Initiate.swift | 0 .../Client/GitClient+Pull.swift | 0 .../Client/GitClient+Push.swift | 0 .../Client/GitClient+Remote.swift | 0 .../Client/GitClient+Stash.swift | 0 .../Client/GitClient+Status.swift | 0 .../Client/GitClient+Validate.swift | 0 .../CESourceControl/Client/GitClient.swift | 0 .../Client/GitClientProtocol.swift | 0 .../Client/GitConfigClient.swift | 0 .../Client/GitConfigExtensions.swift | 0 .../Client/GitConfigRepresentable.swift | 0 .../Clone/GitCheckoutBranchView.swift | 0 .../CESourceControl/Clone/GitCloneView.swift | 0 .../GitCheckoutBranchViewModel.swift | 0 .../Clone/ViewModels/GitCloneViewModel.swift | 0 .../SourceControlManager+Alerts.swift | 0 ...ourceControlManager+BranchOperations.swift | 0 .../SourceControlManager+FileEvents.swift | 0 .../SourceControlManager+FileOperations.swift | 0 ...ourceControlManager+RemoteOperations.swift | 0 .../SourceControlManager+Repository.swift | 0 ...SourceControlManager+StashOperations.swift | 0 .../SourceControlManager.swift | 0 .../SourceControlViewModel.swift | 0 .../UseCases/RepositoryCloner.swift | 0 .../Views/GitBranchesGroup.swift | 0 .../Views/RegexFormatter.swift | 0 .../Views/RemoteBranchPicker.swift | 0 .../SourceControlAddExistingRemoteView.swift | 0 .../Views/SourceControlFetchView.swift | 0 .../Views/SourceControlNewBranchView.swift | 0 .../Views/SourceControlPullView.swift | 0 .../Views/SourceControlPushView.swift | 0 .../Views/SourceControlRenameBranchView.swift | 0 .../Views/SourceControlStashView.swift | 0 .../Views/SourceControlSwitchView.swift | 0 .../Views/ToolbarBranchPicker.swift | 0 .../Views/TrimWhitespaceFormatter.swift | 0 .../Sources/CETerminal/CETerminal.swift | 0 .../Tasks/Models/CEActiveTask.swift | 0 .../Tasks/Models/CETaskStatus.swift | 0 .../CETerminal/Tasks/TaskManager.swift | 0 .../Extensions/LocalProcess+sendText.swift | 0 .../Extensions/SwiftTerm+Color+Init.swift | 0 .../TerminalEmulator/Model/CurrentUser.swift | 0 .../TerminalEmulator/Model/Shell.swift | 0 .../Model/ShellIntegration.swift | 0 .../Model/TerminalCache.swift | 0 .../Views/CEActiveTaskTerminalView.swift | 0 .../Views/CELocalShellTerminalView.swift | 0 .../Views/CETerminalView.swift | 0 .../TerminalEmulatorView+Coordinator.swift | 0 .../Views/TerminalEmulatorView.swift | 0 .../Array+SortURLs.swift | 0 .../CEWorkspaceFile+Recursion.swift | 0 ...WorkspaceFileManager+DirectoryEvents.swift | 0 .../CEWorkspaceFileManager+Error.swift | 0 ...EWorkspaceFileManager+FileManagement.swift | 0 .../CEWorkspaceFileManager.swift | 0 .../DirectoryEventStream.swift | 0 .../Domain/Commands/Command.swift | 0 .../Domain/Editor/EditorCursorPosition.swift | 0 .../Domain/Editor/EditorItemID.swift | 0 .../Editor/FileEditorOverrideValues.swift | 0 .../Domain/Editor/IndentOption.swift | 0 .../Collection+FuzzyMatches.swift | 0 .../FuzzyMatching/FuzzyMatchModels.swift | 0 .../Domain/FuzzyMatching/FuzzyMatchable.swift | 0 .../String+LengthOfMatchingPrefix.swift | 0 .../FuzzyMatching/String+Normalise.swift | 0 .../CodeEditCore/Domain/Git/GitBranch.swift | 0 .../Domain/Git/GitChangedFile.swift | 0 .../CodeEditCore/Domain/Git/GitCommit.swift | 0 .../CodeEditCore/Domain/Git/GitRemote.swift | 0 .../Domain/Git/GitStashEntry.swift | 0 .../CodeEditCore/Domain/Git/GitStatus.swift | 0 .../Domain/Registry/RegistryItem+Source.swift | 0 .../Domain/Registry/RegistryItem.swift | 0 .../Domain/Tasks/TaskNotificationModel.swift | 0 .../Domain/Workspace/CEWorkspaceFile.swift | 0 .../Workspace/WorkspaceFileProviding.swift | 0 .../Domain/WorkspaceSettings/CETask.swift | 0 ...orkspaceSettingsData+ProjectSettings.swift | 0 .../CEWorkspaceSettingsData.swift | 0 .../Collection+subscript_safe.swift | 0 .../Extensions/String+Escaped.swift | 0 .../Extensions/String+SafeOffset.swift | 0 .../Extensions/String+ValidFileName.swift | 0 .../Extensions/URL+AbsolutePath.swift | 0 .../Extensions/URL+ContainsSubPath.swift | 0 .../Extensions/URL+FileName.swift | 0 .../Extensions/URL+ResourceValues.swift | 0 .../Infrastructure/ActiveCursorState.swift | 0 .../Infrastructure/ActiveEditorState.swift | 0 .../Infrastructure/ErrorNotifying.swift | 0 .../CodeEditCore/Infrastructure/Event.swift | 0 .../Infrastructure/EventBus.swift | 0 .../Events/CENotificationEvent.swift | 0 .../Events/GitStatusChangedEvent.swift | 0 .../Events/TaskNotificationEvent.swift | 0 .../Events/WelcomeWindowRequestedEvent.swift | 0 .../Events/WorkspaceFileEvent.swift | 0 .../Infrastructure/FileEditorOverrides.swift | 0 .../Infrastructure/FileRelocator.swift | 0 .../Infrastructure/FindReplaceQuery.swift | 0 .../Infrastructure/ShellClientProtocol.swift | 0 .../TasksConfigurationProviding.swift | 0 .../Infrastructure/WorkspaceFileOpener.swift | 0 .../Infrastructure/WorkspaceNavigator.swift | 0 .../Infrastructure/WorkspaceStateKey.swift | 0 .../WorkspaceStatePersisting.swift | 0 .../CodeEditDocument/CodeFileDocument.swift | 0 .../CodeFileDocumentDelegate.swift | 0 .../CodeEditDocument/FileEncoding.swift | 0 .../LanguageServicesProvider.swift | 0 .../CodeEditDocument/String+Lines.swift | 0 .../CodeEditSettings/GlobPattern.swift | 0 .../KeyboardShortcutWrapper.swift | 0 .../Sources/CodeEditSettings/Loopable.swift | 0 .../Models/AccountsSettings.swift | 0 .../Models/DeveloperSettings.swift | 0 .../Models/GeneralSettings.swift | 0 .../Models/KeybindingsSettings.swift | 0 .../Models/LanguageServerSettings.swift | 0 .../Models/NavigationSettings.swift | 0 .../Models/SearchSettings.swift | 0 .../Models/SettingsData.swift | 0 .../Models/SourceControlAccount.swift | 0 .../Models/SourceControlSettings.swift | 0 .../Models/TerminalSettings.swift | 0 .../Models/TextEditingSettings.swift | 0 .../CodeEditSettings/Models/Theme+Color.swift | 0 .../Models/ThemeSettings.swift | 0 .../CodeEditSettings/NSFont+WithWeight.swift | 0 .../CodeEditSettings/Store/AppSettings.swift | 0 .../Store/CodableDefault+Providers.swift | 0 .../Store/CodableDefault.swift | 0 .../CodeEditSettings/Store/Color+HEX.swift | 0 .../Store/Environment+Theme.swift | 0 .../CodeEditSettings/Store/Settings.swift | 0 .../Sources/CodeEditSettings/Theme.swift | 0 .../Environment+IsFullscreen.swift | 0 .../Environment+ModifierKeys.swift | 0 .../EnvironmentKeys/Environment+Window.swift | 0 .../Sources/CodeEditUI/FileIcon.swift | 0 .../Sources/CodeEditUI/LayoutMetrics.swift | 0 .../Amber.colorset/Contents.json | 0 .../Resources/Colors.xcassets/Contents.json | 0 .../Scarlet.colorset/Contents.json | 0 .../Steel.colorset/Contents.json | 0 .../CodeEditUI/Styles/BlurButtonStyle.swift | 0 .../CodeEditUI/Styles/IconButtonStyle.swift | 0 .../CodeEditUI/Styles/IconToggleStyle.swift | 0 .../Styles/MenuWithButtonStyle.swift | 0 .../Styles/OverlayButtonStyle.swift | 0 .../CodeEditUI/Styles/View+actionBar.swift | 0 .../Views/CECircularProgressView.swift | 0 .../Views/CEContentUnavailableView.swift | 0 .../CodeEditUI/Views/CEOutlineGroup.swift | 0 .../Sources/CodeEditUI/Views/Divided.swift | 0 .../Sources/CodeEditUI/Views/EffectView.swift | 0 .../Views/ErrorDescriptionLabel.swift | 0 .../CodeEditUI/Views/FeatureIcon.swift | 0 .../CodeEditUI/Views/GlassEffectView.swift | 0 .../Sources/CodeEditUI/Views/HelpButton.swift | 0 .../Views/InstantPopoverModifier.swift | 0 .../CodeEditUI/Views/KeyValueTable.swift | 0 .../CodeEditUI/Views/NSTableViewWrapper.swift | 0 .../CodeEditUI/Views/PaneTextField.swift | 0 .../CodeEditUI/Views/PanelDivider.swift | 0 .../CodeEditUI/Views/PopoverContainer.swift | 0 .../Views/PressActionsModifier.swift | 0 .../Views/QuickSearchResultLabel.swift | 0 .../CodeEditUI/Views/SearchField.swift | 0 .../CodeEditUI/Views/SearchPanel.swift | 0 .../CodeEditUI/Views/SearchPanelView.swift | 0 .../CodeEditUI/Views/SegmentedControl.swift | 0 .../SplitView/CodeEditDividerStyle.swift | 0 .../SplitView/Environment+ContentInsets.swift | 0 .../Views/SplitView/SplitView.swift | 0 .../SplitView/SplitViewControllerView.swift | 0 .../Views/SplitView/SplitViewItem.swift | 0 .../Views/SplitView/SplitViewModifiers.swift | 0 .../Views/SplitView/SplitViewReader.swift | 0 .../CodeEditUI/Views/SplitView/Variadic.swift | 0 .../Views/TrackableScrollView.swift | 0 .../Sources/CodeEditUI/Views/View+if.swift | 0 .../CodeEditUI/Views/ViewOffsetKey.swift | 0 .../Sources/ShellClient/ShellClient.swift | 0 .../CELSPTests/SemanticTokenMapTests.swift | 0 .../SemanticTokenStorageTests.swift | 0 .../CESearchTests/AsyncIndexingTests.swift | 0 .../CESearchTests/FindReplaceQueryTests.swift | 0 .../CESearchTests/MemoryIndexingTests.swift | 0 .../CESearchTests/MemorySearchTests.swift | 0 .../Tests/CESearchTests/TemporaryFile.swift | 0 .../GitRefreshActionsTests.swift | 0 .../SourceControlViewModelTests.swift | 0 .../CEWorkspaceFileCoreTests.swift | 0 .../CodeEditCoreTests/FuzzyMatchTests.swift | 0 .../WorkspaceEventsTests.swift | 0 .../WorkspaceSettingsValueTypeTests.swift | 0 .../AtomConstructionTests.swift | 0 .../CodeEditUIUnitTests/FileIconTests.swift | 0 CodeEditTestPlan.xctestplan | 10 +- Packages/Features/CEEditor/Package.swift | 44 ---- Packages/Features/CELSP/Package.swift | 50 ---- .../Features/CENotifications/Package.swift | 24 -- Packages/Features/CESearch/Package.swift | 31 --- .../Features/CESourceControl/Package.swift | 32 --- Packages/Features/CETerminal/Package.swift | 26 --- .../Foundation/CodeEditCore/Package.swift | 22 -- .../Foundation/CodeEditDocument/Package.swift | 31 --- .../Foundation/CodeEditSettings/Package.swift | 23 -- Packages/Foundation/CodeEditUI/Package.swift | 26 --- .../Services/CodeEditServices/Package.swift | 29 --- 463 files changed, 358 insertions(+), 391 deletions(-) create mode 100644 CodeEditModules/Package.resolved create mode 100644 CodeEditModules/Package.swift rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/CEWorkspaceFile+Editor.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Environment+SplitEditor.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/AppActiveCursorState.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/AppActiveEditorState.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/AppFileEditorOverrides.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/DocumentRegistry.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Editor/Editor+History.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Editor/Editor.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/EditorInstance.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/EditorManager.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Environment+ActiveEditor.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Models/Theme+EditorTheme.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/SplitViewData.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/UseCases/EditorRestorer.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/AnyFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/CodeFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/EditorAreaFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/EditorAreaView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/EditorLayoutView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/Environment+LanguageServices.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/FilePreviewView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/ImageFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/LoadingFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/NonTextFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/PDFFileView.swift (100%) rename {Packages/Features/CEEditor => CodeEditModules}/Sources/CEEditor/Views/WindowCodeFileView.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LSPUtil.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/LSPCache+Data.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/LSPCache.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/LanguageServer.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/LanguageServerDocument.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Errors/PackageManagerError.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Errors/RegistryManagerError.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Model/InstallationMethod.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Model/PackageManagerType.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Model/PackageSource.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagerProtocol.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/Protocols/RegistryManaging.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/RegistryItemTemplateParser.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/RegistryManager.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Registry/RegistryViewState.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/AppLanguageServicesProvider.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/LSPService+Events.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/LSPService.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/LSPServiceError.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/LSPServiceProtocol.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/LanguageServerListState.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Service/LanguageServerLogContainer.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Utils/SemanticToken+Position.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Utils/TextView+LSPRange.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Sources/CELSP/Utils/URL+LSPURI.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/Environment+NotificationManager.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/Models/CENotification.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/NotificationManager+Delegate.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/NotificationManager+System.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/NotificationManager.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/Protocols/NotificationManaging.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/Views/NotificationBannerView.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/Views/NotificationPanelView.swift (100%) rename {Packages/Features/CENotifications => CodeEditModules}/Sources/CENotifications/Views/NotificationToolbarItem.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Environment+WorkspaceFileOpener.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Extensions/Array+Index.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindModePicker.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorForm.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/FindNavigator/FindNavigatorView.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/AsyncFileIterator.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/FileHelper.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+Add.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+File.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+Memory.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+Search.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer+Terms.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Indexer/SearchIndexer.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Model/SearchModeModel.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Model/SearchResultFile.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Model/SearchResultMatchModel.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/Model/SearchResultModel.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/SearchState/SearchState+Find.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/SearchState/SearchState+Index.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Sources/CESearch/SearchState/SearchState.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Networking/GitRouter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Parameters.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Utils/GitTime.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/CESourceControl.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Branches.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Clone.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Commit.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+CommitHistory.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Fetch.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Initiate.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Pull.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Push.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Remote.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Stash.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Status.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient+Validate.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClient.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitClientProtocol.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitConfigClient.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitConfigExtensions.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Client/GitConfigRepresentable.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Clone/GitCloneView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+Alerts.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+BranchOperations.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+FileEvents.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+FileOperations.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+Repository.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager+StashOperations.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlManager.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/SourceControlViewModel.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/UseCases/RepositoryCloner.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/GitBranchesGroup.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/RegexFormatter.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/RemoteBranchPicker.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlFetchView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlNewBranchView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlPullView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlPushView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlStashView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/SourceControlSwitchView.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/ToolbarBranchPicker.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/CETerminal.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/Tasks/Models/CEActiveTask.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/Tasks/Models/CETaskStatus.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/Tasks/TaskManager.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Model/Shell.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift (100%) rename {Packages/Features/CETerminal => CodeEditModules}/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/Array+SortURLs.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Commands/Command.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Editor/IndentOption.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Git/GitBranch.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Git/GitCommit.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Git/GitRemote.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Git/GitStatus.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/String+Escaped.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/String+SafeOffset.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/String+ValidFileName.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/URL+FileName.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/Event.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/EventBus.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/FileRelocator.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift (100%) rename {Packages/Foundation/CodeEditDocument => CodeEditModules}/Sources/CodeEditDocument/CodeFileDocument.swift (100%) rename {Packages/Foundation/CodeEditDocument => CodeEditModules}/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift (100%) rename {Packages/Foundation/CodeEditDocument => CodeEditModules}/Sources/CodeEditDocument/FileEncoding.swift (100%) rename {Packages/Foundation/CodeEditDocument => CodeEditModules}/Sources/CodeEditDocument/LanguageServicesProvider.swift (100%) rename {Packages/Foundation/CodeEditDocument => CodeEditModules}/Sources/CodeEditDocument/String+Lines.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/GlobPattern.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Loopable.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/AccountsSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/DeveloperSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/GeneralSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/KeybindingsSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/LanguageServerSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/NavigationSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/SearchSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/SettingsData.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/SourceControlAccount.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/SourceControlSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/TerminalSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/TextEditingSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/Theme+Color.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Models/ThemeSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/NSFont+WithWeight.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Store/AppSettings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Store/CodableDefault.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Store/Color+HEX.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Store/Environment+Theme.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Store/Settings.swift (100%) rename {Packages/Foundation/CodeEditSettings => CodeEditModules}/Sources/CodeEditSettings/Theme.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/FileIcon.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/LayoutMetrics.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Styles/BlurButtonStyle.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Styles/IconButtonStyle.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Styles/IconToggleStyle.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Styles/View+actionBar.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/CECircularProgressView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/CEContentUnavailableView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/CEOutlineGroup.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/Divided.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/EffectView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/FeatureIcon.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/GlassEffectView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/HelpButton.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/InstantPopoverModifier.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/KeyValueTable.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/NSTableViewWrapper.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/PaneTextField.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/PanelDivider.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/PopoverContainer.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/PressActionsModifier.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SearchField.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SearchPanel.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SearchPanelView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SegmentedControl.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/SplitView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/SplitView/Variadic.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/TrackableScrollView.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/View+if.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Sources/CodeEditUI/Views/ViewOffsetKey.swift (100%) rename {Packages/Services/CodeEditServices => CodeEditModules}/Sources/ShellClient/ShellClient.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Tests/CELSPTests/SemanticTokenMapTests.swift (100%) rename {Packages/Features/CELSP => CodeEditModules}/Tests/CELSPTests/SemanticTokenStorageTests.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Tests/CESearchTests/AsyncIndexingTests.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Tests/CESearchTests/FindReplaceQueryTests.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Tests/CESearchTests/MemoryIndexingTests.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Tests/CESearchTests/MemorySearchTests.swift (100%) rename {Packages/Features/CESearch => CodeEditModules}/Tests/CESearchTests/TemporaryFile.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Tests/CESourceControlTests/GitRefreshActionsTests.swift (100%) rename {Packages/Features/CESourceControl => CodeEditModules}/Tests/CESourceControlTests/SourceControlViewModelTests.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Tests/CodeEditCoreTests/FuzzyMatchTests.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift (100%) rename {Packages/Foundation/CodeEditCore => CodeEditModules}/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift (100%) rename {Packages/Foundation/CodeEditUI => CodeEditModules}/Tests/CodeEditUIUnitTests/FileIconTests.swift (100%) delete mode 100644 Packages/Features/CEEditor/Package.swift delete mode 100644 Packages/Features/CELSP/Package.swift delete mode 100644 Packages/Features/CENotifications/Package.swift delete mode 100644 Packages/Features/CESearch/Package.swift delete mode 100644 Packages/Features/CESourceControl/Package.swift delete mode 100644 Packages/Features/CETerminal/Package.swift delete mode 100644 Packages/Foundation/CodeEditCore/Package.swift delete mode 100644 Packages/Foundation/CodeEditDocument/Package.swift delete mode 100644 Packages/Foundation/CodeEditSettings/Package.swift delete mode 100644 Packages/Foundation/CodeEditUI/Package.swift delete mode 100644 Packages/Services/CodeEditServices/Package.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index c03f609b85..a486436794 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -15,7 +15,7 @@ identifier_name: # paths to ignore during linting. excluded: - - "**/.build" # SwiftPM dependency checkouts (inside any local package under Packages/) + - "**/.build" # SwiftPM dependency checkouts (inside CodeEditModules/) - "**/.swiftpm" - DerivedData @@ -40,13 +40,13 @@ custom_rules: message: "Prefer spaces for indents over tabs. See Xcode setting: 'Text Editing' -> 'Indentation'" severity: warning no_ui_in_core: - included: "Packages/Foundation/CodeEditCore/.*\\.swift" + included: "CodeEditModules/Sources/CodeEditCore/.*\\.swift" name: "CodeEditCore charter" regex: "^import (SwiftUI|AppKit|Cocoa)$" message: "CodeEditCore must stay UI-free — move UI-coupled code to CodeEditUI or the owning feature (see docs/ARCHITECTURE.md)" severity: error ui_package_purity: - included: "Packages/Foundation/CodeEditUI/.*\\.swift" + included: "CodeEditModules/Sources/CodeEditUI/.*\\.swift" name: "CodeEditUI charter" regex: "^import (CodeEditCore|CodeEditDocument|CodeEditSettings|CEEditor|CESearch|CENotifications|CELSP|CESourceControl|CETerminal|ShellClient|CEWorkspaceFileManager)$" message: "CodeEditUI depends on CodeEditSymbols only — presentation atoms must not know about models or features (see docs/ARCHITECTURE.md)" diff --git a/CodeEdit.xcworkspace/contents.xcworkspacedata b/CodeEdit.xcworkspace/contents.xcworkspacedata index 4c87b5c153..60da1a46cf 100644 --- a/CodeEdit.xcworkspace/contents.xcworkspacedata +++ b/CodeEdit.xcworkspace/contents.xcworkspacedata @@ -4,49 +4,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/CodeEditModules/Package.resolved b/CodeEditModules/Package.resolved new file mode 100644 index 0000000000..d160a2e5a2 --- /dev/null +++ b/CodeEditModules/Package.resolved @@ -0,0 +1,213 @@ +{ + "originHash" : "d7a7982c2b02eb622b8b88148aad4d55c921a587a92058388ae5b1660edb34cc", + "pins" : [ + { + "identity" : "codeeditlanguages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", + "state" : { + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" + } + }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "state" : { + "revision" : "ee0c00a2343903df9d6ef45ce53228aca8637369", + "version" : "0.15.1" + } + }, + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", + "version" : "0.12.1" + } + }, + { + "identity" : "fseventswrapper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Frizlab/FSEventsWrapper", + "state" : { + "revision" : "70bbea4b108221fcabfce8dbced8502831c0ae04", + "version" : "2.1.0" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "2cf6c756e1e5ef6901ebae16576a7e4e4b834622", + "version" : "6.29.3" + } + }, + { + "identity" : "jsonrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/JSONRPC", + "state" : { + "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", + "version" : "0.9.0" + } + }, + { + "identity" : "languageclient", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageClient", + "state" : { + "revision" : "4f28cc3cad7512470275f65ca2048359553a86f5", + "version" : "0.8.2" + } + }, + { + "identity" : "languageserverprotocol", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", + "state" : { + "revision" : "f7879c782c0845af9c576de7b8baedd946237286", + "version" : "0.14.0" + } + }, + { + "identity" : "processenv", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ProcessEnv", + "state" : { + "revision" : "552f611479a4f28243a1ef2a7376a216d6899f42", + "version" : "1.0.1" + } + }, + { + "identity" : "queue", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattmassicotte/Queue", + "state" : { + "revision" : "38826d0b8838ce5edcdafa1ffb2f507693d5abcb", + "version" : "0.2.2" + } + }, + { + "identity" : "rearrange", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Rearrange", + "state" : { + "revision" : "4de8be41dba304192e87dc0a11e0aa39e72aa2e8", + "version" : "2.1.1" + } + }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore", + "state" : { + "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", + "version" : "1.0.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-glob", + "kind" : "remoteSourceControl", + "location" : "https://github.com/davbeck/swift-glob", + "state" : { + "revision" : "07ba6f47d903a0b1b59f12ca70d6de9949b975d6", + "version" : "0.2.0" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "b384a67cf45d9989aed5aab23e226cbc7bc2cd54", + "version" : "0.65.0" + } + }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thecoolwinter/SwiftTerm", + "state" : { + "branch" : "codeedit", + "revision" : "2f36f54742d3882e69ff009d084e8675b80934bd" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "textformation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextFormation", + "state" : { + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" + } + }, + { + "identity" : "textstory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextStory", + "state" : { + "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", + "version" : "0.9.1" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } + } + ], + "version" : 3 +} diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift new file mode 100644 index 0000000000..74f386d2fa --- /dev/null +++ b/CodeEditModules/Package.swift @@ -0,0 +1,134 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CodeEditModules", + platforms: [.macOS(.v14)], + products: [ + .library(name: "CodeEditCore", targets: ["CodeEditCore"]), + .library(name: "CodeEditUI", targets: ["CodeEditUI"]), + .library(name: "CodeEditDocument", targets: ["CodeEditDocument"]), + .library(name: "CodeEditSettings", targets: ["CodeEditSettings"]), + .library(name: "ShellClient", targets: ["ShellClient"]), + .library(name: "CEWorkspaceFileManager", targets: ["CEWorkspaceFileManager"]), + .library(name: "CEEditor", targets: ["CEEditor"]), + .library(name: "CELSP", targets: ["CELSP"]), + .library(name: "CENotifications", targets: ["CENotifications"]), + .library(name: "CESearch", targets: ["CESearch"]), + .library(name: "CESourceControl", targets: ["CESourceControl"]), + .library(name: "CETerminal", targets: ["CETerminal"]) + ], + dependencies: [ + // Pins match the app's Package.resolved to avoid a second resolved copy. + .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3"), + .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), + .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), + .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), + .package(url: "https://github.com/ChimeHQ/TextStory", exact: "0.9.1"), + .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.8.2"), + .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.14.0"), + .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.9.0"), + .package(url: "https://github.com/weichsel/ZIPFoundation", exact: "0.9.19"), + .package(url: "https://github.com/apple/swift-async-algorithms.git", exact: "1.0.1"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0"), + .package(url: "https://github.com/groue/GRDB.swift.git", from: "6.0.0"), + .package(url: "https://github.com/thecoolwinter/SwiftTerm", branch: "codeedit") + ], + targets: [ + // MARK: - Kernel + // Rule: zero dependencies, no UI imports, platform-free. See docs/ARCHITECTURE.md. + .target(name: "CodeEditCore"), + + // MARK: - Shared substrate + // Rule: CodeEditUI depends on CodeEditSymbols only — no local targets. + .target( + name: "CodeEditUI", + dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")], + resources: [.process("Resources")] + ), + .target( + name: "CodeEditDocument", + dependencies: [ + "CodeEditCore", + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "CodeEditTextView", package: "CodeEditTextView"), + .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), + .product(name: "TextStory", package: "TextStory") + ] + ), + .target(name: "CodeEditSettings", dependencies: ["CodeEditCore"]), + .target(name: "ShellClient", dependencies: ["CodeEditCore"]), + .target(name: "CEWorkspaceFileManager", dependencies: ["CodeEditCore"]), + + // MARK: - Features + // Norm: prefer features to be leaves. Declare any feature→feature edge here. + .target( + name: "CEEditor", + dependencies: [ + "CodeEditCore", + "CodeEditUI", + "CodeEditDocument", + "CodeEditSettings", + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "CodeEditTextView", package: "CodeEditTextView"), + .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), + .product(name: "CodeEditSymbols", package: "CodeEditSymbols"), + .product(name: "GRDB", package: "GRDB.swift"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections") + ], + // The ONLY target permitted to opt out of Swift 6. See docs/ARCHITECTURE.md. + swiftSettings: [.swiftLanguageMode(.v5)] + ), + .target( + name: "CELSP", + dependencies: [ + "CodeEditCore", + "CodeEditDocument", + "CodeEditSettings", + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "CodeEditTextView", package: "CodeEditTextView"), + .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), + .product(name: "LanguageClient", package: "LanguageClient"), + .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), + .product(name: "JSONRPC", package: "JSONRPC"), + .product(name: "ZIPFoundation", package: "ZIPFoundation"), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") + ] + ), + .target(name: "CENotifications", dependencies: ["CodeEditCore", "CodeEditUI"]), + .target(name: "CESearch", dependencies: ["CodeEditCore", "CodeEditUI"]), + .target( + name: "CESourceControl", + dependencies: [ + "CodeEditCore", + "CodeEditSettings", + "CodeEditUI", + .product(name: "CodeEditSymbols", package: "CodeEditSymbols") + ] + ), + .target( + name: "CETerminal", + dependencies: [ + "CodeEditCore", + "CodeEditSettings", + .product(name: "SwiftTerm", package: "SwiftTerm") + ] + ), + + // MARK: - Tests + .testTarget(name: "CodeEditCoreTests", dependencies: ["CodeEditCore"]), + .testTarget(name: "CodeEditUIUnitTests", dependencies: ["CodeEditUI"]), + .testTarget(name: "CESearchTests", dependencies: ["CESearch", "CodeEditCore"]), + .testTarget( + name: "CELSPTests", + dependencies: [ + "CELSP", + .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), + .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol") + ] + ), + .testTarget(name: "CESourceControlTests", dependencies: ["CESourceControl"]) + ] +) diff --git a/Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift b/CodeEditModules/Sources/CEEditor/CEWorkspaceFile+Editor.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/CEWorkspaceFile+Editor.swift rename to CodeEditModules/Sources/CEEditor/CEWorkspaceFile+Editor.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift b/CodeEditModules/Sources/CEEditor/Environment+SplitEditor.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Environment+SplitEditor.swift rename to CodeEditModules/Sources/CEEditor/Environment+SplitEditor.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift rename to CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift rename to CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift rename to CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveCursorState.swift b/CodeEditModules/Sources/CEEditor/Models/AppActiveCursorState.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveCursorState.swift rename to CodeEditModules/Sources/CEEditor/Models/AppActiveCursorState.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift b/CodeEditModules/Sources/CEEditor/Models/AppActiveEditorState.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/AppActiveEditorState.swift rename to CodeEditModules/Sources/CEEditor/Models/AppActiveEditorState.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/AppFileEditorOverrides.swift b/CodeEditModules/Sources/CEEditor/Models/AppFileEditorOverrides.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/AppFileEditorOverrides.swift rename to CodeEditModules/Sources/CEEditor/Models/AppFileEditorOverrides.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/DocumentRegistry.swift b/CodeEditModules/Sources/CEEditor/Models/DocumentRegistry.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/DocumentRegistry.swift rename to CodeEditModules/Sources/CEEditor/Models/DocumentRegistry.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+History.swift b/CodeEditModules/Sources/CEEditor/Models/Editor/Editor+History.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+History.swift rename to CodeEditModules/Sources/CEEditor/Models/Editor/Editor+History.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift b/CodeEditModules/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift rename to CodeEditModules/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor.swift b/CodeEditModules/Sources/CEEditor/Models/Editor/Editor.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Editor/Editor.swift rename to CodeEditModules/Sources/CEEditor/Models/Editor/Editor.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorInstance.swift b/CodeEditModules/Sources/CEEditor/Models/EditorInstance.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/EditorInstance.swift rename to CodeEditModules/Sources/CEEditor/Models/EditorInstance.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift rename to CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift b/CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift rename to CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/EditorManager.swift b/CodeEditModules/Sources/CEEditor/Models/EditorManager.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/EditorManager.swift rename to CodeEditModules/Sources/CEEditor/Models/EditorManager.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift b/CodeEditModules/Sources/CEEditor/Models/Environment+ActiveEditor.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+ActiveEditor.swift rename to CodeEditModules/Sources/CEEditor/Models/Environment+ActiveEditor.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift b/CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift rename to CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift b/CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift rename to CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift b/CodeEditModules/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift rename to CodeEditModules/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift b/CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift rename to CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift b/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Models/Theme+EditorTheme.swift rename to CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/SplitViewData.swift b/CodeEditModules/Sources/CEEditor/SplitViewData.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/SplitViewData.swift rename to CodeEditModules/Sources/CEEditor/SplitViewData.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift b/CodeEditModules/Sources/CEEditor/UseCases/EditorRestorer.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/UseCases/EditorRestorer.swift rename to CodeEditModules/Sources/CEEditor/UseCases/EditorRestorer.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/AnyFileView.swift b/CodeEditModules/Sources/CEEditor/Views/AnyFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/AnyFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/AnyFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/CodeFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaFileView.swift b/CodeEditModules/Sources/CEEditor/Views/EditorAreaFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/EditorAreaFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaView.swift b/CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/EditorAreaView.swift rename to CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift b/CodeEditModules/Sources/CEEditor/Views/EditorLayoutView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/EditorLayoutView.swift rename to CodeEditModules/Sources/CEEditor/Views/EditorLayoutView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/Environment+LanguageServices.swift b/CodeEditModules/Sources/CEEditor/Views/Environment+LanguageServices.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/Environment+LanguageServices.swift rename to CodeEditModules/Sources/CEEditor/Views/Environment+LanguageServices.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift b/CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/FilePreviewView.swift rename to CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/ImageFileView.swift b/CodeEditModules/Sources/CEEditor/Views/ImageFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/ImageFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/ImageFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/LoadingFileView.swift b/CodeEditModules/Sources/CEEditor/Views/LoadingFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/LoadingFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/LoadingFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/NonTextFileView.swift b/CodeEditModules/Sources/CEEditor/Views/NonTextFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/NonTextFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/NonTextFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift b/CodeEditModules/Sources/CEEditor/Views/PDFFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/PDFFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/PDFFileView.swift diff --git a/Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift similarity index 100% rename from Packages/Features/CEEditor/Sources/CEEditor/Views/WindowCodeFileView.swift rename to CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift b/CodeEditModules/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift rename to CodeEditModules/Sources/CELSP/CodeFileDocument+LanguageServerDocument.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift b/CodeEditModules/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift rename to CodeEditModules/Sources/CELSP/Features/DocumentSync/LSPContentCoordinator.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenMap.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LSPUtil.swift b/CodeEditModules/Sources/CELSP/LSPUtil.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LSPUtil.swift rename to CodeEditModules/Sources/CELSP/LSPUtil.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Completion.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Definition.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Hover.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+References.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+Rename.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift b/CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache+Data.swift b/CodeEditModules/Sources/CELSP/LanguageServer/LSPCache+Data.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache+Data.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/LSPCache+Data.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache.swift b/CodeEditModules/Sources/CELSP/LanguageServer/LSPCache.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/LSPCache.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/LSPCache.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift b/CodeEditModules/Sources/CELSP/LanguageServer/LanguageServer.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServer.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/LanguageServer.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift b/CodeEditModules/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift rename to CodeEditModules/Sources/CELSP/LanguageServer/LanguageServerFileMap.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/LanguageServerDocument.swift b/CodeEditModules/Sources/CELSP/LanguageServerDocument.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/LanguageServerDocument.swift rename to CodeEditModules/Sources/CELSP/LanguageServerDocument.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Errors/PackageManagerError.swift b/CodeEditModules/Sources/CELSP/Registry/Errors/PackageManagerError.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Errors/PackageManagerError.swift rename to CodeEditModules/Sources/CELSP/Registry/Errors/PackageManagerError.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Errors/RegistryManagerError.swift b/CodeEditModules/Sources/CELSP/Registry/Errors/RegistryManagerError.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Errors/RegistryManagerError.swift rename to CodeEditModules/Sources/CELSP/Registry/Errors/RegistryManagerError.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod.swift b/CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Model/InstallationMethod.swift rename to CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageManagerType.swift b/CodeEditModules/Sources/CELSP/Registry/Model/PackageManagerType.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageManagerType.swift rename to CodeEditModules/Sources/CELSP/Registry/Model/PackageManagerType.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageSource.swift b/CodeEditModules/Sources/CELSP/Registry/Model/PackageSource.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Model/PackageSource.swift rename to CodeEditModules/Sources/CELSP/Registry/Model/PackageSource.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift b/CodeEditModules/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift rename to CodeEditModules/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagerProtocol.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagerProtocol.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagerProtocol.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagerProtocol.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/FileManager+MakeExecutable.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/InstallStepConfirmation.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallOperation.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerInstallStep.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Install/PackageManagerProgressModel.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/CargoPackageManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/GithubPackageManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/GolangPackageManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/NPMPackageManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagers/Sources/PipPackageManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Cargo.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Gem.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+Golang.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+NPM.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser+PYPI.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSourceParser/PackageSourceParser.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift b/CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/Protocols/RegistryManaging.swift rename to CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryItemTemplateParser.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryItemTemplateParser.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/RegistryItemTemplateParser.swift rename to CodeEditModules/Sources/CELSP/Registry/RegistryItemTemplateParser.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift rename to CodeEditModules/Sources/CELSP/Registry/RegistryManager+HandleRegistryFile.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/RegistryManager.swift rename to CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Registry/RegistryViewState.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryViewState.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Registry/RegistryViewState.swift rename to CodeEditModules/Sources/CELSP/Registry/RegistryViewState.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/AppLanguageServicesProvider.swift b/CodeEditModules/Sources/CELSP/Service/AppLanguageServicesProvider.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/AppLanguageServicesProvider.swift rename to CodeEditModules/Sources/CELSP/Service/AppLanguageServicesProvider.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LSPService+Events.swift b/CodeEditModules/Sources/CELSP/Service/LSPService+Events.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/LSPService+Events.swift rename to CodeEditModules/Sources/CELSP/Service/LSPService+Events.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift b/CodeEditModules/Sources/CELSP/Service/LSPService.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/LSPService.swift rename to CodeEditModules/Sources/CELSP/Service/LSPService.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceError.swift b/CodeEditModules/Sources/CELSP/Service/LSPServiceError.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceError.swift rename to CodeEditModules/Sources/CELSP/Service/LSPServiceError.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceProtocol.swift b/CodeEditModules/Sources/CELSP/Service/LSPServiceProtocol.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/LSPServiceProtocol.swift rename to CodeEditModules/Sources/CELSP/Service/LSPServiceProtocol.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerListState.swift b/CodeEditModules/Sources/CELSP/Service/LanguageServerListState.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerListState.swift rename to CodeEditModules/Sources/CELSP/Service/LanguageServerListState.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift b/CodeEditModules/Sources/CELSP/Service/LanguageServerLogContainer.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Service/LanguageServerLogContainer.swift rename to CodeEditModules/Sources/CELSP/Service/LanguageServerLogContainer.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift b/CodeEditModules/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift rename to CodeEditModules/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Utils/SemanticToken+Position.swift b/CodeEditModules/Sources/CELSP/Utils/SemanticToken+Position.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Utils/SemanticToken+Position.swift rename to CodeEditModules/Sources/CELSP/Utils/SemanticToken+Position.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Utils/TextView+LSPRange.swift b/CodeEditModules/Sources/CELSP/Utils/TextView+LSPRange.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Utils/TextView+LSPRange.swift rename to CodeEditModules/Sources/CELSP/Utils/TextView+LSPRange.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift b/CodeEditModules/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift rename to CodeEditModules/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift diff --git a/Packages/Features/CELSP/Sources/CELSP/Utils/URL+LSPURI.swift b/CodeEditModules/Sources/CELSP/Utils/URL+LSPURI.swift similarity index 100% rename from Packages/Features/CELSP/Sources/CELSP/Utils/URL+LSPURI.swift rename to CodeEditModules/Sources/CELSP/Utils/URL+LSPURI.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/Environment+NotificationManager.swift b/CodeEditModules/Sources/CENotifications/Environment+NotificationManager.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/Environment+NotificationManager.swift rename to CodeEditModules/Sources/CENotifications/Environment+NotificationManager.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/Models/CENotification.swift b/CodeEditModules/Sources/CENotifications/Models/CENotification.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/Models/CENotification.swift rename to CodeEditModules/Sources/CENotifications/Models/CENotification.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+Delegate.swift b/CodeEditModules/Sources/CENotifications/NotificationManager+Delegate.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+Delegate.swift rename to CodeEditModules/Sources/CENotifications/NotificationManager+Delegate.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+System.swift b/CodeEditModules/Sources/CENotifications/NotificationManager+System.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/NotificationManager+System.swift rename to CodeEditModules/Sources/CENotifications/NotificationManager+System.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/NotificationManager.swift b/CodeEditModules/Sources/CENotifications/NotificationManager.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/NotificationManager.swift rename to CodeEditModules/Sources/CENotifications/NotificationManager.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/Protocols/NotificationManaging.swift b/CodeEditModules/Sources/CENotifications/Protocols/NotificationManaging.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/Protocols/NotificationManaging.swift rename to CodeEditModules/Sources/CENotifications/Protocols/NotificationManaging.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift rename to CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift b/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift rename to CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift b/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift rename to CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift b/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift rename to CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationBannerView.swift b/CodeEditModules/Sources/CENotifications/Views/NotificationBannerView.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationBannerView.swift rename to CodeEditModules/Sources/CENotifications/Views/NotificationBannerView.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationPanelView.swift b/CodeEditModules/Sources/CENotifications/Views/NotificationPanelView.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationPanelView.swift rename to CodeEditModules/Sources/CENotifications/Views/NotificationPanelView.swift diff --git a/Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationToolbarItem.swift b/CodeEditModules/Sources/CENotifications/Views/NotificationToolbarItem.swift similarity index 100% rename from Packages/Features/CENotifications/Sources/CENotifications/Views/NotificationToolbarItem.swift rename to CodeEditModules/Sources/CENotifications/Views/NotificationToolbarItem.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Environment+WorkspaceFileOpener.swift b/CodeEditModules/Sources/CESearch/Environment+WorkspaceFileOpener.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Environment+WorkspaceFileOpener.swift rename to CodeEditModules/Sources/CESearch/Environment+WorkspaceFileOpener.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift b/CodeEditModules/Sources/CESearch/Extensions/Array+Index.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Extensions/Array+Index.swift rename to CodeEditModules/Sources/CESearch/Extensions/Array+Index.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindModePicker.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindModePicker.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindModePicker.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindModePicker.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorConfiguration.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorForm.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorForm.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorForm.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorForm.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorIndexBar.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorListViewController.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/SearchResultFileCell.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorView.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorView.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/FindNavigator/FindNavigatorView.swift rename to CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorView.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/AsyncFileIterator.swift b/CodeEditModules/Sources/CESearch/Indexer/AsyncFileIterator.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/AsyncFileIterator.swift rename to CodeEditModules/Sources/CESearch/Indexer/AsyncFileIterator.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/FileHelper.swift b/CodeEditModules/Sources/CESearch/Indexer/FileHelper.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/FileHelper.swift rename to CodeEditModules/Sources/CESearch/Indexer/FileHelper.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Add.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Add.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Add.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Add.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+AsyncController.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+File.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+File.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+File.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+File.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+InternalMethods.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Memory.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Memory.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Memory.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Memory.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+ProgressiveSearch.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Search.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Search.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Search.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Search.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Terms.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Terms.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer+Terms.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer+Terms.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer.swift b/CodeEditModules/Sources/CESearch/Indexer/SearchIndexer.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Indexer/SearchIndexer.swift rename to CodeEditModules/Sources/CESearch/Indexer/SearchIndexer.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Model/SearchModeModel.swift b/CodeEditModules/Sources/CESearch/Model/SearchModeModel.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Model/SearchModeModel.swift rename to CodeEditModules/Sources/CESearch/Model/SearchModeModel.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Model/SearchResultFile.swift b/CodeEditModules/Sources/CESearch/Model/SearchResultFile.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Model/SearchResultFile.swift rename to CodeEditModules/Sources/CESearch/Model/SearchResultFile.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Model/SearchResultMatchModel.swift b/CodeEditModules/Sources/CESearch/Model/SearchResultMatchModel.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Model/SearchResultMatchModel.swift rename to CodeEditModules/Sources/CESearch/Model/SearchResultMatchModel.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/Model/SearchResultModel.swift b/CodeEditModules/Sources/CESearch/Model/SearchResultModel.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/Model/SearchResultModel.swift rename to CodeEditModules/Sources/CESearch/Model/SearchResultModel.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Find.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState+Find.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Find.swift rename to CodeEditModules/Sources/CESearch/SearchState/SearchState+Find.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift rename to CodeEditModules/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Index.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState+Index.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+Index.swift rename to CodeEditModules/Sources/CESearch/SearchState/SearchState+Index.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift rename to CodeEditModules/Sources/CESearch/SearchState/SearchState+MatchExtraction.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift rename to CodeEditModules/Sources/CESearch/SearchState/SearchState+QueryProcessing.swift diff --git a/Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState.swift similarity index 100% rename from Packages/Features/CESearch/Sources/CESearch/SearchState/SearchState.swift rename to CodeEditModules/Sources/CESearch/SearchState/SearchState.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount+Token.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketAccount.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketOAuthConfiguration.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/BitBucketTokenConfiguration.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketRepositories.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Model/BitBucketUser.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketOAuthRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketRepositoryRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketTokenRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Bitbucket/Routers/BitBucketUserRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubAccount.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubConfiguration.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubOpenness.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/GitHubPreviewHeader.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubAccount+deleteReference.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubComment.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubFiles.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubGist.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubIssue.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubPullRequest.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubReview.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubUser.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/PublicKey.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubGistRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubIssueRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubPullRequestRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRepositoryRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubReviewsRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Routers/GitHubUserRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/GitLabAccount.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/GitLabConfiguration.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/GitLabOAuthConfiguration.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAvatarURL.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabCommit.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEvent.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventData.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabEventNote.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabGroupAccess.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabNamespace.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProject.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectAccess.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabProjectHook.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabUser.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabCommitRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabOAuthRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabProjectRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Routers/GitLabUserRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitJSONPostRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitRouter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitRouter.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitRouter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Parameters.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Parameters.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Parameters.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Parameters.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Utils/GitTime.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/GitTime.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Utils/GitTime.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Utils/String+PercentEncoding.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Utils/String+QueryParameters.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift rename to CodeEditModules/Sources/CESourceControl/Accounts/Utils/URL+URLParameters.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/CESourceControl.swift b/CodeEditModules/Sources/CESourceControl/CESourceControl.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/CESourceControl.swift rename to CodeEditModules/Sources/CESourceControl/CESourceControl.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Branches.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Branches.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Branches.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Branches.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Clone.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Clone.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Clone.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Commit.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Commit.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Commit.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+CommitHistory.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+CommitHistory.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+CommitHistory.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+CommitHistory.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Fetch.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Fetch.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Fetch.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Fetch.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Initiate.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Initiate.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Initiate.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Initiate.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Pull.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Pull.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Pull.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Pull.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Push.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Push.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Push.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Push.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Remote.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Remote.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Remote.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Remote.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Stash.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Stash.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Stash.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Stash.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Status.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Status.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Status.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Validate.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Validate.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient+Validate.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient+Validate.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClient.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClient.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClientProtocol.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClientProtocol.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitClientProtocol.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitClientProtocol.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigClient.swift b/CodeEditModules/Sources/CESourceControl/Client/GitConfigClient.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigClient.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitConfigClient.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigExtensions.swift b/CodeEditModules/Sources/CESourceControl/Client/GitConfigExtensions.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigExtensions.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitConfigExtensions.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigRepresentable.swift b/CodeEditModules/Sources/CESourceControl/Client/GitConfigRepresentable.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Client/GitConfigRepresentable.swift rename to CodeEditModules/Sources/CESourceControl/Client/GitConfigRepresentable.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift b/CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift rename to CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCloneView.swift b/CodeEditModules/Sources/CESourceControl/Clone/GitCloneView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Clone/GitCloneView.swift rename to CodeEditModules/Sources/CESourceControl/Clone/GitCloneView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift rename to CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift b/CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift rename to CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Alerts.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+Alerts.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Alerts.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+Alerts.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+BranchOperations.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+BranchOperations.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+BranchOperations.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+BranchOperations.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileEvents.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+FileEvents.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileEvents.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+FileEvents.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileOperations.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+FileOperations.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+FileOperations.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+FileOperations.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+RemoteOperations.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Repository.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+Repository.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+Repository.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+Repository.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+StashOperations.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+StashOperations.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager+StashOperations.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager+StashOperations.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlManager.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlManager.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift b/CodeEditModules/Sources/CESourceControl/SourceControlViewModel.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/SourceControlViewModel.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlViewModel.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift b/CodeEditModules/Sources/CESourceControl/UseCases/RepositoryCloner.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/UseCases/RepositoryCloner.swift rename to CodeEditModules/Sources/CESourceControl/UseCases/RepositoryCloner.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/GitBranchesGroup.swift b/CodeEditModules/Sources/CESourceControl/Views/GitBranchesGroup.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/GitBranchesGroup.swift rename to CodeEditModules/Sources/CESourceControl/Views/GitBranchesGroup.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/RegexFormatter.swift b/CodeEditModules/Sources/CESourceControl/Views/RegexFormatter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/RegexFormatter.swift rename to CodeEditModules/Sources/CESourceControl/Views/RegexFormatter.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/RemoteBranchPicker.swift b/CodeEditModules/Sources/CESourceControl/Views/RemoteBranchPicker.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/RemoteBranchPicker.swift rename to CodeEditModules/Sources/CESourceControl/Views/RemoteBranchPicker.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlFetchView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlFetchView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlFetchView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlNewBranchView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlNewBranchView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlNewBranchView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlNewBranchView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlPullView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPullView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlPullView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPushView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlPushView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlPushView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlPushView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlStashView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlStashView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlStashView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlStashView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlSwitchView.swift b/CodeEditModules/Sources/CESourceControl/Views/SourceControlSwitchView.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/SourceControlSwitchView.swift rename to CodeEditModules/Sources/CESourceControl/Views/SourceControlSwitchView.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/ToolbarBranchPicker.swift b/CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/ToolbarBranchPicker.swift rename to CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift diff --git a/Packages/Features/CESourceControl/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift b/CodeEditModules/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift similarity index 100% rename from Packages/Features/CESourceControl/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift rename to CodeEditModules/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/CETerminal.swift b/CodeEditModules/Sources/CETerminal/CETerminal.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/CETerminal.swift rename to CodeEditModules/Sources/CETerminal/CETerminal.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CEActiveTask.swift b/CodeEditModules/Sources/CETerminal/Tasks/Models/CEActiveTask.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CEActiveTask.swift rename to CodeEditModules/Sources/CETerminal/Tasks/Models/CEActiveTask.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift b/CodeEditModules/Sources/CETerminal/Tasks/Models/CETaskStatus.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/Tasks/Models/CETaskStatus.swift rename to CodeEditModules/Sources/CETerminal/Tasks/Models/CETaskStatus.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/Tasks/TaskManager.swift b/CodeEditModules/Sources/CETerminal/Tasks/TaskManager.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/Tasks/TaskManager.swift rename to CodeEditModules/Sources/CETerminal/Tasks/TaskManager.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/Shell.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/Shell.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/Shell.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/Shell.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift diff --git a/Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift similarity index 100% rename from Packages/Features/CETerminal/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Array+SortURLs.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/Array+SortURLs.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/Array+SortURLs.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/Array+SortURLs.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFile+Recursion.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+DirectoryEvents.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+Error.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager+FileManagement.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/CEWorkspaceFileManager.swift diff --git a/Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift b/CodeEditModules/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift rename to CodeEditModules/Sources/CEWorkspaceFileManager/DirectoryEventStream.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Commands/Command.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Commands/Command.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Commands/Command.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorCursorPosition.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Editor/FileEditorOverrideValues.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/IndentOption.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Editor/IndentOption.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Editor/IndentOption.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Editor/IndentOption.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift b/CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/Collection+FuzzyMatches.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift b/CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchModels.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift b/CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/FuzzyMatchable.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift b/CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/String+LengthOfMatchingPrefix.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift b/CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/FuzzyMatching/String+Normalise.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitBranch.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitBranch.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Git/GitBranch.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitCommit.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitCommit.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Git/GitCommit.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitRemote.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitRemote.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Git/GitRemote.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStashEntry.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Git/GitStatus.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Registry/RegistryItem+Source.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Registry/RegistryItem.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Tasks/TaskNotificationModel.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Workspace/WorkspaceFileProviding.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift b/CodeEditModules/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/WorkspaceSettings/CETask.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift b/CodeEditModules/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData+ProjectSettings.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift b/CodeEditModules/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/WorkspaceSettings/CEWorkspaceSettingsData.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/String+Escaped.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+Escaped.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/String+Escaped.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+SafeOffset.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/String+SafeOffset.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+SafeOffset.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/String+SafeOffset.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+ValidFileName.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/String+ValidFileName.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/String+ValidFileName.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/String+ValidFileName.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/URL+FileName.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+FileName.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/URL+FileName.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift b/CodeEditModules/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift rename to CodeEditModules/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveCursorState.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveEditorState.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/ErrorNotifying.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Event.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Event.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Event.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/EventBus.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/EventBus.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/EventBus.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/CENotificationEvent.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/GitStatusChangedEvent.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/TaskNotificationEvent.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/WelcomeWindowRequestedEvent.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/WorkspaceFileEvent.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/FileEditorOverrides.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/FileRelocator.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FileRelocator.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/FileRelocator.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/ShellClientProtocol.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/TasksConfigurationProviding.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceFileOpener.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceStateKey.swift diff --git a/Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceStatePersisting.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift similarity index 100% rename from Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocument.swift rename to CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift similarity index 100% rename from Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift rename to CodeEditModules/Sources/CodeEditDocument/CodeFileDocumentDelegate.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/FileEncoding.swift b/CodeEditModules/Sources/CodeEditDocument/FileEncoding.swift similarity index 100% rename from Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/FileEncoding.swift rename to CodeEditModules/Sources/CodeEditDocument/FileEncoding.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift b/CodeEditModules/Sources/CodeEditDocument/LanguageServicesProvider.swift similarity index 100% rename from Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/LanguageServicesProvider.swift rename to CodeEditModules/Sources/CodeEditDocument/LanguageServicesProvider.swift diff --git a/Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/String+Lines.swift b/CodeEditModules/Sources/CodeEditDocument/String+Lines.swift similarity index 100% rename from Packages/Foundation/CodeEditDocument/Sources/CodeEditDocument/String+Lines.swift rename to CodeEditModules/Sources/CodeEditDocument/String+Lines.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/GlobPattern.swift b/CodeEditModules/Sources/CodeEditSettings/GlobPattern.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/GlobPattern.swift rename to CodeEditModules/Sources/CodeEditSettings/GlobPattern.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift b/CodeEditModules/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift rename to CodeEditModules/Sources/CodeEditSettings/KeyboardShortcutWrapper.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift b/CodeEditModules/Sources/CodeEditSettings/Loopable.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Loopable.swift rename to CodeEditModules/Sources/CodeEditSettings/Loopable.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/AccountsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/AccountsSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/DeveloperSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/DeveloperSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/GeneralSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/KeybindingsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/KeybindingsSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/LanguageServerSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/NavigationSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/NavigationSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SearchSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SearchSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SettingsData.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SettingsData.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SettingsData.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/SettingsData.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlAccount.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlAccount.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlAccount.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/SourceControlAccount.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/SourceControlSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TerminalSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TerminalSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/TextEditingSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift b/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/Theme+Color.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/ThemeSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Models/ThemeSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/NSFont+WithWeight.swift b/CodeEditModules/Sources/CodeEditSettings/NSFont+WithWeight.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/NSFont+WithWeight.swift rename to CodeEditModules/Sources/CodeEditSettings/NSFont+WithWeight.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Store/AppSettings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/AppSettings.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/AppSettings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/CodableDefault.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Color+HEX.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Color+HEX.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/Color+HEX.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Environment+Theme.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Settings.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Store/Settings.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift diff --git a/Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift b/CodeEditModules/Sources/CodeEditSettings/Theme.swift similarity index 100% rename from Packages/Foundation/CodeEditSettings/Sources/CodeEditSettings/Theme.swift rename to CodeEditModules/Sources/CodeEditSettings/Theme.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift b/CodeEditModules/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift rename to CodeEditModules/Sources/CodeEditUI/EnvironmentKeys/Environment+IsFullscreen.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift b/CodeEditModules/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift rename to CodeEditModules/Sources/CodeEditUI/EnvironmentKeys/Environment+ModifierKeys.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift b/CodeEditModules/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift rename to CodeEditModules/Sources/CodeEditUI/EnvironmentKeys/Environment+Window.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/FileIcon.swift b/CodeEditModules/Sources/CodeEditUI/FileIcon.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/FileIcon.swift rename to CodeEditModules/Sources/CodeEditUI/FileIcon.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift b/CodeEditModules/Sources/CodeEditUI/LayoutMetrics.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/LayoutMetrics.swift rename to CodeEditModules/Sources/CodeEditUI/LayoutMetrics.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json b/CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json rename to CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Amber.colorset/Contents.json diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json b/CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json rename to CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Contents.json diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json b/CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json rename to CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Scarlet.colorset/Contents.json diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json b/CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json rename to CodeEditModules/Sources/CodeEditUI/Resources/Colors.xcassets/Steel.colorset/Contents.json diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift b/CodeEditModules/Sources/CodeEditUI/Styles/BlurButtonStyle.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/BlurButtonStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Styles/BlurButtonStyle.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift b/CodeEditModules/Sources/CodeEditUI/Styles/IconButtonStyle.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconButtonStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Styles/IconButtonStyle.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift b/CodeEditModules/Sources/CodeEditUI/Styles/IconToggleStyle.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/IconToggleStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Styles/IconToggleStyle.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift b/CodeEditModules/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift b/CodeEditModules/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Styles/OverlayButtonStyle.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift b/CodeEditModules/Sources/CodeEditUI/Styles/View+actionBar.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Styles/View+actionBar.swift rename to CodeEditModules/Sources/CodeEditUI/Styles/View+actionBar.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CECircularProgressView.swift b/CodeEditModules/Sources/CodeEditUI/Views/CECircularProgressView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CECircularProgressView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/CECircularProgressView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift b/CodeEditModules/Sources/CodeEditUI/Views/CEContentUnavailableView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEContentUnavailableView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/CEContentUnavailableView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift b/CodeEditModules/Sources/CodeEditUI/Views/CEOutlineGroup.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/CEOutlineGroup.swift rename to CodeEditModules/Sources/CodeEditUI/Views/CEOutlineGroup.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift b/CodeEditModules/Sources/CodeEditUI/Views/Divided.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/Divided.swift rename to CodeEditModules/Sources/CodeEditUI/Views/Divided.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift b/CodeEditModules/Sources/CodeEditUI/Views/EffectView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/EffectView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/EffectView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift b/CodeEditModules/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift rename to CodeEditModules/Sources/CodeEditUI/Views/ErrorDescriptionLabel.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift b/CodeEditModules/Sources/CodeEditUI/Views/FeatureIcon.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/FeatureIcon.swift rename to CodeEditModules/Sources/CodeEditUI/Views/FeatureIcon.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift b/CodeEditModules/Sources/CodeEditUI/Views/GlassEffectView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/GlassEffectView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/GlassEffectView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift b/CodeEditModules/Sources/CodeEditUI/Views/HelpButton.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/HelpButton.swift rename to CodeEditModules/Sources/CodeEditUI/Views/HelpButton.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift b/CodeEditModules/Sources/CodeEditUI/Views/InstantPopoverModifier.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/InstantPopoverModifier.swift rename to CodeEditModules/Sources/CodeEditUI/Views/InstantPopoverModifier.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/KeyValueTable.swift b/CodeEditModules/Sources/CodeEditUI/Views/KeyValueTable.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/KeyValueTable.swift rename to CodeEditModules/Sources/CodeEditUI/Views/KeyValueTable.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/NSTableViewWrapper.swift b/CodeEditModules/Sources/CodeEditUI/Views/NSTableViewWrapper.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/NSTableViewWrapper.swift rename to CodeEditModules/Sources/CodeEditUI/Views/NSTableViewWrapper.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift b/CodeEditModules/Sources/CodeEditUI/Views/PaneTextField.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PaneTextField.swift rename to CodeEditModules/Sources/CodeEditUI/Views/PaneTextField.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift b/CodeEditModules/Sources/CodeEditUI/Views/PanelDivider.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PanelDivider.swift rename to CodeEditModules/Sources/CodeEditUI/Views/PanelDivider.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift b/CodeEditModules/Sources/CodeEditUI/Views/PopoverContainer.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PopoverContainer.swift rename to CodeEditModules/Sources/CodeEditUI/Views/PopoverContainer.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift b/CodeEditModules/Sources/CodeEditUI/Views/PressActionsModifier.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/PressActionsModifier.swift rename to CodeEditModules/Sources/CodeEditUI/Views/PressActionsModifier.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift b/CodeEditModules/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift rename to CodeEditModules/Sources/CodeEditUI/Views/QuickSearchResultLabel.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift b/CodeEditModules/Sources/CodeEditUI/Views/SearchField.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchField.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SearchField.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift b/CodeEditModules/Sources/CodeEditUI/Views/SearchPanel.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanel.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SearchPanel.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanelView.swift b/CodeEditModules/Sources/CodeEditUI/Views/SearchPanelView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SearchPanelView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SearchPanelView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift b/CodeEditModules/Sources/CodeEditUI/Views/SegmentedControl.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SegmentedControl.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SegmentedControl.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/CodeEditDividerStyle.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/Environment+ContentInsets.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitView.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewControllerView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewItem.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewModifiers.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/SplitViewReader.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift b/CodeEditModules/Sources/CodeEditUI/Views/SplitView/Variadic.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/SplitView/Variadic.swift rename to CodeEditModules/Sources/CodeEditUI/Views/SplitView/Variadic.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift b/CodeEditModules/Sources/CodeEditUI/Views/TrackableScrollView.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/TrackableScrollView.swift rename to CodeEditModules/Sources/CodeEditUI/Views/TrackableScrollView.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift b/CodeEditModules/Sources/CodeEditUI/Views/View+if.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/View+if.swift rename to CodeEditModules/Sources/CodeEditUI/Views/View+if.swift diff --git a/Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ViewOffsetKey.swift b/CodeEditModules/Sources/CodeEditUI/Views/ViewOffsetKey.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Sources/CodeEditUI/Views/ViewOffsetKey.swift rename to CodeEditModules/Sources/CodeEditUI/Views/ViewOffsetKey.swift diff --git a/Packages/Services/CodeEditServices/Sources/ShellClient/ShellClient.swift b/CodeEditModules/Sources/ShellClient/ShellClient.swift similarity index 100% rename from Packages/Services/CodeEditServices/Sources/ShellClient/ShellClient.swift rename to CodeEditModules/Sources/ShellClient/ShellClient.swift diff --git a/Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenMapTests.swift b/CodeEditModules/Tests/CELSPTests/SemanticTokenMapTests.swift similarity index 100% rename from Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenMapTests.swift rename to CodeEditModules/Tests/CELSPTests/SemanticTokenMapTests.swift diff --git a/Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenStorageTests.swift b/CodeEditModules/Tests/CELSPTests/SemanticTokenStorageTests.swift similarity index 100% rename from Packages/Features/CELSP/Tests/CELSPTests/SemanticTokenStorageTests.swift rename to CodeEditModules/Tests/CELSPTests/SemanticTokenStorageTests.swift diff --git a/Packages/Features/CESearch/Tests/CESearchTests/AsyncIndexingTests.swift b/CodeEditModules/Tests/CESearchTests/AsyncIndexingTests.swift similarity index 100% rename from Packages/Features/CESearch/Tests/CESearchTests/AsyncIndexingTests.swift rename to CodeEditModules/Tests/CESearchTests/AsyncIndexingTests.swift diff --git a/Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift b/CodeEditModules/Tests/CESearchTests/FindReplaceQueryTests.swift similarity index 100% rename from Packages/Features/CESearch/Tests/CESearchTests/FindReplaceQueryTests.swift rename to CodeEditModules/Tests/CESearchTests/FindReplaceQueryTests.swift diff --git a/Packages/Features/CESearch/Tests/CESearchTests/MemoryIndexingTests.swift b/CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift similarity index 100% rename from Packages/Features/CESearch/Tests/CESearchTests/MemoryIndexingTests.swift rename to CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift diff --git a/Packages/Features/CESearch/Tests/CESearchTests/MemorySearchTests.swift b/CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift similarity index 100% rename from Packages/Features/CESearch/Tests/CESearchTests/MemorySearchTests.swift rename to CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift diff --git a/Packages/Features/CESearch/Tests/CESearchTests/TemporaryFile.swift b/CodeEditModules/Tests/CESearchTests/TemporaryFile.swift similarity index 100% rename from Packages/Features/CESearch/Tests/CESearchTests/TemporaryFile.swift rename to CodeEditModules/Tests/CESearchTests/TemporaryFile.swift diff --git a/Packages/Features/CESourceControl/Tests/CESourceControlTests/GitRefreshActionsTests.swift b/CodeEditModules/Tests/CESourceControlTests/GitRefreshActionsTests.swift similarity index 100% rename from Packages/Features/CESourceControl/Tests/CESourceControlTests/GitRefreshActionsTests.swift rename to CodeEditModules/Tests/CESourceControlTests/GitRefreshActionsTests.swift diff --git a/Packages/Features/CESourceControl/Tests/CESourceControlTests/SourceControlViewModelTests.swift b/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift similarity index 100% rename from Packages/Features/CESourceControl/Tests/CESourceControlTests/SourceControlViewModelTests.swift rename to CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift b/CodeEditModules/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift rename to CodeEditModules/Tests/CodeEditCoreTests/CEWorkspaceFileCoreTests.swift diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzyMatchTests.swift b/CodeEditModules/Tests/CodeEditCoreTests/FuzzyMatchTests.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/FuzzyMatchTests.swift rename to CodeEditModules/Tests/CodeEditCoreTests/FuzzyMatchTests.swift diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift b/CodeEditModules/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift rename to CodeEditModules/Tests/CodeEditCoreTests/WorkspaceEventsTests.swift diff --git a/Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift b/CodeEditModules/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift similarity index 100% rename from Packages/Foundation/CodeEditCore/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift rename to CodeEditModules/Tests/CodeEditCoreTests/WorkspaceSettingsValueTypeTests.swift diff --git a/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift b/CodeEditModules/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift rename to CodeEditModules/Tests/CodeEditUIUnitTests/AtomConstructionTests.swift diff --git a/Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/FileIconTests.swift b/CodeEditModules/Tests/CodeEditUIUnitTests/FileIconTests.swift similarity index 100% rename from Packages/Foundation/CodeEditUI/Tests/CodeEditUIUnitTests/FileIconTests.swift rename to CodeEditModules/Tests/CodeEditUIUnitTests/FileIconTests.swift diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index cdd0f06607..23634ff0eb 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -35,35 +35,35 @@ }, { "target" : { - "containerPath" : "container:Packages\/Foundation\/CodeEditCore", + "containerPath" : "container:CodeEditModules", "identifier" : "CodeEditCoreTests", "name" : "CodeEditCoreTests" } }, { "target" : { - "containerPath" : "container:Packages\/Features\/CESearch", + "containerPath" : "container:CodeEditModules", "identifier" : "CESearchTests", "name" : "CESearchTests" } }, { "target" : { - "containerPath" : "container:Packages\/Foundation\/CodeEditUI", + "containerPath" : "container:CodeEditModules", "identifier" : "CodeEditUIUnitTests", "name" : "CodeEditUIUnitTests" } }, { "target" : { - "containerPath" : "container:Packages\/Features\/CELSP", + "containerPath" : "container:CodeEditModules", "identifier" : "CELSPTests", "name" : "CELSPTests" } }, { "target" : { - "containerPath" : "container:Packages\/Features\/CESourceControl", + "containerPath" : "container:CodeEditModules", "identifier" : "CESourceControlTests", "name" : "CESourceControlTests" } diff --git a/Packages/Features/CEEditor/Package.swift b/Packages/Features/CEEditor/Package.swift deleted file mode 100644 index 9657d9d53c..0000000000 --- a/Packages/Features/CEEditor/Package.swift +++ /dev/null @@ -1,44 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CEEditor", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CEEditor", targets: ["CEEditor"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditUI"), - .package(path: "../../Foundation/CodeEditDocument"), - .package(path: "../../Foundation/CodeEditSettings"), - .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), - .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), - .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), - .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3"), - .package(url: "https://github.com/groue/GRDB.swift.git", from: "6.0.0"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0") - ], - targets: [ - .target( - name: "CEEditor", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditUI", package: "CodeEditUI"), - .product(name: "CodeEditDocument", package: "CodeEditDocument"), - .product(name: "CodeEditSettings", package: "CodeEditSettings"), - .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), - .product(name: "CodeEditTextView", package: "CodeEditTextView"), - .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), - .product(name: "CodeEditSymbols", package: "CodeEditSymbols"), - .product(name: "GRDB", package: "GRDB.swift"), - .product(name: "OrderedCollections", package: "swift-collections"), - .product(name: "DequeModule", package: "swift-collections") - ], - swiftSettings: [ - .swiftLanguageMode(.v5) - ] - ) - ] -) diff --git a/Packages/Features/CELSP/Package.swift b/Packages/Features/CELSP/Package.swift deleted file mode 100644 index eff7e0684c..0000000000 --- a/Packages/Features/CELSP/Package.swift +++ /dev/null @@ -1,50 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CELSP", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CELSP", targets: ["CELSP"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditDocument"), - .package(path: "../../Foundation/CodeEditSettings"), - .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), - .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), - .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), - .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.8.2"), - .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.14.0"), - .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.9.0"), - .package(url: "https://github.com/weichsel/ZIPFoundation", exact: "0.9.19"), - .package(url: "https://github.com/apple/swift-async-algorithms.git", exact: "1.0.1") - ], - targets: [ - .target( - name: "CELSP", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditDocument", package: "CodeEditDocument"), - .product(name: "CodeEditSettings", package: "CodeEditSettings"), - .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), - .product(name: "CodeEditTextView", package: "CodeEditTextView"), - .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), - .product(name: "LanguageClient", package: "LanguageClient"), - .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), - .product(name: "JSONRPC", package: "JSONRPC"), - .product(name: "ZIPFoundation", package: "ZIPFoundation"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") - ] - ), - .testTarget( - name: "CELSPTests", - dependencies: [ - "CELSP", - .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), - .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol") - ] - ) - ] -) diff --git a/Packages/Features/CENotifications/Package.swift b/Packages/Features/CENotifications/Package.swift deleted file mode 100644 index af65c0b562..0000000000 --- a/Packages/Features/CENotifications/Package.swift +++ /dev/null @@ -1,24 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CENotifications", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CENotifications", targets: ["CENotifications"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditUI") - ], - targets: [ - .target( - name: "CENotifications", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditUI", package: "CodeEditUI") - ] - ) - ] -) diff --git a/Packages/Features/CESearch/Package.swift b/Packages/Features/CESearch/Package.swift deleted file mode 100644 index 688743ce4f..0000000000 --- a/Packages/Features/CESearch/Package.swift +++ /dev/null @@ -1,31 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CESearch", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CESearch", targets: ["CESearch"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditUI") - ], - targets: [ - .target( - name: "CESearch", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditUI", package: "CodeEditUI") - ] - ), - .testTarget( - name: "CESearchTests", - dependencies: [ - "CESearch", - .product(name: "CodeEditCore", package: "CodeEditCore") - ] - ) - ] -) diff --git a/Packages/Features/CESourceControl/Package.swift b/Packages/Features/CESourceControl/Package.swift deleted file mode 100644 index 1fada2d040..0000000000 --- a/Packages/Features/CESourceControl/Package.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CESourceControl", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CESourceControl", targets: ["CESourceControl"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditSettings"), - .package(path: "../../Foundation/CodeEditUI"), - .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3") - ], - targets: [ - .target( - name: "CESourceControl", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditSettings", package: "CodeEditSettings"), - .product(name: "CodeEditUI", package: "CodeEditUI"), - .product(name: "CodeEditSymbols", package: "CodeEditSymbols") - ] - ), - .testTarget( - name: "CESourceControlTests", - dependencies: ["CESourceControl"] - ) - ] -) diff --git a/Packages/Features/CETerminal/Package.swift b/Packages/Features/CETerminal/Package.swift deleted file mode 100644 index 83f2f81a01..0000000000 --- a/Packages/Features/CETerminal/Package.swift +++ /dev/null @@ -1,26 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CETerminal", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CETerminal", targets: ["CETerminal"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore"), - .package(path: "../../Foundation/CodeEditSettings"), - .package(url: "https://github.com/thecoolwinter/SwiftTerm", branch: "codeedit") - ], - targets: [ - .target( - name: "CETerminal", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditSettings", package: "CodeEditSettings"), - .product(name: "SwiftTerm", package: "SwiftTerm") - ] - ) - ] -) diff --git a/Packages/Foundation/CodeEditCore/Package.swift b/Packages/Foundation/CodeEditCore/Package.swift deleted file mode 100644 index d9ace66786..0000000000 --- a/Packages/Foundation/CodeEditCore/Package.swift +++ /dev/null @@ -1,22 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CodeEditCore", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CodeEditCore", targets: ["CodeEditCore"]) - ], - dependencies: [], - targets: [ - .target( - name: "CodeEditCore", - dependencies: [] - ), - .testTarget( - name: "CodeEditCoreTests", - dependencies: ["CodeEditCore"] - ) - ] -) diff --git a/Packages/Foundation/CodeEditDocument/Package.swift b/Packages/Foundation/CodeEditDocument/Package.swift deleted file mode 100644 index ab5520545e..0000000000 --- a/Packages/Foundation/CodeEditDocument/Package.swift +++ /dev/null @@ -1,31 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CodeEditDocument", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CodeEditDocument", targets: ["CodeEditDocument"]) - ], - dependencies: [ - // Pins match the app's Package.resolved to avoid a second resolved copy. - .package(path: "../CodeEditCore"), - .package(url: "https://github.com/CodeEditApp/CodeEditSourceEditor", exact: "0.15.1"), - .package(url: "https://github.com/CodeEditApp/CodeEditTextView.git", exact: "0.12.1"), - .package(url: "https://github.com/CodeEditApp/CodeEditLanguages.git", exact: "0.1.20"), - .package(url: "https://github.com/ChimeHQ/TextStory", exact: "0.9.1") - ], - targets: [ - .target( - name: "CodeEditDocument", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore"), - .product(name: "CodeEditSourceEditor", package: "CodeEditSourceEditor"), - .product(name: "CodeEditTextView", package: "CodeEditTextView"), - .product(name: "CodeEditLanguages", package: "CodeEditLanguages"), - .product(name: "TextStory", package: "TextStory") - ] - ) - ] -) diff --git a/Packages/Foundation/CodeEditSettings/Package.swift b/Packages/Foundation/CodeEditSettings/Package.swift deleted file mode 100644 index 2608ae4e35..0000000000 --- a/Packages/Foundation/CodeEditSettings/Package.swift +++ /dev/null @@ -1,23 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CodeEditSettings", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CodeEditSettings", targets: ["CodeEditSettings"]) - ], - dependencies: [ - // Pins match the app's Package.resolved to avoid a second resolved copy. - .package(path: "../CodeEditCore") - ], - targets: [ - .target( - name: "CodeEditSettings", - dependencies: [ - .product(name: "CodeEditCore", package: "CodeEditCore") - ] - ) - ] -) diff --git a/Packages/Foundation/CodeEditUI/Package.swift b/Packages/Foundation/CodeEditUI/Package.swift deleted file mode 100644 index f40a3d0d90..0000000000 --- a/Packages/Foundation/CodeEditUI/Package.swift +++ /dev/null @@ -1,26 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CodeEditUI", - platforms: [.macOS(.v14)], - products: [ - .library(name: "CodeEditUI", targets: ["CodeEditUI"]) - ], - dependencies: [ - // Pin matches the app's Package.resolved to avoid a second resolved copy. - .package(url: "https://github.com/CodeEditApp/CodeEditSymbols.git", exact: "0.2.3") - ], - targets: [ - .target( - name: "CodeEditUI", - dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")], - resources: [.process("Resources")] - ), - .testTarget( - name: "CodeEditUIUnitTests", - dependencies: ["CodeEditUI"] - ) - ] -) diff --git a/Packages/Services/CodeEditServices/Package.swift b/Packages/Services/CodeEditServices/Package.swift deleted file mode 100644 index d624688371..0000000000 --- a/Packages/Services/CodeEditServices/Package.swift +++ /dev/null @@ -1,29 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "CodeEditServices", - platforms: [.macOS(.v14)], - products: [ - // One product per service: every consumer's manifest names exactly the - // services it links (decided 2026-07-12; replaced the single umbrella). - .library(name: "ShellClient", targets: ["ShellClient"]), - .library(name: "CEWorkspaceFileManager", targets: ["CEWorkspaceFileManager"]) - ], - dependencies: [ - .package(path: "../../Foundation/CodeEditCore") - ], - targets: [ - // Tier rule: service targets depend on CodeEditCore ONLY — - // never on sibling targets, CodeEditUI, features, or Factory. - .target( - name: "ShellClient", - dependencies: [.product(name: "CodeEditCore", package: "CodeEditCore")] - ), - .target( - name: "CEWorkspaceFileManager", - dependencies: [.product(name: "CodeEditCore", package: "CodeEditCore")] - ) - ] -) From f5616e43673b758f4cb77a7e8dd8fe70856ad2e1 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 7 Aug 2026 18:41:33 +0200 Subject: [PATCH 235/335] Fix: Stop tracking the redundant CodeEditModules/Package.resolved --- .gitignore | 8 +- CodeEditModules/Package.resolved | 213 ------------------------------- 2 files changed, 4 insertions(+), 217 deletions(-) delete mode 100644 CodeEditModules/Package.resolved diff --git a/.gitignore b/.gitignore index 0b354eb762..f1ae65a228 100644 --- a/.gitignore +++ b/.gitignore @@ -49,10 +49,10 @@ playground.xcworkspace .build/ -# Per-package resolved files for local workspace packages — the workspace's -# shared Package.resolved is authoritative; these are Xcode-generated noise. -Packages/**/Package.resolved -Packages/**/.swiftpm/ +# Resolved file for the local CodeEditModules package — the workspace's +# shared Package.resolved is authoritative; this is Xcode/SwiftPM-generated noise. +CodeEditModules/Package.resolved +CodeEditModules/.swiftpm/ # CocoaPods # diff --git a/CodeEditModules/Package.resolved b/CodeEditModules/Package.resolved deleted file mode 100644 index d160a2e5a2..0000000000 --- a/CodeEditModules/Package.resolved +++ /dev/null @@ -1,213 +0,0 @@ -{ - "originHash" : "d7a7982c2b02eb622b8b88148aad4d55c921a587a92058388ae5b1660edb34cc", - "pins" : [ - { - "identity" : "codeeditlanguages", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", - "state" : { - "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", - "version" : "0.1.20" - } - }, - { - "identity" : "codeeditsourceeditor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", - "state" : { - "revision" : "ee0c00a2343903df9d6ef45ce53228aca8637369", - "version" : "0.15.1" - } - }, - { - "identity" : "codeeditsymbols", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", - "state" : { - "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", - "version" : "0.2.3" - } - }, - { - "identity" : "codeedittextview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", - "state" : { - "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", - "version" : "0.12.1" - } - }, - { - "identity" : "fseventswrapper", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Frizlab/FSEventsWrapper", - "state" : { - "revision" : "70bbea4b108221fcabfce8dbced8502831c0ae04", - "version" : "2.1.0" - } - }, - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift.git", - "state" : { - "revision" : "2cf6c756e1e5ef6901ebae16576a7e4e4b834622", - "version" : "6.29.3" - } - }, - { - "identity" : "jsonrpc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/JSONRPC", - "state" : { - "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", - "version" : "0.9.0" - } - }, - { - "identity" : "languageclient", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/LanguageClient", - "state" : { - "revision" : "4f28cc3cad7512470275f65ca2048359553a86f5", - "version" : "0.8.2" - } - }, - { - "identity" : "languageserverprotocol", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", - "state" : { - "revision" : "f7879c782c0845af9c576de7b8baedd946237286", - "version" : "0.14.0" - } - }, - { - "identity" : "processenv", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/ProcessEnv", - "state" : { - "revision" : "552f611479a4f28243a1ef2a7376a216d6899f42", - "version" : "1.0.1" - } - }, - { - "identity" : "queue", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattmassicotte/Queue", - "state" : { - "revision" : "38826d0b8838ce5edcdafa1ffb2f507693d5abcb", - "version" : "0.2.2" - } - }, - { - "identity" : "rearrange", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/Rearrange", - "state" : { - "revision" : "4de8be41dba304192e87dc0a11e0aa39e72aa2e8", - "version" : "2.1.1" - } - }, - { - "identity" : "semaphore", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/Semaphore", - "state" : { - "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", - "version" : "0.1.0" - } - }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", - "version" : "1.0.1" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", - "version" : "1.6.0" - } - }, - { - "identity" : "swift-glob", - "kind" : "remoteSourceControl", - "location" : "https://github.com/davbeck/swift-glob", - "state" : { - "revision" : "07ba6f47d903a0b1b59f12ca70d6de9949b975d6", - "version" : "0.2.0" - } - }, - { - "identity" : "swiftlintplugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/lukepistrol/SwiftLintPlugin", - "state" : { - "revision" : "b384a67cf45d9989aed5aab23e226cbc7bc2cd54", - "version" : "0.65.0" - } - }, - { - "identity" : "swiftterm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/thecoolwinter/SwiftTerm", - "state" : { - "branch" : "codeedit", - "revision" : "2f36f54742d3882e69ff009d084e8675b80934bd" - } - }, - { - "identity" : "swifttreesitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", - "state" : { - "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", - "version" : "0.25.0" - } - }, - { - "identity" : "textformation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/TextFormation", - "state" : { - "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", - "version" : "0.9.0" - } - }, - { - "identity" : "textstory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/TextStory", - "state" : { - "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", - "version" : "0.9.1" - } - }, - { - "identity" : "tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter", - "state" : { - "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", - "version" : "0.25.10" - } - }, - { - "identity" : "zipfoundation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/weichsel/ZIPFoundation", - "state" : { - "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", - "version" : "0.9.19" - } - } - ], - "version" : 3 -} From 59cd61224914f035d6c2f98360cc1d22a8e4e24c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 7 Aug 2026 18:47:12 +0200 Subject: [PATCH 236/335] CI: Rewrite the package audit for the consolidated manifest --- .github/scripts/audit_package_imports.py | 171 +++++++++++++++-------- 1 file changed, 114 insertions(+), 57 deletions(-) diff --git a/.github/scripts/audit_package_imports.py b/.github/scripts/audit_package_imports.py index fd31c079c7..7e1cb05121 100755 --- a/.github/scripts/audit_package_imports.py +++ b/.github/scripts/audit_package_imports.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Audit local Swift packages: every `import` must be declared in the package -manifest, and the tier rules from docs/ARCHITECTURE.md must hold. +"""Audit the CodeEditModules package: every `import` must be declared in the +manifest, and the three rules from docs/ARCHITECTURE.md must hold. Why: Xcode workspace builds share one build directory, so an undeclared import -of a sibling local package compiles fine ("leaky import") and only breaks a -standalone `swift build`. This script makes manifest honesty a PR gate. +of a sibling target compiles fine ("leaky import") and only breaks a standalone +`swift build`. This script makes manifest honesty a PR gate. Usage: python3 .github/scripts/audit_package_imports.py (from anywhere) """ @@ -13,7 +13,8 @@ from pathlib import Path REPO = Path(__file__).resolve().parents[2] -PACKAGES = REPO / "Packages" +PACKAGE = REPO / "CodeEditModules" +MANIFEST = PACKAGE / "Package.swift" # Apple SDK modules used in this codebase; extend when a new system framework is adopted. SYSTEM_MODULES = { @@ -23,81 +24,137 @@ "Swift", "XCTest", "Testing", } -LOCAL_PRODUCTS = { - "CodeEditCore", "CodeEditUI", "CodeEditDocument", "CodeEditSettings", - "CEEditor", "CESearch", "CENotifications", "CELSP", "CESourceControl", - "CETerminal", "ShellClient", "CEWorkspaceFileManager", -} -FEATURE_PRODUCTS = {"CEEditor", "CESearch", "CENotifications", "CELSP", "CESourceControl", "CETerminal"} UI_FRAMEWORKS = {"SwiftUI", "AppKit", "Cocoa"} +# Rule 4: the single target permitted to opt out of Swift 6. +SWIFT5_ALLOWED = {"CEEditor"} + IMPORT_RE = re.compile( r"^\s*(?:@[\w()]+\s+)?import\s+(?:struct\s+|class\s+|enum\s+|func\s+|var\s+)?([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE, ) -PRODUCT_DEP_RE = re.compile(r'\.product\(\s*name:\s*"([^"]+)"') -TARGET_RE = re.compile(r'\.(?:target|executableTarget|testTarget)\(\s*name:\s*"([^"]+)"') - - -def manifest_declared(manifest_text: str) -> set: - """Modules a target in this package may legitimately import.""" - declared = set(PRODUCT_DEP_RE.findall(manifest_text)) - declared |= set(TARGET_RE.findall(manifest_text)) # own targets - # bare-string dependencies inside dependencies: [...] arrays - for match in re.findall(r"dependencies:\s*\[([^\]]*)\]", manifest_text, re.DOTALL): - declared |= set(re.findall(r'"([A-Za-z][\w-]*)"', match)) - return declared +TARGET_START_RE = re.compile(r"\.(target|testTarget)\(\s*name:\s*\"([^\"]+)\"") + + +def balanced_block(text, open_index): + """Return text from the '(' at open_index through its matching ')'.""" + depth = 0 + for i in range(open_index, len(text)): + if text[i] == "(": + depth += 1 + elif text[i] == ")": + depth -= 1 + if depth == 0: + return text[open_index:i + 1] + raise ValueError("unbalanced parentheses in manifest") + + +def parse_targets(text): + """Map target name -> {kind, deps, swift_modes} from the manifest.""" + targets = {} + for match in TARGET_START_RE.finditer(text): + kind, name = match.group(1), match.group(2) + paren = text.index("(", match.start()) + block = balanced_block(text, paren) + deps = set(re.findall(r'"([A-Za-z][\w.\-]*)"', _dependencies_slice(block))) + deps |= set(re.findall(r'\.product\(\s*name:\s*"([^"]+)"', block)) + deps.discard(name) + targets[name] = { + "kind": kind, + "deps": deps, + "swift_modes": set(re.findall(r"\.swiftLanguageMode\(\.(\w+)\)", block)), + } + return targets + + +def _dependencies_slice(block): + """The text inside this target's `dependencies: [...]`, or '' if absent.""" + match = re.search(r"dependencies:\s*\[", block) + if not match: + return "" + start = block.index("[", match.start()) + depth = 0 + for i in range(start, len(block)): + if block[i] == "[": + depth += 1 + elif block[i] == "]": + depth -= 1 + if depth == 0: + return block[start:i + 1] + return "" def main() -> int: - failures = [] - manifests = sorted(PACKAGES.glob("*/*/Package.swift")) - if not manifests: - print(f"Package audit FAILED: no manifests found under {PACKAGES}") + if not MANIFEST.exists(): + print(f"Package audit FAILED: no manifest at {MANIFEST}") return 1 - for manifest in manifests: - pkg_dir = manifest.parent - pkg_name = pkg_dir.name - tier = pkg_dir.parent.name # Foundation | Services | Features - text = manifest.read_text() - declared = manifest_declared(text) - own_targets = set(TARGET_RE.findall(text)) | {pkg_name} - # A package's own targets are not dependencies — exclude them from tier analysis. - local_deps = (declared - own_targets) & LOCAL_PRODUCTS - - # --- tier rules on the manifest itself --- - if pkg_name == "CodeEditCore" and PRODUCT_DEP_RE.search(text): - failures.append(f"{pkg_name}: CodeEditCore must have zero dependencies") - if pkg_name == "CodeEditUI" and local_deps: - failures.append(f"{pkg_name}: CodeEditUI may not depend on local packages (CodeEditSymbols only)") - if tier == "Services" and local_deps - {"CodeEditCore"}: - failures.append( - f"{pkg_name}: service targets may depend on CodeEditCore only " - f"(found {sorted(local_deps - {'CodeEditCore'})})" - ) - if tier == "Features" and (local_deps & FEATURE_PRODUCTS) - {pkg_name}: + text = MANIFEST.read_text() + targets = parse_targets(text) + library_targets = {n for n, t in targets.items() if t["kind"] == "target"} + failures = [] + + # --- Rule 1: CodeEditCore purity (manifest half) --- + if targets.get("CodeEditCore", {}).get("deps"): + failures.append( + "CodeEditCore must have zero dependencies " + f"(found {sorted(targets['CodeEditCore']['deps'])})" + ) + + # --- Rule 2: CodeEditUI purity --- + ui_local = targets.get("CodeEditUI", {}).get("deps", set()) & library_targets + if ui_local: + failures.append( + "CodeEditUI may not depend on local targets — CodeEditSymbols only " + f"(found {sorted(ui_local)})" + ) + + # --- Rule 4: language-mode assertion --- + for name, target in sorted(targets.items()): + if "v5" in target["swift_modes"] and name not in SWIFT5_ALLOWED: failures.append( - f"{pkg_name}: feature packages may not depend on other feature packages " - f"(found {sorted((local_deps & FEATURE_PRODUCTS) - {pkg_name})})" + f"{name}: only {sorted(SWIFT5_ALLOWED)} may declare .swiftLanguageMode(.v5) — " + "every other target must stay on Swift 6" ) - # --- import honesty per source file --- - allowed = declared | SYSTEM_MODULES | {pkg_name} - for swift in sorted((pkg_dir / "Sources").rglob("*.swift")): + # --- Rule 3: import honesty, plus Rule 1's no-UI half --- + for name, target in sorted(targets.items()): + source_dir = PACKAGE / ("Tests" if target["kind"] == "testTarget" else "Sources") / name + if not source_dir.is_dir(): + failures.append(f"{name}: declared in the manifest but {source_dir} does not exist") + continue + system_modules = SYSTEM_MODULES - UI_FRAMEWORKS if name == "CodeEditCore" else SYSTEM_MODULES + allowed = target["deps"] | system_modules | {name} + for swift in sorted(source_dir.rglob("*.swift")): rel = swift.relative_to(REPO) for module in sorted(set(IMPORT_RE.findall(swift.read_text()))): if module not in allowed: - failures.append(f"{rel}: import {module} is not declared in {pkg_name}/Package.swift") - if pkg_name == "CodeEditCore" and module in UI_FRAMEWORKS: - failures.append(f"{rel}: {module} import violates the CodeEditCore no-UI charter") + failures.append(f"{rel}: import {module} is not declared for target {name}") + if name == "CodeEditCore" and module in UI_FRAMEWORKS: + failures.append(f"{rel}: {module} import violates the CodeEditCore no-UI rule") if failures: print(f"Package audit FAILED ({len(failures)} violations):") for failure in failures: print(f" {failure}") return 1 - print(f"Package audit passed ({len(manifests)} packages).") + + # --- Norm (informational only): hub heuristic --- + dependents = {n: 0 for n in targets} + for target in targets.values(): + for dep in target["deps"]: + if dep in dependents: + dependents[dep] += 1 + hubs = sorted( + n for n, t in targets.items() + if t["kind"] == "target" + and dependents[n] >= 3 + and len(t["deps"] & library_targets) >= 3 + ) + if hubs: + print(f"Note — hub targets under review (>=3 dependents and >=3 local deps): {hubs}") + + print(f"Package audit passed ({len(library_targets)} library targets).") return 0 From 9e4c491bd10c782cb90a569e1b60f5fe4f629035 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 7 Aug 2026 22:03:59 +0200 Subject: [PATCH 237/335] Docs: Retarget the remaining tier-era references at CodeEditModules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four references survived the consolidation because no task owned them: - docs/ARCHITECTURE.md's topology section still described "nine local Swift packages, grouped by tier" and claimed every local package builds with Swift 6 (CEEditor declares .swiftLanguageMode(.v5) and is the sole exception). - CONTRIBUTING.md told contributors that features start as Packages/Features/CE packages and that feature-to-feature imports are forbidden. Leaf-ness is now a review norm, not an enforced gate. - The package audit's docstring justified itself by claiming a leaky import "only breaks a standalone swift build". That backstop never existed for the 7 targets that transitively need CodeEditSymbols, which cannot compile under plain SwiftPM at all — so the audit is their only defence, a stronger reason. - A CESearch comment pointed at Packages/Foundation/CodeEditCore. --- .github/scripts/audit_package_imports.py | 11 ++++-- CONTRIBUTING.md | 17 +++++---- .../CESearch/SearchState/SearchState.swift | 2 +- docs/ARCHITECTURE.md | 38 ++++++++++++------- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/.github/scripts/audit_package_imports.py b/.github/scripts/audit_package_imports.py index 7e1cb05121..2394c3f340 100755 --- a/.github/scripts/audit_package_imports.py +++ b/.github/scripts/audit_package_imports.py @@ -2,9 +2,14 @@ """Audit the CodeEditModules package: every `import` must be declared in the manifest, and the three rules from docs/ARCHITECTURE.md must hold. -Why: Xcode workspace builds share one build directory, so an undeclared import -of a sibling target compiles fine ("leaky import") and only breaks a standalone -`swift build`. This script makes manifest honesty a PR gate. +Why: for most targets this script is the ONLY defence against a leaky import. +Xcode workspace builds share one build directory, so an undeclared import of a +sibling target compiles fine. The usual backstop — "it breaks a standalone +`swift build`" — does not exist here: external CodeEditSymbols never declares +its .xcassets under `resources:`, so SwiftPM synthesises no `Bundle.module` +accessor and the dependency itself fails to compile. 7 of the 12 targets need it +transitively, so plain `swift build` is unavailable for them and nothing else +would catch the violation. Hence manifest honesty is a PR gate. Usage: python3 .github/scripts/audit_package_imports.py (from anywhere) """ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53a1cfd368..322b41cca7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,14 +30,15 @@ Please read our guide on [Code Style](https://github.com/CodeEditApp/CodeEdit/wi ## Architecture -CodeEdit is organized as a tiered workspace of local Swift packages (foundation, services, -features) plus a thin app target that composes them. Before adding files, please consult the -decision tree in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — it answers "where does my code -go?" in a few steps. The short version: new features start as `Packages/Features/CE` -packages, feature packages never import each other, and shared code moves to a foundation -package only when it has multiple consumers *and* passes that package's dependency charter. -CI enforces these rules (SwiftLint charter rules + a package import audit), so a misplaced -file will fail checks. +CodeEdit is a thin app target plus one local multi-target Swift package, `CodeEditModules`, +whose single `Package.swift` holds the entire local dependency graph. Before adding files, +please consult the decision tree in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — it answers +"where does my code go?" in a few steps. The short version: a new feature starts as a new +target in `CodeEditModules/Package.swift`, `CodeEditCore` stays dependency-free and UI-free, +and `CodeEditUI` depends on `CodeEditSymbols` alone. CI enforces those as hard rules (SwiftLint +charter rules + a package import audit), so a misplaced file will fail checks. Features should +also prefer to be leaves that nothing else depends on — that one is a review preference rather +than a gate, so declare any target-to-target edge in the manifest where reviewers can see it. ## Pull Request diff --git a/CodeEditModules/Sources/CESearch/SearchState/SearchState.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState.swift index 2015cba4ae..aaae4fe4d7 100644 --- a/CodeEditModules/Sources/CESearch/SearchState/SearchState.swift +++ b/CodeEditModules/Sources/CESearch/SearchState/SearchState.swift @@ -36,7 +36,7 @@ public final class SearchState: ObservableObject { @Published public var replaceText: String = "" /// The find/replace primitive shared with the Editor feature, kept in sync with - /// `searchQuery`/`replaceText` below. See `Packages/Foundation/CodeEditCore`. + /// `searchQuery`/`replaceText` below. See `CodeEditModules/Sources/CodeEditCore`. public let query = FindReplaceQuery() private var queryBridgeCancellables: Set = [] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 534f6fe28c..e78dd48702 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -6,29 +6,41 @@ before you add files will save you a failed check. ## Package topology -The workspace contains one app project and nine local Swift packages, grouped by tier: +The workspace contains one app project and one local Swift package holding 12 library targets +and 5 test targets: ``` CodeEdit.xcworkspace ├── CodeEdit.xcodeproj — app shell + composition UI -└── Packages/ - ├── Foundation/ +└── CodeEditModules/ + ├── Package.swift — the entire local dependency graph, in one file + ├── Sources/ │ ├── CodeEditCore — pure types, EventBus, command interfaces (no UI/IO, zero deps) │ ├── CodeEditUI — shared presentation atoms (→ CodeEditSymbols only) │ ├── CodeEditDocument — CodeFileDocument + editor-framework bridging protocols - │ └── CodeEditSettings — settings model + store (UI pages stay app-side) - ├── Features/ — CEEditor, CESearch, CENotifications, CELSP, - │ CESourceControl, CETerminal (one package per feature) - └── Services/CodeEditServices — ShellClient, CEWorkspaceFileManager (one target each) + │ ├── CodeEditSettings — settings model + store (UI pages stay app-side) + │ ├── ShellClient — Process adapter + │ ├── CEWorkspaceFileManager — FileManager + FSEvents workspace tree + │ └── CEEditor, CESearch, CENotifications, CELSP, CESourceControl, CETerminal + │ — one target per feature + └── Tests/ — CodeEditCoreTests, CodeEditUIUnitTests, CESearchTests, + CELSPTests, CESourceControlTests ``` -Naming: `CodeEdit*` = foundation substrate (peer-named with the external CodeEdit libraries), -`CE*` = feature packages (peer-named with the `CE*` domain types). Services are named after -their primary type. +Each library target publishes a like-named `.library` product, and the app target links the ones +it needs. Inside the manifest, targets reference each other by bare name, so the whole graph is +legible in a single file — which is the point. See +[History](#history-why-the-2022-module-split-failed) for what the previous arrangement cost. -All local packages build with Swift 6 strict concurrency. The app target is still Swift 5 — -write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers -aren't isolated (it cascades). +Naming: `CodeEdit*` marks substrate peer-named with the external CodeEdit libraries +(`CodeEditSourceEditor`, `CodeEditSymbols`, …); `CE*` marks app-internal feature contexts, +peer-named with the `CE*` domain types. Apply `CE` only where the bare name would collide with a +stdlib/SwiftUI/AppKit/vendor type — it is a collision-avoider, not a namespace. + +Every target builds with Swift 6 strict concurrency **except `CEEditor`**, which declares +`.swiftLanguageMode(.v5)` and is the sole exception. The app target is still Swift 5 — write new +app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't +isolated (it cascades). ## History: why the 2022 module split failed From 62a0da52b8e097f073dbcfc1019bed8854dd72c9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 8 Aug 2026 11:43:12 +0200 Subject: [PATCH 238/335] Docs: Resolve the standalone-build contradiction in ARCHITECTURE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit corrected the package audit's docstring to say the "undeclared imports only break a standalone swift build" backstop never existed for the 7 targets that transitively need CodeEditSymbols, but left the same claim standing in two places in this file — in Rules item 3 and in Enforcement. Both now state the stronger, true reason: for most of the graph the audit is the only defence, because no standalone build is available to fall back on. --- docs/ARCHITECTURE.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e78dd48702..82dec79c76 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -97,7 +97,9 @@ Three checks are enforced in CI. Each one blocks a specific failure documented i a deliberate consequence, not an accident. 3. **Import honesty.** Every `import` in a target's sources must be declared in that target's manifest dependencies. Xcode workspace builds share one build directory, so an undeclared import - of a sibling compiles fine and only breaks a standalone `swift build`. + of a sibling compiles fine and nothing else catches it. For the 7 targets that transitively need + `CodeEditSymbols`, a standalone `swift build` is not available as a backstop either — see + [Enforcement](#enforcement) — so this check is their only defence, not a redundant one. Plus one assertion: **only `CEEditor` may declare `.swiftLanguageMode(.v5)`.** Every other target inherits Swift 6 from the package's tools version. A target silently dropping to Swift 5 would lose @@ -240,8 +242,9 @@ Two automated checks keep this document honest; both run on every PR: in every target is declared in that target's manifest dependencies, and that the three [Rules](#rules) plus the language-mode assertion hold. It exists because Xcode workspace builds share one build directory, so an undeclared import of a sibling target compiles fine - locally and the violation stays invisible until a standalone build breaks. Every target is - declared in `CodeEditModules/Package.swift`. + locally. For the 7 targets that transitively need `CodeEditSymbols` there is no standalone + `swift build` to fall back on either (see the quirk above), so for most of the graph this + audit is the only thing standing between a leaky import and `main`. Run both locally from the repo root: From e69a66e22677d08fb15bb733170a3f4de0d2a631 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 21:24:18 +0200 Subject: [PATCH 239/335] Test: Add settings format round-trip safety net Adds a CodeEditSettingsTests target with a fully-populated settings.json fixture in which no value sits at its default, so a silent fallback to defaults during decoding fails the test rather than passing vacuously. Also lands the unknown-section preservation test, disabled with its body commented out until SettingsStore exists. --- CodeEditModules/Package.swift | 5 + .../Fixtures/full-settings.json | 161 ++++++++++++++++++ .../Fixtures/unknown-sections.json | 15 ++ .../SettingsFormatTests.swift | 55 ++++++ CodeEditTestPlan.xctestplan | 7 + 5 files changed, 243 insertions(+) create mode 100644 CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/full-settings.json create mode 100644 CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/unknown-sections.json create mode 100644 CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 74f386d2fa..c6aa9e86ef 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -119,6 +119,11 @@ let package = Package( // MARK: - Tests .testTarget(name: "CodeEditCoreTests", dependencies: ["CodeEditCore"]), + .testTarget( + name: "CodeEditSettingsTests", + dependencies: ["CodeEditSettings"], + resources: [.copy("Fixtures")] + ), .testTarget(name: "CodeEditUIUnitTests", dependencies: ["CodeEditUI"]), .testTarget(name: "CESearchTests", dependencies: ["CESearch", "CodeEditCore"]), .testTarget( diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/full-settings.json b/CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/full-settings.json new file mode 100644 index 0000000000..370dd19b29 --- /dev/null +++ b/CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/full-settings.json @@ -0,0 +1,161 @@ +{ + "accounts" : { + "sourceControlAccounts" : { + "gitAccounts" : [ + + ], + "sshKey" : "/Users/test/.ssh/id_ed25519" + } + }, + "developerSettings" : { + "lspBinaries" : { + + }, + "showInternalDevelopmentInspector" : true + }, + "general" : { + "appAppearance" : "dark", + "dimEditorsWithoutFocus" : true, + "fileExtensionsVisibility" : { + "hideAll" : { + + } + }, + "fileIconStyle" : "monochrome", + "findNavigatorDetail" : 10, + "hiddenFileExtensions" : { + "extensions" : [ + "log", + "tmp" + ] + }, + "inspectorTabBarPosition" : "side", + "isAutoSaveOn" : false, + "issueNavigatorDetail" : 30, + "navigatorTabBarPosition" : "side", + "projectNavigatorSize" : "large", + "reopenBehavior" : "newDocument", + "reopenWindowAfterClose" : "quit", + "revealFileOnFocusChange" : true, + "showEditorJumpBar" : false, + "showIssues" : "minimized", + "showLiveIssues" : false, + "shownFileExtensions" : { + "extensions" : [ + "rs", + "toml" + ] + } + }, + "keybindings" : { + "keybindings" : { + + } + }, + "languageServers" : { + "installedLanguageServers" : { + + } + }, + "navigation" : { + "navigationStyle" : "openInPlace" + }, + "search" : { + "ignoreGlobPatterns" : [ + + ] + }, + "sourceControl" : { + "general" : { + "addRemoveAutomatically" : false, + "controlNavigatorOrder" : "sortByDate", + "fetchRefreshServerStatus" : false, + "includeUpstreamChanges" : false, + "openFeedbackInBrowser" : false, + "refreshStatusLocally" : false, + "revisionComparisonLayout" : "localRight", + "selectFilesToCommit" : false, + "showSourceControlChanges" : false, + "sourceControlIsEnabled" : false + }, + "git" : { + "showMergeCommitsPerFileLog" : true + } + }, + "terminal" : { + "cursorBlink" : true, + "cursorStyle" : "bar", + "darkAppearance" : true, + "font" : { + "name" : "Menlo", + "size" : 14, + "weight" : 0.5 + }, + "optionAsMeta" : true, + "shell" : "zsh", + "useEditorTheme" : false, + "useLoginShell" : false, + "useShellIntegration" : false, + "useTextEditorFont" : false, + "useThemeBackground" : false + }, + "textEditing" : { + "autocompleteBraces" : false, + "bracketEmphasis" : { + "color" : { + "color" : "FF0000" + }, + "highlightType" : "bordered", + "useCustomColor" : true + }, + "defaultTabWidth" : 8, + "enableTypeOverCompletion" : false, + "font" : { + "name" : "Menlo", + "size" : 16, + "weight" : 0.5 + }, + "indentOption" : { + "indentType" : "tab", + "spaceCount" : 2 + }, + "invisibleCharacters" : { + "carriageReturnReplacement" : "R", + "enabled" : true, + "lineFeedReplacement" : "F", + "lineSeparatorReplacement" : "L", + "paragraphSeparatorReplacement" : "P", + "showLineEndings" : false, + "showSpaces" : false, + "showTabs" : false, + "spaceReplacement" : "S", + "tabReplacement" : "T" + }, + "letterSpacing" : 1.5, + "lineHeightMultiple" : 1.5, + "overscroll" : "large", + "reformatAtColumn" : 100, + "showFoldingRibbon" : false, + "showGutter" : false, + "showMinimap" : false, + "showReformattingGuide" : true, + "useSystemCursor" : false, + "warningCharacters" : { + "characters" : [ + 894, + "Greek Question Mark" + ], + "enabled" : false + }, + "wrapLinesToEditorWidth" : false + }, + "theme" : { + "matchAppearance" : false, + "overrides" : { + + }, + "selectedDarkTheme" : "Solarized (Dark)", + "selectedLightTheme" : "Solarized (Light)", + "useThemeBackground" : false + } +} diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/unknown-sections.json b/CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/unknown-sections.json new file mode 100644 index 0000000000..a2cd2fa9eb --- /dev/null +++ b/CodeEditModules/Tests/CodeEditSettingsTests/Fixtures/unknown-sections.json @@ -0,0 +1,15 @@ +{ + "general" : { + "fileIconStyle" : "monochrome" + }, + "someFutureFeature" : { + "enabled" : true, + "threshold" : 42 + }, + "extensions" : { + "com.example.myextension" : { + "apiKey" : "abc123", + "nested" : { "deep" : [ 1, 2, 3 ] } + } + } +} diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift new file mode 100644 index 0000000000..d8408394f8 --- /dev/null +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -0,0 +1,55 @@ +// +// SettingsFormatTests.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/26. +// + +import Testing +import Foundation +@testable import CodeEditSettings + +struct SettingsFormatTests { + + private func fixture(_ name: String) throws -> Data { + let url = try #require( + Bundle.module.url(forResource: name, withExtension: "json", subdirectory: "Fixtures") + ) + return try Data(contentsOf: url) + } + + private func parsed(_ data: Data) throws -> NSDictionary { + try #require(JSONSerialization.jsonObject(with: data) as? NSDictionary) + } + + /// Decoding then encoding a fully-populated settings file must not change any key or value. + /// + /// The fixture deliberately holds no default values: a fixture of defaults would pass even if + /// decoding silently fell back to defaults, which is the failure this guards against. + @Test + func fullSettingsRoundTripsUnchanged() throws { + let original = try fixture("full-settings") + let decoded = try JSONDecoder().decode(SettingsData.self, from: original) + let reencoded = try JSONEncoder().encode(decoded) + + #expect(try parsed(reencoded) == parsed(original)) + } + + /// Every section present on disk must survive a save, whether or not anything is registered to + /// read it. This is what keeps a disabled or not-yet-loaded extension's configuration alive. + /// + /// The body is commented out rather than merely disabled because `SettingsStore` does not exist + /// yet and a `.disabled` trait does not prevent compilation. Task 9 introduces the store, + /// uncomments this, and drops the trait. + @Test(.disabled("Target behaviour introduced in Task 9: SettingsStore preservation")) + func unknownSectionsSurviveASave() throws { +// let original = try fixture("unknown-sections") +// let store = try SettingsStore(data: original) +// let saved = try store.encoded() +// +// let before = try parsed(original) +// let after = try parsed(saved) +// #expect(after["someFutureFeature"] as? NSDictionary == before["someFutureFeature"] as? NSDictionary) +// #expect(after["extensions"] as? NSDictionary == before["extensions"] as? NSDictionary) + } +} diff --git a/CodeEditTestPlan.xctestplan b/CodeEditTestPlan.xctestplan index 23634ff0eb..4dce4bc57e 100644 --- a/CodeEditTestPlan.xctestplan +++ b/CodeEditTestPlan.xctestplan @@ -40,6 +40,13 @@ "name" : "CodeEditCoreTests" } }, + { + "target" : { + "containerPath" : "container:CodeEditModules", + "identifier" : "CodeEditSettingsTests", + "name" : "CodeEditSettingsTests" + } + }, { "target" : { "containerPath" : "container:CodeEditModules", From 73798aacc45082ad97519dfcbcf7b30a12fcb938 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 21:29:44 +0200 Subject: [PATCH 240/335] Refactor: Lift settings structs out of the SettingsData namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All eleven settings structs were declared inside `extension SettingsData`, making the aggregate a namespace for types it should not own — CELSP's public RegistryManaging protocol carried SettingsData.InstalledLanguageServer in its signature. Each struct is now top level, with its helper types nested under the struct that owns them (SettingsData.TerminalShell -> TerminalSettings.Shell, SettingsData.InstalledLanguageServer -> LanguageServerSettings.Installed, the ten General helpers under GeneralSettings, and so on). Nesting is not encoded, and no property name or CodingKeys was touched, so the on-disk format is unchanged — verified by the round-trip test. --- CodeEdit/App/CodeEditApp.swift | 2 +- .../InvisibleCharacterWarningList.swift | 2 +- .../Controls/WarningCharactersView.swift | 2 +- .../GeneralSettings/GeneralSettingsView.swift | 52 +-- .../NavigationSettingsView.swift | 4 +- .../SourceControlGeneralView.swift | 8 +- .../TerminalSettingsView.swift | 12 +- .../InvisiblesSettingsView.swift | 2 +- .../TextEditingSettingsView.swift | 20 +- .../Settings/Search/SettingsData+Search.swift | 20 +- .../SettingsData+CommandRegistration.swift | 2 +- .../InspectorArea/FileInspectorView.swift | 8 +- .../InspectorArea/InspectorAreaView.swift | 2 +- .../NavigatorArea/NavigatorAreaView.swift | 2 +- .../ProjectNavigatorViewController.swift | 8 +- .../WorkspacePanel/WorkspacePanelTabBar.swift | 2 +- .../WorkspacePanel/WorkspacePanelView.swift | 4 +- .../Sources/CEEditor/Views/CodeFileView.swift | 4 +- .../Registry/Protocols/RegistryManaging.swift | 2 +- .../CELSP/Registry/RegistryManager.swift | 2 +- .../Views/CELocalShellTerminalView.swift | 2 +- .../Models/AccountsSettings.swift | 15 +- .../Models/DeveloperSettings.swift | 16 +- .../Models/GeneralSettings.swift | 95 ++--- .../Models/KeybindingsSettings.swift | 31 +- .../Models/LanguageServerSettings.swift | 16 +- .../Models/NavigationSettings.swift | 15 +- .../Models/SearchSettings.swift | 12 +- .../Models/SourceControlSettings.swift | 28 +- .../Models/TerminalSettings.swift | 63 ++- .../Models/TextEditingSettings.swift | 391 +++++++++--------- .../Models/ThemeSettings.swift | 147 ++++--- .../Store/CodableDefault+Providers.swift | 32 +- .../FileExtensionVisibilityTests.swift | 4 +- 34 files changed, 497 insertions(+), 530 deletions(-) diff --git a/CodeEdit/App/CodeEditApp.swift b/CodeEdit/App/CodeEditApp.swift index fc46c01c47..228889627d 100644 --- a/CodeEdit/App/CodeEditApp.swift +++ b/CodeEdit/App/CodeEditApp.swift @@ -23,7 +23,7 @@ struct CodeEditApp: App { CodeFileDocument.delegateProvider = { [dependencies = appdelegate.dependencies] in dependencies.codeFileDocumentDelegate } - SettingsData.TextEditingSettings.registerCommands(in: appdelegate.dependencies.commandManager) + TextEditingSettings.registerCommands(in: appdelegate.dependencies.commandManager) SettingsData.reconcileDefaultKeybindings(keybindingManager: appdelegate.dependencies.keybindingManager) } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Controls/InvisibleCharacterWarningList.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/InvisibleCharacterWarningList.swift index 4a6fd7a7e3..6699fcbc24 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Controls/InvisibleCharacterWarningList.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Controls/InvisibleCharacterWarningList.swift @@ -42,7 +42,7 @@ struct InvisibleCharacterWarningList: View { Button { // Add defaults without removing user's data. We do still override notes here. items = items.merging( - SettingsData.TextEditingSettings.WarningCharacters.default.characters, + TextEditingSettings.WarningCharacters.default.characters, uniquingKeysWith: { _, defaults in defaults } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Controls/WarningCharactersView.swift b/CodeEdit/AuxiliaryWindows/Settings/Controls/WarningCharactersView.swift index 9a7b8f0d66..ab72f6d0bd 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Controls/WarningCharactersView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Controls/WarningCharactersView.swift @@ -9,7 +9,7 @@ import SwiftUI import CodeEditSettings struct WarningCharactersView: View { - typealias Config = SettingsData.TextEditingSettings.WarningCharacters + typealias Config = TextEditingSettings.WarningCharacters @Binding var warningCharacters: Config diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift index 769e38897d..345b32e4be 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/GeneralSettings/GeneralSettingsView.swift @@ -80,12 +80,12 @@ private extension GeneralSettingsView { var appearance: some View { Picker("Appearance", selection: $settings.appAppearance) { Text("System") - .tag(SettingsData.Appearances.system) + .tag(GeneralSettings.Appearances.system) Divider() Text("Light") - .tag(SettingsData.Appearances.light) + .tag(GeneralSettings.Appearances.light) Text("Dark") - .tag(SettingsData.Appearances.dark) + .tag(GeneralSettings.Appearances.dark) } .onChange(of: settings.appAppearance) { _, tag in tag.applyAppearance() @@ -96,9 +96,9 @@ private extension GeneralSettingsView { var showIssues: some View { Picker("Show Issues", selection: $settings.showIssues) { Text("Show Inline") - .tag(SettingsData.Issues.inline) + .tag(GeneralSettings.Issues.inline) Text("Show Minimized") - .tag(SettingsData.Issues.minimized) + .tag(GeneralSettings.Issues.minimized) } } @@ -118,14 +118,14 @@ private extension GeneralSettingsView { Group { Picker("File Extensions", selection: $settings.fileExtensionsVisibility) { Text("Hide all") - .tag(SettingsData.FileExtensionsVisibility.hideAll) + .tag(GeneralSettings.FileExtensionsVisibility.hideAll) Text("Show all") - .tag(SettingsData.FileExtensionsVisibility.showAll) + .tag(GeneralSettings.FileExtensionsVisibility.showAll) Divider() Text("Show only") - .tag(SettingsData.FileExtensionsVisibility.showOnly) + .tag(GeneralSettings.FileExtensionsVisibility.showOnly) Text("Hide only") - .tag(SettingsData.FileExtensionsVisibility.hideOnly) + .tag(GeneralSettings.FileExtensionsVisibility.hideOnly) } if case .showOnly = settings.fileExtensionsVisibility { TextField("", text: $settings.shownFileExtensions.string, axis: .vertical) @@ -143,9 +143,9 @@ private extension GeneralSettingsView { var fileIconStyle: some View { Picker("File Icon Style", selection: $settings.fileIconStyle) { Text("Color") - .tag(SettingsData.FileIconStyle.color) + .tag(GeneralSettings.FileIconStyle.color) Text("Monochrome") - .tag(SettingsData.FileIconStyle.monochrome) + .tag(GeneralSettings.FileIconStyle.monochrome) } .pickerStyle(.radioGroup) } @@ -153,9 +153,9 @@ private extension GeneralSettingsView { var navigatorTabBarPosition: some View { Picker("Navigator Tab Bar Position", selection: $settings.navigatorTabBarPosition) { Text("Top") - .tag(SettingsData.SidebarTabBarPosition.top) + .tag(GeneralSettings.SidebarTabBarPosition.top) Text("Side") - .tag(SettingsData.SidebarTabBarPosition.side) + .tag(GeneralSettings.SidebarTabBarPosition.side) } .pickerStyle(.radioGroup) } @@ -163,9 +163,9 @@ private extension GeneralSettingsView { var inspectorTabBarPosition: some View { Picker("Inspector Tab Bar Position", selection: $settings.inspectorTabBarPosition) { Text("Top") - .tag(SettingsData.SidebarTabBarPosition.top) + .tag(GeneralSettings.SidebarTabBarPosition.top) Text("Side") - .tag(SettingsData.SidebarTabBarPosition.side) + .tag(GeneralSettings.SidebarTabBarPosition.side) } .pickerStyle(.radioGroup) } @@ -173,12 +173,12 @@ private extension GeneralSettingsView { var reopenBehavior: some View { Picker("Reopen Behavior", selection: $settings.reopenBehavior) { Text("Welcome Screen") - .tag(SettingsData.ReopenBehavior.welcome) + .tag(GeneralSettings.ReopenBehavior.welcome) Divider() Text("Open Panel") - .tag(SettingsData.ReopenBehavior.openPanel) + .tag(GeneralSettings.ReopenBehavior.openPanel) Text("New Document") - .tag(SettingsData.ReopenBehavior.newDocument) + .tag(GeneralSettings.ReopenBehavior.newDocument) } } @@ -188,29 +188,29 @@ private extension GeneralSettingsView { selection: $settings.reopenWindowAfterClose ) { Text("Do nothing") - .tag(SettingsData.ReopenWindowBehavior.doNothing) + .tag(GeneralSettings.ReopenWindowBehavior.doNothing) Divider() Text("Show Welcome Window") - .tag(SettingsData.ReopenWindowBehavior.showWelcomeWindow) + .tag(GeneralSettings.ReopenWindowBehavior.showWelcomeWindow) Text("Quit") - .tag(SettingsData.ReopenWindowBehavior.quit) + .tag(GeneralSettings.ReopenWindowBehavior.quit) } } var projectNavigatorSize: some View { Picker("Project Navigator Size", selection: $settings.projectNavigatorSize) { Text("Small") - .tag(SettingsData.ProjectNavigatorSize.small) + .tag(GeneralSettings.ProjectNavigatorSize.small) Text("Medium") - .tag(SettingsData.ProjectNavigatorSize.medium) + .tag(GeneralSettings.ProjectNavigatorSize.medium) Text("Large") - .tag(SettingsData.ProjectNavigatorSize.large) + .tag(GeneralSettings.ProjectNavigatorSize.large) } } var findNavigatorDetail: some View { Picker("Find Navigator Detail", selection: $settings.findNavigatorDetail) { - ForEach(SettingsData.NavigatorDetail.allCases, id: \.self) { tag in + ForEach(GeneralSettings.NavigatorDetail.allCases, id: \.self) { tag in Text(tag.label).tag(tag) } } @@ -219,7 +219,7 @@ private extension GeneralSettingsView { // TODO: Implement reflecting Issue Navigator Detail preference and remove disabled modifier var issueNavigatorDetail: some View { Picker("Issue Navigator Detail", selection: $settings.issueNavigatorDetail) { - ForEach(SettingsData.NavigatorDetail.allCases, id: \.self) { tag in + ForEach(GeneralSettings.NavigatorDetail.allCases, id: \.self) { tag in Text(tag.label).tag(tag) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift index 7431fd47c4..b78f59ca90 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/NavigationSettings/NavigationSettingsView.swift @@ -25,9 +25,9 @@ private extension NavigationSettingsView { private var navigationStyle: some View { Picker("Navigation Style", selection: $settings.navigationStyle) { Text("Open in Tabs") - .tag(SettingsData.NavigationStyle.openInTabs) + .tag(NavigationSettings.NavigationStyle.openInTabs) Text("Open in Place") - .tag(SettingsData.NavigationStyle.openInPlace) + .tag(NavigationSettings.NavigationStyle.openInPlace) } } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift index 351fa070f5..a52f9a6626 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/SourceControlSettings/SourceControlGeneralView.swift @@ -90,9 +90,9 @@ private extension SourceControlGeneralView { selection: $settings.revisionComparisonLayout ) { Text("Local Revision on Left Side") - .tag(SettingsData.RevisionComparisonLayout.localLeft) + .tag(SourceControlSettings.RevisionComparisonLayout.localLeft) Text("Local Revision on Right Side") - .tag(SettingsData.RevisionComparisonLayout.localRight) + .tag(SourceControlSettings.RevisionComparisonLayout.localRight) } } @@ -102,9 +102,9 @@ private extension SourceControlGeneralView { selection: $settings.controlNavigatorOrder ) { Text("Sort by Name") - .tag(SettingsData.ControlNavigatorOrder.sortByName) + .tag(SourceControlSettings.ControlNavigatorOrder.sortByName) Text("Sort by Date") - .tag(SettingsData.ControlNavigatorOrder.sortByDate) + .tag(SourceControlSettings.ControlNavigatorOrder.sortByDate) } } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift index defc690143..212df2de82 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift @@ -42,23 +42,23 @@ private extension TerminalSettingsView { @ViewBuilder private var shellSelector: some View { Picker("Shell", selection: $settings.shell) { Text("System Default") - .tag(SettingsData.TerminalShell.system) + .tag(TerminalSettings.Shell.system) Divider() Text("Zsh") - .tag(SettingsData.TerminalShell.zsh) + .tag(TerminalSettings.Shell.zsh) Text("Bash") - .tag(SettingsData.TerminalShell.bash) + .tag(TerminalSettings.Shell.bash) } } private var cursorStyle: some View { Picker("Terminal Cursor Style", selection: $settings.cursorStyle) { Text("Block") - .tag(SettingsData.TerminalCursorStyle.block) + .tag(TerminalSettings.CursorStyle.block) Text("Underline") - .tag(SettingsData.TerminalCursorStyle.underline) + .tag(TerminalSettings.CursorStyle.underline) Text("Bar") - .tag(SettingsData.TerminalCursorStyle.bar) + .tag(TerminalSettings.CursorStyle.bar) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift index 59247cbeab..8dcdda3c95 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/InvisiblesSettingsView.swift @@ -9,7 +9,7 @@ import SwiftUI import CodeEditSettings struct InvisiblesSettingsView: View { - typealias Config = SettingsData.TextEditingSettings.InvisibleCharactersConfig + typealias Config = TextEditingSettings.InvisibleCharactersConfig @Binding var invisibleCharacters: Config diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift index 01e5e28745..aa86635208 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/TextEditingSettings/TextEditingSettingsView.swift @@ -102,19 +102,19 @@ private extension TextEditingSettingsView { selection: $textEditing.overscroll ) { Text("None") - .tag(SettingsData.TextEditingSettings.OverscrollOption.none) + .tag(TextEditingSettings.OverscrollOption.none) Divider() Text("Small") .tag( - SettingsData.TextEditingSettings.OverscrollOption.small + TextEditingSettings.OverscrollOption.small ) Text("Medium") .tag( - SettingsData.TextEditingSettings.OverscrollOption.medium + TextEditingSettings.OverscrollOption.medium ) Text("Large") .tag( - SettingsData.TextEditingSettings.OverscrollOption.large + TextEditingSettings.OverscrollOption.large ) } } @@ -134,9 +134,9 @@ private extension TextEditingSettingsView { Group { Picker("Prefer Indent Using", selection: $textEditing.indentOption.indentType) { Text("Tabs") - .tag(SettingsData.TextEditingSettings.IndentOption.IndentType.tab) + .tag(TextEditingSettings.IndentOption.IndentType.tab) Text("Spaces") - .tag(SettingsData.TextEditingSettings.IndentOption.IndentType.spaces) + .tag(TextEditingSettings.IndentOption.IndentType.spaces) } if textEditing.indentOption.indentType == .spaces { HStack { @@ -192,11 +192,11 @@ private extension TextEditingSettingsView { "Bracket Pair Highlight", selection: $textEditing.bracketEmphasis.highlightType ) { - Text("Disabled").tag(SettingsData.TextEditingSettings.BracketPairEmphasis.HighlightType.disabled) + Text("Disabled").tag(TextEditingSettings.BracketPairEmphasis.HighlightType.disabled) Divider() - Text("Bordered").tag(SettingsData.TextEditingSettings.BracketPairEmphasis.HighlightType.bordered) - Text("Flash").tag(SettingsData.TextEditingSettings.BracketPairEmphasis.HighlightType.flash) - Text("Underline").tag(SettingsData.TextEditingSettings.BracketPairEmphasis.HighlightType.underline) + Text("Bordered").tag(TextEditingSettings.BracketPairEmphasis.HighlightType.bordered) + Text("Flash").tag(TextEditingSettings.BracketPairEmphasis.HighlightType.flash) + Text("Underline").tag(TextEditingSettings.BracketPairEmphasis.HighlightType.underline) } if [.bordered, .underline].contains(textEditing.bracketEmphasis.highlightType) { Toggle("Use Custom Color", isOn: $textEditing.bracketEmphasis.useCustomColor) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift index d80fa02578..003771db83 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift @@ -14,7 +14,7 @@ import CodeEditSettings // app rather than the CodeEditSettings package. One conformance extension per // persisted settings page, plus `propertiesOf`. -extension SettingsData.GeneralSettings: SearchableSettingsPage { +extension GeneralSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Appearance", @@ -45,7 +45,7 @@ extension SettingsData.GeneralSettings: SearchableSettingsPage { } } -extension SettingsData.AccountsSettings: SearchableSettingsPage { +extension AccountsSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Accounts", @@ -56,7 +56,7 @@ extension SettingsData.AccountsSettings: SearchableSettingsPage { } } -extension SettingsData.NavigationSettings: SearchableSettingsPage { +extension NavigationSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Navigation Style", @@ -65,7 +65,7 @@ extension SettingsData.NavigationSettings: SearchableSettingsPage { } } -extension SettingsData.ThemeSettings: SearchableSettingsPage { +extension ThemeSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Automatically Change theme based on system appearance", @@ -84,7 +84,7 @@ extension SettingsData.ThemeSettings: SearchableSettingsPage { } } -extension SettingsData.TextEditingSettings: SearchableSettingsPage { +extension TextEditingSettings: SearchableSettingsPage { var searchKeys: [String] { var keys = [ "Prefer Indent Using", @@ -114,7 +114,7 @@ extension SettingsData.TextEditingSettings: SearchableSettingsPage { } } -extension SettingsData.TerminalSettings: SearchableSettingsPage { +extension TerminalSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Shell", @@ -129,7 +129,7 @@ extension SettingsData.TerminalSettings: SearchableSettingsPage { } } -extension SettingsData.SourceControlSettings: SearchableSettingsPage { +extension SourceControlSettings: SearchableSettingsPage { var searchKeys: [String] { [ "General", @@ -153,7 +153,7 @@ extension SettingsData.SourceControlSettings: SearchableSettingsPage { } } -extension SettingsData.SearchSettings: SearchableSettingsPage { +extension SearchSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Ignore Glob Patterns", @@ -163,7 +163,7 @@ extension SettingsData.SearchSettings: SearchableSettingsPage { } } -extension SettingsData.LanguageServerSettings: SearchableSettingsPage { +extension LanguageServerSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Language Servers", @@ -177,7 +177,7 @@ extension SettingsData.LanguageServerSettings: SearchableSettingsPage { } } -extension SettingsData.DeveloperSettings: SearchableSettingsPage { +extension DeveloperSettings: SearchableSettingsPage { var searchKeys: [String] { [ "Developer", diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift index f22c21d19d..a66468f170 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift @@ -9,7 +9,7 @@ import Foundation import CodeEditCore import CodeEditSettings -extension SettingsData.TextEditingSettings { +extension TextEditingSettings { /// Registers toggle-able text-editing preferences with the command palette. /// Invoked once at app startup (previously ran as a side effect of decoding). static func registerCommands(in mgr: CommandManaging) { diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift index 9645afc06d..e61b91237a 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift @@ -29,13 +29,13 @@ struct FileInspectorView: View { @State private var languageId: String? - @State var indentOption: SettingsData.TextEditingSettings.IndentOption = .init(indentType: .tab) + @State var indentOption: TextEditingSettings.IndentOption = .init(indentType: .tab) @State var defaultTabWidth: Int = 0 @State var wrapLines: Bool = false - func updateFileOptions(_ textEditingOverride: SettingsData.TextEditingSettings? = nil) { + func updateFileOptions(_ textEditingOverride: TextEditingSettings? = nil) { let textEditingSettings = textEditingOverride ?? textEditing let values = file.map { fileEditorOverrides.overrides(for: $0) } indentOption = values?.indentOption ?? textEditingSettings.indentOption @@ -159,8 +159,8 @@ struct FileInspectorView: View { private var indentUsing: some View { Picker("Indent using", selection: $indentOption.indentType) { - Text("Spaces").tag(SettingsData.TextEditingSettings.IndentOption.IndentType.spaces) - Text("Tabs").tag(SettingsData.TextEditingSettings.IndentOption.IndentType.tab) + Text("Spaces").tag(TextEditingSettings.IndentOption.IndentType.spaces) + Text("Tabs").tag(TextEditingSettings.IndentOption.IndentType.tab) } .onChange(of: indentOption) { _, newValue in if let file { diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift index 494fdc182a..8fbb639c7b 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift @@ -13,7 +13,7 @@ struct InspectorAreaView: View { @ObservedObject public var viewModel: InspectorAreaViewModel @AppSettings(\.general.inspectorTabBarPosition) - var sidebarPosition: SettingsData.SidebarTabBarPosition + var sidebarPosition: GeneralSettings.SidebarTabBarPosition @AppSettings(\.developerSettings.showInternalDevelopmentInspector) var showInternalDevelopmentInspector diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift index 4a1d7b1e20..45929ceb0b 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift @@ -13,7 +13,7 @@ struct NavigatorAreaView: View { @ObservedObject public var viewModel: NavigatorAreaViewModel @AppSettings(\.general.navigatorTabBarPosition) - var sidebarPosition: SettingsData.SidebarTabBarPosition + var sidebarPosition: GeneralSettings.SidebarTabBarPosition init(viewModel: NavigatorAreaViewModel) { self.viewModel = viewModel diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 192a9268af..1e6f31a257 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -45,7 +45,7 @@ final class ProjectNavigatorViewController: NSViewController { var workspaceNavigator: WorkspaceNavigator = NoOpWorkspaceNavigator() weak var activeEditorState: (any ActiveEditorState)? - var iconColor: SettingsData.FileIconStyle = .color { + var iconColor: GeneralSettings.FileIconStyle = .color { willSet { if newValue != iconColor { outlineView?.reloadData() @@ -57,7 +57,7 @@ final class ProjectNavigatorViewController: NSViewController { // Cells are only built by `outlineView(_:viewFor:)`, so without a reload a preference change // leaves every visible label showing the text it was born with — the same reason `iconColor` // and `rowHeight` reload below. - var fileExtensionsVisibility: SettingsData.FileExtensionsVisibility = .showAll { + var fileExtensionsVisibility: GeneralSettings.FileExtensionsVisibility = .showAll { willSet { if newValue != fileExtensionsVisibility { outlineView?.reloadData() @@ -65,7 +65,7 @@ final class ProjectNavigatorViewController: NSViewController { } } - var shownFileExtensions: SettingsData.FileExtensions = .default { + var shownFileExtensions: GeneralSettings.FileExtensions = .default { willSet { if newValue != shownFileExtensions { outlineView?.reloadData() @@ -73,7 +73,7 @@ final class ProjectNavigatorViewController: NSViewController { } } - var hiddenFileExtensions: SettingsData.FileExtensions = .default { + var hiddenFileExtensions: GeneralSettings.FileExtensions = .default { willSet { if newValue != hiddenFileExtensions { outlineView?.reloadData() diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift index 8de0393f69..f146dc9187 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift @@ -17,7 +17,7 @@ struct WorkspacePanelTabBar: View { @Binding var items: [Tab] @Binding var selection: Tab? - var position: SettingsData.SidebarTabBarPosition + var position: GeneralSettings.SidebarTabBarPosition @State private var tabLocations: [Tab: CGRect] = [:] @State private var tabWidth: [Tab: CGFloat] = [:] diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift index 028056977d..033e6957f5 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift @@ -17,14 +17,14 @@ struct WorkspacePanelView: @Environment(\.colorScheme) private var colorScheme - var sidebarPosition: SettingsData.SidebarTabBarPosition + var sidebarPosition: GeneralSettings.SidebarTabBarPosition var darkDivider: Bool init( viewModel: ViewModel, selectedTab: Binding, tabItems: Binding<[Tab]>, - sidebarPosition: SettingsData.SidebarTabBarPosition, + sidebarPosition: GeneralSettings.SidebarTabBarPosition, darkDivider: Bool = false ) { self.viewModel = viewModel diff --git a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift index 70a52ecf23..7c19ec3713 100644 --- a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift @@ -219,7 +219,7 @@ struct CodeFileView: View { // This extension is kept here because it should not be used elsewhere in the app and may cause confusion // due to the similar type name from the CETV module. -private extension SettingsData.TextEditingSettings.IndentOption { +private extension TextEditingSettings.IndentOption { func textViewOption() -> CodeEditSourceEditor.IndentOption { switch self.indentType { case .spaces: @@ -230,7 +230,7 @@ private extension SettingsData.TextEditingSettings.IndentOption { } } -private extension SettingsData.TextEditingSettings.InvisibleCharactersConfig { +private extension TextEditingSettings.InvisibleCharactersConfig { func textViewOption() -> InvisibleCharactersConfiguration { guard self.enabled else { return .empty } var config = InvisibleCharactersConfiguration( diff --git a/CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift b/CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift index 2b3b4eb9d2..afb6f22ad5 100644 --- a/CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift +++ b/CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift @@ -17,7 +17,7 @@ import CodeEditCore @MainActor public protocol RegistryManaging: AnyObject { var viewState: RegistryViewState { get } - var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] { get } + var installedLanguageServers: [String: LanguageServerSettings.Installed] { get } func loadRegistryIfNeeded() func setPackageEnabled(packageName: String, enabled: Bool) diff --git a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift index 12864a5d4a..eb1a7e8b3e 100644 --- a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift +++ b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift @@ -41,7 +41,7 @@ public final class RegistryManager: RegistryManaging { nonisolated(unsafe) private var cleanupTimer: Timer? @AppSettings(\.languageServers.installedLanguageServers) - public var installedLanguageServers: [String: SettingsData.InstalledLanguageServer] + public var installedLanguageServers: [String: LanguageServerSettings.Installed] private let eventBus: EventBus private let errorNotifier: ErrorNotifying diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift index 888826ccdc..7545761ec0 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift @@ -127,7 +127,7 @@ public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalV } /// Returns a string of a shell path to use - func getShell(_ shellType: Shell?, userSetting: SettingsData.TerminalShell) -> (Shell, String)? { + func getShell(_ shellType: Shell?, userSetting: TerminalSettings.Shell) -> (Shell, String)? { if let shellType { return (shellType, shellType.defaultPath) } diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift index 1fe6ddd48a..9d4eb3844b 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift @@ -7,16 +7,13 @@ import Foundation -extension SettingsData { +/// The global settings for source control accounts +public struct AccountsSettings: Codable, Hashable { + /// The list of git accounts the user has saved + @CodableDefault public var sourceControlAccounts: GitAccounts = .init() - /// The global settings for source control accounts - public struct AccountsSettings: Codable, Hashable { - /// The list of git accounts the user has saved - @CodableDefault public var sourceControlAccounts: GitAccounts = .init() - - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} public struct GitAccounts: Codable, Hashable { /// This id will store the account name as the identifiable diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift index 21ba35dac4..c61f176bdc 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift @@ -7,16 +7,14 @@ import Foundation -extension SettingsData { - public struct DeveloperSettings: Codable, Hashable { +public struct DeveloperSettings: Codable, Hashable { - /// A dictionary that stores a file type and a path to an LSP binary - @CodableDefault public var lspBinaries: [String: String] = [:] + /// A dictionary that stores a file type and a path to an LSP binary + @CodableDefault public var lspBinaries: [String: String] = [:] - /// Toggle for showing the internal development inspector - @CodableDefault public var showInternalDevelopmentInspector = false + /// Toggle for showing the internal development inspector + @CodableDefault public var showInternalDevelopmentInspector = false - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} } diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift index bb374eedd8..bc39f9b607 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift @@ -7,77 +7,70 @@ import SwiftUI -extension SettingsData { +/// The general global setting +public struct GeneralSettings: Codable, Hashable { - /// The general global setting - public struct GeneralSettings: Codable, Hashable { - - /// The appearance of the app - @CodableDefault public var appAppearance: Appearances = .system + /// The appearance of the app + @CodableDefault public var appAppearance: Appearances = .system - /// The show issues behavior of the app - @CodableDefault public var showIssues: Issues = .inline + /// The show issues behavior of the app + @CodableDefault public var showIssues: Issues = .inline - /// The show live issues behavior of the app - @CodableDefault public var showLiveIssues = true + /// The show live issues behavior of the app + @CodableDefault public var showLiveIssues = true - /// Show editor jump bar - @CodableDefault public var showEditorJumpBar = true + /// Show editor jump bar + @CodableDefault public var showEditorJumpBar = true - /// Dims editors without focus - @CodableDefault public var dimEditorsWithoutFocus = false + /// Dims editors without focus + @CodableDefault public var dimEditorsWithoutFocus = false - /// The show file extensions behavior of the app - @CodableDefault public var fileExtensionsVisibility: - FileExtensionsVisibility = .showAll + /// The show file extensions behavior of the app + @CodableDefault public var fileExtensionsVisibility: + FileExtensionsVisibility = .showAll - /// The file extensions collection to display - @CodableDefault public var shownFileExtensions: FileExtensions = .default + /// The file extensions collection to display + @CodableDefault public var shownFileExtensions: FileExtensions = .default - /// The file extensions collection to hide - @CodableDefault public var hiddenFileExtensions: FileExtensions = .default + /// The file extensions collection to hide + @CodableDefault public var hiddenFileExtensions: FileExtensions = .default - /// The style for file icons - @CodableDefault public var fileIconStyle: FileIconStyle = .color + /// The style for file icons + @CodableDefault public var fileIconStyle: FileIconStyle = .color - /// The position for the navigator sidebar tab bar - @CodableDefault public var navigatorTabBarPosition: - SidebarTabBarPosition = .top + /// The position for the navigator sidebar tab bar + @CodableDefault public var navigatorTabBarPosition: + SidebarTabBarPosition = .top - /// The position for the inspector sidebar tab bar - @CodableDefault public var inspectorTabBarPosition: - SidebarTabBarPosition = .top + /// The position for the inspector sidebar tab bar + @CodableDefault public var inspectorTabBarPosition: + SidebarTabBarPosition = .top - /// The reopen behavior of the app - @CodableDefault public var reopenBehavior: ReopenBehavior = .welcome + /// The reopen behavior of the app + @CodableDefault public var reopenBehavior: ReopenBehavior = .welcome - /// Decides what the app does after a workspace is closed - @CodableDefault public var reopenWindowAfterClose: - ReopenWindowBehavior = .doNothing + /// Decides what the app does after a workspace is closed + @CodableDefault public var reopenWindowAfterClose: + ReopenWindowBehavior = .doNothing - /// The size of the project navigator - @CodableDefault public var projectNavigatorSize: ProjectNavigatorSize = .medium + /// The size of the project navigator + @CodableDefault public var projectNavigatorSize: ProjectNavigatorSize = .medium - /// The Find Navigator Detail line limit - @CodableDefault public var findNavigatorDetail: NavigatorDetail = .upTo3 + /// The Find Navigator Detail line limit + @CodableDefault public var findNavigatorDetail: NavigatorDetail = .upTo3 - /// The Issue Navigator Detail line limit - @CodableDefault public var issueNavigatorDetail: NavigatorDetail = .upTo3 + /// The Issue Navigator Detail line limit + @CodableDefault public var issueNavigatorDetail: NavigatorDetail = .upTo3 - /// The reveal file in navigator when focus changes behavior of the app. - @CodableDefault public var revealFileOnFocusChange = false + /// The reveal file in navigator when focus changes behavior of the app. + @CodableDefault public var revealFileOnFocusChange = false - /// Auto save behavior toggle - @CodableDefault public var isAutoSaveOn = true + /// Auto save behavior toggle + @CodableDefault public var isAutoSaveOn = true - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} - /// The appearance of the app - /// - **system**: uses the system appearance - /// - **dark**: always uses dark appearance - /// - **light**: always uses light appearance public enum Appearances: String, Codable { case system case light diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift index 2cb51d391c..b0d0d68026 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift @@ -7,25 +7,22 @@ import Foundation -extension SettingsData { +/// The global settings for text editing +public struct KeybindingsSettings: Codable, Hashable { - /// The global settings for text editing - public struct KeybindingsSettings: Codable, Hashable { + /// An integer indicating how many spaces a `tab` will generate + public var keybindings: [String: KeyboardShortcutWrapper] = .init() - /// An integer indicating how many spaces a `tab` will generate - public var keybindings: [String: KeyboardShortcutWrapper] = .init() + /// Default initializer — empty; bundled defaults are seeded by the app at + /// startup via `SettingsData.reconcileDefaultKeybindings()`. + public init() {} - /// Default initializer — empty; bundled defaults are seeded by the app at - /// startup via `SettingsData.reconcileDefaultKeybindings()`. - public init() {} - - /// Explicit decoder init for setting default values when key is not present in `JSON` - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.keybindings = try container.decodeIfPresent( - [String: KeyboardShortcutWrapper].self, - forKey: .keybindings - ) ?? .init() - } + /// Explicit decoder init for setting default values when key is not present in `JSON` + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.keybindings = try container.decodeIfPresent( + [String: KeyboardShortcutWrapper].self, + forKey: .keybindings + ) ?? .init() } } diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift index abcced41da..c1aceff43f 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift @@ -7,18 +7,16 @@ import Foundation -extension SettingsData { - public struct LanguageServerSettings: Codable, Hashable { +public struct LanguageServerSettings: Codable, Hashable { - /// Stores the currently installed language servers. The key is the name of the language server. - @CodableDefault public var installedLanguageServers: - [String: InstalledLanguageServer] = [:] + /// Stores the currently installed language servers. The key is the name of the language server. + @CodableDefault public var installedLanguageServers: + [String: Installed] = [:] - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} - public struct InstalledLanguageServer: Codable, Hashable { + public struct Installed: Codable, Hashable { public let packageName: String public var isEnabled: Bool public let version: String diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift index 57126e13a2..dd6d94e8ab 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift @@ -7,17 +7,14 @@ import Foundation -extension SettingsData { +/// The global settings for the terminal emulator +public struct NavigationSettings: Codable, Hashable { - /// The global settings for the terminal emulator - public struct NavigationSettings: Codable, Hashable { + /// Navigation style used + @CodableDefault public var navigationStyle: NavigationStyle = .openInTabs - /// Navigation style used - @CodableDefault public var navigationStyle: NavigationStyle = .openInTabs - - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} public enum NavigationStyle: String, Codable, Hashable { case openInTabs diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift index 6a8d427ce4..10c6ae93ad 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift @@ -7,13 +7,11 @@ import Foundation -extension SettingsData { - public struct SearchSettings: Codable, Hashable { +public struct SearchSettings: Codable, Hashable { - /// List of Glob Patterns that determine which files or directories to ignore - @CodableDefault public var ignoreGlobPatterns: [GlobPattern] = [] + /// List of Glob Patterns that determine which files or directories to ignore + @CodableDefault public var ignoreGlobPatterns: [GlobPattern] = [] - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} } diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift index 97c099a737..12b3f61db7 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift @@ -7,25 +7,23 @@ import Foundation -extension SettingsData { - /// The global settings for source control - public struct SourceControlSettings: Codable, Hashable { +/// The global settings for source control +public struct SourceControlSettings: Codable, Hashable { - /// The general source control settings - public var general: SourceControlGeneral = .init() + /// The general source control settings + public var general: SourceControlGeneral = .init() - /// The source control git settings - public var git: SourceControlGit = .init() + /// The source control git settings + public var git: SourceControlGit = .init() - /// Default initializer - public init() {} + /// Default initializer + public init() {} - /// Explicit decoder init for setting default values when key is not present in `JSON` - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.general = try container.decodeIfPresent(SourceControlGeneral.self, forKey: .general) ?? .init() - self.git = try container.decodeIfPresent(SourceControlGit.self, forKey: .git) ?? .init() - } + /// Explicit decoder init for setting default values when key is not present in `JSON` + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.general = try container.decodeIfPresent(SourceControlGeneral.self, forKey: .general) ?? .init() + self.git = try container.decodeIfPresent(SourceControlGit.self, forKey: .git) ?? .init() } public struct SourceControlGeneral: Codable, Hashable { diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift index ec6321b32d..2ee3cad6be 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift @@ -1,5 +1,5 @@ // -// TerminalPreferences.swift +// TerminalSettings.swift // CodeEditModules/Settings // // Created by Nanashi Li on 2022/04/08. @@ -8,65 +8,62 @@ import AppKit import Foundation -extension SettingsData { +/// The global settings for the terminal emulator +public struct TerminalSettings: Codable, Hashable { - /// The global settings for the terminal emulator - public struct TerminalSettings: Codable, Hashable { + /// If true terminal will use editor theme. + @CodableDefault public var useEditorTheme = true - /// If true terminal will use editor theme. - @CodableDefault public var useEditorTheme = true + /// If true terminal appearance will always be `dark`. Otherwise it adapts to the system setting. + @CodableDefault public var darkAppearance = false - /// If true terminal appearance will always be `dark`. Otherwise it adapts to the system setting. - @CodableDefault public var darkAppearance = false + /// If true, the terminal uses the background color of the theme, otherwise it is clear + @CodableDefault public var useThemeBackground = true - /// If true, the terminal uses the background color of the theme, otherwise it is clear - @CodableDefault public var useThemeBackground = true + /// If true, the terminal treats the `Option` key as the `Meta` key + @CodableDefault public var optionAsMeta = false - /// If true, the terminal treats the `Option` key as the `Meta` key - @CodableDefault public var optionAsMeta = false + /// The selected shell to use. + @CodableDefault public var shell: Shell = .system - /// The selected shell to use. - @CodableDefault public var shell: TerminalShell = .system + /// The font to use in terminal. + @CodableDefault public var font: Font = .init() - /// The font to use in terminal. - @CodableDefault public var font: TerminalFont = .init() + // The cursor style to use in terminal + @CodableDefault public var cursorStyle: CursorStyle = .block - // The cursor style to use in terminal - @CodableDefault public var cursorStyle: TerminalCursorStyle = .block + // Toggle for blinking cursor or not + @CodableDefault public var cursorBlink = false - // Toggle for blinking cursor or not - @CodableDefault public var cursorBlink = false + // Use font settings from Text Editing + @CodableDefault public var useTextEditorFont = true - // Use font settings from Text Editing - @CodableDefault public var useTextEditorFont = true + /// If `true`, use injection scripts for terminal features like automatic tab title. + @CodableDefault public var useShellIntegration = true - /// If `true`, use injection scripts for terminal features like automatic tab title. - @CodableDefault public var useShellIntegration = true + /// If `true`, use a login shell. + @CodableDefault public var useLoginShell = true - /// If `true`, use a login shell. - @CodableDefault public var useLoginShell = true - - /// Default initializer - public init() {} - } + /// Default initializer + public init() {} /// The shell options. /// - **bash**: uses the default bash shell /// - **zsh**: uses the ZSH shell /// - **system**: uses the system default shell (most likely ZSH) - public enum TerminalShell: String, Codable, Hashable { + public enum Shell: String, Codable, Hashable { case bash case zsh case system } - public enum TerminalCursorStyle: String, Codable, Hashable { + public enum CursorStyle: String, Codable, Hashable { case block case underline case bar } - public struct TerminalFont: Codable, Hashable { + public struct Font: Codable, Hashable { /// The font size for the custom font public var size: Double = 12 diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift index 695beb15ee..4a2f8e1ed9 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift @@ -9,216 +9,213 @@ import AppKit import CodeEditCore import Foundation -extension SettingsData { - - /// The global settings for text editing - public struct TextEditingSettings: Codable, Hashable { - - /// An integer indicating how many spaces a `tab` will appear as visually. - public var defaultTabWidth: Int = 4 - - /// The behavior of a `tab` keypress. If `.tab`, will insert a tab character. If `.spaces` will insert - /// `.spaceCount` spaces instead. - public var indentOption: IndentOption = IndentOption(indentType: .spaces, spaceCount: 4) - - /// The font to use in editor. - public var font: EditorFont = .init() - - /// A flag indicating whether type-over completion is enabled - public var enableTypeOverCompletion: Bool = true - - /// A flag indicating whether braces are automatically completed - public var autocompleteBraces: Bool = true - - /// A flag indicating whether to wrap lines to editor width - public var wrapLinesToEditorWidth: Bool = true - - /// The percentage of overscroll to apply to the text view - public var overscroll: OverscrollOption = .medium - - /// A multiplier for setting the line height. Defaults to `1.2` - public var lineHeightMultiple: Double = 1.2 - - /// A multiplier for setting the letter spacing, `1` being no spacing and - /// `2` is one character of spacing between letters, defaults to `1`. - public var letterSpacing: Double = 1.0 - - /// The behavior of bracket pair highlights. - public var bracketEmphasis: BracketPairEmphasis = BracketPairEmphasis() - - /// Use the system cursor for the source editor. - public var useSystemCursor: Bool = true - - /// Toggle the gutter in the editor. - public var showGutter: Bool = true - - /// Toggle the minimap in the editor. - public var showMinimap: Bool = true - - /// Toggle the code folding ribbon. - public var showFoldingRibbon: Bool = true - - /// The column at which to reformat text - public var reformatAtColumn: Int = 80 - - /// Show the reformatting guide in the editor - public var showReformattingGuide: Bool = false - - public var invisibleCharacters: InvisibleCharactersConfig = .default - - /// Map of unicode character codes to a note about them - public var warningCharacters: WarningCharacters = .default - - /// Default initializer - public init() {} - - /// Explicit decoder init for setting default values when key is not present in `JSON` - public init(from decoder: Decoder) throws { // swiftlint:disable:this function_body_length - let container = try decoder.container(keyedBy: CodingKeys.self) - self.defaultTabWidth = try container.decodeIfPresent(Int.self, forKey: .defaultTabWidth) ?? 4 - self.indentOption = try container.decodeIfPresent( - IndentOption.self, - forKey: .indentOption - ) ?? IndentOption(indentType: .spaces, spaceCount: 4) - self.font = try container.decodeIfPresent(EditorFont.self, forKey: .font) ?? .init() - self.enableTypeOverCompletion = try container.decodeIfPresent( - Bool.self, - forKey: .enableTypeOverCompletion - ) ?? true - self.autocompleteBraces = try container.decodeIfPresent( - Bool.self, - forKey: .autocompleteBraces - ) ?? true - self.wrapLinesToEditorWidth = try container.decodeIfPresent( - Bool.self, - forKey: .wrapLinesToEditorWidth - ) ?? true - self.overscroll = try container.decodeIfPresent( - OverscrollOption.self, - forKey: .overscroll - ) ?? .medium - self.lineHeightMultiple = try container.decodeIfPresent( - Double.self, - forKey: .lineHeightMultiple - ) ?? 1.2 - self.letterSpacing = try container.decodeIfPresent( - Double.self, - forKey: .letterSpacing - ) ?? 1 - self.bracketEmphasis = try container.decodeIfPresent( - BracketPairEmphasis.self, - forKey: .bracketEmphasis - ) ?? BracketPairEmphasis() - if #available(macOS 14, *) { - self.useSystemCursor = try container.decodeIfPresent(Bool.self, forKey: .useSystemCursor) ?? true - } else { - self.useSystemCursor = false - } - - self.showGutter = try container.decodeIfPresent(Bool.self, forKey: .showGutter) ?? true - self.showMinimap = try container.decodeIfPresent(Bool.self, forKey: .showMinimap) ?? true - self.showFoldingRibbon = try container.decodeIfPresent(Bool.self, forKey: .showFoldingRibbon) ?? true - self.reformatAtColumn = try container.decodeIfPresent(Int.self, forKey: .reformatAtColumn) ?? 80 - self.showReformattingGuide = try container.decodeIfPresent( - Bool.self, - forKey: .showReformattingGuide - ) ?? false - self.invisibleCharacters = try container.decodeIfPresent( - InvisibleCharactersConfig.self, - forKey: .invisibleCharacters - ) ?? .default - self.warningCharacters = try container.decodeIfPresent( - WarningCharacters.self, - forKey: .warningCharacters - ) ?? .default +/// The global settings for text editing +public struct TextEditingSettings: Codable, Hashable { + + /// An integer indicating how many spaces a `tab` will appear as visually. + public var defaultTabWidth: Int = 4 + + /// The behavior of a `tab` keypress. If `.tab`, will insert a tab character. If `.spaces` will insert + /// `.spaceCount` spaces instead. + public var indentOption: IndentOption = IndentOption(indentType: .spaces, spaceCount: 4) + + /// The font to use in editor. + public var font: EditorFont = .init() + + /// A flag indicating whether type-over completion is enabled + public var enableTypeOverCompletion: Bool = true + + /// A flag indicating whether braces are automatically completed + public var autocompleteBraces: Bool = true + + /// A flag indicating whether to wrap lines to editor width + public var wrapLinesToEditorWidth: Bool = true + + /// The percentage of overscroll to apply to the text view + public var overscroll: OverscrollOption = .medium + + /// A multiplier for setting the line height. Defaults to `1.2` + public var lineHeightMultiple: Double = 1.2 + + /// A multiplier for setting the letter spacing, `1` being no spacing and + /// `2` is one character of spacing between letters, defaults to `1`. + public var letterSpacing: Double = 1.0 + + /// The behavior of bracket pair highlights. + public var bracketEmphasis: BracketPairEmphasis = BracketPairEmphasis() + + /// Use the system cursor for the source editor. + public var useSystemCursor: Bool = true + + /// Toggle the gutter in the editor. + public var showGutter: Bool = true + + /// Toggle the minimap in the editor. + public var showMinimap: Bool = true + + /// Toggle the code folding ribbon. + public var showFoldingRibbon: Bool = true + + /// The column at which to reformat text + public var reformatAtColumn: Int = 80 + + /// Show the reformatting guide in the editor + public var showReformattingGuide: Bool = false + + public var invisibleCharacters: InvisibleCharactersConfig = .default + + /// Map of unicode character codes to a note about them + public var warningCharacters: WarningCharacters = .default + + /// Default initializer + public init() {} + + /// Explicit decoder init for setting default values when key is not present in `JSON` + public init(from decoder: Decoder) throws { // swiftlint:disable:this function_body_length + let container = try decoder.container(keyedBy: CodingKeys.self) + self.defaultTabWidth = try container.decodeIfPresent(Int.self, forKey: .defaultTabWidth) ?? 4 + self.indentOption = try container.decodeIfPresent( + IndentOption.self, + forKey: .indentOption + ) ?? IndentOption(indentType: .spaces, spaceCount: 4) + self.font = try container.decodeIfPresent(EditorFont.self, forKey: .font) ?? .init() + self.enableTypeOverCompletion = try container.decodeIfPresent( + Bool.self, + forKey: .enableTypeOverCompletion + ) ?? true + self.autocompleteBraces = try container.decodeIfPresent( + Bool.self, + forKey: .autocompleteBraces + ) ?? true + self.wrapLinesToEditorWidth = try container.decodeIfPresent( + Bool.self, + forKey: .wrapLinesToEditorWidth + ) ?? true + self.overscroll = try container.decodeIfPresent( + OverscrollOption.self, + forKey: .overscroll + ) ?? .medium + self.lineHeightMultiple = try container.decodeIfPresent( + Double.self, + forKey: .lineHeightMultiple + ) ?? 1.2 + self.letterSpacing = try container.decodeIfPresent( + Double.self, + forKey: .letterSpacing + ) ?? 1 + self.bracketEmphasis = try container.decodeIfPresent( + BracketPairEmphasis.self, + forKey: .bracketEmphasis + ) ?? BracketPairEmphasis() + if #available(macOS 14, *) { + self.useSystemCursor = try container.decodeIfPresent(Bool.self, forKey: .useSystemCursor) ?? true + } else { + self.useSystemCursor = false } - /// Re-exported from `CodeEditCore`. Keeps `SettingsData.TextEditingSettings.IndentOption` - /// valid for all existing call sites while the underlying type lives in the Core package. - public typealias IndentOption = CodeEditCore.IndentOption - - public struct BracketPairEmphasis: Codable, Hashable { - /// The type of highlight to use - public var highlightType: HighlightType = .flash - public var useCustomColor: Bool = false - /// The color to use for the highlight. - public var color: Theme.Attributes = Theme.Attributes(color: "FFFFFF", bold: false, italic: false) - - public enum HighlightType: String, Codable { - case disabled - case bordered - case flash - case underline - } + self.showGutter = try container.decodeIfPresent(Bool.self, forKey: .showGutter) ?? true + self.showMinimap = try container.decodeIfPresent(Bool.self, forKey: .showMinimap) ?? true + self.showFoldingRibbon = try container.decodeIfPresent(Bool.self, forKey: .showFoldingRibbon) ?? true + self.reformatAtColumn = try container.decodeIfPresent(Int.self, forKey: .reformatAtColumn) ?? 80 + self.showReformattingGuide = try container.decodeIfPresent( + Bool.self, + forKey: .showReformattingGuide + ) ?? false + self.invisibleCharacters = try container.decodeIfPresent( + InvisibleCharactersConfig.self, + forKey: .invisibleCharacters + ) ?? .default + self.warningCharacters = try container.decodeIfPresent( + WarningCharacters.self, + forKey: .warningCharacters + ) ?? .default + } + + /// Re-exported from `CodeEditCore`. Keeps `TextEditingSettings.IndentOption` + /// valid for all existing call sites while the underlying type lives in the Core package. + public typealias IndentOption = CodeEditCore.IndentOption + + public struct BracketPairEmphasis: Codable, Hashable { + /// The type of highlight to use + public var highlightType: HighlightType = .flash + public var useCustomColor: Bool = false + /// The color to use for the highlight. + public var color: Theme.Attributes = Theme.Attributes(color: "FFFFFF", bold: false, italic: false) + + public enum HighlightType: String, Codable { + case disabled + case bordered + case flash + case underline } + } - public enum OverscrollOption: String, Codable { - case none - case small - case medium - case large - - public var overscrollPercentage: CGFloat { - switch self { - case .none: return 0 - case .small: return 0.25 - case .medium: return 0.5 - case .large: return 0.75 - } + public enum OverscrollOption: String, Codable { + case none + case small + case medium + case large + + public var overscrollPercentage: CGFloat { + switch self { + case .none: return 0 + case .small: return 0.25 + case .medium: return 0.5 + case .large: return 0.75 } } + } - public struct InvisibleCharactersConfig: Equatable, Hashable, Codable { - nonisolated(unsafe) public static var `default`: InvisibleCharactersConfig = { - InvisibleCharactersConfig( - enabled: false, - showSpaces: true, - showTabs: true, - showLineEndings: true - ) - }() - - public var enabled: Bool - - public var showSpaces: Bool - public var showTabs: Bool - public var showLineEndings: Bool - - public var spaceReplacement: String = "·" - public var tabReplacement: String = "→" - - // Controlled by `showLineEndings` - public var carriageReturnReplacement: String = "↵" - public var lineFeedReplacement: String = "¬" - public var paragraphSeparatorReplacement: String = "¶" - public var lineSeparatorReplacement: String = "⏎" - } + public struct InvisibleCharactersConfig: Equatable, Hashable, Codable { + nonisolated(unsafe) public static var `default`: InvisibleCharactersConfig = { + InvisibleCharactersConfig( + enabled: false, + showSpaces: true, + showTabs: true, + showLineEndings: true + ) + }() + + public var enabled: Bool + + public var showSpaces: Bool + public var showTabs: Bool + public var showLineEndings: Bool + + public var spaceReplacement: String = "·" + public var tabReplacement: String = "→" + + // Controlled by `showLineEndings` + public var carriageReturnReplacement: String = "↵" + public var lineFeedReplacement: String = "¬" + public var paragraphSeparatorReplacement: String = "¶" + public var lineSeparatorReplacement: String = "⏎" + } - public struct WarningCharacters: Equatable, Hashable, Codable { - nonisolated(unsafe) public static let `default`: WarningCharacters = - WarningCharacters(enabled: true, characters: [ - 0x0003: "End of text", + public struct WarningCharacters: Equatable, Hashable, Codable { + nonisolated(unsafe) public static let `default`: WarningCharacters = + WarningCharacters(enabled: true, characters: [ + 0x0003: "End of text", - 0x00A0: "Non-breaking space", - 0x202F: "Narrow non-breaking space", - 0x200B: "Zero-width space", - 0x200C: "Zero-width non-joiner", - 0x2029: "Paragraph separator", + 0x00A0: "Non-breaking space", + 0x202F: "Narrow non-breaking space", + 0x200B: "Zero-width space", + 0x200C: "Zero-width non-joiner", + 0x2029: "Paragraph separator", - 0x2013: "Em-dash", - 0x00AD: "Soft hyphen", + 0x2013: "Em-dash", + 0x00AD: "Soft hyphen", - 0x2018: "Left single quote", - 0x2019: "Right single quote", - 0x201C: "Left double quote", - 0x201D: "Right double quote", + 0x2018: "Left single quote", + 0x2019: "Right single quote", + 0x201C: "Left double quote", + 0x201D: "Right double quote", - 0x037E: "Greek Question Mark" - ]) + 0x037E: "Greek Question Mark" + ]) - public var enabled: Bool - public var characters: [UInt16: String] - } + public var enabled: Bool + public var characters: [UInt16: String] } public struct EditorFont: Codable, Hashable { diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift index 337a4f5cdb..f1ffe95938 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift @@ -7,91 +7,88 @@ import Foundation -extension SettingsData { +/// A dictionary containing the keys and associated ``Theme/Attributes`` of overridden properties +/// +/// ```json +/// { +/// "editor" : { +/// "background" : { +/// "color" : "#123456" +/// }, +/// ... +/// }, +/// "terminal" : { +/// "blue" : { +/// "color" : "#1100FF" +/// }, +/// ... +/// } +/// } +/// ``` +public typealias ThemeOverrides = [String: [String: Theme.Attributes]] - /// A dictionary containing the keys and associated ``Theme/Attributes`` of overridden properties +/// The global settings for themes +public struct ThemeSettings: Codable, Hashable { + + /// The name of the currently selected dark theme + public var selectedDarkTheme: String = "Default (Dark)" + + /// The name of the currently selected light theme + public var selectedLightTheme: String = "Default (Light)" + + /// The name of the currently selected theme + public var selectedTheme: String? + + /// Use the system background that matches the appearance setting + public var useThemeBackground: Bool = true + + /// Automatically change theme based on system appearance + public var matchAppearance: Bool = true + + /// Dictionary of themes containing overrides /// /// ```json /// { - /// "editor" : { - /// "background" : { - /// "color" : "#123456" + /// "overrides" : { + /// "DefaultDark" : { + /// "editor" : { + /// "background" : { + /// "color" : "#123456" + /// }, + /// ... + /// }, + /// "terminal" : { + /// "blue" : { + /// "color" : "#1100FF" + /// }, + /// ... + /// } + /// ... /// }, /// ... /// }, - /// "terminal" : { - /// "blue" : { - /// "color" : "#1100FF" - /// }, - /// ... - /// } + /// ... /// } /// ``` - public typealias ThemeOverrides = [String: [String: Theme.Attributes]] - - /// The global settings for themes - public struct ThemeSettings: Codable, Hashable { - - /// The name of the currently selected dark theme - public var selectedDarkTheme: String = "Default (Dark)" - - /// The name of the currently selected light theme - public var selectedLightTheme: String = "Default (Light)" - - /// The name of the currently selected theme - public var selectedTheme: String? - - /// Use the system background that matches the appearance setting - public var useThemeBackground: Bool = true - - /// Automatically change theme based on system appearance - public var matchAppearance: Bool = true - - /// Dictionary of themes containing overrides - /// - /// ```json - /// { - /// "overrides" : { - /// "DefaultDark" : { - /// "editor" : { - /// "background" : { - /// "color" : "#123456" - /// }, - /// ... - /// }, - /// "terminal" : { - /// "blue" : { - /// "color" : "#1100FF" - /// }, - /// ... - /// } - /// ... - /// }, - /// ... - /// }, - /// ... - /// } - /// ``` - public var overrides: [String: ThemeOverrides] = [:] + public var overrides: [String: ThemeOverrides] = [:] - /// Default initializer - public init() {} + /// Default initializer + public init() {} - /// Explicit decoder init for setting default values when key is not present in `JSON` - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.selectedDarkTheme = try container.decodeIfPresent( - String.self, forKey: .selectedDarkTheme - ) ?? selectedDarkTheme - self.selectedLightTheme = try container.decodeIfPresent( - String.self, forKey: .selectedLightTheme - ) ?? selectedLightTheme - self.selectedTheme = try container.decodeIfPresent(String.self, forKey: .selectedTheme) - self.useThemeBackground = try container.decodeIfPresent(Bool.self, forKey: .useThemeBackground) ?? true - self.matchAppearance = try container.decodeIfPresent( - Bool.self, forKey: .matchAppearance - ) ?? true - self.overrides = try container.decodeIfPresent([String: ThemeOverrides].self, forKey: .overrides) ?? [:] - } + /// Explicit decoder init for setting default values when key is not present in `JSON` + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.selectedDarkTheme = try container.decodeIfPresent( + String.self, forKey: .selectedDarkTheme + ) ?? selectedDarkTheme + self.selectedLightTheme = try container.decodeIfPresent( + String.self, forKey: .selectedLightTheme + ) ?? selectedLightTheme + self.selectedTheme = try container.decodeIfPresent(String.self, forKey: .selectedTheme) + self.useThemeBackground = try container.decodeIfPresent(Bool.self, forKey: .useThemeBackground) ?? true + self.matchAppearance = try container.decodeIfPresent( + Bool.self, forKey: .matchAppearance + ) ?? true + self.overrides = try container.decodeIfPresent([String: ThemeOverrides].self, forKey: .overrides) ?? [:] } } diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift index 37db943076..92bf356b81 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift @@ -20,21 +20,21 @@ public enum DefaultFalse: DefaultValueProvider { // MARK: - Terminal Defaults public enum DefaultTerminalShell: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.TerminalShell.system + nonisolated(unsafe) public static let defaultValue = TerminalSettings.Shell.system } public enum DefaultTerminalCursorStyle: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.TerminalCursorStyle.block + nonisolated(unsafe) public static let defaultValue = TerminalSettings.CursorStyle.block } public enum DefaultTerminalFont: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.TerminalFont() + nonisolated(unsafe) public static let defaultValue = TerminalSettings.Font() } // MARK: - Navigation Defaults public enum DefaultNavigationStyle: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.NavigationStyle.openInTabs + nonisolated(unsafe) public static let defaultValue = NavigationSettings.NavigationStyle.openInTabs } // MARK: - Collection Defaults @@ -48,13 +48,13 @@ public enum DefaultEmptyStringDictionary: DefaultValueProvider { } public enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue: [String: SettingsData.InstalledLanguageServer] = [:] + nonisolated(unsafe) public static let defaultValue: [String: LanguageServerSettings.Installed] = [:] } // MARK: - Account Defaults public enum DefaultGitAccounts: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.GitAccounts() + nonisolated(unsafe) public static let defaultValue = AccountsSettings.GitAccounts() } public enum DefaultEmptySourceControlAccounts: DefaultValueProvider { @@ -68,41 +68,41 @@ public enum DefaultEmptyString: DefaultValueProvider { // MARK: - General Settings Defaults public enum DefaultAppearance: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.Appearances.system + nonisolated(unsafe) public static let defaultValue = GeneralSettings.Appearances.system } public enum DefaultIssues: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.Issues.inline + nonisolated(unsafe) public static let defaultValue = GeneralSettings.Issues.inline } public enum DefaultFileExtensionsVisibility: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.FileExtensionsVisibility.showAll + nonisolated(unsafe) public static let defaultValue = GeneralSettings.FileExtensionsVisibility.showAll } public enum DefaultFileExtensions: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.FileExtensions.default + nonisolated(unsafe) public static let defaultValue = GeneralSettings.FileExtensions.default } public enum DefaultFileIconStyle: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.FileIconStyle.color + nonisolated(unsafe) public static let defaultValue = GeneralSettings.FileIconStyle.color } public enum DefaultSidebarTabBarPositionTop: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.SidebarTabBarPosition.top + nonisolated(unsafe) public static let defaultValue = GeneralSettings.SidebarTabBarPosition.top } public enum DefaultReopenBehavior: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.ReopenBehavior.welcome + nonisolated(unsafe) public static let defaultValue = GeneralSettings.ReopenBehavior.welcome } public enum DefaultReopenWindowBehavior: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.ReopenWindowBehavior.doNothing + nonisolated(unsafe) public static let defaultValue = GeneralSettings.ReopenWindowBehavior.doNothing } public enum DefaultProjectNavigatorSize: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.ProjectNavigatorSize.medium + nonisolated(unsafe) public static let defaultValue = GeneralSettings.ProjectNavigatorSize.medium } public enum DefaultNavigatorDetail: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = SettingsData.NavigatorDetail.upTo3 + nonisolated(unsafe) public static let defaultValue = GeneralSettings.NavigatorDetail.upTo3 } diff --git a/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift b/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift index 2a69ef1167..d07a50632b 100644 --- a/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift +++ b/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift @@ -15,7 +15,7 @@ import CodeEditSettings /// general settings are restored in `tearDown`. final class FileExtensionVisibilityTests: XCTestCase { - private var original: SettingsData.GeneralSettings! + private var original: GeneralSettings! override func setUp() { super.setUp() @@ -77,7 +77,7 @@ final class FileExtensionVisibilityTests: XCTestCase { } func testExtensionlessNamesAreUnaffected() { - for mode in [SettingsData.FileExtensionsVisibility.hideAll, .showAll] { + for mode in [GeneralSettings.FileExtensionsVisibility.hideAll, .showAll] { Settings.shared.preferences.general.fileExtensionsVisibility = mode XCTAssertEqual(label("LICENSE"), "LICENSE", "mode \(mode)") XCTAssertEqual(label("Makefile"), "Makefile", "mode \(mode)") From 16995b3c7aa657d895bc3aea8943238905670539 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 21:31:52 +0200 Subject: [PATCH 241/335] Refactor: Give each settings struct a stable section key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds SettingsSection, conformed by all eleven settings structs. The key is the existing top-level key in settings.json, taken from the SettingsData field name rather than inferred from the type name — notably developerSettings, not developer. A test asserts all eleven keys, since getting one wrong would silently orphan that section for every existing user. --- .../Models/AccountsSettings.swift | 5 ++++- .../Models/DeveloperSettings.swift | 5 ++++- .../Models/GeneralSettings.swift | 5 ++++- .../Models/KeybindingsSettings.swift | 5 ++++- .../Models/LanguageServerSettings.swift | 5 ++++- .../Models/NavigationSettings.swift | 5 ++++- .../Models/SearchSettings.swift | 5 ++++- .../Models/SourceControlSettings.swift | 5 ++++- .../Models/TerminalSettings.swift | 5 ++++- .../Models/TextEditingSettings.swift | 5 ++++- .../Models/ThemeSettings.swift | 5 ++++- .../Store/SettingsSection.swift | 22 +++++++++++++++++++ .../SettingsFormatTests.swift | 19 ++++++++++++++++ 13 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/SettingsSection.swift diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift index 9d4eb3844b..f0106981d9 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift @@ -8,7 +8,10 @@ import Foundation /// The global settings for source control accounts -public struct AccountsSettings: Codable, Hashable { +public struct AccountsSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "accounts" /// The list of git accounts the user has saved @CodableDefault public var sourceControlAccounts: GitAccounts = .init() diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift index c61f176bdc..a949a995af 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/DeveloperSettings.swift @@ -7,7 +7,10 @@ import Foundation -public struct DeveloperSettings: Codable, Hashable { +public struct DeveloperSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "developerSettings" /// A dictionary that stores a file type and a path to an LSP binary @CodableDefault public var lspBinaries: [String: String] = [:] diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift index bc39f9b607..8305ae6f14 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/GeneralSettings.swift @@ -8,7 +8,10 @@ import SwiftUI /// The general global setting -public struct GeneralSettings: Codable, Hashable { +public struct GeneralSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "general" /// The appearance of the app @CodableDefault public var appAppearance: Appearances = .system diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift index b0d0d68026..bbd84cfe71 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift @@ -8,7 +8,10 @@ import Foundation /// The global settings for text editing -public struct KeybindingsSettings: Codable, Hashable { +public struct KeybindingsSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "keybindings" /// An integer indicating how many spaces a `tab` will generate public var keybindings: [String: KeyboardShortcutWrapper] = .init() diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift index c1aceff43f..69c7ee9531 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift @@ -7,7 +7,10 @@ import Foundation -public struct LanguageServerSettings: Codable, Hashable { +public struct LanguageServerSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "languageServers" /// Stores the currently installed language servers. The key is the name of the language server. @CodableDefault public var installedLanguageServers: diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift index dd6d94e8ab..f02ad26b9e 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/NavigationSettings.swift @@ -8,7 +8,10 @@ import Foundation /// The global settings for the terminal emulator -public struct NavigationSettings: Codable, Hashable { +public struct NavigationSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "navigation" /// Navigation style used @CodableDefault public var navigationStyle: NavigationStyle = .openInTabs diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift index 10c6ae93ad..978508ae7a 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/SearchSettings.swift @@ -7,7 +7,10 @@ import Foundation -public struct SearchSettings: Codable, Hashable { +public struct SearchSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "search" /// List of Glob Patterns that determine which files or directories to ignore @CodableDefault public var ignoreGlobPatterns: [GlobPattern] = [] diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift index 12b3f61db7..2856cabee0 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift @@ -8,7 +8,10 @@ import Foundation /// The global settings for source control -public struct SourceControlSettings: Codable, Hashable { +public struct SourceControlSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "sourceControl" /// The general source control settings public var general: SourceControlGeneral = .init() diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift index 2ee3cad6be..560cdee682 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift @@ -9,7 +9,10 @@ import AppKit import Foundation /// The global settings for the terminal emulator -public struct TerminalSettings: Codable, Hashable { +public struct TerminalSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "terminal" /// If true terminal will use editor theme. @CodableDefault public var useEditorTheme = true diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift index 4a2f8e1ed9..bbeb92402d 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift @@ -10,7 +10,10 @@ import CodeEditCore import Foundation /// The global settings for text editing -public struct TextEditingSettings: Codable, Hashable { +public struct TextEditingSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "textEditing" /// An integer indicating how many spaces a `tab` will appear as visually. public var defaultTabWidth: Int = 4 diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift index f1ffe95938..1458b45039 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift @@ -28,7 +28,10 @@ import Foundation public typealias ThemeOverrides = [String: [String: Theme.Attributes]] /// The global settings for themes -public struct ThemeSettings: Codable, Hashable { +public struct ThemeSettings: SettingsSection { + + /// The top-level key this section occupies in `settings.json`. + public static let settingsKey = "theme" /// The name of the currently selected dark theme public var selectedDarkTheme: String = "Default (Dark)" diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsSection.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsSection.swift new file mode 100644 index 0000000000..80725bb23a --- /dev/null +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsSection.swift @@ -0,0 +1,22 @@ +// +// SettingsSection.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/26. +// + +/// One independently-stored group of settings, addressed by a stable key. +/// +/// The key **is** the top-level key in `settings.json`. Changing it orphans every existing user's +/// values for that section, so treat it as a published contract rather than an implementation +/// detail. +/// +/// Deliberately not `Sendable`: `TerminalSettings.Font` carries an `NSFont.Weight`, and requiring +/// `Sendable` here would cascade into `CEEditor`, which is still Swift 5. +public protocol SettingsSection: Codable, Hashable { + /// The top-level key this section occupies in `settings.json`. + static var settingsKey: String { get } + + /// A section must be constructible at its defaults, for when the key is absent from the file. + init() +} diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index d8408394f8..babe3db9a6 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -35,6 +35,25 @@ struct SettingsFormatTests { #expect(try parsed(reencoded) == parsed(original)) } + /// Section keys must match the JSON keys `SettingsData` already uses, or existing settings + /// files silently orphan their values. + /// + /// Note `developerSettings`, not `developer` — the key is the existing field name. + @Test + func sectionKeysMatchTheOnDiskKeys() { + #expect(GeneralSettings.settingsKey == "general") + #expect(AccountsSettings.settingsKey == "accounts") + #expect(NavigationSettings.settingsKey == "navigation") + #expect(ThemeSettings.settingsKey == "theme") + #expect(TextEditingSettings.settingsKey == "textEditing") + #expect(TerminalSettings.settingsKey == "terminal") + #expect(SourceControlSettings.settingsKey == "sourceControl") + #expect(KeybindingsSettings.settingsKey == "keybindings") + #expect(SearchSettings.settingsKey == "search") + #expect(LanguageServerSettings.settingsKey == "languageServers") + #expect(DeveloperSettings.settingsKey == "developerSettings") + } + /// Every section present on disk must survive a save, whether or not anything is registered to /// read it. This is what keeps a disabled or not-yet-loaded extension's configuration alive. /// From 7f24f859a87be38509742f9dd8a6a7ec6280e416 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 21:33:49 +0200 Subject: [PATCH 242/335] Feat: Add per-section settings reading for feature packages Adds SettingsReading, the @SettingsValue property wrapper and a \.settingsReader environment key, so feature packages can read one settings section without naming SettingsData or reaching Settings.shared. SettingsReading is deliberately neither Sendable nor @MainActor: the store that will conform to it in a later slice cannot have actor-isolated methods without breaking the conformance, and EnvironmentKey requires a nonisolated static default. --- .../Store/SettingsValue.swift | 81 +++++++++++++++++++ .../SettingsFormatTests.swift | 16 ++++ 2 files changed, 97 insertions(+) create mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift new file mode 100644 index 0000000000..f06b81316b --- /dev/null +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -0,0 +1,81 @@ +// +// SettingsValue.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/26. +// + +import SwiftUI + +/// Read access to settings, one section at a time. +/// +/// Feature packages depend on this rather than on the concrete store, so they never name the +/// app-wide aggregate and never reach a singleton. +/// +/// Deliberately neither `Sendable` nor `@MainActor`: conforming types include a store class whose +/// methods cannot be actor-isolated without breaking this conformance, and `EnvironmentKey` +/// requires a nonisolated static default. +public protocol SettingsReading { + /// The current value of `type`, or its defaults if the section is absent. + func value(_ type: S.Type) -> S +} + +/// A reader that always answers with defaults. The environment's fallback, so a preview with no +/// store configured still renders. +public struct DefaultSettingsReader: SettingsReading { + public init() {} + + public func value(_ type: S.Type) -> S { + S() + } +} + +/// A fixed reader for tests and SwiftUI previews. +public struct SnapshotSettingsReader: SettingsReading { + private let sections: [String: any SettingsSection] + + public init(_ sections: [String: any SettingsSection]) { + self.sections = sections + } + + public func value(_ type: S.Type) -> S { + sections[S.settingsKey] as? S ?? S() + } +} + +public struct SettingsReaderKey: EnvironmentKey { + /// Defaults are a legitimate value here — a preview with no store configured should render. + nonisolated(unsafe) public static let defaultValue: SettingsReading = DefaultSettingsReader() +} + +public extension EnvironmentValues { + /// The settings reader for the current view tree. + var settingsReader: SettingsReading { + get { self[SettingsReaderKey.self] } + set { self[SettingsReaderKey.self] = newValue } + } +} + +/// Reads one property of one settings section inside a SwiftUI view. +/// +/// ```swift +/// @SettingsValue(TerminalSettings.self, \.cursorBlink) private var cursorBlink +/// ``` +/// +/// Only valid inside a `View`. AppKit types must be handed the value by their +/// `NSViewRepresentable` instead — read at the SwiftUI boundary, pass by value inward. +@propertyWrapper +public struct SettingsValue: DynamicProperty { + @Environment(\.settingsReader) + private var reader + + private let keyPath: KeyPath + + public init(_ section: S.Type, _ keyPath: KeyPath) { + self.keyPath = keyPath + } + + public var wrappedValue: Value { + reader.value(S.self)[keyPath: keyPath] + } +} diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index babe3db9a6..96ab76f056 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -54,6 +54,22 @@ struct SettingsFormatTests { #expect(DeveloperSettings.settingsKey == "developerSettings") } + @Test + func snapshotReaderReturnsTheGivenSection() { + var terminal = TerminalSettings() + terminal.cursorBlink = true + let reader = SnapshotSettingsReader([TerminalSettings.settingsKey: terminal]) + + #expect(reader.value(TerminalSettings.self).cursorBlink == true) + } + + @Test + func snapshotReaderFallsBackToDefaults() { + let reader = SnapshotSettingsReader([:]) + + #expect(reader.value(TerminalSettings.self) == TerminalSettings()) + } + /// Every section present on disk must survive a save, whether or not anything is registered to /// read it. This is what keeps a disabled or not-yet-loaded extension's configuration alive. /// From bf37d9d39f2063efe4751320c87bb449dddd8ef1 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 21:41:13 +0200 Subject: [PATCH 243/335] Refactor: Inject terminal settings instead of reading the singleton --- .../Views/CEActiveTaskTerminalView.swift | 6 +- .../Views/CELocalShellTerminalView.swift | 35 ++++++++++- .../Views/TerminalEmulatorView.swift | 61 ++++++------------- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift index 777f2aa35c..d075cb1abc 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift @@ -17,9 +17,9 @@ public class CEActiveTaskTerminalView: CELocalShellTerminalView { activeTask.status == .running || activeTask.status == .stopped } - init(activeTask: CEActiveTask) { + init(activeTask: CEActiveTask, settings: TerminalSettings = TerminalSettings()) { self.activeTask = activeTask - super.init(frame: .zero) + super.init(frame: .zero, settings: settings) } public required init?(coder: NSCoder) { @@ -32,7 +32,7 @@ public class CEActiveTaskTerminalView: CELocalShellTerminalView { environment: [String] = [], interactive: Bool = true ) { - let terminalSettings = Settings.shared.preferences.terminal + let terminalSettings = settings var terminalEnvironment: [String] = Terminal.getEnvironmentVariables() terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal") diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift index 7545761ec0..e814b0ceaa 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift @@ -59,12 +59,19 @@ public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalV @preconcurrency LocalProcessDelegate { public var process: LocalProcess! - override public init(frame: CGRect) { + /// The terminal settings this view was last configured with. Set at construction and kept + /// current by ``apply(settings:)``, which `TerminalEmulatorView` calls from `updateNSView` so a + /// running terminal picks up changes without being reopened. + public internal(set) var settings: TerminalSettings + + public init(frame: CGRect, settings: TerminalSettings = TerminalSettings()) { + self.settings = settings super.init(frame: frame) setup() } public required init?(coder: NSCoder) { + self.settings = TerminalSettings() super.init(coder: coder) setup() } @@ -88,7 +95,7 @@ public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalV environment: [String] = [], interactive: Bool = true ) { - let terminalSettings = Settings.shared.preferences.terminal + let terminalSettings = settings var terminalEnvironment: [String] = Terminal.getEnvironmentVariables() terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal") @@ -126,6 +133,30 @@ public class CELocalShellTerminalView: CETerminalView, @preconcurrency TerminalV } } + /// Re-applies the parts of ``TerminalSettings`` this view can act on by itself: the cursor + /// style/blink and the option-as-meta key mapping. Font and theme-derived colours stay with + /// `TerminalEmulatorView`, which also needs the current theme and text-editing font settings. + /// + /// `TerminalEmulatorView.updateNSView` calls this on every settings change so a running + /// terminal reflects new settings without being reopened. + public func apply(settings: TerminalSettings) { + self.settings = settings + optionAsMetaKey = settings.optionAsMeta + cursorStyleChanged(source: getTerminal(), newStyle: Self.cursorStyle(for: settings)) + } + + /// The `SwiftTerm.CursorStyle` for the given terminal settings' cursor shape and blink state. + static func cursorStyle(for settings: TerminalSettings) -> CursorStyle { + switch settings.cursorStyle { + case .block: + return settings.cursorBlink ? .blinkBlock : .steadyBlock + case .underline: + return settings.cursorBlink ? .blinkUnderline : .steadyUnderline + case .bar: + return settings.cursorBlink ? .blinkBar : .steadyBar + } + } + /// Returns a string of a shell path to use func getShell(_ shellType: Shell?, userSetting: TerminalSettings.Shell) -> (Shell, String)? { if let shellType { diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift index b1f817b637..4c07e89012 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -24,10 +24,12 @@ public struct TerminalEmulatorView: NSViewRepresentable { case task(activeTask: CEActiveTask) } - @AppSettings(\.terminal) - var terminalSettings - @AppSettings(\.textEditing.font) - var fontSettings + @SettingsValue(TerminalSettings.self, \.self) + private var terminalSettings + @SettingsValue(TextEditingSettings.self, \.font) + private var fontSettings + @SettingsValue(ThemeSettings.self, \.matchAppearance) + private var themeMatchAppearance @Environment(\.currentTheme) private var currentTheme @@ -75,29 +77,15 @@ public struct TerminalEmulatorView: NSViewRepresentable { // MARK: - Settings - private func getTerminalCursor() -> CursorStyle { - let blink = terminalSettings.cursorBlink - switch terminalSettings.cursorStyle { - case .block: - return blink ? .blinkBlock : .steadyBlock - case .underline: - return blink ? .blinkUnderline : .steadyUnderline - case .bar: - return blink ? .blinkBar : .steadyBar - } - } - - /// Returns true if the `option` key should be treated as the `meta` key. - private var optionAsMeta: Bool { - terminalSettings.optionAsMeta + /// Whether the dark variant of the theme should be used: the theme is following system + /// appearance and the terminal is configured to always be dark. + private var useDarkTheme: Bool { + themeMatchAppearance && terminalSettings.darkAppearance } /// Returns the mapped array of `SwiftTerm.Color` objects of ANSI Colors private var colors: [SwiftTerm.Color] { - guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? currentDarkTheme - : currentTheme - else { + guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { return [] } return selectedTheme.terminal.ansiColors.map { color in @@ -107,10 +95,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the `cursor` color of the selected theme private var cursorColor: NSColor { - guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? currentDarkTheme - : currentTheme - else { + guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { return NSColor(.accentColor) } return NSColor(selectedTheme.terminal.cursor.swiftColor) @@ -118,10 +103,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the `selection` color of the selected theme private var selectionColor: NSColor { - guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? currentDarkTheme - : currentTheme - else { + guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { return NSColor(.accentColor) } return NSColor(selectedTheme.terminal.selection.swiftColor) @@ -129,10 +111,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the `text` color of the selected theme private var textColor: NSColor { - guard let selectedTheme = Settings[\.theme].matchAppearance && Settings[\.terminal].darkAppearance - ? currentDarkTheme - : currentTheme - else { + guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { return NSColor(.primary) } return NSColor(selectedTheme.terminal.text.swiftColor) @@ -161,7 +140,8 @@ public struct TerminalEmulatorView: NSViewRepresentable { switch mode { case .shell(let shellType): let isCached = TerminalCache.shared.getTerminalView(terminalID) != nil - view = TerminalCache.shared.getTerminalView(terminalID) ?? CELocalShellTerminalView(frame: .zero) + view = TerminalCache.shared.getTerminalView(terminalID) + ?? CELocalShellTerminalView(frame: .zero, settings: terminalSettings) if !isCached { view.startProcess(workspaceURL: url, shell: shellType) configureView(view) @@ -170,7 +150,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { if let output = activeTask.output { view = output } else { - let newView = CEActiveTaskTerminalView(activeTask: activeTask) + let newView = CEActiveTaskTerminalView(activeTask: activeTask, settings: terminalSettings) activeTask.output = newView view = newView } @@ -197,9 +177,8 @@ public struct TerminalEmulatorView: NSViewRepresentable { terminal.selectedTextBackgroundColor = selectionColor terminal.nativeForegroundColor = textColor terminal.nativeBackgroundColor = terminalSettings.useThemeBackground ? backgroundColor : .clear - terminal.cursorStyleChanged(source: terminal.getTerminal(), newStyle: getTerminalCursor()) terminal.layer?.backgroundColor = CGColor.clear - terminal.optionAsMetaKey = optionAsMeta + terminal.apply(settings: terminalSettings) } private func scroller(_ terminal: CELocalShellTerminalView) -> NSScroller? { @@ -212,6 +191,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { } public func updateNSView(_ view: CELocalShellTerminalView, context: Context) { + view.font = font view.installColors(self.colors) view.caretColor = cursorColor.withAlphaComponent(0.5) view.caretTextColor = cursorColor.withAlphaComponent(0.5) @@ -219,8 +199,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { view.nativeForegroundColor = textColor view.nativeBackgroundColor = terminalSettings.useThemeBackground ? backgroundColor : .clear view.layer?.backgroundColor = .clear - view.optionAsMetaKey = optionAsMeta - view.cursorStyleChanged(source: view.getTerminal(), newStyle: getTerminalCursor()) + view.apply(settings: terminalSettings) view.appearance = colorAppearance view.getTerminal().softReset() view.feed(text: "") // send empty character to force colors to be redrawn From ea721ed53284f759cc66464c58c6fc28ae945fd4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 21:48:54 +0200 Subject: [PATCH 244/335] Refactor: Inject source control settings instead of reading the singleton --- CodeEdit/App/AppDependencies.swift | 6 ++++++ .../SourceControlNavigator/GitChangedFileLabel.swift | 7 +++++-- CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift | 3 ++- .../CESourceControl/SourceControlManager+FileEvents.swift | 2 +- .../Sources/CESourceControl/SourceControlManager.swift | 6 +++++- .../CESourceControl/Views/ToolbarBranchPicker.swift | 5 ++++- 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index c97b5b8dae..6d0b93acf7 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -8,6 +8,7 @@ import CELSP import CodeEditCore import CodeEditDocument +import CodeEditSettings import CENotifications import ShellClient @@ -29,6 +30,11 @@ final class AppDependencies { private(set) lazy var shellClient: ShellClientProtocol = ShellClient() + /// Feature-side settings access. Defaults-only until Task 9/10 wires a store-backed reader + /// over `Settings.shared`'s persisted data — feature packages already read through this + /// interface, so that swap will not touch any of their call sites. + private(set) lazy var settingsReader: SettingsReading = DefaultSettingsReader() + private(set) lazy var commandManager: CommandManaging = CommandManager() private(set) lazy var keybindingManager: KeybindingManaging = KeybindingManager() diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift index d8a0ccfc99..81b69f0068 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift @@ -10,6 +10,7 @@ import SwiftUI import ShellClient import CEWorkspaceFileManager import CodeEditCore +import CodeEditSettings import CodeEditUI struct GitChangedFileLabel: View { @@ -48,7 +49,8 @@ struct GitChangedFileLabel: View { .environmentObject(SourceControlManager( workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), - eventBus: EventBus() + eventBus: EventBus(), + settingsReader: DefaultSettingsReader() )) GitChangedFileLabel(file: GitChangedFile( @@ -60,7 +62,8 @@ struct GitChangedFileLabel: View { .environmentObject(SourceControlManager( workspaceURL: URL(filePath: "/Users/CodeEdit"), shellClient: ShellClient(), - eventBus: EventBus() + eventBus: EventBus(), + settingsReader: DefaultSettingsReader() )) }.padding() } diff --git a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift index 6dbdb043a4..cb871d3d25 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift @@ -32,7 +32,8 @@ enum WorkspaceFactory { let sourceControlManager = SourceControlManager( workspaceURL: url, shellClient: dependencies.shellClient, - eventBus: eventBus + eventBus: eventBus, + settingsReader: dependencies.settingsReader ) let workspaceFileManager = CEWorkspaceFileManager( folderUrl: url, diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlManager+FileEvents.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager+FileEvents.swift index 2f92585ab9..2a27048ca0 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlManager+FileEvents.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlManager+FileEvents.swift @@ -66,7 +66,7 @@ extension SourceControlManager { case .childrenIndexed: Task { await self.refreshAllChangedFiles() } case let .filesystemChanged(paths): - let settings = Settings.shared.preferences.sourceControl.general + let settings = settingsReader.value(SourceControlSettings.self).general guard settings.sourceControlIsEnabled && settings.refreshStatusLocally else { return } dispatch(Self.gitRefreshActions(for: paths, workspaceRelativePath: workspaceURL.relativePath)) } diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift index 3f43e666a3..47b1dab8ab 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift @@ -9,6 +9,7 @@ import Combine import Foundation import OSLog import CodeEditCore +import CodeEditSettings /// Stores git state for the workspace and delegates operations to ``GitClient``. /// @@ -33,6 +34,7 @@ public final class SourceControlManager: ObservableObject { public let workspaceURL: URL let eventBus: EventBus + let settingsReader: SettingsReading var fileEventCancellables: Set = [] // MARK: - Git State @@ -73,10 +75,12 @@ public final class SourceControlManager: ObservableObject { public init( workspaceURL: URL, shellClient: ShellClientProtocol, - eventBus: EventBus + eventBus: EventBus, + settingsReader: SettingsReading ) { self.workspaceURL = workspaceURL self.eventBus = eventBus + self.settingsReader = settingsReader gitClient = GitClient(directoryURL: workspaceURL, shellClient: shellClient) gitConfig = GitConfigClient(shellClient: shellClient) subscribeToWorkspaceFileEvents() diff --git a/CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift b/CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift index 0e17118b11..910760edbb 100644 --- a/CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift +++ b/CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift @@ -24,6 +24,9 @@ public struct ToolbarBranchPicker: View { @State private var displayPopover: Bool = false @State private var currentBranch: GitBranch? + @SettingsValue(SourceControlSettings.self, \.general.sourceControlIsEnabled) + private var sourceControlIsEnabled + /// Initializes the picker with the workspace's display name (shown when no /// branch is available) and its source-control manager. public init( @@ -88,7 +91,7 @@ public struct ToolbarBranchPicker: View { self.currentBranch = branch } .task { - if Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled { + if sourceControlIsEnabled { await self.sourceControlManager?.refreshCurrentBranch() await self.sourceControlManager?.refreshBranches() } From a6dd0b58c14cba4b5697c88f20b2117768325201 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 22:05:10 +0200 Subject: [PATCH 245/335] Fix: Bridge SettingsReading to the Settings.shared singleton CETerminal and CESourceControl were cut over to the injected SettingsReading abstraction, but AppDependencies still supplied DefaultSettingsReader, so both features silently ignored the user's settings.json. LegacySettingsReader is a stopgap that answers from Settings.shared.preferences until the section-keyed store replaces the singleton. --- CodeEdit/App/AppDependencies.swift | 8 ++-- CodeEdit/App/Environment+AppCommands.swift | 3 ++ .../Settings/LegacySettingsReader.swift | 46 +++++++++++++++++++ .../App/LegacySettingsReaderTests.swift | 31 +++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift create mode 100644 CodeEditTests/App/LegacySettingsReaderTests.swift diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index 6d0b93acf7..7e6bd8641b 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -30,10 +30,10 @@ final class AppDependencies { private(set) lazy var shellClient: ShellClientProtocol = ShellClient() - /// Feature-side settings access. Defaults-only until Task 9/10 wires a store-backed reader - /// over `Settings.shared`'s persisted data — feature packages already read through this - /// interface, so that swap will not touch any of their call sites. - private(set) lazy var settingsReader: SettingsReading = DefaultSettingsReader() + /// Feature-side settings access. Bridged onto `Settings.shared` via `LegacySettingsReader` + /// as a stopgap until a later task wires a real, section-keyed store — feature packages + /// already read through this interface, so that swap will not touch any of their call sites. + private(set) lazy var settingsReader: SettingsReading = LegacySettingsReader() private(set) lazy var commandManager: CommandManaging = CommandManager() diff --git a/CodeEdit/App/Environment+AppCommands.swift b/CodeEdit/App/Environment+AppCommands.swift index a2768fc6a8..a877b090e4 100644 --- a/CodeEdit/App/Environment+AppCommands.swift +++ b/CodeEdit/App/Environment+AppCommands.swift @@ -8,6 +8,7 @@ import CELSP import SwiftUI import CodeEditCore +import CodeEditSettings import CEEditor import CENotifications import CESearch @@ -106,6 +107,7 @@ extension View { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) + .environment(\.settingsReader, dependencies.settingsReader) } } @@ -123,5 +125,6 @@ extension Scene { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) + .environment(\.settingsReader, dependencies.settingsReader) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift new file mode 100644 index 0000000000..44fecf35ed --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift @@ -0,0 +1,46 @@ +// +// LegacySettingsReader.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import CodeEditSettings + +/// A `SettingsReading` bridge over the existing `Settings.shared` singleton. +/// +/// This is a **stopgap**: it exists only because feature packages have already been cut over to +/// read through `SettingsReading`, but no store backed by real, persisted settings has been built +/// yet to supply the environment. Without this bridge, every `@SettingsValue` read (and every +/// direct `SettingsReading.value(_:)` call) silently falls back to section defaults, ignoring the +/// user's `settings.json` — see the plan's Task 6b for the regression this fixes. +/// +/// It is deleted once a later task replaces the singleton-backed store with the real, +/// section-keyed store. +struct LegacySettingsReader: SettingsReading { + func value(_ type: S.Type) -> S { + Self.sections(from: Settings.shared.preferences)[S.settingsKey] as? S ?? + // Explicit fallback, not a silent catch-all: a section with no entry in `sections(from:)` + // has no field on `SettingsData` to read from, so it answers with defaults until that + // map is updated (or, more likely, until this bridge is deleted in favor of the real + // store). + S() + } + + /// All eleven `SettingsData` fields, keyed by their section's `settingsKey`. + private static func sections(from preferences: SettingsData) -> [String: any SettingsSection] { + [ + GeneralSettings.settingsKey: preferences.general, + AccountsSettings.settingsKey: preferences.accounts, + NavigationSettings.settingsKey: preferences.navigation, + ThemeSettings.settingsKey: preferences.theme, + TextEditingSettings.settingsKey: preferences.textEditing, + TerminalSettings.settingsKey: preferences.terminal, + SourceControlSettings.settingsKey: preferences.sourceControl, + KeybindingsSettings.settingsKey: preferences.keybindings, + SearchSettings.settingsKey: preferences.search, + LanguageServerSettings.settingsKey: preferences.languageServers, + DeveloperSettings.settingsKey: preferences.developerSettings + ] + } +} diff --git a/CodeEditTests/App/LegacySettingsReaderTests.swift b/CodeEditTests/App/LegacySettingsReaderTests.swift new file mode 100644 index 0000000000..1b92ca37f3 --- /dev/null +++ b/CodeEditTests/App/LegacySettingsReaderTests.swift @@ -0,0 +1,31 @@ +// +// LegacySettingsReaderTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import Foundation +import Testing +import CodeEditSettings +@testable import CodeEdit + +/// Verifies `LegacySettingsReader` actually reads through to `Settings.shared`, rather than +/// answering with section defaults like `DefaultSettingsReader` would. A test that only checked +/// defaults would pass against either reader and prove nothing about the bridge. +struct LegacySettingsReaderTests { + @Test + func readsMutatedValueFromTheSingleton() throws { + let originalValue = Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled + defer { + Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = originalValue + } + + // Non-default: `SourceControlGeneral.sourceControlIsEnabled` defaults to `true`, so + // `DefaultSettingsReader` (or an unmapped fallback) would never produce `false` here. + Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = false + + let reader: SettingsReading = LegacySettingsReader() + #expect(reader.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) + } +} From ba5e8b81e6138b1cbc85c2ad8be262b38208847e Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 22:26:21 +0200 Subject: [PATCH 246/335] Fix: Reach ToolbarBranchPicker's standalone hosting root with the real settings reader SettingsInjector now also injects \.settingsReader (backed by LegacySettingsReader, change-driven via its existing @ObservedObject on Settings.shared), and the branch-picker toolbar item is wrapped in it. That NSHostingView is a separate SwiftUI environment root that never received .appServices, so ToolbarBranchPicker's sourceControlIsEnabled read was still falling through to defaults-only regardless of settings.json. --- .../AuxiliaryWindows/Settings/SettingsInjector.swift | 5 +++++ .../CodeEditWindowController+Toolbar.swift | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index ec7c81ed44..6d88582859 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -17,5 +17,10 @@ struct SettingsInjector: View { var body: some View { content .environment(\.settings, settings.preferences) + // `@ObservedObject` above means this recomputes on every `Settings.shared` change, + // so wrapped trees using `@SettingsValue`/`SettingsReading` re-render on settings + // changes too — a bare one-shot `.environment(\.settingsReader, LegacySettingsReader())` + // would not. + .environment(\.settingsReader, LegacySettingsReader()) } } diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift index 336d3af711..fe95b1d737 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift @@ -167,10 +167,12 @@ extension CodeEditWindowController { case .branchPicker: let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker) let view = NSHostingView( - rootView: ToolbarBranchPicker( - fallbackTitle: workspace?.workspaceFileManager.folderUrl.lastPathComponent ?? "Empty", - sourceControlManager: workspace?.sourceControlManager - ) + rootView: SettingsInjector { + ToolbarBranchPicker( + fallbackTitle: workspace?.workspaceFileManager.folderUrl.lastPathComponent ?? "Empty", + sourceControlManager: workspace?.sourceControlManager + ) + } ) toolbarItem.view = view toolbarItem.isBordered = false From 50bc01e2f2f8e01d408f9ec557fac16a2b2b9f25 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 22:39:23 +0200 Subject: [PATCH 247/335] Refactor: Inject language server settings instead of reading the singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RegistryManager and LSPService now take SettingsReading through their initializers instead of reading Settings.shared via @AppSettings. installedLanguageServers is read-write (install/uninstall/enable persist to settings.json), and SettingsReading has no write half, so RegistryManager also takes a narrow LanguageServerRegistryWriting seam for that one persistence call — bridged onto Settings.shared by LegacyLanguageServerRegistryWriter, the same stopgap pattern as LegacySettingsReader. RegistryManager.swift:19's Settings.shared.baseURL is left alone (an Application Support path, not a settings value). --- CodeEdit/App/AppDependencies.swift | 6 ++-- .../LegacyLanguageServerRegistryWriter.swift | 21 +++++++++++++ .../LanguageServerRegistryWriting.swift | 30 +++++++++++++++++++ .../CELSP/Registry/RegistryManager.swift | 18 ++++++++--- .../Sources/CELSP/Service/LSPService.swift | 6 ++-- .../LSP/LSPServiceDocumentObjectsTests.swift | 3 +- CodeEditTests/Features/LSP/Registry.swift | 5 +++- 7 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift create mode 100644 CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index 7e6bd8641b..3da2a38d08 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -47,7 +47,7 @@ final class AppDependencies { private(set) lazy var softwareUpdater = SoftwareUpdater() private(set) lazy var lspService: LSPService = { - let service = LSPService() + let service = LSPService(settingsReader: settingsReader) // Property-injected (not init-injected): the window manager's construction consumes // `lspService`, so init injection in both directions would recurse. Resolved at call // time, long after both objects exist. @@ -62,7 +62,9 @@ final class AppDependencies { private(set) lazy var registryManager = RegistryManager( eventBus: eventBus, errorNotifier: errorNotifier, - shellClient: shellClient + shellClient: shellClient, + settingsReader: settingsReader, + registryWriter: LegacyLanguageServerRegistryWriter() ) private(set) lazy var workspaceWindowManager = WorkspaceWindowManager(dependencies: self) diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift new file mode 100644 index 0000000000..bb4934ccdc --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift @@ -0,0 +1,21 @@ +// +// LegacyLanguageServerRegistryWriter.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import CELSP +import CodeEditSettings + +/// A `LanguageServerRegistryWriting` bridge over the existing `Settings.shared` singleton. +/// +/// Mirrors `LegacySettingsReader`'s role, but for the one settings write `CELSP` needs to make +/// (`RegistryManager` persisting install/uninstall/enable changes). It is deleted alongside +/// `LegacySettingsReader` once a later task replaces the singleton-backed store with the real, +/// section-keyed store. +struct LegacyLanguageServerRegistryWriter: LanguageServerRegistryWriting { + func persistInstalledLanguageServers(_ servers: [String: LanguageServerSettings.Installed]) { + Settings[\.languageServers.installedLanguageServers] = servers + } +} diff --git a/CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift b/CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift new file mode 100644 index 0000000000..d30d5e8489 --- /dev/null +++ b/CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift @@ -0,0 +1,30 @@ +// +// LanguageServerRegistryWriting.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/26. +// + +import CodeEditSettings + +/// Persists `RegistryManager`'s installed-language-server registry back to settings. +/// +/// `SettingsReading` (the settings-ownership refactor's read seam) has no write half, and +/// `RegistryManager` is the one place in the app that mutates this particular registry +/// (install/uninstall/enable-disable). Rather than reintroduce `Settings.shared` into `CELSP` or +/// grow `SettingsReading` into a general read/write interface for one caller, this protocol asks +/// for exactly the capability `RegistryManager` needs. It is a stopgap, like `LegacySettingsReader` +/// — both are expected to fold into the real, section-keyed settings store once that lands. +public protocol LanguageServerRegistryWriting { + /// Persists the full registry, replacing whatever was previously stored. + func persistInstalledLanguageServers(_ servers: [String: LanguageServerSettings.Installed]) +} + +/// A writer that discards every write. For tests and previews where persistence is irrelevant. +public final class NoOpLanguageServerRegistryWriting: LanguageServerRegistryWriting { + /// Creates a writer that discards every write. + public init() {} + + /// Discards the registry instead of persisting it. + public func persistInstalledLanguageServers(_ servers: [String: LanguageServerSettings.Installed]) {} +} diff --git a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift index eb1a7e8b3e..fbed3986e9 100644 --- a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift +++ b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift @@ -40,17 +40,27 @@ public final class RegistryManager: RegistryManaging { /// invalidated from `deinit`, which cannot be actor-isolated. nonisolated(unsafe) private var cleanupTimer: Timer? - @AppSettings(\.languageServers.installedLanguageServers) - public var installedLanguageServers: [String: LanguageServerSettings.Installed] + public private(set) var installedLanguageServers: [String: LanguageServerSettings.Installed] { + didSet { registryWriter.persistInstalledLanguageServers(installedLanguageServers) } + } private let eventBus: EventBus private let errorNotifier: ErrorNotifying private let shellClient: ShellClientProtocol - - public init(eventBus: EventBus, errorNotifier: ErrorNotifying, shellClient: ShellClientProtocol) { + private let registryWriter: LanguageServerRegistryWriting + + public init( + eventBus: EventBus, + errorNotifier: ErrorNotifying, + shellClient: ShellClientProtocol, + settingsReader: SettingsReading, + registryWriter: LanguageServerRegistryWriting + ) { self.eventBus = eventBus self.errorNotifier = errorNotifier self.shellClient = shellClient + self.registryWriter = registryWriter + self.installedLanguageServers = settingsReader.value(LanguageServerSettings.self).installedLanguageServers } deinit { diff --git a/CodeEditModules/Sources/CELSP/Service/LSPService.swift b/CodeEditModules/Sources/CELSP/Service/LSPService.swift index fe15cd88bd..dcafa67350 100644 --- a/CodeEditModules/Sources/CELSP/Service/LSPService.swift +++ b/CodeEditModules/Sources/CELSP/Service/LSPService.swift @@ -132,9 +132,6 @@ public final class LSPService: LSPServiceProtocol { /// created on demand and removed when the document closes. private var documentObjects: [String: LanguageServerDocumentObjects] = [:] - @AppSettings(\.developerSettings.lspBinaries) - var lspBinaries - /// Returns the language-server objects for a document, creating and storing them on first use. /// A document without a URI (e.g. untitled) gets a fresh, unstored instance — it has no server. func languageServerObjects(for document: CodeFileDocument) -> LanguageServerDocumentObjects { @@ -160,8 +157,9 @@ public final class LSPService: LSPServiceProtocol { /// before any document opens. public var workspaceFinder: (URL) -> URL? = { _ in nil } - public init() { + public init(settingsReader: SettingsReading) { // Load the LSP binaries from the developer menu + let lspBinaries = settingsReader.value(DeveloperSettings.self).lspBinaries for binary in lspBinaries { if let language = LanguageIdentifier(rawValue: binary.key) { self.languageConfigs[language] = LanguageServerBinary( diff --git a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift index f7757aeca7..f6735445f2 100644 --- a/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift +++ b/CodeEditTests/Features/LSP/LSPServiceDocumentObjectsTests.swift @@ -8,11 +8,12 @@ @testable import CELSP import XCTest import CodeEditDocument +import CodeEditSettings @testable import CodeEdit @MainActor final class LSPServiceDocumentObjectsTests: XCTestCase { - private func makeService() -> LSPService { LSPService() } + private func makeService() -> LSPService { LSPService(settingsReader: SnapshotSettingsReader([:])) } private func makeDocument(path: String) throws -> CodeFileDocument { let url = FileManager.default.temporaryDirectory diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index 50bf830293..bd200ec615 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -9,6 +9,7 @@ import Testing import Foundation import CodeEditCore +import CodeEditSettings import ShellClient @testable import CodeEdit @@ -18,7 +19,9 @@ struct RegistryTests { var registry: RegistryManager = RegistryManager( eventBus: EventBus(), errorNotifier: NoOpErrorNotifier(), - shellClient: ShellClient() + shellClient: ShellClient(), + settingsReader: SnapshotSettingsReader([:]), + registryWriter: NoOpLanguageServerRegistryWriting() ) // MARK: - Download Tests From 84c3a4726ad29a423c0ec91ceec0fb2abed93ffb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 23:42:38 +0200 Subject: [PATCH 248/335] Refactor: Give the settings seam a write half MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SettingsReading` was read-only, so Task 7 had to invent a CELSP-local `LanguageServerRegistryWriting` protocol to persist installed language servers, and `@SettingsValue` could not express a `Toggle` binding. Add `SettingsAccessing: SettingsReading` with a section-granular `setValue(_:)`. One protocol rather than a separate `SettingsWriting`, because `@SettingsValue.projectedValue` needs read and write from the same environment value — two keys would let a subtree hold a live reader next to a defaulted writer, so reads would look right while writes vanished. `@SettingsValue` now takes a `WritableKeyPath`, gains a setter and a `Binding` projection. Both existing call sites keep working unchanged; every settings field is a `var`. The bespoke protocol and its app-side adapter are deleted: `RegistryManager` takes a single `settingsAccessor` and persists through the general seam. `LegacySettingsReader` becomes `LegacySettingsStore`, now writing through `Settings[_:]` so changes reach the existing `@Published` -> throttle -> `savePreferences` pipeline, with reads and writes sharing one accessor table. Tests replace the discarding double with a recording spy and assert the three `RegistryManager` mutations plus an app-side round trip; each was verified to fail with the write path severed. `LegacySettingsStoreTests` is `@MainActor` and serialized — mutating the `Settings` singleton off the main thread drives an AppKit update from a background thread and crashes the test host. --- CodeEdit/App/AppDependencies.swift | 14 +- CodeEdit/App/Environment+AppCommands.swift | 4 +- .../LegacyLanguageServerRegistryWriter.swift | 21 --- .../Settings/LegacySettingsReader.swift | 46 ------- .../Settings/LegacySettingsStore.swift | 69 ++++++++++ .../Settings/SettingsInjector.swift | 4 +- .../Workspace/WorkspaceFactory.swift | 2 +- .../LanguageServerRegistryWriting.swift | 30 ----- .../CELSP/Registry/RegistryManager.swift | 20 ++- .../Store/SettingsAccessing.swift | 28 ++++ .../Store/SettingsValue.swift | 64 ++++++--- .../App/LegacySettingsReaderTests.swift | 31 ----- .../App/LegacySettingsStoreTests.swift | 83 ++++++++++++ .../App/RecordingSettingsStore.swift | 38 ++++++ CodeEditTests/Features/LSP/Registry.swift | 3 +- .../LSP/RegistryManagerPersistenceTests.swift | 121 ++++++++++++++++++ 16 files changed, 415 insertions(+), 163 deletions(-) delete mode 100644 CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift delete mode 100644 CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift create mode 100644 CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift delete mode 100644 CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift create mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/SettingsAccessing.swift delete mode 100644 CodeEditTests/App/LegacySettingsReaderTests.swift create mode 100644 CodeEditTests/App/LegacySettingsStoreTests.swift create mode 100644 CodeEditTests/App/RecordingSettingsStore.swift create mode 100644 CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index 3da2a38d08..0dda97d02a 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -30,10 +30,11 @@ final class AppDependencies { private(set) lazy var shellClient: ShellClientProtocol = ShellClient() - /// Feature-side settings access. Bridged onto `Settings.shared` via `LegacySettingsReader` - /// as a stopgap until a later task wires a real, section-keyed store — feature packages - /// already read through this interface, so that swap will not touch any of their call sites. - private(set) lazy var settingsReader: SettingsReading = LegacySettingsReader() + /// Feature-side settings access, read and write. Bridged onto `Settings.shared` via + /// `LegacySettingsStore` as a stopgap until a later task wires a real, section-keyed store — + /// feature packages already go through this interface, so that swap will not touch any of + /// their call sites. + private(set) lazy var settingsAccessor: SettingsAccessing = LegacySettingsStore() private(set) lazy var commandManager: CommandManaging = CommandManager() @@ -47,7 +48,7 @@ final class AppDependencies { private(set) lazy var softwareUpdater = SoftwareUpdater() private(set) lazy var lspService: LSPService = { - let service = LSPService(settingsReader: settingsReader) + let service = LSPService(settingsReader: settingsAccessor) // Property-injected (not init-injected): the window manager's construction consumes // `lspService`, so init injection in both directions would recurse. Resolved at call // time, long after both objects exist. @@ -63,8 +64,7 @@ final class AppDependencies { eventBus: eventBus, errorNotifier: errorNotifier, shellClient: shellClient, - settingsReader: settingsReader, - registryWriter: LegacyLanguageServerRegistryWriter() + settingsAccessor: settingsAccessor ) private(set) lazy var workspaceWindowManager = WorkspaceWindowManager(dependencies: self) diff --git a/CodeEdit/App/Environment+AppCommands.swift b/CodeEdit/App/Environment+AppCommands.swift index a877b090e4..f67d846941 100644 --- a/CodeEdit/App/Environment+AppCommands.swift +++ b/CodeEdit/App/Environment+AppCommands.swift @@ -107,7 +107,7 @@ extension View { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) - .environment(\.settingsReader, dependencies.settingsReader) + .environment(\.settingsAccessor, dependencies.settingsAccessor) } } @@ -125,6 +125,6 @@ extension Scene { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) - .environment(\.settingsReader, dependencies.settingsReader) + .environment(\.settingsAccessor, dependencies.settingsAccessor) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift deleted file mode 100644 index bb4934ccdc..0000000000 --- a/CodeEdit/AuxiliaryWindows/Settings/LegacyLanguageServerRegistryWriter.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// LegacyLanguageServerRegistryWriter.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 09/08/2026. -// - -import CELSP -import CodeEditSettings - -/// A `LanguageServerRegistryWriting` bridge over the existing `Settings.shared` singleton. -/// -/// Mirrors `LegacySettingsReader`'s role, but for the one settings write `CELSP` needs to make -/// (`RegistryManager` persisting install/uninstall/enable changes). It is deleted alongside -/// `LegacySettingsReader` once a later task replaces the singleton-backed store with the real, -/// section-keyed store. -struct LegacyLanguageServerRegistryWriter: LanguageServerRegistryWriting { - func persistInstalledLanguageServers(_ servers: [String: LanguageServerSettings.Installed]) { - Settings[\.languageServers.installedLanguageServers] = servers - } -} diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift deleted file mode 100644 index 44fecf35ed..0000000000 --- a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsReader.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// LegacySettingsReader.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 09/08/2026. -// - -import CodeEditSettings - -/// A `SettingsReading` bridge over the existing `Settings.shared` singleton. -/// -/// This is a **stopgap**: it exists only because feature packages have already been cut over to -/// read through `SettingsReading`, but no store backed by real, persisted settings has been built -/// yet to supply the environment. Without this bridge, every `@SettingsValue` read (and every -/// direct `SettingsReading.value(_:)` call) silently falls back to section defaults, ignoring the -/// user's `settings.json` — see the plan's Task 6b for the regression this fixes. -/// -/// It is deleted once a later task replaces the singleton-backed store with the real, -/// section-keyed store. -struct LegacySettingsReader: SettingsReading { - func value(_ type: S.Type) -> S { - Self.sections(from: Settings.shared.preferences)[S.settingsKey] as? S ?? - // Explicit fallback, not a silent catch-all: a section with no entry in `sections(from:)` - // has no field on `SettingsData` to read from, so it answers with defaults until that - // map is updated (or, more likely, until this bridge is deleted in favor of the real - // store). - S() - } - - /// All eleven `SettingsData` fields, keyed by their section's `settingsKey`. - private static func sections(from preferences: SettingsData) -> [String: any SettingsSection] { - [ - GeneralSettings.settingsKey: preferences.general, - AccountsSettings.settingsKey: preferences.accounts, - NavigationSettings.settingsKey: preferences.navigation, - ThemeSettings.settingsKey: preferences.theme, - TextEditingSettings.settingsKey: preferences.textEditing, - TerminalSettings.settingsKey: preferences.terminal, - SourceControlSettings.settingsKey: preferences.sourceControl, - KeybindingsSettings.settingsKey: preferences.keybindings, - SearchSettings.settingsKey: preferences.search, - LanguageServerSettings.settingsKey: preferences.languageServers, - DeveloperSettings.settingsKey: preferences.developerSettings - ] - } -} diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift new file mode 100644 index 0000000000..5a30e0e39f --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift @@ -0,0 +1,69 @@ +// +// LegacySettingsStore.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import CodeEditSettings + +/// A `SettingsAccessing` bridge over the existing `Settings.shared` singleton. +/// +/// This is a **stopgap**: it exists only because feature packages have already been cut over to +/// read and write through `SettingsAccessing`, but no store backed by real, persisted settings has +/// been built yet to supply the environment. Without this bridge, every `@SettingsValue` read (and +/// every direct `SettingsReading.value(_:)` call) silently falls back to section defaults, ignoring +/// the user's `settings.json` — see the plan's Task 6b for the regression this fixes. +/// +/// Writes go through `Settings[_:]`, i.e. straight into `Settings.shared.preferences`, so they +/// reach the existing `@Published` → `throttle(for: 2)` → `savePreferences` pipeline. Anything that +/// mutated a copy instead would appear to work and lose the value on relaunch. +/// +/// It is deleted once a later task replaces the singleton-backed store with the real, +/// section-keyed store. +struct LegacySettingsStore: SettingsAccessing { + func value(_ type: S.Type) -> S { + Self.accessors[S.settingsKey]?.read(Settings.shared.preferences) as? S ?? + // Explicit fallback, not a silent catch-all: a section with no entry in `accessors` + // has no field on `SettingsData` to read from, so it answers with defaults until that + // map is updated (or, more likely, until this bridge is deleted in favor of the real + // store). + S() + } + + func setValue(_ value: S) { + // A section absent from `accessors` has nowhere to go on `SettingsData`. Every section + // declared today is mapped below; a new one that forgets to register here would be + // discarded, which is why the map is the single source of truth for both directions. + Self.accessors[S.settingsKey]?.write(value) + } + + /// A read/write pair for one `SettingsData` field, type-erased over its section type. + private struct SectionAccessor { + let read: (SettingsData) -> any SettingsSection + let write: (any SettingsSection) -> Void + + init(_ keyPath: WritableKeyPath) { + read = { $0[keyPath: keyPath] } + write = { value in + guard let value = value as? S else { return } + Settings[keyPath] = value + } + } + } + + /// All eleven `SettingsData` fields, keyed by their section's `settingsKey`. + private static let accessors: [String: SectionAccessor] = [ + GeneralSettings.settingsKey: SectionAccessor(\.general), + AccountsSettings.settingsKey: SectionAccessor(\.accounts), + NavigationSettings.settingsKey: SectionAccessor(\.navigation), + ThemeSettings.settingsKey: SectionAccessor(\.theme), + TextEditingSettings.settingsKey: SectionAccessor(\.textEditing), + TerminalSettings.settingsKey: SectionAccessor(\.terminal), + SourceControlSettings.settingsKey: SectionAccessor(\.sourceControl), + KeybindingsSettings.settingsKey: SectionAccessor(\.keybindings), + SearchSettings.settingsKey: SectionAccessor(\.search), + LanguageServerSettings.settingsKey: SectionAccessor(\.languageServers), + DeveloperSettings.settingsKey: SectionAccessor(\.developerSettings) + ] +} diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index 6d88582859..49a96f489c 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -19,8 +19,8 @@ struct SettingsInjector: View { .environment(\.settings, settings.preferences) // `@ObservedObject` above means this recomputes on every `Settings.shared` change, // so wrapped trees using `@SettingsValue`/`SettingsReading` re-render on settings - // changes too — a bare one-shot `.environment(\.settingsReader, LegacySettingsReader())` + // changes too — a bare one-shot `.environment(\.settingsAccessor, LegacySettingsStore())` // would not. - .environment(\.settingsReader, LegacySettingsReader()) + .environment(\.settingsAccessor, LegacySettingsStore()) } } diff --git a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift index cb871d3d25..2fb0f04c73 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift @@ -33,7 +33,7 @@ enum WorkspaceFactory { workspaceURL: url, shellClient: dependencies.shellClient, eventBus: eventBus, - settingsReader: dependencies.settingsReader + settingsReader: dependencies.settingsAccessor ) let workspaceFileManager = CEWorkspaceFileManager( folderUrl: url, diff --git a/CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift b/CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift deleted file mode 100644 index d30d5e8489..0000000000 --- a/CodeEditModules/Sources/CELSP/Registry/Protocols/LanguageServerRegistryWriting.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// LanguageServerRegistryWriting.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 09/08/26. -// - -import CodeEditSettings - -/// Persists `RegistryManager`'s installed-language-server registry back to settings. -/// -/// `SettingsReading` (the settings-ownership refactor's read seam) has no write half, and -/// `RegistryManager` is the one place in the app that mutates this particular registry -/// (install/uninstall/enable-disable). Rather than reintroduce `Settings.shared` into `CELSP` or -/// grow `SettingsReading` into a general read/write interface for one caller, this protocol asks -/// for exactly the capability `RegistryManager` needs. It is a stopgap, like `LegacySettingsReader` -/// — both are expected to fold into the real, section-keyed settings store once that lands. -public protocol LanguageServerRegistryWriting { - /// Persists the full registry, replacing whatever was previously stored. - func persistInstalledLanguageServers(_ servers: [String: LanguageServerSettings.Installed]) -} - -/// A writer that discards every write. For tests and previews where persistence is irrelevant. -public final class NoOpLanguageServerRegistryWriting: LanguageServerRegistryWriting { - /// Creates a writer that discards every write. - public init() {} - - /// Discards the registry instead of persisting it. - public func persistInstalledLanguageServers(_ servers: [String: LanguageServerSettings.Installed]) {} -} diff --git a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift index fbed3986e9..e978a2487a 100644 --- a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift +++ b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift @@ -40,27 +40,35 @@ public final class RegistryManager: RegistryManaging { /// invalidated from `deinit`, which cannot be actor-isolated. nonisolated(unsafe) private var cleanupTimer: Timer? + /// Every mutation persists through the settings seam. Note that the initializer's seeding + /// assignment happens inside `init` and therefore does *not* fire `didSet` — construction + /// deliberately writes nothing back. public private(set) var installedLanguageServers: [String: LanguageServerSettings.Installed] { - didSet { registryWriter.persistInstalledLanguageServers(installedLanguageServers) } + didSet { + var settings = settingsAccessor.value(LanguageServerSettings.self) + settings.installedLanguageServers = installedLanguageServers + settingsAccessor.setValue(settings) + } } private let eventBus: EventBus private let errorNotifier: ErrorNotifying private let shellClient: ShellClientProtocol - private let registryWriter: LanguageServerRegistryWriting + private let settingsAccessor: SettingsAccessing public init( eventBus: EventBus, errorNotifier: ErrorNotifying, shellClient: ShellClientProtocol, - settingsReader: SettingsReading, - registryWriter: LanguageServerRegistryWriting + settingsAccessor: SettingsAccessing ) { self.eventBus = eventBus self.errorNotifier = errorNotifier self.shellClient = shellClient - self.registryWriter = registryWriter - self.installedLanguageServers = settingsReader.value(LanguageServerSettings.self).installedLanguageServers + self.settingsAccessor = settingsAccessor + self.installedLanguageServers = settingsAccessor + .value(LanguageServerSettings.self) + .installedLanguageServers } deinit { diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsAccessing.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsAccessing.swift new file mode 100644 index 0000000000..b923796c79 --- /dev/null +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsAccessing.swift @@ -0,0 +1,28 @@ +// +// SettingsAccessing.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 09/08/26. +// + +/// Read *and* write access to settings, one section at a time. +/// +/// The write half of the settings seam. It extends ``SettingsReading`` rather than standing beside +/// it as a separate `SettingsWriting`, because a read-write consumer — most importantly +/// ``SettingsValue``'s `projectedValue`, which must produce a `Binding` — needs both halves from a +/// *single* value. Two protocols would mean two environment keys, and a view could then be handed a +/// live reader next to a defaulted writer: reads would look correct while writes silently vanished. +/// One protocol makes that state unrepresentable. +/// +/// Consumers that only read should keep depending on ``SettingsReading``; every `SettingsAccessing` +/// satisfies it, so narrowing costs nothing. +/// +/// Deliberately neither `Sendable` nor `@MainActor`, for the same reasons documented on +/// ``SettingsReading``. +public protocol SettingsAccessing: SettingsReading { + /// Stores `value`, replacing the whole section it belongs to. + /// + /// Section-granular by design: a caller changing one field reads the section, mutates it and + /// writes it back, so it never has to name the app-wide settings aggregate. + func setValue(_ value: S) +} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index f06b81316b..e63becf64d 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -20,14 +20,22 @@ public protocol SettingsReading { func value(_ type: S.Type) -> S } -/// A reader that always answers with defaults. The environment's fallback, so a preview with no -/// store configured still renders. -public struct DefaultSettingsReader: SettingsReading { +/// A reader that always answers with defaults, and **discards every write**. The environment's +/// fallback, so a preview with no store configured still renders. +/// +/// The discarding write is the dangerous half: a view whose subtree never received a real store +/// (most easily by sitting behind an `NSHostingView`/`NSHostingController` boundary, which +/// `@Environment` does not cross) will read plausible defaults and *appear* to save, losing the +/// user's change with no error. Treat reaching this type outside a `#Preview` as a wiring bug. +public struct DefaultSettingsReader: SettingsAccessing { public init() {} public func value(_ type: S.Type) -> S { S() } + + /// Discards `value`. See the type's documentation — this is a no-op, not a save. + public func setValue(_ value: S) {} } /// A fixed reader for tests and SwiftUI previews. @@ -43,39 +51,65 @@ public struct SnapshotSettingsReader: SettingsReading { } } -public struct SettingsReaderKey: EnvironmentKey { +public struct SettingsAccessorKey: EnvironmentKey { /// Defaults are a legitimate value here — a preview with no store configured should render. - nonisolated(unsafe) public static let defaultValue: SettingsReading = DefaultSettingsReader() + nonisolated(unsafe) public static let defaultValue: SettingsAccessing = DefaultSettingsReader() } public extension EnvironmentValues { - /// The settings reader for the current view tree. - var settingsReader: SettingsReading { - get { self[SettingsReaderKey.self] } - set { self[SettingsReaderKey.self] = newValue } + /// The settings accessor for the current view tree. + /// + /// Typed as ``SettingsAccessing`` rather than ``SettingsReading`` so that ``SettingsValue`` can + /// vend a `Binding` from the same value it reads through. + var settingsAccessor: SettingsAccessing { + get { self[SettingsAccessorKey.self] } + set { self[SettingsAccessorKey.self] = newValue } } } -/// Reads one property of one settings section inside a SwiftUI view. +/// Reads and writes one property of one settings section inside a SwiftUI view. /// /// ```swift /// @SettingsValue(TerminalSettings.self, \.cursorBlink) private var cursorBlink +/// @SettingsValue(TextEditingSettings.self, \.showMinimap) private var showMinimap +/// Toggle("Show Minimap", isOn: $showMinimap) /// ``` /// /// Only valid inside a `View`. AppKit types must be handed the value by their /// `NSViewRepresentable` instead — read at the SwiftUI boundary, pass by value inward. +/// +/// The key path is a `WritableKeyPath` even for read-only uses: every settings field is a `var`, so +/// requiring it costs read-only call sites nothing and keeps one property wrapper for both jobs. @propertyWrapper public struct SettingsValue: DynamicProperty { - @Environment(\.settingsReader) - private var reader + @Environment(\.settingsAccessor) + private var accessor - private let keyPath: KeyPath + private let keyPath: WritableKeyPath - public init(_ section: S.Type, _ keyPath: KeyPath) { + public init(_ section: S.Type, _ keyPath: WritableKeyPath) { self.keyPath = keyPath } public var wrappedValue: Value { - reader.value(S.self)[keyPath: keyPath] + get { + accessor.value(S.self)[keyPath: keyPath] + } + // Read-modify-write of the whole section: the accessor is section-granular, and this is the + // only way to change one field without naming the settings aggregate. + nonmutating set { + var section = accessor.value(S.self) + section[keyPath: keyPath] = newValue + accessor.setValue(section) + } + } + + /// A binding to the setting, for controls like `Toggle` and `TextField`. + public var projectedValue: Binding { + Binding { + wrappedValue + } set: { + wrappedValue = $0 + } } } diff --git a/CodeEditTests/App/LegacySettingsReaderTests.swift b/CodeEditTests/App/LegacySettingsReaderTests.swift deleted file mode 100644 index 1b92ca37f3..0000000000 --- a/CodeEditTests/App/LegacySettingsReaderTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// LegacySettingsReaderTests.swift -// CodeEditTests -// -// Created by Matthijs Eikelenboom on 09/08/2026. -// - -import Foundation -import Testing -import CodeEditSettings -@testable import CodeEdit - -/// Verifies `LegacySettingsReader` actually reads through to `Settings.shared`, rather than -/// answering with section defaults like `DefaultSettingsReader` would. A test that only checked -/// defaults would pass against either reader and prove nothing about the bridge. -struct LegacySettingsReaderTests { - @Test - func readsMutatedValueFromTheSingleton() throws { - let originalValue = Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled - defer { - Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = originalValue - } - - // Non-default: `SourceControlGeneral.sourceControlIsEnabled` defaults to `true`, so - // `DefaultSettingsReader` (or an unmapped fallback) would never produce `false` here. - Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = false - - let reader: SettingsReading = LegacySettingsReader() - #expect(reader.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) - } -} diff --git a/CodeEditTests/App/LegacySettingsStoreTests.swift b/CodeEditTests/App/LegacySettingsStoreTests.swift new file mode 100644 index 0000000000..8980f0b147 --- /dev/null +++ b/CodeEditTests/App/LegacySettingsStoreTests.swift @@ -0,0 +1,83 @@ +// +// LegacySettingsStoreTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import Foundation +import Testing +import CodeEditSettings +@testable import CodeEdit + +/// Verifies `LegacySettingsStore` actually reads and writes through to `Settings.shared`, rather +/// than answering with section defaults like `DefaultSettingsReader` would. A test that only +/// checked defaults would pass against either implementation and prove nothing about the bridge. +/// +/// These tests mutate a process-wide singleton, so each one restores what it found. They are +/// `@MainActor` and `.serialized` because `Settings` is an `ObservableObject` with live SwiftUI +/// observers in the test host: mutating `preferences` off the main thread drives an AppKit update +/// from a cooperative-pool thread and trips the Main Thread Checker, and two of them running +/// concurrently would also race on the save/restore of the same field. +@MainActor +@Suite(.serialized) +struct LegacySettingsStoreTests { + @Test + func readsMutatedValueFromTheSingleton() throws { + let originalValue = Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled + defer { + Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = originalValue + } + + // Non-default: `SourceControlGeneral.sourceControlIsEnabled` defaults to `true`, so + // `DefaultSettingsReader` (or an unmapped fallback) would never produce `false` here. + Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = false + + let store: SettingsReading = LegacySettingsStore() + #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) + } + + @Test + func writtenSectionReachesTheSingletonAndReadsBack() throws { + let originalValue = Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled + defer { + Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = originalValue + } + // Start from the default so the assertion below cannot pass on pre-existing state. + Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = true + + let store: SettingsAccessing = LegacySettingsStore() + var section = store.value(SourceControlSettings.self) + section.general.sourceControlIsEnabled = false + store.setValue(section) + + // Round trip through the seam... + #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) + // ...and, crucially, into `Settings.shared.preferences` itself, which is what the + // `@Published` → throttle → `savePreferences` pipeline persists. A write that only + // mutated a copy would pass the line above and still lose the value on relaunch. + #expect(Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled == false) + } + + @Test + func writtenSectionIsVisibleToAnIndependentReader() throws { + let original = Settings.shared.preferences.languageServers.installedLanguageServers + defer { + Settings.shared.preferences.languageServers.installedLanguageServers = original + } + Settings.shared.preferences.languageServers.installedLanguageServers = [:] + + let writer: SettingsAccessing = LegacySettingsStore() + var section = writer.value(LanguageServerSettings.self) + section.installedLanguageServers = [ + "round-trip-test": .init(packageName: "round-trip-test", isEnabled: false, version: "9.9.9") + ] + writer.setValue(section) + + // A *different* instance, to prove the value lives in the store and not in the writer. + let reader: SettingsReading = LegacySettingsStore() + let readBack = reader.value(LanguageServerSettings.self).installedLanguageServers["round-trip-test"] + #expect(readBack?.version == "9.9.9") + #expect(readBack?.isEnabled == false) + } +} diff --git a/CodeEditTests/App/RecordingSettingsStore.swift b/CodeEditTests/App/RecordingSettingsStore.swift new file mode 100644 index 0000000000..fe9e264908 --- /dev/null +++ b/CodeEditTests/App/RecordingSettingsStore.swift @@ -0,0 +1,38 @@ +// +// RecordingSettingsStore.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import CodeEditSettings + +/// A `SettingsAccessing` spy: it stores what is written and reads it back, and remembers every +/// write in order. +/// +/// Deliberately *not* a no-op. A discarding double makes a disconnected write path look healthy — +/// which is exactly how the write half shipped untested the first time. +final class RecordingSettingsStore: SettingsAccessing { + private var sections: [String: any SettingsSection] + + /// Every section handed to ``setValue(_:)``, oldest first. + private(set) var writes: [any SettingsSection] = [] + + init(_ sections: [String: any SettingsSection] = [:]) { + self.sections = sections + } + + func value(_ type: S.Type) -> S { + sections[S.settingsKey] as? S ?? S() + } + + func setValue(_ value: S) { + sections[S.settingsKey] = value + writes.append(value) + } + + /// The most recently written section of the given type, or `nil` if none was ever written. + func lastWrite(_ type: S.Type) -> S? { + writes.reversed().compactMap { $0 as? S }.first + } +} diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index bd200ec615..75f35e0f75 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -20,8 +20,7 @@ struct RegistryTests { eventBus: EventBus(), errorNotifier: NoOpErrorNotifier(), shellClient: ShellClient(), - settingsReader: SnapshotSettingsReader([:]), - registryWriter: NoOpLanguageServerRegistryWriting() + settingsAccessor: RecordingSettingsStore() ) // MARK: - Download Tests diff --git a/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift b/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift new file mode 100644 index 0000000000..43f8c2a527 --- /dev/null +++ b/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift @@ -0,0 +1,121 @@ +// +// RegistryManagerPersistenceTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +@testable import CELSP +import Testing +import Foundation +import CodeEditCore +import CodeEditSettings +import ShellClient +@testable import CodeEdit + +/// Asserts that every `RegistryManager` mutation of `installedLanguageServers` is written back +/// through the settings seam, with the value the user would expect to find after a relaunch. +/// +/// These assertions are all about *non-default* state — an empty registry, or one that never +/// reaches the store, fails them. That is the point: a no-op writer must not pass. +@MainActor +@Suite +struct RegistryManagerPersistenceTests { + private static let packageName = "codeedit-registry-persistence-test-package" + + private func makeStore(installed: [String: LanguageServerSettings.Installed]) -> RecordingSettingsStore { + var section = LanguageServerSettings() + section.installedLanguageServers = installed + return RecordingSettingsStore([LanguageServerSettings.settingsKey: section]) + } + + private func makeManager(store: RecordingSettingsStore) -> RegistryManager { + RegistryManager( + eventBus: EventBus(), + errorNotifier: NoOpErrorNotifier(), + shellClient: ShellClient(), + settingsAccessor: store + ) + } + + private func installed(_ isEnabled: Bool) -> LanguageServerSettings.Installed { + .init(packageName: Self.packageName, isEnabled: isEnabled, version: "1.2.3") + } + + @Test + func seedingReadsThroughButDoesNotPersist() throws { + let store = makeStore(installed: [Self.packageName: installed(true)]) + let manager = makeManager(store: store) + + // The seed is read from the store, not invented. + #expect(manager.installedLanguageServers[Self.packageName]?.version == "1.2.3") + // Assigning inside `init` does not fire `didSet`, so construction must write nothing. + #expect(store.writes.isEmpty) + } + + @Test + func setPackageEnabledPersists() throws { + let store = makeStore(installed: [Self.packageName: installed(true)]) + let manager = makeManager(store: store) + + manager.setPackageEnabled(packageName: Self.packageName, enabled: false) + + let written = try #require(store.lastWrite(LanguageServerSettings.self)) + #expect(written.installedLanguageServers[Self.packageName]?.isEnabled == false) + // And the store now answers with it, i.e. a relaunch would see the change. + #expect(store.value(LanguageServerSettings.self).installedLanguageServers[Self.packageName]?.isEnabled == false) + } + + @Test + func removeLanguageServerPersists() async throws { + let store = makeStore(installed: [Self.packageName: installed(true)]) + let manager = makeManager(store: store) + + // No directory exists for this name, so the manager takes its "already gone" path and + // still has to persist the removal. + #expect(!FileManager.default.fileExists(atPath: manager.installPath.appending(path: Self.packageName).path)) + + try await manager.removeLanguageServer(packageName: Self.packageName) + + let written = try #require(store.lastWrite(LanguageServerSettings.self)) + #expect(written.installedLanguageServers[Self.packageName] == nil) + #expect(store.value(LanguageServerSettings.self).installedLanguageServers.isEmpty) + } + + @Test + func successfulInstallPersists() async throws { + let store = makeStore(installed: [:]) + let manager = makeManager(store: store) + + // A zero-step operation completes immediately without shelling out, which is enough to + // reach the completion branch that records the installed package. + let package = RegistryItem( + name: Self.packageName, + description: "", + homepage: "", + licenses: [], + languages: [], + categories: [], + source: .init(id: "pkg:npm/\(Self.packageName)@4.5.6", asset: nil, build: nil, versionOverrides: nil), + bin: nil + ) + let operation = PackageManagerInstallOperation(package: package, steps: [], shellClient: ShellClient()) + + try manager.startInstallation(operation: operation) + + // The install runs in a detached-from-us `Task`; wait for the write rather than sleeping a + // fixed amount. + var attempts = 0 + while store.lastWrite(LanguageServerSettings.self) == nil && attempts < 200 { + attempts += 1 + try await Task.sleep(for: .milliseconds(10)) + } + + let written = try #require( + store.lastWrite(LanguageServerSettings.self), + "Install completed without persisting the registry" + ) + #expect(written.installedLanguageServers[Self.packageName]?.version == "4.5.6") + #expect(written.installedLanguageServers[Self.packageName]?.isEnabled == true) + } +} From 419d3480249497992e4ce55860ee8eecffe3d494 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 9 Aug 2026 23:56:14 +0200 Subject: [PATCH 249/335] Fix: Guard the settings write seam and cover its view-side path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps from review of the write half. `SettingsAccessing` is deliberately nonisolated, so nothing stopped a write from landing on a background thread — where the `@Published` change on `Settings.shared` drives AppKit through SwiftUI observers and corrupts state instead of failing. `@MainActor` cannot express this: an isolated member cannot satisfy the nonisolated protocol requirement, so it would push isolation onto every caller in the Swift 5 app target. `MainActor.assertIsolated()` at the write entry gives the guarantee with no isolation in any signature, traps in debug and vanishes in release. Today's only caller is already `@MainActor`; the CEEditor cutover adds many more. The unmapped-section drop and the accessor's cast failure were documented but silent. Both now `assertionFailure` — a wiring mistake in either is a lost user write. `@SettingsValue`'s setter and `projectedValue` had no test. Cover both by hosting a probe view with a recording store injected, asserting the read came through the store, the write reached it, and a sibling field the probe never touches survives — the last of which is what distinguishes a read-modify-write from a section rebuilt at defaults. These live in the app test target because `@Environment` only resolves in a view SwiftUI is rendering, which needs an app-hosted runner. The probe deliberately never orders its window front: an on-screen window in the shared test process crashed the plan when this suite ran alongside others. --- .../Settings/LegacySettingsStore.swift | 28 +++- .../App/SettingsValueWriteTests.swift | 146 ++++++++++++++++++ 2 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 CodeEditTests/App/SettingsValueWriteTests.swift diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift index 5a30e0e39f..96c45185ac 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift @@ -32,10 +32,20 @@ struct LegacySettingsStore: SettingsAccessing { } func setValue(_ value: S) { - // A section absent from `accessors` has nowhere to go on `SettingsData`. Every section - // declared today is mapped below; a new one that forgets to register here would be - // discarded, which is why the map is the single source of truth for both directions. - Self.accessors[S.settingsKey]?.write(value) + // `SettingsAccessing` is deliberately nonisolated (see the protocol's docs), so the + // compiler cannot enforce this. A write lands in `Settings.shared.preferences`, whose + // `@Published` change drives AppKit through SwiftUI observers — off the main thread that + // corrupts AppKit state rather than failing cleanly. Loud in debug, unchanged in release. + MainActor.assertIsolated("Settings must be written on the main thread") + + guard let accessor = Self.accessors[S.settingsKey] else { + // A section absent from `accessors` has nowhere to go on `SettingsData`, so the write + // would vanish. Every section declared today is mapped below; this fires only if a new + // one forgets to register, which is a wiring bug, not a runtime condition. + assertionFailure("No SettingsData field registered for section '\(S.settingsKey)'") + return + } + accessor.write(value) } /// A read/write pair for one `SettingsData` field, type-erased over its section type. @@ -46,7 +56,15 @@ struct LegacySettingsStore: SettingsAccessing { init(_ keyPath: WritableKeyPath) { read = { $0[keyPath: keyPath] } write = { value in - guard let value = value as? S else { return } + guard let value = value as? S else { + // Only reachable if two sections share a `settingsKey`, or a key was mapped to + // the wrong `SettingsData` field. Either way the user's write is being dropped. + assertionFailure( + "Section '\(S.settingsKey)' is registered for \(S.self) but was handed " + + "\(type(of: value))" + ) + return + } Settings[keyPath] = value } } diff --git a/CodeEditTests/App/SettingsValueWriteTests.swift b/CodeEditTests/App/SettingsValueWriteTests.swift new file mode 100644 index 0000000000..13d488a99e --- /dev/null +++ b/CodeEditTests/App/SettingsValueWriteTests.swift @@ -0,0 +1,146 @@ +// +// SettingsValueWriteTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import AppKit +import SwiftUI +import Testing +import CodeEditSettings +@testable import CodeEdit + +/// Covers `@SettingsValue`'s write half — the `nonmutating set` and the `projectedValue` binding — +/// end to end: seed a section, render a real view, have it write one field, and assert the whole +/// section came back through the accessor with that field changed. +/// +/// **Why these live in the app test target rather than `CodeEditModules/Tests`.** `@SettingsValue` +/// is a `DynamicProperty` that resolves its accessor from `@Environment`, and `@Environment` only +/// resolves inside a view SwiftUI is actually rendering. Constructing the wrapper directly would +/// silently read `DefaultSettingsReader` and assert nothing. Rendering needs an app-hosted runner +/// with a real run loop, which the package test bundles are not (the same constraint that sent +/// `CENotifications`' tests back here). The write-capable double, `RecordingSettingsStore`, is +/// already here too. +@MainActor +@Suite(.serialized) +struct SettingsValueWriteTests { + /// Captures what the view saw when it rendered, so the read side is proven rather than assumed. + private final class ObservedValue { + var value: Bool? + } + + /// Writes through `wrappedValue`'s `nonmutating set`. + private struct WrappedValueProbe: View { + @SettingsValue(SourceControlSettings.self, \.general.sourceControlIsEnabled) + private var sourceControlIsEnabled + + let observed: ObservedValue + + var body: some View { + Color.clear.onAppear { + observed.value = sourceControlIsEnabled + sourceControlIsEnabled = true + } + } + } + + /// Writes through the `Binding` vended by `projectedValue`, the way a `Toggle` would. + private struct ProjectedValueProbe: View { + @SettingsValue(SourceControlSettings.self, \.general.sourceControlIsEnabled) + private var sourceControlIsEnabled + + let observed: ObservedValue + + var body: some View { + Color.clear.onAppear { + let binding: Binding = $sourceControlIsEnabled + observed.value = binding.wrappedValue + binding.wrappedValue = true + } + } + } + + /// A store seeded with two **non-default** fields (both default to `true`), so a view that + /// failed to reach this store would observe `true` and fail the read assertion. + /// + /// `refreshStatusLocally` is the untouched sibling: the probes never write it, so it is what + /// distinguishes a read-modify-write from a section rebuilt at its defaults. + private func makeSeededStore() -> RecordingSettingsStore { + var section = SourceControlSettings() + section.general.sourceControlIsEnabled = false + section.general.refreshStatusLocally = false + return RecordingSettingsStore([SourceControlSettings.settingsKey: section]) + } + + /// Hosts `view` long enough for SwiftUI to evaluate its body, then waits until it writes. + /// + /// The window is deliberately **never ordered front**: attaching the hosting view is enough to + /// drive the update cycle, and an on-screen window makes the shared app-hosted test process + /// talk to the window server, which destabilised the whole test plan when this suite ran + /// concurrently with others. + private func render(_ view: some View, until store: RecordingSettingsStore) async { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 64, height: 64), + styleMask: [.borderless], + backing: .buffered, + defer: true + ) + defer { window.contentView = nil } + + let hostingView = NSHostingView(rootView: view) + window.contentView = hostingView + hostingView.layoutSubtreeIfNeeded() + + var attempts = 0 + while store.writes.isEmpty && attempts < 200 { + attempts += 1 + try? await Task.sleep(for: .milliseconds(10)) + } + } + + @Test + func wrappedValueSetterWritesTheWholeSectionBack() async throws { + let store = makeSeededStore() + let observed = ObservedValue() + + await render( + WrappedValueProbe(observed: observed).environment(\.settingsAccessor, store), + until: store + ) + + // The view read through the injected store, not through defaults. + #expect(observed.value == false) + + let written = try #require( + store.lastWrite(SourceControlSettings.self), + "@SettingsValue's setter never reached the accessor" + ) + #expect(written.general.sourceControlIsEnabled == true) + // Read-modify-write, not replace-with-defaults: the sibling field the probe never touched + // still carries its seeded, non-default value. + #expect(written.general.refreshStatusLocally == false) + #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == true) + } + + @Test + func projectedValueBindingWritesTheWholeSectionBack() async throws { + let store = makeSeededStore() + let observed = ObservedValue() + + await render( + ProjectedValueProbe(observed: observed).environment(\.settingsAccessor, store), + until: store + ) + + #expect(observed.value == false) + + let written = try #require( + store.lastWrite(SourceControlSettings.self), + "@SettingsValue's projectedValue binding never reached the accessor" + ) + #expect(written.general.sourceControlIsEnabled == true) + #expect(written.general.refreshStatusLocally == false) + #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == true) + } +} From 7bcd37a1c46f509bd7e3f56148ffd8d0a394b963 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 00:09:08 +0200 Subject: [PATCH 250/335] Refactor: Cut CEEditor over to the settings-seam @SettingsValue Replaces all @AppSettings sites and the Settings.shared singleton reads in CEEditor with @SettingsValue, so the package stops naming the app-side SettingsData aggregate. Includes an AppKit read (EditorJumpBarMenu's JumpBarMenuItem) not covered by @SettingsValue directly: fileIconStyle is now read at the EditorJumpBarComponent SwiftUI boundary and threaded in by value. Also fixes a pre-existing staleness bug: CodeFileView's editor font was seeded into @State once at first render instead of tracking the live setting. --- .../Views/EditorJumpBarComponent.swift | 5 ++ .../JumpBar/Views/EditorJumpBarMenu.swift | 16 ++++-- .../TabBar/Tabs/Tab/EditorTabView.swift | 2 +- .../EditorTabBarTrailingAccessories.swift | 4 +- .../Sources/CEEditor/Views/CodeFileView.swift | 49 +++++++++---------- .../CEEditor/Views/EditorAreaView.swift | 6 +-- 6 files changed, 46 insertions(+), 36 deletions(-) diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift index c35a793660..d550e24df5 100644 --- a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift +++ b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift @@ -7,6 +7,7 @@ import SwiftUI import CodeEditCore +import CodeEditSettings import Combine import CodeEditSymbols @@ -15,6 +16,9 @@ struct EditorJumpBarComponent: View { private let tappedOpenFile: (CEWorkspaceFile) -> Void private let isLastItem: Bool + @SettingsValue(GeneralSettings.self, \.fileIconStyle) + var fileIconStyle + @Environment(\.colorScheme) var colorScheme @@ -62,6 +66,7 @@ struct EditorJumpBarComponent: View { button.menu = EditorJumpBarMenu( fileItems: siblings, fileManager: fileManager, + fileIconStyle: fileIconStyle, tappedOpenFile: tappedOpenFile ) button.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift index 996cecb652..f4570a4c6d 100644 --- a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift +++ b/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift @@ -13,19 +13,28 @@ final class EditorJumpBarMenu: NSMenu, NSMenuDelegate { private let fileItems: [CEWorkspaceFile] private weak var fileManager: (any WorkspaceFileProviding)? private let tappedOpenFile: (CEWorkspaceFile) -> Void + private let fileIconStyle: GeneralSettings.FileIconStyle + /// - Parameter fileIconStyle: Read at the SwiftUI boundary (`EditorJumpBarComponent`) and handed + /// in by value — this type is AppKit, so it cannot use `@SettingsValue` itself. init( fileItems: [CEWorkspaceFile], fileManager: any WorkspaceFileProviding, + fileIconStyle: GeneralSettings.FileIconStyle, tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.fileItems = fileItems self.fileManager = fileManager + self.fileIconStyle = fileIconStyle self.tappedOpenFile = tappedOpenFile super.init(title: "") delegate = self fileItems.forEach { item in - let menuItem = JumpBarMenuItem(fileItem: item, tappedOpenFile: tappedOpenFile) + let menuItem = JumpBarMenuItem( + fileItem: item, + fileIconStyle: fileIconStyle, + tappedOpenFile: tappedOpenFile + ) menuItem.onStateImage = nil self.addItem(menuItem) } @@ -52,6 +61,7 @@ final class EditorJumpBarMenu: NSMenu, NSMenuDelegate { let menu = EditorJumpBarMenu( fileItems: children, fileManager: fileManager, + fileIconStyle: fileIconStyle, tappedOpenFile: tappedOpenFile ) return menu @@ -63,10 +73,10 @@ final class EditorJumpBarMenu: NSMenu, NSMenuDelegate { final class JumpBarMenuItem: NSMenuItem { private let fileItem: CEWorkspaceFile private let tappedOpenFile: (CEWorkspaceFile) -> Void - private let generalSettings = Settings.shared.preferences.general init( fileItem: CEWorkspaceFile, + fileIconStyle: GeneralSettings.FileIconStyle, tappedOpenFile: @escaping (CEWorkspaceFile) -> Void ) { self.fileItem = fileItem @@ -80,7 +90,7 @@ final class JumpBarMenuItem: NSMenuItem { submenu = subMenu color = NSColor(named: "FolderBlue") ?? .systemBlue } - if generalSettings.fileIconStyle == .monochrome { + if fileIconStyle == .monochrome { color = NSColor(named: "CoolGray") ?? .systemGray } let image = fileItem.nsIcon.withSymbolConfiguration(.init(paletteColors: [color])) diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift index 5947a58dd5..45a61f4351 100644 --- a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift +++ b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift @@ -30,7 +30,7 @@ struct EditorTabView: View { @StateObject private var fileObserver: EditorTabFileObserver - @AppSettings(\.general.fileIconStyle) + @SettingsValue(GeneralSettings.self, \.fileIconStyle) var fileIconStyle /// Is cursor hovering over the entire tab. diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 996f220f76..7eb83cdfdf 100644 --- a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -11,9 +11,9 @@ import CodeEditDocument import CodeEditUI struct EditorTabBarTrailingAccessories: View { - @AppSettings(\.textEditing.wrapLinesToEditorWidth) + @SettingsValue(TextEditingSettings.self, \.wrapLinesToEditorWidth) var wrapLinesToEditorWidth - @AppSettings(\.textEditing.showMinimap) + @SettingsValue(TextEditingSettings.self, \.showMinimap) var showMinimap @Environment(\.splitEditor) diff --git a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift index 7c19ec3713..8efc3470f8 100644 --- a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift @@ -27,41 +27,41 @@ struct CodeFileView: View { private var textViewCoordinators: [TextViewCoordinator] private var highlightProviders: [any HighlightProviding] = [] - @AppSettings(\.textEditing.defaultTabWidth) + @SettingsValue(TextEditingSettings.self, \.defaultTabWidth) var defaultTabWidth - @AppSettings(\.textEditing.indentOption) + @SettingsValue(TextEditingSettings.self, \.indentOption) var indentOption - @AppSettings(\.textEditing.lineHeightMultiple) + @SettingsValue(TextEditingSettings.self, \.lineHeightMultiple) var lineHeightMultiple - @AppSettings(\.textEditing.wrapLinesToEditorWidth) + @SettingsValue(TextEditingSettings.self, \.wrapLinesToEditorWidth) var wrapLinesToEditorWidth - @AppSettings(\.textEditing.overscroll) + @SettingsValue(TextEditingSettings.self, \.overscroll) var overscroll - @AppSettings(\.textEditing.font) + @SettingsValue(TextEditingSettings.self, \.font) var settingsFont - @AppSettings(\.theme.useThemeBackground) + @SettingsValue(ThemeSettings.self, \.useThemeBackground) var useThemeBackground - @AppSettings(\.theme.matchAppearance) + @SettingsValue(ThemeSettings.self, \.matchAppearance) var matchAppearance - @AppSettings(\.textEditing.letterSpacing) + @SettingsValue(TextEditingSettings.self, \.letterSpacing) var letterSpacing - @AppSettings(\.textEditing.bracketEmphasis) + @SettingsValue(TextEditingSettings.self, \.bracketEmphasis) var bracketEmphasis - @AppSettings(\.textEditing.useSystemCursor) + @SettingsValue(TextEditingSettings.self, \.useSystemCursor) var useSystemCursor - @AppSettings(\.textEditing.showGutter) + @SettingsValue(TextEditingSettings.self, \.showGutter) var showGutter - @AppSettings(\.textEditing.showMinimap) + @SettingsValue(TextEditingSettings.self, \.showMinimap) var showMinimap - @AppSettings(\.textEditing.showFoldingRibbon) + @SettingsValue(TextEditingSettings.self, \.showFoldingRibbon) var showFoldingRibbon - @AppSettings(\.textEditing.reformatAtColumn) + @SettingsValue(TextEditingSettings.self, \.reformatAtColumn) var reformatAtColumn - @AppSettings(\.textEditing.showReformattingGuide) + @SettingsValue(TextEditingSettings.self, \.showReformattingGuide) var showReformattingGuide - @AppSettings(\.textEditing.invisibleCharacters) + @SettingsValue(TextEditingSettings.self, \.invisibleCharacters) var invisibleCharactersConfiguration - @AppSettings(\.textEditing.warningCharacters) + @SettingsValue(TextEditingSettings.self, \.warningCharacters) var warningCharacters @Environment(\.colorScheme) @@ -116,8 +116,6 @@ struct CodeFileView: View { injectedTheme! } - @State private var font: NSFont = Settings[\.textEditing].font.current - @Environment(\.edgeInsets) private var edgeInsets @@ -129,7 +127,7 @@ struct CodeFileView: View { appearance: .init( theme: currentTheme.editor.editorTheme, useThemeBackground: useThemeBackground, - font: font, + font: settingsFont.current, lineHeightMultiple: lineHeightMultiple, letterSpacing: letterSpacing, wrapLines: codeFile.wrapLines ?? wrapLinesToEditorWidth, @@ -190,21 +188,18 @@ struct CodeFileView: View { .colorScheme(currentTheme.appearance == .dark ? .dark : .light) // minHeight zero fixes a bug where the app would freeze if the contents of the file are empty. .frame(minHeight: .zero, maxHeight: .infinity) - .onChange(of: settingsFont) { _, newFontSetting in - font = newFontSetting.current - } } /// Determines the style of bracket emphasis based on the `bracketEmphasis` setting and the current theme. /// - Returns: The emphasis style to use for bracket pair emphasis. private func getBracketPairEmphasis() -> BracketPairEmphasis? { - let color = if Settings[\.textEditing].bracketEmphasis.useCustomColor { - Settings[\.textEditing].bracketEmphasis.color.nsColor + let color = if bracketEmphasis.useCustomColor { + bracketEmphasis.color.nsColor } else { currentTheme.editor.text.nsColor.withAlphaComponent(0.8) } - switch Settings[\.textEditing].bracketEmphasis.highlightType { + switch bracketEmphasis.highlightType { case .disabled: return nil case .flash: diff --git a/CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift b/CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift index ef4e1c9b81..ce1d38339e 100644 --- a/CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift @@ -14,13 +14,13 @@ import CodeEditTextView import UniformTypeIdentifiers struct EditorAreaView: View { - @AppSettings(\.general.showEditorJumpBar) + @SettingsValue(GeneralSettings.self, \.showEditorJumpBar) var showEditorJumpBar - @AppSettings(\.navigation.navigationStyle) + @SettingsValue(NavigationSettings.self, \.navigationStyle) var navigationStyle - @AppSettings(\.general.dimEditorsWithoutFocus) + @SettingsValue(GeneralSettings.self, \.dimEditorsWithoutFocus) var dimEditorsWithoutFocus @ObservedObject var editor: Editor From 204a2757130028129d62b4aa845f2ee535f63210 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 00:39:42 +0200 Subject: [PATCH 251/335] Fix: Give the settings seam an explicit invalidation signal `@SettingsValue`'s only environment dependency was `\.settingsAccessor`, an existential over a stateless struct. Whether SwiftUI re-renders a reader when that key is rewritten is unspecified, and in the workspace window it is not even rewritten with a new value: `.appServices(_:)` injects a process-lifetime instance from `AppDependencies`, applied closer to the leaf than the observing `SettingsInjector`. `Settings` now publishes a `revision` counter, bumped once per change to `preferences`, carried in its own `Equatable` `\.settingsRevision` environment key that `SettingsValue` reads. The two injection points that observe `Settings` supply it (`SettingsInjector`, `CodeEditApp`); `appServices(_:)` observes nothing, so it keeps supplying the accessor alone and can no longer overwrite a live signal with a frozen one. The new test guards the end-to-end outcome only. Measured while writing it, a probe re-rendered on a settings change even with the revision removed, the accessor injected once above the observer, and an `EquatableView` barrier in between - so the mechanism itself is not separable in a test. That is the point: the behaviour was already working by means nobody specified, and now it works by means that are written down. --- CodeEdit/App/CodeEditApp.swift | 4 + CodeEdit/App/Environment+AppCommands.swift | 12 ++ .../Settings/SettingsInjector.swift | 11 +- .../CodeEditSettings/Store/Settings.swift | 21 ++++ .../Store/SettingsValue.swift | 29 ++++- .../App/SettingsSeamInvalidationTests.swift | 113 ++++++++++++++++++ 6 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 CodeEditTests/App/SettingsSeamInvalidationTests.swift diff --git a/CodeEdit/App/CodeEditApp.swift b/CodeEdit/App/CodeEditApp.swift index 228889627d..8a3c8a5edf 100644 --- a/CodeEdit/App/CodeEditApp.swift +++ b/CodeEdit/App/CodeEditApp.swift @@ -87,6 +87,10 @@ struct CodeEditApp: App { } } .environment(\.settings, settings.preferences) // Add settings to each window environment + // The settings seam's invalidation signal, for the scene roots that never pass through + // `SettingsInjector`. `appServices(_:)` cannot supply it — it holds no observation — but + // this body does, via the `@ObservedObject` above. + .environment(\.settingsRevision, settings.revision) .appServices(appdelegate.dependencies) } } diff --git a/CodeEdit/App/Environment+AppCommands.swift b/CodeEdit/App/Environment+AppCommands.swift index f67d846941..0c81de709b 100644 --- a/CodeEdit/App/Environment+AppCommands.swift +++ b/CodeEdit/App/Environment+AppCommands.swift @@ -107,6 +107,12 @@ extension View { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) + // Accessor only — deliberately *not* `\.settingsRevision`. `dependencies` observes + // nothing, so any revision from here would be frozen, and this modifier is applied + // closer to the leaf than `SettingsInjector` is: it would overwrite a live signal with + // a dead one. The revision comes from the observing injectors instead + // (`SettingsInjector`, `CodeEditApp`), and the accessor injected here is equivalent to + // theirs, so overwriting it changes nothing. .environment(\.settingsAccessor, dependencies.settingsAccessor) } } @@ -125,6 +131,12 @@ extension Scene { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) + // Accessor only — deliberately *not* `\.settingsRevision`. `dependencies` observes + // nothing, so any revision from here would be frozen, and this modifier is applied + // closer to the leaf than `SettingsInjector` is: it would overwrite a live signal with + // a dead one. The revision comes from the observing injectors instead + // (`SettingsInjector`, `CodeEditApp`), and the accessor injected here is equivalent to + // theirs, so overwriting it changes nothing. .environment(\.settingsAccessor, dependencies.settingsAccessor) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index 49a96f489c..0718dda7e9 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -17,10 +17,13 @@ struct SettingsInjector: View { var body: some View { content .environment(\.settings, settings.preferences) - // `@ObservedObject` above means this recomputes on every `Settings.shared` change, - // so wrapped trees using `@SettingsValue`/`SettingsReading` re-render on settings - // changes too — a bare one-shot `.environment(\.settingsAccessor, LegacySettingsStore())` - // would not. .environment(\.settingsAccessor, LegacySettingsStore()) + // The seam's invalidation signal. Rewriting the accessor above is *not* enough: + // `LegacySettingsStore` is a stateless struct behind an existential, so whether SwiftUI + // treats the rewrite as a change is unspecified — and `.appServices(_:)`, applied + // closer to the leaf in `CodeEditSplitViewController`, overwrites it with a + // process-lifetime instance anyway. `settingsRevision` is `Equatable` and lives in its + // own key, so neither of those can defeat it. + .environment(\.settingsRevision, settings.revision) } } diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift index 3c66ca9106..50fad34e1a 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift @@ -22,6 +22,7 @@ public final class Settings: ObservableObject { nonisolated(unsafe) public static let shared: Settings = .init() private var storeTask: AnyCancellable! + private var revisionTask: AnyCancellable! private init() { self.preferences = .init() @@ -30,6 +31,11 @@ public final class Settings: ObservableObject { self.storeTask = self.$preferences.throttle(for: 2, scheduler: RunLoop.main, latest: true).sink { try? self.savePreferences($0) } + // Bumped on the `willSet` emission, i.e. before SwiftUI runs the update pass that the same + // change schedules, so a body evaluated for this change already sees the new revision. + self.revisionTask = self.$preferences.dropFirst().sink { [weak self] _ in + self?.revision &+= 1 + } } public static subscript(_ path: WritableKeyPath, suite: Settings = .shared) -> T { @@ -46,6 +52,21 @@ public final class Settings: ObservableObject { /// Changes are saved automatically. @Published public var preferences: SettingsData + /// A counter incremented once per change to ``preferences``. + /// + /// This is the settings seam's **invalidation signal**. Views reach settings through + /// ``SettingsValue``, whose only environment dependency would otherwise be + /// ``EnvironmentValues/settingsAccessor`` — an existential holding a stateless store that never + /// compares unequal to itself. Re-rendering on a settings change would then rest on SwiftUI + /// treating a rewritten non-`Equatable` existential as a change, which is unspecified. + /// An `Int` is `Equatable`, so an injector publishing it into + /// ``EnvironmentValues/settingsRevision`` makes the invalidation explicit and precise: it + /// changes exactly when settings change, and never otherwise. + /// + /// Deliberately not derived from `SettingsData` itself — the seam types must not name the + /// app-wide aggregate. + @Published public private(set) var revision: Int = 0 + /// Load and construct ``Settings`` model from /// `~/Library/Application Support/CodeEdit/settings.json` private func loadSettings() -> SettingsData { diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index e63becf64d..32e8f67514 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -56,6 +56,11 @@ public struct SettingsAccessorKey: EnvironmentKey { nonisolated(unsafe) public static let defaultValue: SettingsAccessing = DefaultSettingsReader() } +public struct SettingsRevisionKey: EnvironmentKey { + /// `0` forever: a subtree with no injector has no settings to change under it. + public static let defaultValue: Int = 0 +} + public extension EnvironmentValues { /// The settings accessor for the current view tree. /// @@ -65,6 +70,21 @@ public extension EnvironmentValues { get { self[SettingsAccessorKey.self] } set { self[SettingsAccessorKey.self] = newValue } } + + /// Changes once per settings change; see ``Settings/revision``. + /// + /// The seam's invalidation signal, kept in its own `Equatable` key rather than folded into + /// ``settingsAccessor``. Two keys, two jobs: the accessor answers *what the value is* and is + /// legitimately a stable, stateless instance, while the revision answers *whether anything + /// changed*. That separation is what lets a non-observing injection point (`appServices(_:)`) + /// supply the accessor without also having to fake a change signal it cannot compute. + /// + /// Injected by any view that observes ``Settings``. A subtree that receives an accessor but no + /// revision reads correct values and never re-renders on change — inject both, or neither. + var settingsRevision: Int { + get { self[SettingsRevisionKey.self] } + set { self[SettingsRevisionKey.self] = newValue } + } } /// Reads and writes one property of one settings section inside a SwiftUI view. @@ -85,6 +105,10 @@ public struct SettingsValue: DynamicProperty { @Environment(\.settingsAccessor) private var accessor + /// Not a source of data — a source of *invalidation*. See ``EnvironmentValues/settingsRevision``. + @Environment(\.settingsRevision) + private var revision + private let keyPath: WritableKeyPath public init(_ section: S.Type, _ keyPath: WritableKeyPath) { @@ -93,7 +117,10 @@ public struct SettingsValue: DynamicProperty { public var wrappedValue: Value { get { - accessor.value(S.self)[keyPath: keyPath] + // Read, not merely declared: an unread `@Environment` is a dependency SwiftUI does not + // document itself as tracking, and being tracked is this property's entire purpose. + _ = revision + return accessor.value(S.self)[keyPath: keyPath] } // Read-modify-write of the whole section: the accessor is section-granular, and this is the // only way to change one field without naming the settings aggregate. diff --git a/CodeEditTests/App/SettingsSeamInvalidationTests.swift b/CodeEditTests/App/SettingsSeamInvalidationTests.swift new file mode 100644 index 0000000000..6cd4c0adba --- /dev/null +++ b/CodeEditTests/App/SettingsSeamInvalidationTests.swift @@ -0,0 +1,113 @@ +// +// SettingsSeamInvalidationTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 10/08/2026. +// + +import AppKit +import SwiftUI +import Testing +import CodeEditSettings +@testable import CodeEdit + +/// Covers the settings seam's *invalidation* half: that a change to `Settings.shared` actually +/// re-renders a view reading through `@SettingsValue`. +/// +/// This is the production path end to end — `SettingsInjector` → `\.settingsRevision` + +/// `LegacySettingsStore` → `@SettingsValue` — not a stand-in. It exists because the read/write +/// tests next door pass just as happily when nothing ever re-renders: they render once. +/// +/// **What it does not prove.** It is a guard on the *outcome*, not on the mechanism: it passes with +/// `\.settingsRevision` injected and without it. Measured while writing it — with the revision +/// removed, with the accessor injected once above the observing view so it is never rewritten, and +/// with the probe behind an `EquatableView` returning `true` — the probe still re-rendered every +/// time. SwiftUI appears to re-evaluate a view holding `@Environment`-backed `DynamicProperty` +/// state whenever an ancestor's body reruns. That is the unspecified behaviour the revision key +/// replaces with a documented one, so this test cannot distinguish the two and should not be read +/// as evidence that it can. +/// +/// `TerminalSettings.cursorBlink` is chosen because no other suite touches it. The suites that +/// mutate `Settings.shared` are not serialized against each other, so sharing a field with +/// `LegacySettingsStoreTests` or `FileExtensionVisibilityTests` would be a real race. +@MainActor +@Suite(.serialized) +struct SettingsSeamInvalidationTests { + /// Records the value observed on every body evaluation, so a *missing* re-render is a visible + /// absence rather than a stale-but-plausible reading. + private final class BodyRecorder { + var observed: [Bool] = [] + } + + private struct RevisionProbe: View { + @SettingsValue(TerminalSettings.self, \.cursorBlink) + private var cursorBlink + + let recorder: BodyRecorder + + var body: some View { + recorder.observed.append(cursorBlink) + return Color.clear + } + } + + /// Hosts `view` without ever ordering the window front — an on-screen window in the shared + /// app-hosted test process destabilised the whole plan. Attaching the hosting view and laying + /// it out is enough to drive SwiftUI's update cycle. + private func host(_ view: V) -> (NSWindow, NSHostingView) { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 64, height: 64), + styleMask: [.borderless], + backing: .buffered, + defer: true + ) + let hostingView = NSHostingView(rootView: view) + window.contentView = hostingView + hostingView.layoutSubtreeIfNeeded() + return (window, hostingView) + } + + private func settle(_ hostingView: NSHostingView, until: () -> Bool) async { + var attempts = 0 + while !until() && attempts < 200 { + attempts += 1 + hostingView.layoutSubtreeIfNeeded() + try? await Task.sleep(for: .milliseconds(10)) + } + } + + @Test + func changingSettingsBumpsTheRevision() { + let original = Settings.shared.preferences.terminal.cursorBlink + defer { Settings.shared.preferences.terminal.cursorBlink = original } + + let before = Settings.shared.revision + Settings.shared.preferences.terminal.cursorBlink = !original + #expect(Settings.shared.revision == before + 1) + } + + @Test + func settingsChangeReRendersAViewReadingThroughTheSeam() async throws { + let original = Settings.shared.preferences.terminal.cursorBlink + defer { Settings.shared.preferences.terminal.cursorBlink = original } + + Settings.shared.preferences.terminal.cursorBlink = false + let recorder = BodyRecorder() + let (window, hostingView) = host(SettingsInjector { RevisionProbe(recorder: recorder) }) + defer { window.contentView = nil } + + await settle(hostingView) { !recorder.observed.isEmpty } + // The probe reached the real store rather than `DefaultSettingsReader`, and saw the seeded + // value. (Defaults would also read `false` here, which is why the assertion that matters is + // the one below: defaults can never *change*.) + #expect(recorder.observed.last == false) + + Settings.shared.preferences.terminal.cursorBlink = true + + await settle(hostingView) { recorder.observed.last == true } + #expect( + recorder.observed.last == true, + "A settings change did not re-render a view reading through @SettingsValue" + ) + } +} From f4cc1e8d0d3a7c61db470cb7aec0ec7002a89d1a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 00:39:55 +0200 Subject: [PATCH 252/335] Fix: Make DefaultSettingsReader's silent fallback loud It answered every read with defaults and discarded every write with no error. `@Environment` does not cross an `NSHostingView`/`NSHostingController` boundary, and the app has eight standalone hosting roots with no injector, so the next contributor to put a `@SettingsValue` view behind one would have lost the user's setting silently - the same class of bug that already shipped once on this branch undetected by lint, the import audit and the test suite. Both methods now `assertionFailure` unless `XCODE_RUNNING_FOR_PREVIEWS` is set, where rendering without a store is legitimate. The read still returns its defaults rather than trapping in release, so a mis-wired subtree degrades instead of dying. Neither assertion fires anywhere in the test plan. --- .../Store/SettingsValue.swift | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index 32e8f67514..d7462795da 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 09/08/26. // +import Foundation import SwiftUI /// Read access to settings, one section at a time. @@ -26,16 +27,43 @@ public protocol SettingsReading { /// The discarding write is the dangerous half: a view whose subtree never received a real store /// (most easily by sitting behind an `NSHostingView`/`NSHostingController` boundary, which /// `@Environment` does not cross) will read plausible defaults and *appear* to save, losing the -/// user's change with no error. Treat reaching this type outside a `#Preview` as a wiring bug. +/// user's change with no error. Reaching this type outside a `#Preview` is therefore treated as a +/// wiring bug: both methods `assertionFailure` unless `XCODE_RUNNING_FOR_PREVIEWS` is set, so the +/// bug is loud in debug and unchanged in release. public struct DefaultSettingsReader: SettingsAccessing { + /// Previews legitimately render with no store configured; everywhere else, reaching this type + /// is a wiring bug worth a debug trap. + private static var isRunningInPreviews: Bool { + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + } + public init() {} + /// Answers with defaults. Reads are only *misleading*, not destructive, so they trap in debug + /// but the value is still returned — a preview or a mis-wired subtree keeps rendering. public func value(_ type: S.Type) -> S { - S() + if !Self.isRunningInPreviews { + assertionFailure( + "Read of '\(S.settingsKey)' fell back to defaults: this view subtree never received " + + "a settings accessor. A standalone NSHostingView/NSHostingController root needs " + + "`.appServices(_:)` or `SettingsInjector` — `@Environment` does not cross a " + + "hosting boundary." + ) + } + return S() } /// Discards `value`. See the type's documentation — this is a no-op, not a save. - public func setValue(_ value: S) {} + public func setValue(_ value: S) { + if !Self.isRunningInPreviews { + assertionFailure( + "Write to '\(S.settingsKey)' was discarded: this view subtree never received a " + + "settings accessor. A standalone NSHostingView/NSHostingController root needs " + + "`.appServices(_:)` or `SettingsInjector` — `@Environment` does not cross a " + + "hosting boundary." + ) + } + } } /// A fixed reader for tests and SwiftUI previews. From d7acd89ad6c37a70cbefd0d3129f58278917a983 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 00:40:04 +0200 Subject: [PATCH 253/335] Docs: Document the settings seam and its hosting-boundary hazard Neither ARCHITECTURE.md nor CLAUDE.md mentioned the seam, while the app target still reads the singleton through `@AppSettings` in ~30 files - a newcomer had nothing to choose between `@SettingsValue`, init-injected `SettingsReading` and `SettingsAccessing`. Both files now carry the three roles, the app-target-only status of `@AppSettings`, the `NSHostingView` hazard that a new hosting root must answer with `.appServices(_:)` or `SettingsInjector`, the explicit `\.settingsRevision` invalidation contract, and `LegacySettingsStore`'s stopgap status. Only ARCHITECTURE.md is tracked; CLAUDE.md is local. --- docs/ARCHITECTURE.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 82dec79c76..7bfca640e1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -201,6 +201,38 @@ Grouping is **purpose-first**: (the presentation-state split), and views issue commands through protocol-typed environment keys. +## Reading and writing settings + +Feature packages reach settings through the **settings seam** in `CodeEditSettings` +(`Store/SettingsValue.swift`, `Store/SettingsAccessing.swift`) — never through a singleton and +never by naming the app-wide `SettingsData` aggregate. Three roles, pick by consumer kind: + +| Consumer | Use | Why | +| --- | --- | --- | +| SwiftUI view | `@SettingsValue(TerminalSettings.self, \.cursorBlink)` | Resolves from the environment; `$`-projects a `Binding` for `Toggle`/`TextField`. | +| Read-only object (managers, services) | `SettingsReading` by initializer | No environment outside a view; narrow protocol makes read-only visible at the call site. | +| Object that also writes | `SettingsAccessing` by initializer | The read+write half; every `SettingsAccessing` satisfies `SettingsReading`. | + +Access is **section-granular**: `value(_:)`/`setValue(_:)` deal in whole `SettingsSection` values, +so a caller changing one field reads its section, mutates it and writes it back. + +- **`@AppSettings` is app-target only.** It reads the `Settings.shared` singleton directly and is + what ~30 app-target files still use. Feature packages must not use it; new app-target code + should prefer the seam. +- **`@Environment` does not cross an `NSHostingView`/`NSHostingController` boundary.** A new + standalone hosting root must be given `.appServices(_:)` or wrapped in `SettingsInjector`, or its + subtree falls back to `DefaultSettingsReader` — plausible defaults, and **writes discarded**. + That fallback `assertionFailure`s outside SwiftUI previews precisely because it is otherwise + silent. +- **Invalidation is explicit.** `SettingsValue` also depends on the `Equatable` + `\.settingsRevision` environment key, fed from `Settings.revision`. Rewriting the accessor is not + a re-render signal: it is a stateless value behind an existential. Any injection point that + *observes* `Settings` supplies the revision (`SettingsInjector`, `CodeEditApp`); `appServices(_:)` + observes nothing, so it supplies the accessor only. +- **`LegacySettingsStore` is a stopgap.** It is the concrete accessor today, bridging to + `Settings.shared` so writes reach the existing throttled save pipeline. A section-keyed store + replaces it in a later slice. + ## Creating a new feature target 1. Create the folder `CodeEditModules/Sources/CE/` and add a target and product for it in From 6a04dade226432b44ce9355bc969fcbdd6052415 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 00:46:21 +0200 Subject: [PATCH 254/335] Test: Clarify what the settings-seam invalidation test guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames settingsChangeReRendersAViewReadingThroughTheSeam to settingsChangeReachesAViewThroughTheSeam and documents what it does and does not prove. Review recommended deleting it because it passes with and without the revision key. Kept instead: it guards the outcome — that a settings change reaches a view at all — which is the failure mode that has recurred most on this branch. It just must not be mistaken for a guard on the revision mechanism, which is what the rename and comment fix. --- CodeEditTests/App/SettingsSeamInvalidationTests.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CodeEditTests/App/SettingsSeamInvalidationTests.swift b/CodeEditTests/App/SettingsSeamInvalidationTests.swift index 6cd4c0adba..478a96e9f9 100644 --- a/CodeEditTests/App/SettingsSeamInvalidationTests.swift +++ b/CodeEditTests/App/SettingsSeamInvalidationTests.swift @@ -86,8 +86,16 @@ struct SettingsSeamInvalidationTests { #expect(Settings.shared.revision == before + 1) } + /// Guards the **outcome** users care about: a settings change reaches a view reading through the + /// seam. + /// + /// It deliberately does NOT isolate the `\.settingsRevision` mechanism — it passes with and + /// without that key, because SwiftUI also re-evaluates the probe when `SettingsInjector`'s body + /// reruns. Do not read a pass here as evidence the revision signal works; that is + /// ``changingSettingsBumpsTheRevision``'s job. What this catches is the seam silently failing to + /// deliver updated values at all — the failure mode that has recurred most on this branch. @Test - func settingsChangeReRendersAViewReadingThroughTheSeam() async throws { + func settingsChangeReachesAViewThroughTheSeam() async throws { let original = Settings.shared.preferences.terminal.cursorBlink defer { Settings.shared.preferences.terminal.cursorBlink = original } From b7623b76b2a686e022387cd27a9d6338454e9553 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 15:51:40 +0200 Subject: [PATCH 255/335] Feat: Add a section-keyed settings store that preserves unknown sections --- .../CodeEditSettings/Store/JSONValue.swift | 58 +++++++++++++++++ .../Store/SettingsStore.swift | 65 +++++++++++++++++++ .../SettingsFormatTests.swift | 38 +++++++---- 3 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/JSONValue.swift create mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/JSONValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/JSONValue.swift new file mode 100644 index 0000000000..d4ea79eb6c --- /dev/null +++ b/CodeEditModules/Sources/CodeEditSettings/Store/JSONValue.swift @@ -0,0 +1,58 @@ +// +// JSONValue.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 10/08/26. +// + +import Foundation + +/// A parsed JSON tree, used to hold settings sections nothing is registered to decode. +/// +/// This is what lets a disabled, uninstalled or not-yet-loaded extension's configuration survive a +/// save: the store re-emits these verbatim rather than dropping keys it does not understand. +public enum JSONValue: Codable, Hashable, Sendable { + case null + case bool(Bool) + case integer(Int) + case number(Double) + case string(String) + case array([JSONValue]) + case object([String: JSONValue]) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + // Order matters. `Bool` before the numerics: Foundation will decode `true` as `1` if a + // numeric type is tried first, which would rewrite booleans as numbers on save. `Int` + // before `Double`: decoding `42` as `Double` re-emits it as `42` today but loses the + // integer/float distinction the file was written with. + if container.decodeNil() { + self = .null + } else if let value = try? container.decode(Bool.self) { + self = .bool(value) + } else if let value = try? container.decode(Int.self) { + self = .integer(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode([JSONValue].self) { + self = .array(value) + } else { + self = .object(try container.decode([String: JSONValue].self)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .null: try container.encodeNil() + case .bool(let value): try container.encode(value) + case .integer(let value): try container.encode(value) + case .number(let value): try container.encode(value) + case .string(let value): try container.encode(value) + case .array(let value): try container.encode(value) + case .object(let value): try container.encode(value) + } + } +} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift new file mode 100644 index 0000000000..ba7a629711 --- /dev/null +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift @@ -0,0 +1,65 @@ +// +// SettingsStore.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 10/08/26. +// + +import Foundation + +/// Settings storage keyed by section. +/// +/// Sections nothing is registered to read are held as ``JSONValue`` and re-emitted verbatim on +/// save. That is what makes an unknown or disabled extension's configuration safe: the previous +/// store decoded only known keys and wrote only known keys, so a file written by a newer build lost +/// keys when an older build saved. +/// +/// Not `@MainActor`: it is constructed and used from the main actor in practice, but isolating it +/// would break the deliberately-nonisolated ``SettingsAccessing`` conformance added in Task 3. +public final class SettingsStore { + + private var sections: [String: JSONValue] + + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + /// An empty store, with every section at its defaults. + public init() { + self.sections = [:] + } + + /// Loads a store from a previously-encoded `settings.json`. + public init(data: Data) throws { + self.sections = try JSONDecoder().decode([String: JSONValue].self, from: data) + } + + /// Reads or writes the section for `type`, falling back to its defaults when absent or + /// undecodable. + public subscript(_ type: S.Type) -> S { + get { + guard let raw = sections[S.settingsKey], + let data = try? encoder.encode(raw), + let decoded = try? decoder.decode(S.self, from: data) + else { + return S() + } + return decoded + } + set { + guard let data = try? encoder.encode(newValue), + let raw = try? decoder.decode(JSONValue.self, from: data) + else { + assertionFailure("Section '\(S.settingsKey)' failed to round-trip through JSONValue") + return + } + sections[S.settingsKey] = raw + } + } + + /// Every section read from disk, including ones nothing is registered to decode. + public func encoded() throws -> Data { + let data = try encoder.encode(sections) + let object = try JSONSerialization.jsonObject(with: data) + return try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]) + } +} diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index 96ab76f056..964f7636c4 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -72,19 +72,31 @@ struct SettingsFormatTests { /// Every section present on disk must survive a save, whether or not anything is registered to /// read it. This is what keeps a disabled or not-yet-loaded extension's configuration alive. - /// - /// The body is commented out rather than merely disabled because `SettingsStore` does not exist - /// yet and a `.disabled` trait does not prevent compilation. Task 9 introduces the store, - /// uncomments this, and drops the trait. - @Test(.disabled("Target behaviour introduced in Task 9: SettingsStore preservation")) + @Test func unknownSectionsSurviveASave() throws { -// let original = try fixture("unknown-sections") -// let store = try SettingsStore(data: original) -// let saved = try store.encoded() -// -// let before = try parsed(original) -// let after = try parsed(saved) -// #expect(after["someFutureFeature"] as? NSDictionary == before["someFutureFeature"] as? NSDictionary) -// #expect(after["extensions"] as? NSDictionary == before["extensions"] as? NSDictionary) + let original = try fixture("unknown-sections") + let store = try SettingsStore(data: original) + let saved = try store.encoded() + + let before = try parsed(original) + let after = try parsed(saved) + #expect(after["someFutureFeature"] as? NSDictionary == before["someFutureFeature"] as? NSDictionary) + #expect(after["extensions"] as? NSDictionary == before["extensions"] as? NSDictionary) + } + + /// A registered section must survive the store's decode/encode cycle with non-default values + /// intact — not merely be replaced by a section rebuilt at defaults. + @Test + func registeredSectionRoundTripsThroughTheStore() throws { + let store = try SettingsStore(data: fixture("full-settings")) + + var terminal = store[TerminalSettings.self] + #expect(terminal.cursorBlink == true, "fixture seeds a non-default value") + terminal.cursorStyle = .underline + store[TerminalSettings.self] = terminal + + let reloaded = try SettingsStore(data: store.encoded()) + #expect(reloaded[TerminalSettings.self].cursorStyle == .underline) + #expect(reloaded[TerminalSettings.self].cursorBlink == true, "untouched field survived") } } From 581d746ccca0cde73eeff1438555a3a9339d61a1 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 15:59:16 +0200 Subject: [PATCH 256/335] Refactor: Persist settings section by section, preserving unknown sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings now loads and saves through SettingsStore rather than decoding and encoding one monolithic SettingsData root. Sections the aggregate has no field for are held as JSONValue and re-emitted verbatim, so a disabled or not-yet-loaded extension's configuration survives a save. Settings' public API is unchanged — shared, preferences, revision, the keypath subscript, baseURL and settingsURL all keep their signatures, so no call site moves. The save throttle, atomic write and revision counter are untouched. --- .../CodeEditSettings/Store/Settings.swift | 48 ++++++++++++++++--- .../SettingsFormatTests.swift | 21 ++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift index 50fad34e1a..37d78f3206 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift @@ -21,10 +21,12 @@ public final class Settings: ObservableObject { /// The publicly available singleton instance of ``SettingsModel`` nonisolated(unsafe) public static let shared: Settings = .init() + private var store: SettingsStore private var storeTask: AnyCancellable! private var revisionTask: AnyCancellable! private init() { + self.store = SettingsStore() self.preferences = .init() self.preferences = loadSettings() @@ -69,26 +71,60 @@ public final class Settings: ObservableObject { /// Load and construct ``Settings`` model from /// `~/Library/Application Support/CodeEdit/settings.json` + /// + /// Builds a ``SettingsStore`` from the file on disk (or an empty one if it is absent or + /// unreadable) and populates ``SettingsData`` from it section by section, so sections nothing + /// here decodes are preserved verbatim in ``store`` for the next save. private func loadSettings() -> SettingsData { if !filemanager.fileExists(atPath: settingsURL.path) { try? filemanager.createDirectory(at: baseURL, withIntermediateDirectories: false) + self.store = SettingsStore() return .init() } guard let json = try? Data(contentsOf: settingsURL), - let prefs = try? JSONDecoder().decode(SettingsData.self, from: json) + let loadedStore = try? SettingsStore(data: json) else { + self.store = SettingsStore() return .init() } - return prefs + self.store = loadedStore + + var data = SettingsData() + data.general = store[GeneralSettings.self] + data.accounts = store[AccountsSettings.self] + data.navigation = store[NavigationSettings.self] + data.theme = store[ThemeSettings.self] + data.textEditing = store[TextEditingSettings.self] + data.terminal = store[TerminalSettings.self] + data.sourceControl = store[SourceControlSettings.self] + data.keybindings = store[KeybindingsSettings.self] + data.search = store[SearchSettings.self] + data.languageServers = store[LanguageServerSettings.self] + data.developerSettings = store[DeveloperSettings.self] + return data } - /// Save``Settings`` model to + /// Save ``Settings`` model to /// `~/Library/Application Support/CodeEdit/settings.json` + /// + /// Writes each ``SettingsData`` field back into ``store`` before encoding, so sections the + /// store holds but ``SettingsData`` has no field for (an unknown or disabled extension's + /// configuration) are re-emitted unchanged. private func savePreferences(_ data: SettingsData) throws { - let data = try JSONEncoder().encode(data) - let json = try JSONSerialization.jsonObject(with: data) - let prettyJSON = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) + store[GeneralSettings.self] = data.general + store[AccountsSettings.self] = data.accounts + store[NavigationSettings.self] = data.navigation + store[ThemeSettings.self] = data.theme + store[TextEditingSettings.self] = data.textEditing + store[TerminalSettings.self] = data.terminal + store[SourceControlSettings.self] = data.sourceControl + store[KeybindingsSettings.self] = data.keybindings + store[SearchSettings.self] = data.search + store[LanguageServerSettings.self] = data.languageServers + store[DeveloperSettings.self] = data.developerSettings + + let prettyJSON = try store.encoded() try prettyJSON.write(to: settingsURL, options: .atomic) } diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index 964f7636c4..88b9d13595 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -99,4 +99,25 @@ struct SettingsFormatTests { #expect(reloaded[TerminalSettings.self].cursorStyle == .underline) #expect(reloaded[TerminalSettings.self].cursorBlink == true, "untouched field survived") } + + /// Loading and saving through `Settings` must not drop sections it has no field for. + /// + /// This is the guarantee extensions depend on: a user who disables an extension must not lose + /// its configuration the next time anything else is saved. + @Test + @MainActor + func savingPreservesSectionsSettingsDataDoesNotKnow() throws { + let original = try fixture("unknown-sections") + let store = try SettingsStore(data: original) + + // Simulate the load → mutate a known section → save cycle `Settings` performs. + var general = store[GeneralSettings.self] + general.fileIconStyle = .color + store[GeneralSettings.self] = general + + let after = try parsed(store.encoded()) + let before = try parsed(original) + #expect(after["someFutureFeature"] as? NSDictionary == before["someFutureFeature"] as? NSDictionary) + #expect(after["extensions"] as? NSDictionary == before["extensions"] as? NSDictionary) + } } From 26300f4bfcb955272cfe9a289338e1bb8fbe54ef Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 16:31:42 +0200 Subject: [PATCH 257/335] Refactor: Own settings in the composition root and retire the singleton --- CodeEdit/App/AppDelegate.swift | 19 ++- CodeEdit/App/AppDependencies.swift | 22 ++- CodeEdit/App/CodeEditApp.swift | 21 +-- .../WorkspaceWindowManager.swift | 2 +- .../Feedback/FeedbackModel.swift | 8 +- .../Feedback/FeedbackView.swift | 8 +- .../Feedback/FeedbackWindowController.swift | 4 +- .../Settings/AppSettings.swift | 63 ++++++++ .../Settings/AppSettingsStore.swift | 115 +++++++++++++ ...ft => KeybindingsSettings+Reconcile.swift} | 14 +- .../Settings/LegacySettingsStore.swift | 87 ---------- .../Settings/PageAndSettings.swift | 2 +- .../LocationsSettings/LocationsSettings.swift | 25 +-- .../LocationsSettingsView.swift | 4 +- .../SearchSettings/SearchSettingsModel.swift | 28 +++- .../Pages/ThemeSettings/ThemeModel+CRUD.swift | 12 +- .../Pages/ThemeSettings/ThemeModel.swift | 31 +++- ...+Search.swift => SettingsSearchKeys.swift} | 29 ++-- .../Settings/SettingsData.swift | 96 +++++++++++ .../Settings/SettingsInjector.swift | 54 +++++-- .../Settings/SettingsView.swift | 2 - ...EditingSettings+CommandRegistration.swift} | 24 ++- .../CodeEditSplitViewController.swift | 6 +- .../CodeEditWindowController+Toolbar.swift | 32 ++-- .../CodeEditWindowController.swift | 12 +- .../InspectorArea/FileInspectorView.swift | 11 +- .../HistoryInspectorModel.swift | 6 +- .../HistoryInspectorView.swift | 6 + .../InspectorArea/InspectorAreaView.swift | 8 +- .../OutlineView/FileSystemTableViewCell.swift | 26 ++- .../ProjectNavigatorOutlineView.swift | 17 +- .../ProjectNavigatorTableViewCell.swift | 19 ++- ...ViewController+NSOutlineViewDelegate.swift | 5 +- .../ProjectNavigatorViewController.swift | 48 +++--- .../SourceControlNavigatorHistoryView.swift | 2 +- .../AppCodeFileDocumentDelegate.swift | 9 +- .../Files/CEWorkspaceFile+Presentation.swift | 10 +- .../CELSP/Registry/RegistryManager.swift | 6 +- .../Models/SettingsData.swift | 87 ---------- .../CodeEditSettings/Store/AppSettings.swift | 53 ------ .../CodeEditSettings/Store/Settings.swift | 151 ------------------ .../Store/SettingsLocation.swift | 30 ++++ .../Store/SettingsValue.swift | 4 +- .../SettingsFormatTests.swift | 29 +++- CodeEditTests/App/AppSettingsStoreTests.swift | 119 ++++++++++++++ .../App/LegacySettingsStoreTests.swift | 83 ---------- .../App/SettingsSeamInvalidationTests.swift | 47 +++--- CodeEditTests/Features/LSP/Registry.swift | 5 +- .../LSP/RegistryManagerPersistenceTests.swift | 3 +- .../Features/Tasks/TaskManagerTests.swift | 6 +- .../FileExtensionVisibilityTests.swift | 80 +++++----- 51 files changed, 877 insertions(+), 713 deletions(-) create mode 100644 CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift create mode 100644 CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift rename CodeEdit/AuxiliaryWindows/Settings/{SettingsData+KeybindingReconcile.swift => KeybindingsSettings+Reconcile.swift} (55%) delete mode 100644 CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift rename CodeEdit/AuxiliaryWindows/Settings/Search/{SettingsData+Search.swift => SettingsSearchKeys.swift} (81%) create mode 100644 CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift rename CodeEdit/AuxiliaryWindows/Settings/{SettingsData+CommandRegistration.swift => TextEditingSettings+CommandRegistration.swift} (62%) delete mode 100644 CodeEditModules/Sources/CodeEditSettings/Models/SettingsData.swift delete mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/AppSettings.swift delete mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift create mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/SettingsLocation.swift create mode 100644 CodeEditTests/App/AppSettingsStoreTests.swift delete mode 100644 CodeEditTests/App/LegacySettingsStoreTests.swift diff --git a/CodeEdit/App/AppDelegate.swift b/CodeEdit/App/AppDelegate.swift index 81a91864f4..97c0828c6d 100644 --- a/CodeEdit/App/AppDelegate.swift +++ b/CodeEdit/App/AppDelegate.swift @@ -35,12 +35,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var cancellables = Set() + /// Hands the settings store to the three pre-existing singletons that cannot take it through + /// `init`. Composition-root privilege: nothing else may do this. + private func installSettingsStore() { + ThemeModel.shared.configure(settings: dependencies.settingsAccessor) + FeedbackModel.shared.settingsAccessor = dependencies.settingsAccessor + SearchSettingsModel.shared.configure(settings: dependencies.settingsAccessor) + } + func applicationDidFinishLaunching(_ notification: Notification) { - CodeFileDocument.isAutoSaveOnProvider = { - Settings.shared.preferences.general.isAutoSaveOn + CodeFileDocument.isAutoSaveOnProvider = { [settings = dependencies.settingsAccessor] in + settings.value(GeneralSettings.self).isAutoSaveOn } + installSettingsStore() enableWindowSizeSaveOnQuit() - Settings.shared.preferences.general.appAppearance.applyAppearance() + dependencies.settingsAccessor.value(GeneralSettings.self).appAppearance.applyAppearance() checkForFilesToOpen() // Subscribe to the welcome window event published by WorkspaceWindowManager @@ -129,7 +138,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } func handleOpen() { - let behavior = Settings.shared.preferences.general.reopenBehavior + let behavior = dependencies.settingsAccessor.value(GeneralSettings.self).reopenBehavior switch behavior { case .welcome: if !tryFocusWindow(id: .welcome) { @@ -205,7 +214,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @IBAction func openFeedback(_ sender: Any) { if tryFocusWindow(of: FeedbackView.self) { return } - FeedbackView().showWindow() + FeedbackView().showWindow(settingsStore: dependencies.settingsStore) } @IBAction private func checkForUpdates(_ sender: Any) { diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index 0dda97d02a..4727fb4a82 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -30,11 +30,15 @@ final class AppDependencies { private(set) lazy var shellClient: ShellClientProtocol = ShellClient() - /// Feature-side settings access, read and write. Bridged onto `Settings.shared` via - /// `LegacySettingsStore` as a stopgap until a later task wires a real, section-keyed store — - /// feature packages already go through this interface, so that swap will not touch any of - /// their call sites. - private(set) lazy var settingsAccessor: SettingsAccessing = LegacySettingsStore() + /// The app's settings: the one store, owned here rather than reached through a singleton. + /// + /// Concrete (not `SettingsAccessing`) because the injectors also need its `revision` publisher + /// to observe. Consumers that only read or write settings take ``settingsAccessor`` instead. + private(set) lazy var settingsStore = AppSettingsStore() + + /// Feature-side settings access, read and write. The same object as ``settingsStore``, narrowed + /// to the seam's protocol so nothing outside the composition root names the concrete type. + private(set) lazy var settingsAccessor: SettingsAccessing = settingsStore private(set) lazy var commandManager: CommandManaging = CommandManager() @@ -64,7 +68,10 @@ final class AppDependencies { eventBus: eventBus, errorNotifier: errorNotifier, shellClient: shellClient, - settingsAccessor: settingsAccessor + settingsAccessor: settingsAccessor, + // Handed its install location rather than reading it from a singleton. A later slice gives + // `RegistryManager` a proper home for this path; until then the composition root supplies it. + installPath: settingsStore.baseURL.appending(path: "Language Servers") ) private(set) lazy var workspaceWindowManager = WorkspaceWindowManager(dependencies: self) @@ -87,6 +94,7 @@ final class AppDependencies { AppCodeFileDocumentDelegate( lspService: lspService, windowManager: workspaceWindowManager, - languageServices: languageServicesProvider + languageServices: languageServicesProvider, + settingsStore: settingsStore ) } diff --git a/CodeEdit/App/CodeEditApp.swift b/CodeEdit/App/CodeEditApp.swift index 8a3c8a5edf..bd9c01fd52 100644 --- a/CodeEdit/App/CodeEditApp.swift +++ b/CodeEdit/App/CodeEditApp.swift @@ -15,20 +15,26 @@ import AboutWindow @main struct CodeEditApp: App { @NSApplicationDelegateAdaptor var appdelegate: AppDelegate - @ObservedObject var settings = Settings.shared init() { NSMenuItem.swizzle() NSSplitViewItem.swizzle() - CodeFileDocument.delegateProvider = { [dependencies = appdelegate.dependencies] in + let dependencies = appdelegate.dependencies + CodeFileDocument.delegateProvider = { dependencies.codeFileDocumentDelegate } - TextEditingSettings.registerCommands(in: appdelegate.dependencies.commandManager) - SettingsData.reconcileDefaultKeybindings(keybindingManager: appdelegate.dependencies.keybindingManager) + TextEditingSettings.registerCommands( + in: dependencies.commandManager, + settings: dependencies.settingsAccessor + ) + KeybindingsSettings.reconcileDefaults( + keybindingManager: dependencies.keybindingManager, + settings: dependencies.settingsAccessor + ) } var body: some Scene { - Group { + SettingsSceneInjector(store: appdelegate.dependencies.settingsStore) { WelcomeWindow( subtitleView: { WelcomeSubtitleView() }, actions: { dismissWindow in @@ -86,11 +92,6 @@ struct CodeEditApp: App { CodeEditCommands(dependencies: appdelegate.dependencies) } } - .environment(\.settings, settings.preferences) // Add settings to each window environment - // The settings seam's invalidation signal, for the scene roots that never pass through - // `SettingsInjector`. `appServices(_:)` cannot supply it — it holds no observation — but - // this body does, via the `@ObservedObject` above. - .environment(\.settingsRevision, settings.revision) .appServices(appdelegate.dependencies) } } diff --git a/CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManager.swift b/CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManager.swift index 6f6b720b13..707f31f659 100644 --- a/CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManager.swift +++ b/CodeEdit/App/WorkspaceLifecycle/WorkspaceWindowManager.swift @@ -188,7 +188,7 @@ final class WorkspaceWindowManager: WorkspaceWindowManaging { } private func handleLastWorkspaceClosed() { - switch Settings[\.general].reopenWindowAfterClose { + switch dependencies.settingsAccessor.value(GeneralSettings.self).reopenWindowAfterClose { case .showWelcomeWindow: if let welcomeWindow = NSApp.findWindow(.welcome) { welcomeWindow.makeKeyAndOrderFront(nil) diff --git a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackModel.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackModel.swift index cb9ea480a2..dce2d3fe06 100644 --- a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackModel.swift +++ b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackModel.swift @@ -15,6 +15,10 @@ public class FeedbackModel: ObservableObject { private let keychain = CodeEditKeychain() + /// The settings store. Property-injected by `AppDelegate` at launch — see `ThemeModel`'s + /// `settingsAccessor` for why these pre-existing singletons take their store this way. + public var settingsAccessor: SettingsAccessing = DefaultSettingsReader() + @Environment(\.openURL) var openIssueURL @@ -140,7 +144,7 @@ public class FeedbackModel: ObservableObject { expectation: String?, actuallyHappened: String? ) { - let gitAccounts = Settings[\.accounts].sourceControlAccounts.gitAccounts + let gitAccounts = settingsAccessor.value(AccountsSettings.self).sourceControlAccounts.gitAccounts let firstGitAccount = gitAccounts.first let config = GitHubTokenConfiguration(keychain.get(firstGitAccount!.name)) @@ -159,7 +163,7 @@ public class FeedbackModel: ObservableObject { ) { response in switch response { case .success(let issue): - if Settings[\.sourceControl].general.openFeedbackInBrowser { + if self.settingsAccessor.value(SourceControlSettings.self).general.openFeedbackInBrowser { self.openIssueURL(issue.htmlURL ?? URL(string: "https://github.com/CodeEditApp/CodeEdit/issues")!) } self.isSubmitted.toggle() diff --git a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift index 7794423851..afa9946c97 100644 --- a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift +++ b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift @@ -211,7 +211,11 @@ struct FeedbackView: View { } } - func showWindow() { - FeedbackWindowController(view: self, size: NSSize(width: 1028, height: 762)).showWindow(nil) + func showWindow(settingsStore: AppSettingsStore) { + FeedbackWindowController( + view: self, + size: NSSize(width: 1028, height: 762), + settingsStore: settingsStore + ).showWindow(nil) } } diff --git a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift index 824125d3bd..f975690086 100644 --- a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift +++ b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift @@ -8,8 +8,8 @@ import SwiftUI final class FeedbackWindowController: NSWindowController, NSToolbarDelegate { - convenience init(view: T, size: NSSize) { - let hostingController = NSHostingController(rootView: SettingsInjector { view }) + convenience init(view: T, size: NSSize, settingsStore: AppSettingsStore) { + let hostingController = NSHostingController(rootView: SettingsInjector(store: settingsStore) { view }) let window = NSWindow(contentViewController: hostingController) self.init(window: window) window.title = "Feedback for CodeEdit" diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift new file mode 100644 index 0000000000..ab41fa6da2 --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift @@ -0,0 +1,63 @@ +// +// AppSettings.swift +// CodeEdit +// +// Created by Wouter Hennen on 12/04/2023. +// + +import SwiftUI +import CodeEditSettings + +/// Reads and writes one setting inside a SwiftUI view, addressed through the app-wide +/// ``SettingsData`` aggregate. +/// +/// The legacy app-side counterpart of ``SettingsValue``. Both now resolve the same way — through +/// ``EnvironmentValues/settingsAccessor`` for the value and ``EnvironmentValues/settingsRevision`` +/// for invalidation — so a view can use either without a difference in behaviour. The distinction +/// that remains is what they may name: `SettingsValue` names one section and works inside feature +/// packages, `AppSettings` names the app-wide aggregate and therefore cannot. New code should prefer +/// `SettingsValue`; this wrapper exists so its ~50 existing call sites did not all have to move in +/// one change. +/// +/// **Only valid inside a `View`.** It used to read a singleton, so it also worked in models and +/// AppKit types; it no longer does, and such a use resolves to `DefaultSettingsReader`, which traps +/// in debug. Non-view types take a ``SettingsReading``/``SettingsAccessing`` by initializer instead. +@propertyWrapper +struct AppSettings: DynamicProperty where T: Equatable { + + @Environment(\.settingsAccessor) + private var accessor + + /// Not a source of data — a source of *invalidation*. See ``EnvironmentValues/settingsRevision``. + @Environment(\.settingsRevision) + private var revision + + private let keyPath: WritableKeyPath + + init(_ keyPath: WritableKeyPath) { + self.keyPath = keyPath + } + + var wrappedValue: T { + get { + // Read, not merely declared: an unread `@Environment` is a dependency SwiftUI does not + // document itself as tracking, and being tracked is this property's entire purpose. + _ = revision + return SettingsData(accessor: accessor)[keyPath: keyPath] + } + nonmutating set { + // `SettingsData` is a stateless façade, so this "local" mutation writes straight through + // to the accessor — the section is read, the field replaced, the section written back. + var settings = SettingsData(accessor: accessor) + settings[keyPath: keyPath] = newValue + } + } + + var projectedValue: Binding { + Binding { + wrappedValue + } set: { + wrappedValue = $0 + } + } +} diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift b/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift new file mode 100644 index 0000000000..e0f9b66241 --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift @@ -0,0 +1,115 @@ +// +// AppSettingsStore.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 10/08/26. +// + +import Combine +import Foundation +import CodeEditSettings + +/// The app's single settings store: owns the on-disk state, the save pipeline and the seam's +/// invalidation signal. +/// +/// Constructed and owned by `AppDependencies`, the app-scope composition root — this type has no +/// `shared`. It replaces `Settings.shared`, which reached the same state from anywhere and could +/// therefore never be substituted in a test or a second configuration. +/// +/// Not `@MainActor`: it conforms to ``SettingsAccessing``, which is deliberately nonisolated so it +/// can be an `EnvironmentKey` value (see that protocol's documentation). Main-thread use is asserted +/// at the write entry point instead. +final class AppSettingsStore: ObservableObject, SettingsAccessing { + + /// Section-keyed storage. Sections nothing here decodes are held verbatim and re-emitted on + /// save, so a disabled extension's configuration survives. + private let store: SettingsStore + + /// One element per write. Throttled rather than debounced so a continuous stream of changes + /// (dragging a font-size slider) still reaches disk at a bounded interval instead of only when + /// the user stops. + private let saveRequests = PassthroughSubject() + + private var saveTask: AnyCancellable? + + /// A counter incremented once per change to any section. + /// + /// This is the settings seam's **invalidation signal**. Views reach settings through + /// ``SettingsValue`` or `AppSettings`, whose only other environment dependency is + /// ``EnvironmentValues/settingsAccessor`` — an existential holding a store that never compares + /// unequal to itself. Re-rendering on a settings change would then rest on SwiftUI treating a + /// rewritten non-`Equatable` existential as a change, which is unspecified. An `Int` is + /// `Equatable`, so an injector publishing it into ``EnvironmentValues/settingsRevision`` makes + /// the invalidation explicit and precise. + @Published private(set) var revision: Int = 0 + + /// `~/Library/Application Support/CodeEdit/` — the folder settings and adjacent app data live in. + var baseURL: URL { SettingsLocation.baseURL } + + /// The file this store loads from and saves to. Injectable so a test can point a store at a + /// temporary file instead of the user's real `settings.json`. + private let settingsURL: URL + + init(settingsURL: URL = SettingsLocation.settingsFileURL) { + self.settingsURL = settingsURL + self.store = Self.loadStore(at: settingsURL) + + self.saveTask = saveRequests + .throttle(for: 2, scheduler: RunLoop.main, latest: true) + .sink { [weak self] in + try? self?.save() + } + } + + // MARK: - SettingsAccessing + + func value(_ type: S.Type) -> S { + store[S.self] + } + + func setValue(_ value: S) { + // `SettingsAccessing` is deliberately nonisolated (see the protocol's docs), so the compiler + // cannot enforce this. A write bumps `revision`, whose `@Published` change drives AppKit + // through SwiftUI observers — off the main thread that corrupts AppKit state rather than + // failing cleanly. Loud in debug, unchanged in release. + MainActor.assertIsolated("Settings must be written on the main thread") + + store[S.self] = value + // Mutate first, then publish: a view body re-evaluated by this revision change already reads + // the new value. `Settings` achieved the same ordering by bumping on `$preferences`' + // `willSet`; doing it explicitly after the store write makes the guarantee local instead of + // dependent on Combine's emission timing. + revision &+= 1 + saveRequests.send() + } + + // MARK: - Persistence + + /// Builds the store from `settings.json`, or an empty one when the file is absent or unreadable. + private static func loadStore(at url: URL) -> SettingsStore { + let fileManager = FileManager.default + + guard fileManager.fileExists(atPath: url.path) else { + try? fileManager.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: false + ) + return SettingsStore() + } + + guard let json = try? Data(contentsOf: url), + let loaded = try? SettingsStore(data: json) + else { + return SettingsStore() + } + return loaded + } + + /// Writes every section — including the ones nothing here decodes — to `settings.json`. + /// + /// The write is `.atomic` so an interrupted save cannot leave a truncated file where the user's + /// settings used to be. + private func save() throws { + try store.encoded().write(to: settingsURL, options: .atomic) + } +} diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsData+KeybindingReconcile.swift b/CodeEdit/AuxiliaryWindows/Settings/KeybindingsSettings+Reconcile.swift similarity index 55% rename from CodeEdit/AuxiliaryWindows/Settings/SettingsData+KeybindingReconcile.swift rename to CodeEdit/AuxiliaryWindows/Settings/KeybindingsSettings+Reconcile.swift index eacbd66956..acc8be1fed 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsData+KeybindingReconcile.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/KeybindingsSettings+Reconcile.swift @@ -1,5 +1,5 @@ // -// SettingsData+KeybindingReconcile.swift +// KeybindingsSettings+Reconcile.swift // CodeEdit // // Created by Matthijs Eikelenboom. @@ -9,17 +9,17 @@ import Foundation import CodeEditSettings import CodeEditCore -extension SettingsData { +extension KeybindingsSettings { /// Merges bundled-default keybindings (from `default_keybindings.json`, owned by /// `KeybindingManager`) into the persisted settings, adding only keys the user does /// not already have. Preserves user overrides. Invoked once at app startup — /// previously ran as a side effect of decoding `KeybindingsSettings`. - static func reconcileDefaultKeybindings(keybindingManager: KeybindingManaging) { + static func reconcileDefaults(keybindingManager: KeybindingManaging, settings: SettingsAccessing) { let defaults = keybindingManager.keyboardShortcuts - var current = Settings.shared.preferences.keybindings.keybindings - for (key, _) in defaults where current[key] == nil { - current[key] = keybindingManager.named(with: key) + var section = settings.value(KeybindingsSettings.self) + for (key, _) in defaults where section.keybindings[key] == nil { + section.keybindings[key] = keybindingManager.named(with: key) } - Settings.shared.preferences.keybindings.keybindings = current + settings.setValue(section) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift b/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift deleted file mode 100644 index 96c45185ac..0000000000 --- a/CodeEdit/AuxiliaryWindows/Settings/LegacySettingsStore.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// LegacySettingsStore.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 09/08/2026. -// - -import CodeEditSettings - -/// A `SettingsAccessing` bridge over the existing `Settings.shared` singleton. -/// -/// This is a **stopgap**: it exists only because feature packages have already been cut over to -/// read and write through `SettingsAccessing`, but no store backed by real, persisted settings has -/// been built yet to supply the environment. Without this bridge, every `@SettingsValue` read (and -/// every direct `SettingsReading.value(_:)` call) silently falls back to section defaults, ignoring -/// the user's `settings.json` — see the plan's Task 6b for the regression this fixes. -/// -/// Writes go through `Settings[_:]`, i.e. straight into `Settings.shared.preferences`, so they -/// reach the existing `@Published` → `throttle(for: 2)` → `savePreferences` pipeline. Anything that -/// mutated a copy instead would appear to work and lose the value on relaunch. -/// -/// It is deleted once a later task replaces the singleton-backed store with the real, -/// section-keyed store. -struct LegacySettingsStore: SettingsAccessing { - func value(_ type: S.Type) -> S { - Self.accessors[S.settingsKey]?.read(Settings.shared.preferences) as? S ?? - // Explicit fallback, not a silent catch-all: a section with no entry in `accessors` - // has no field on `SettingsData` to read from, so it answers with defaults until that - // map is updated (or, more likely, until this bridge is deleted in favor of the real - // store). - S() - } - - func setValue(_ value: S) { - // `SettingsAccessing` is deliberately nonisolated (see the protocol's docs), so the - // compiler cannot enforce this. A write lands in `Settings.shared.preferences`, whose - // `@Published` change drives AppKit through SwiftUI observers — off the main thread that - // corrupts AppKit state rather than failing cleanly. Loud in debug, unchanged in release. - MainActor.assertIsolated("Settings must be written on the main thread") - - guard let accessor = Self.accessors[S.settingsKey] else { - // A section absent from `accessors` has nowhere to go on `SettingsData`, so the write - // would vanish. Every section declared today is mapped below; this fires only if a new - // one forgets to register, which is a wiring bug, not a runtime condition. - assertionFailure("No SettingsData field registered for section '\(S.settingsKey)'") - return - } - accessor.write(value) - } - - /// A read/write pair for one `SettingsData` field, type-erased over its section type. - private struct SectionAccessor { - let read: (SettingsData) -> any SettingsSection - let write: (any SettingsSection) -> Void - - init(_ keyPath: WritableKeyPath) { - read = { $0[keyPath: keyPath] } - write = { value in - guard let value = value as? S else { - // Only reachable if two sections share a `settingsKey`, or a key was mapped to - // the wrong `SettingsData` field. Either way the user's write is being dropped. - assertionFailure( - "Section '\(S.settingsKey)' is registered for \(S.self) but was handed " - + "\(type(of: value))" - ) - return - } - Settings[keyPath] = value - } - } - } - - /// All eleven `SettingsData` fields, keyed by their section's `settingsKey`. - private static let accessors: [String: SectionAccessor] = [ - GeneralSettings.settingsKey: SectionAccessor(\.general), - AccountsSettings.settingsKey: SectionAccessor(\.accounts), - NavigationSettings.settingsKey: SectionAccessor(\.navigation), - ThemeSettings.settingsKey: SectionAccessor(\.theme), - TextEditingSettings.settingsKey: SectionAccessor(\.textEditing), - TerminalSettings.settingsKey: SectionAccessor(\.terminal), - SourceControlSettings.settingsKey: SectionAccessor(\.sourceControl), - KeybindingsSettings.settingsKey: SectionAccessor(\.keybindings), - SearchSettings.settingsKey: SectionAccessor(\.search), - LanguageServerSettings.settingsKey: SectionAccessor(\.languageServers), - DeveloperSettings.settingsKey: SectionAccessor(\.developerSettings) - ] -} diff --git a/CodeEdit/AuxiliaryWindows/Settings/PageAndSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/PageAndSettings.swift index 0414a1b201..f635266f4b 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/PageAndSettings.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/PageAndSettings.swift @@ -15,6 +15,6 @@ struct PageAndSettings: Identifiable, Equatable { init(_ page: SettingsPage) { self.page = page - self.settings = SettingsData().propertiesOf(page.name) + self.settings = SettingsPage.propertiesOf(page.name) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettings.swift index cf9332cc33..283427b8c7 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettings.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettings.swift @@ -8,18 +8,19 @@ import Foundation import CodeEditSettings -extension SettingsData { +/// The Locations settings page. +/// +/// Not a `SettingsSection`: it persists nothing, it only lists where things already live. It used to +/// be nested inside `SettingsData`, which read as if it were one of the stored sections. +struct LocationsSettings: SearchableSettingsPage { - struct LocationsSettings: SearchableSettingsPage { - - /// The search keys - var searchKeys: [String] { - [ - "Settings Location", - "Themes Location", - "Extensions Location" - ] - .map { NSLocalizedString($0, comment: "") } - } + /// The search keys + var searchKeys: [String] { + [ + "Settings Location", + "Themes Location", + "Extensions Location" + ] + .map { NSLocalizedString($0, comment: "") } } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift index 9b3399c04d..bab7f6a2e0 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift @@ -24,9 +24,9 @@ struct LocationsSettingsView: View { private extension LocationsSettingsView { @ViewBuilder private var applicationSupportLocation: some View { - ExternalLink(destination: Settings.shared.baseURL) { + ExternalLink(destination: SettingsLocation.baseURL) { Text("Application Support") - Text(Settings.shared.baseURL.path) + Text(SettingsLocation.baseURL.path) .font(.footnote) .foregroundColor(.secondary) } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsModel.swift index 7d84a2d84a..3e10fec63e 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsModel.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/SearchSettings/SearchSettingsModel.swift @@ -16,11 +16,22 @@ import CodeEditSettings /// private var searchSettigs: SearchSettingsModel = .shared /// ``` final class SearchSettingsModel: ObservableObject { - /// Reads settings file for Search Settings and updates the values in this model - /// correspondingly - private init() { - let value = Settings[\.search].ignoreGlobPatterns - self.ignoreGlobPatterns = value + private init() {} + + /// The settings store. Property-injected by `AppDelegate` at launch — see `ThemeModel`'s + /// `settingsAccessor` for why these pre-existing singletons take their store this way. + private var settingsAccessor: SettingsAccessing = DefaultSettingsReader() + + /// Suppresses the write-back that `ignoreGlobPatterns`' `didSet` would otherwise perform while + /// seeding it *from* the store, which would save the value that was just read. + private var isSeeding = false + + /// Installs the store and seeds this model from it. Called once, by the composition root. + func configure(settings: SettingsAccessing) { + settingsAccessor = settings + isSeeding = true + ignoreGlobPatterns = settings.value(SearchSettings.self).ignoreGlobPatterns + isSeeding = false } static let shared: SearchSettingsModel = .init() @@ -53,10 +64,13 @@ final class SearchSettingsModel: ObservableObject { /// Stores the new values from the Search Settings Model into the settings.json whenever /// `ignoreGlobPatterns` is updated - @Published var ignoreGlobPatterns: [GlobPattern] { + @Published var ignoreGlobPatterns: [GlobPattern] = [] { didSet { + guard !isSeeding else { return } DispatchQueue.main.async { - Settings[\.search].ignoreGlobPatterns = self.ignoreGlobPatterns + var section = self.settingsAccessor.value(SearchSettings.self) + section.ignoreGlobPatterns = self.ignoreGlobPatterns + self.settingsAccessor.setValue(section) } } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift index afebdb39ad..97eba1c21c 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift @@ -14,19 +14,19 @@ extension ThemeModel { func loadThemes() throws { themes.removeAll() - let prefs = Settings.shared.preferences - themes = try repository.loadAllThemes(overrides: prefs.theme.overrides) + let prefs = settingsAccessor.value(ThemeSettings.self) + themes = try repository.loadAllThemes(overrides: prefs.overrides) // Select initial themes based on preferences self.selectedDarkTheme = self.darkThemes.first { - $0.name == prefs.theme.selectedDarkTheme + $0.name == prefs.selectedDarkTheme } ?? self.darkThemes.first self.selectedLightTheme = self.lightThemes.first { - $0.name == prefs.theme.selectedLightTheme + $0.name == prefs.selectedLightTheme } ?? self.lightThemes.first - let userSelectedTheme = self.themes.first { $0.name == prefs.theme.selectedTheme } + let userSelectedTheme = self.themes.first { $0.name == prefs.selectedTheme } let systemAppearance = NSAppearance.currentDrawing().name if userSelectedTheme != nil { @@ -116,7 +116,7 @@ extension ThemeModel { func delete(_ theme: Theme) { do { try repository.delete(theme) - Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name) + updateThemeSettings { $0.overrides.removeValue(forKey: theme.name) } try self.loadThemes() } catch { print(error) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift index 8f3b82262d..d938c3d999 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift @@ -19,8 +19,18 @@ import UniformTypeIdentifiers final class ThemeModel: ObservableObject { static let shared: ThemeModel = .init() - @AppSettings(\.theme) - var settings + /// The settings store, installed by `configure(settings:)`. `ThemeModel` is a pre-existing + /// singleton that this change does not dismantle, so it cannot take the store through `init`. + /// Left at `DefaultSettingsReader` it traps in debug, which is the point — a missing install is + /// a wiring bug, not a runtime condition. + private(set) var settingsAccessor: SettingsAccessing = DefaultSettingsReader() + + /// Read-modify-write of the whole `ThemeSettings` section, the granularity the store works in. + func updateThemeSettings(_ mutate: (inout ThemeSettings) -> Void) { + var section = settingsAccessor.value(ThemeSettings.self) + mutate(§ion) + settingsAccessor.setValue(section) + } /// Default instance of the `FileManager` let filemanager = FileManager.default @@ -60,8 +70,7 @@ final class ThemeModel: ObservableObject { @Published var selectedLightTheme: Theme? { didSet { DispatchQueue.main.async { - Settings.shared - .preferences.theme.selectedLightTheme = self.selectedLightTheme?.name ?? "Broken" + self.updateThemeSettings { $0.selectedLightTheme = self.selectedLightTheme?.name ?? "Broken" } } } } @@ -71,8 +80,7 @@ final class ThemeModel: ObservableObject { @Published var selectedDarkTheme: Theme? { didSet { DispatchQueue.main.async { - Settings.shared - .preferences.theme.selectedDarkTheme = self.selectedDarkTheme?.name ?? "Broken" + self.updateThemeSettings { $0.selectedDarkTheme = self.selectedDarkTheme?.name ?? "Broken" } } } } @@ -90,7 +98,7 @@ final class ThemeModel: ObservableObject { @Published var selectedTheme: Theme? { didSet { DispatchQueue.main.async { - Settings[\.theme].selectedTheme = self.selectedTheme?.name + self.updateThemeSettings { $0.selectedTheme = self.selectedTheme?.name } } } } @@ -113,6 +121,15 @@ final class ThemeModel: ObservableObject { themesURL: base.appending(path: "Themes", directoryHint: .isDirectory), bundledThemesURL: Bundle.main.resourceURL?.appending(path: "DefaultThemes", directoryHint: .isDirectory) ) + } + + /// Installs the settings store and loads the themes from disk. + /// + /// Loading is deliberately *not* in `init`: it reads `theme` out of the store, and `init` runs + /// the moment anything first touches `shared` — including the very line that would assign the + /// store. The load would then read `DefaultSettingsReader` and select the wrong theme. + func configure(settings: SettingsAccessing) { + settingsAccessor = settings do { try loadThemes() } catch { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift similarity index 81% rename from CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift rename to CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift index 003771db83..e9d7c42a8d 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsData+Search.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift @@ -189,28 +189,35 @@ extension DeveloperSettings: SearchableSettingsPage { } } -extension SettingsData { +extension SettingsPage { // swiftlint:disable cyclomatic_complexity - func propertiesOf(_ name: SettingsPage.Name) -> [SettingsPage] { + /// The searchable settings of one page. + /// + /// Reads no stored value — every `searchKeys` list is a constant of its section type — so this + /// is a static function on the page rather than a method on the settings aggregate, which is now + /// a façade that would need a store just to answer it. + static func propertiesOf(_ name: SettingsPage.Name) -> [SettingsPage] { var settings: [SettingsPage] = [] switch name { case .general: - general.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + GeneralSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .accounts: - accounts.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + AccountsSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .navigation: - navigation.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + NavigationSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .theme: - theme.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + ThemeSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .textEditing: - textEditing.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + TextEditingSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .terminal: - terminal.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + TerminalSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .search: - search.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + SearchSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .sourceControl: - sourceControl.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + SourceControlSettings().searchKeys.forEach { + settings.append(.init(name, isSetting: true, settingName: $0)) + } case .location: LocationsSettings().searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } case .languageServers: @@ -218,7 +225,7 @@ extension SettingsData { settings.append(.init(name, isSetting: true, settingName: $0)) } case .developer: - developerSettings.searchKeys.forEach { settings.append(.init(name, isSetting: true, settingName: $0)) } + 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/AuxiliaryWindows/Settings/SettingsData.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift new file mode 100644 index 0000000000..43bfba884e --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift @@ -0,0 +1,96 @@ +// +// SettingsData.swift +// CodeEdit +// +// Created by Lukas Pistrol on 01.04.22. +// + +import CodeEditSettings + +/// # SettingsData +/// +/// A **façade** over a ``SettingsAccessing``, presenting the app's eleven settings sections as one +/// aggregate so `AppSettings` key paths like `\.theme.matchAppearance` keep working. +/// +/// It is no longer `Codable` and holds no state: the store owns persistence, one section at a time, +/// and every property here forwards to it. That is what lets a section be moved into its owning +/// feature package without the aggregate following it — the aggregate is app-side presentation +/// convenience, not the storage format. +/// +/// Writes are section-granular by construction: setting `\.theme.matchAppearance` reads the whole +/// `ThemeSettings`, mutates the one field and writes the section back. +struct SettingsData { + + /// The store every property reads and writes through. + private let accessor: any SettingsAccessing + + init(accessor: any SettingsAccessing) { + self.accessor = accessor + } + + /// The general global settings + var general: GeneralSettings { + get { accessor.value(GeneralSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for accounts + var accounts: AccountsSettings { + get { accessor.value(AccountsSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for navigation + var navigation: NavigationSettings { + get { accessor.value(NavigationSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for themes + var theme: ThemeSettings { + get { accessor.value(ThemeSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for text editing + var textEditing: TextEditingSettings { + get { accessor.value(TextEditingSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for the terminal emulator + var terminal: TerminalSettings { + get { accessor.value(TerminalSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for source control + var sourceControl: SourceControlSettings { + get { accessor.value(SourceControlSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// The global settings for keybindings + var keybindings: KeybindingsSettings { + get { accessor.value(KeybindingsSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// Search Settings + var search: SearchSettings { + get { accessor.value(SearchSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// Language Server Settings + var languageServers: LanguageServerSettings { + get { accessor.value(LanguageServerSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } + + /// Developer settings for CodeEdit developers + var developerSettings: DeveloperSettings { + get { accessor.value(DeveloperSettings.self) } + nonmutating set { accessor.setValue(newValue) } + } +} diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index 0718dda7e9..0244d06bd8 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -8,22 +8,56 @@ import SwiftUI import CodeEditSettings +/// Supplies the settings seam to a view subtree, and re-renders it when settings change. +/// +/// Wrap every **standalone** `NSHostingView`/`NSHostingController` root in this: `@Environment` does +/// not cross a hosting boundary, so a root that skips it hands its whole subtree +/// `DefaultSettingsReader` — plausible defaults for reads, silently discarded writes. struct SettingsInjector: View { - @ObservedObject var settings = Settings.shared + /// Observed, not merely held: this view's job is to re-inject `revision` when it changes. + @ObservedObject var store: AppSettingsStore @ViewBuilder var content: Content + init(store: AppSettingsStore, @ViewBuilder content: () -> Content) { + self.store = store + self.content = content() + } + var body: some View { content - .environment(\.settings, settings.preferences) - .environment(\.settingsAccessor, LegacySettingsStore()) - // The seam's invalidation signal. Rewriting the accessor above is *not* enough: - // `LegacySettingsStore` is a stateless struct behind an existential, so whether SwiftUI - // treats the rewrite as a change is unspecified — and `.appServices(_:)`, applied - // closer to the leaf in `CodeEditSplitViewController`, overwrites it with a - // process-lifetime instance anyway. `settingsRevision` is `Equatable` and lives in its - // own key, so neither of those can defeat it. - .environment(\.settingsRevision, settings.revision) + .environment(\.settingsAccessor, store) + // The seam's invalidation signal. Rewriting the accessor above is *not* enough: it is a + // stable instance behind an existential, so whether SwiftUI treats the rewrite as a + // change is unspecified — and `.appServices(_:)`, applied closer to the leaf in + // `CodeEditSplitViewController`, overwrites it with the same instance anyway. + // `settingsRevision` is `Equatable` and lives in its own key, so neither can defeat it. + .environment(\.settingsRevision, store.revision) + } +} + +/// The scene-level counterpart of ``SettingsInjector``. +/// +/// The app's scenes are not inside any hosting root, so they need their own injector — and it must +/// be a `Scene`, since a `View` cannot wrap one. `CodeEditApp` cannot observe the store itself: it +/// reaches it through the `NSApplicationDelegateAdaptor`, which is unavailable until every stored +/// property is initialized, so the observation lives here instead. +struct SettingsSceneInjector: Scene { + + /// Observed, not merely held: this scene's job is to re-inject `revision` when it changes. + @ObservedObject var store: AppSettingsStore + + var content: Content + + init(store: AppSettingsStore, @SceneBuilder content: () -> Content) { + self.store = store + self.content = content() + } + + var body: some Scene { + content + .environment(\.settingsAccessor, store) + .environment(\.settingsRevision, store.revision) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsView.swift index b330f3353c..938280ee42 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsView.swift @@ -105,8 +105,6 @@ struct SettingsView: View { ), ] - @ObservedObject private var settings: CodeEditSettings.Settings = .shared - let updater: SoftwareUpdater /// Searches through an array of pages to check if a page name exists in the array diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift b/CodeEdit/AuxiliaryWindows/Settings/TextEditingSettings+CommandRegistration.swift similarity index 62% rename from CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift rename to CodeEdit/AuxiliaryWindows/Settings/TextEditingSettings+CommandRegistration.swift index a66468f170..a1f6ec4188 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsData+CommandRegistration.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/TextEditingSettings+CommandRegistration.swift @@ -1,5 +1,5 @@ // -// SettingsData+CommandRegistration.swift +// TextEditingSettings+CommandRegistration.swift // CodeEdit // // Created by Matthijs Eikelenboom. @@ -12,41 +12,49 @@ import CodeEditSettings extension TextEditingSettings { /// Registers toggle-able text-editing preferences with the command palette. /// Invoked once at app startup (previously ran as a side effect of decoding). - static func registerCommands(in mgr: CommandManaging) { + /// + /// `settings` is captured by the command closures, which outlive this call — the composition + /// root owns the store, so the capture is of the one live store, not a copy of its values. + static func registerCommands(in mgr: CommandManaging, settings: SettingsAccessing) { + func toggle(_ keyPath: WritableKeyPath) { + var section = settings.value(TextEditingSettings.self) + section[keyPath: keyPath].toggle() + settings.setValue(section) + } mgr.addCommand( name: "Toggle Type-Over Completion", title: "Toggle Type-Over Completion", id: "prefs.text_editing.type_over_completion" ) { - Settings[\.textEditing].enableTypeOverCompletion.toggle() + toggle(\.enableTypeOverCompletion) } mgr.addCommand( name: "Toggle Autocomplete Braces", title: "Toggle Autocomplete Braces", id: "prefs.text_editing.autocomplete_braces" ) { - Settings[\.textEditing].autocompleteBraces.toggle() + toggle(\.autocompleteBraces) } mgr.addCommand( name: "Toggle Word Wrap", title: "Toggle Word Wrap", id: "prefs.text_editing.wrap_lines_to_editor_width" ) { - Settings[\.textEditing].wrapLinesToEditorWidth.toggle() + toggle(\.wrapLinesToEditorWidth) } mgr.addCommand(name: "Toggle Minimap", title: "Toggle Minimap", id: "prefs.text_editing.toggle_minimap") { - Settings[\.textEditing].showMinimap.toggle() + toggle(\.showMinimap) } mgr.addCommand(name: "Toggle Gutter", title: "Toggle Gutter", id: "prefs.text_editing.toggle_gutter") { - Settings[\.textEditing].showGutter.toggle() + toggle(\.showGutter) } mgr.addCommand( name: "Toggle Folding Ribbon", title: "Toggle Folding Ribbon", id: "prefs.text_editing.toggle_folding_ribbon" ) { - Settings[\.textEditing].showFoldingRibbon.toggle() + toggle(\.showFoldingRibbon) } } } diff --git a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift index c6a373883c..c1a9e55be1 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift @@ -116,7 +116,7 @@ final class CodeEditSplitViewController: NSSplitViewController { navigatorViewModel: NavigatorAreaViewModel, activeEditorState: AppActiveEditorState ) -> NSSplitViewItem { - makeNavigator(view: SettingsInjector { + makeNavigator(view: SettingsInjector(store: dependencies.settingsStore) { NavigatorAreaView(viewModel: navigatorViewModel) .environment(\.workspace, workspace) .environmentObject(workspace.editorManager) @@ -138,7 +138,7 @@ final class CodeEditSplitViewController: NSSplitViewController { activeEditorState: AppActiveEditorState, activeCursorState: AppActiveCursorState ) -> NSSplitViewItem { - let workspaceView = SettingsInjector { + let workspaceView = SettingsInjector(store: dependencies.settingsStore) { WindowObserver(window: WindowBox(value: windowRef)) { WorkspaceView() .environmentObject(workspace.editorManager) @@ -170,7 +170,7 @@ final class CodeEditSplitViewController: NSSplitViewController { activeEditorState: AppActiveEditorState, fileEditorOverrides: AppFileEditorOverrides ) -> NSSplitViewItem { - makeInspector(view: SettingsInjector { + makeInspector(view: SettingsInjector(store: dependencies.settingsStore) { InspectorAreaView(viewModel: InspectorAreaViewModel()) .environmentObject(workspace.editorManager) .environmentObject(workspace.sourceControlManager) diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift index fe95b1d737..1995c06239 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController+Toolbar.swift @@ -167,7 +167,7 @@ extension CodeEditWindowController { case .branchPicker: let toolbarItem = NSToolbarItem(itemIdentifier: .branchPicker) let view = NSHostingView( - rootView: SettingsInjector { + rootView: SettingsInjector(store: dependencies.settingsStore) { ToolbarBranchPicker( fallbackTitle: workspace?.workspaceFileManager.folderUrl.lastPathComponent ?? "Empty", sourceControlManager: workspace?.sourceControlManager @@ -212,8 +212,12 @@ extension CodeEditWindowController { guard let taskManager = workspace?.taskManager else { return nil } + // Wrapped like every other standalone hosting root in this file: `@Environment` does not + // cross the boundary, so without it this subtree reads settings defaults and discards writes. let view = NSHostingView( - rootView: StopTaskToolbarButton(taskManager: taskManager) + rootView: SettingsInjector(store: dependencies.settingsStore) { + StopTaskToolbarButton(taskManager: taskManager) + } ) toolbarItem.view = view @@ -226,8 +230,10 @@ extension CodeEditWindowController { guard let taskManager = workspace?.taskManager else { return nil } let view = NSHostingView( - rootView: StartTaskToolbarButton(taskManager: taskManager) - .environmentObject(utilityAreaModel) + rootView: SettingsInjector(store: dependencies.settingsStore) { + StartTaskToolbarButton(taskManager: taskManager) + .environmentObject(utilityAreaModel) + } ) toolbarItem.view = view @@ -238,7 +244,9 @@ extension CodeEditWindowController { let toolbarItem = NSToolbarItem(itemIdentifier: .notificationItem) guard let workspace = workspace else { return nil } let view = NSHostingView( - rootView: NotificationToolbarItem().environmentObject(notificationPanel) + rootView: SettingsInjector(store: dependencies.settingsStore) { + NotificationToolbarItem().environmentObject(notificationPanel) + } ) toolbarItem.view = view return toolbarItem @@ -252,12 +260,14 @@ extension CodeEditWindowController { else { return nil } let view = NSHostingView( - rootView: ActivityViewer( - workspaceFileManager: workspace?.workspaceFileManager, - workspaceSettingsManager: workspaceSettingsManager, - taskNotificationHandler: taskNotificationHandler, - taskManager: taskManager - ) + rootView: SettingsInjector(store: dependencies.settingsStore) { + ActivityViewer( + workspaceFileManager: workspace?.workspaceFileManager, + workspaceSettingsManager: workspaceSettingsManager, + taskNotificationHandler: taskNotificationHandler, + taskManager: taskManager + ) + } ) let weakWidth = view.widthAnchor.constraint(equalToConstant: 650) diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index 0d80ecba83..e07cb64179 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -169,7 +169,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs panel.close() self.panelOpen = false } - panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) + panel.contentView = NSHostingView( + rootView: SettingsInjector(store: dependencies.settingsStore) { contentView } + ) window?.addChildWindow(panel, ordered: .above) panel.makeKeyAndOrderFront(self) self.panelOpen = true @@ -221,7 +223,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs .environment(\.languageServices, dependencies.languageServicesProvider) .environment(\.currentTheme, ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first!) - panel.contentView = NSHostingView(rootView: SettingsInjector { contentView }) + panel.contentView = NSHostingView( + rootView: SettingsInjector(store: dependencies.settingsStore) { contentView } + ) window?.addChildWindow(panel, ordered: .above) panel.makeKeyAndOrderFront(self) self.panelOpen = true @@ -269,7 +273,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs .environmentObject(workspaceSettingsManager) .environmentObject(taskManager) - settingsWindow.contentView = NSHostingView(rootView: contentView) + settingsWindow.contentView = NSHostingView( + rootView: SettingsInjector(store: dependencies.settingsStore) { contentView } + ) settingsWindow.titlebarAppearsTransparent = true settingsWindow.setContentSize(NSSize(width: 515, height: 515)) settingsWindow.setAccessibilityTitle("Workspace Settings") diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift index e61b91237a..7ad4035a62 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/FileInspectorView.swift @@ -18,6 +18,9 @@ struct FileInspectorView: View { @Environment(\.fileRelocator) private var fileRelocator + @AppSettings(\.general) + private var generalSettings + @AppSettings(\.textEditing) private var textEditing @@ -86,10 +89,12 @@ struct FileInspectorView: View { if let file { TextField("Name", text: $fileName) .background( - fileName != file.fileName() && !file.validateFileName(for: fileName) ? Color(errorRed) : Color.clear + fileName != file.fileName() + && !file.validateFileName(for: fileName, prefs: generalSettings) + ? Color(errorRed) : Color.clear ) .onSubmit { - if file.validateFileName(for: fileName) { + if file.validateFileName(for: fileName, prefs: generalSettings) { let destinationURL = file.url .deletingLastPathComponent() .appending(path: fileName) @@ -103,7 +108,7 @@ struct FileInspectorView: View { } } } else { - fileName = file.labelFileName() + fileName = file.labelFileName(generalSettings) } } } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift index 0c202d8379..9358bda363 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift @@ -11,6 +11,10 @@ import CodeEditSettings import CodeEditCore final class HistoryInspectorModel: ObservableObject { + /// The settings store. Assigned by `HistoryInspectorView` from the environment, alongside the + /// source-control manager — this model is created by a view and configured the same way. + var settingsAccessor: SettingsAccessing = DefaultSettingsReader() + private(set) var sourceControlManager: SourceControlManager? /// The base URL of the workspace @@ -47,7 +51,7 @@ final class HistoryInspectorModel: ObservableObject { branchName: nil, maxCount: 40, fileLocalPath: fileURL, - showMergeCommits: Settings.shared.preferences.sourceControl.git.showMergeCommitsPerFileLog + showMergeCommits: settingsAccessor.value(SourceControlSettings.self).git.showMergeCommitsPerFileLog ) await setCommitHistory(commitHistory) } catch { diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 50c0f5f27f..37f0c0e9f2 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -19,6 +19,9 @@ struct HistoryInspectorView: View { @Environment(\.activeEditorState) private var activeEditorState + @Environment(\.settingsAccessor) + private var settingsAccessor + @ObservedObject private var model: HistoryInspectorModel @State var selection: GitCommit? @@ -55,6 +58,9 @@ struct HistoryInspectorView: View { } } .task { + // The model is created by this view, so this view configures it — the same shape as + // `setWorkspace` below. + model.settingsAccessor = settingsAccessor await model.setWorkspace(sourceControlManager: sourceControlManager) await model.setFile(url: activeEditorState.selectedFile?.url.path()) } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift index 8fbb639c7b..9c68e68656 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift @@ -20,7 +20,6 @@ struct InspectorAreaView: View { init(viewModel: InspectorAreaViewModel) { self.viewModel = viewModel - updateTabs() } private func updateTabs() { @@ -53,6 +52,13 @@ struct InspectorAreaView: View { .formStyle(.grouped) .accessibilityElement(children: .contain) .accessibilityLabel("inspector") + // Seeded here, not in `init`: `showInternalDevelopmentInspector` reads the environment, + // which SwiftUI only populates once the view is in the hierarchy. Called from `init` it + // silently returned the section default — invisible while settings came from a singleton, + // and a hard trap now that they come from the environment. + .onAppear { + updateTabs() + } .onChange(of: showInternalDevelopmentInspector) { _, _ in updateTabs() } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift index e3836833b6..92bc8e81c6 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift @@ -17,7 +17,7 @@ class FileSystemTableViewCell: StandardTableViewCell { var changeLabelLargeWidth: NSLayoutConstraint! var changeLabelSmallWidth: NSLayoutConstraint! - private let prefs = Settings.shared.preferences.general + let prefs: GeneralSettings private var navigatorFilter: String? /// Initializes the `OutlineTableViewCell` with an `icon` and `label` @@ -28,7 +28,16 @@ class FileSystemTableViewCell: StandardTableViewCell { /// - isEditable: Set to true if the user should be able to edit the file name. /// - navigatorFilter: An optional string use to filter the navigator area. /// (Used for bolding and changing primary/secondary color). - init(frame frameRect: NSRect, item: CEWorkspaceFile?, isEditable: Bool = true, navigatorFilter: String? = nil) { + /// - generalSettings: The general settings, by value. AppKit cells cannot read the + /// environment, so the controller that builds them hands the value down. + init( + frame frameRect: NSRect, + item: CEWorkspaceFile?, + isEditable: Bool = true, + navigatorFilter: String? = nil, + generalSettings: GeneralSettings + ) { + self.prefs = generalSettings super.init(frame: frameRect, isEditable: isEditable) self.navigatorFilter = navigatorFilter @@ -48,7 +57,7 @@ class FileSystemTableViewCell: StandardTableViewCell { imageView?.image = item.nsIcon imageView?.contentTintColor = color(for: item) - let fileName = item.labelFileName() + let fileName = item.labelFileName(prefs) let fontSize = textField?.font?.pointSize ?? 12 guard let filter = navigatorFilter?.trimmingCharacters(in: .whitespacesAndNewlines), !filter.isEmpty else { @@ -108,6 +117,7 @@ class FileSystemTableViewCell: StandardTableViewCell { /// *Not Implemented* override init(frame frameRect: NSRect) { + self.prefs = GeneralSettings() super.init(frame: frameRect) fatalError(""" init(frame: ) isn't implemented on `OutlineTableViewCell`. @@ -157,20 +167,22 @@ let errorRed = NSColor(red: 1, green: 0, blue: 0, alpha: 0.2) extension FileSystemTableViewCell: NSTextFieldDelegate { func controlTextDidChange(_ obj: Notification) { guard let fileItem else { return } - textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed + textField?.backgroundColor = + fileItem.validateFileName(for: textField?.stringValue ?? "", prefs: prefs) ? .none : errorRed } func controlTextDidEndEditing(_ obj: Notification) { guard let fileItem else { return } do { - textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed - if fileItem.validateFileName(for: textField?.stringValue ?? "") { + textField?.backgroundColor = + fileItem.validateFileName(for: textField?.stringValue ?? "", prefs: prefs) ? .none : errorRed + if fileItem.validateFileName(for: textField?.stringValue ?? "", prefs: prefs) { let newURL = fileItem.url .deletingLastPathComponent() .appending(path: textField?.stringValue ?? "") try workspace?.workspaceFileManager.move(file: fileItem, to: newURL) } else { - textField?.stringValue = fileItem.labelFileName() + textField?.stringValue = fileItem.labelFileName(prefs) } } catch { let alert = NSAlert(error: error) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 03f832a373..285a0b2dd3 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -24,13 +24,18 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @Environment(\.workspaceNavigator) private var workspaceNavigator - @StateObject var prefs: CodeEditSettings.Settings = .shared + @AppSettings(\.general) + private var generalSettings + + @Environment(\.settingsAccessor) + private var settingsAccessor typealias NSViewControllerType = ProjectNavigatorViewController func makeNSViewController(context: Context) -> ProjectNavigatorViewController { let controller = ProjectNavigatorViewController() - controller.iconColor = prefs.preferences.general.fileIconStyle + controller.generalSettings = generalSettings + controller.settingsAccessor = settingsAccessor controller.activeEditorState = activeEditorState controller.workspaceNavigator = workspaceNavigator @@ -48,11 +53,9 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } func updateNSViewController(_ nsViewController: ProjectNavigatorViewController, context: Context) { - nsViewController.iconColor = prefs.preferences.general.fileIconStyle - nsViewController.rowHeight = prefs.preferences.general.projectNavigatorSize.rowHeight - nsViewController.fileExtensionsVisibility = prefs.preferences.general.fileExtensionsVisibility - nsViewController.shownFileExtensions = prefs.preferences.general.shownFileExtensions - nsViewController.hiddenFileExtensions = prefs.preferences.general.hiddenFileExtensions + nsViewController.settingsAccessor = settingsAccessor + nsViewController.generalSettings = generalSettings + nsViewController.rowHeight = generalSettings.projectNavigatorSize.rowHeight /// if the window becomes active from background, it will restore the selection to outline view. nsViewController.updateSelection(itemID: activeEditorState.selectedFile?.id) return diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index 52c1bf30ed..9112c61784 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditSettings import CodeEditCore protocol OutlineTableViewCellDelegate: AnyObject { @@ -31,9 +32,16 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { item: CEWorkspaceFile?, isEditable: Bool = true, delegate: OutlineTableViewCellDelegate? = nil, - navigatorFilter: String? = nil + navigatorFilter: String? = nil, + generalSettings: GeneralSettings ) { - super.init(frame: frameRect, item: item, isEditable: isEditable, navigatorFilter: navigatorFilter) + super.init( + frame: frameRect, + item: item, + isEditable: isEditable, + navigatorFilter: navigatorFilter, + generalSettings: generalSettings + ) self.textField?.setAccessibilityIdentifier("ProjectNavigatorTableViewCell-\(item?.name ?? "")") self.delegate = delegate } @@ -57,14 +65,15 @@ final class ProjectNavigatorTableViewCell: FileSystemTableViewCell { override func controlTextDidEndEditing(_ obj: Notification) { guard let fileItem else { return } - textField?.backgroundColor = fileItem.validateFileName(for: textField?.stringValue ?? "") ? .none : errorRed - if fileItem.validateFileName(for: textField?.stringValue ?? "") { + textField?.backgroundColor = + fileItem.validateFileName(for: textField?.stringValue ?? "", prefs: prefs) ? .none : errorRed + if fileItem.validateFileName(for: textField?.stringValue ?? "", prefs: prefs) { let destinationURL = fileItem.url .deletingLastPathComponent() .appending(path: textField?.stringValue ?? "") delegate?.moveFile(file: fileItem, to: destinationURL) } else { - textField?.stringValue = fileItem.labelFileName() + textField?.stringValue = fileItem.labelFileName(prefs) } delegate?.cellDidFinishEditing() } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index e1a1ad6bea..7b2e180d13 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -31,7 +31,8 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { frame: frameRect, item: item as? CEWorkspaceFile, delegate: self, - navigatorFilter: workspace?.projectNavigatorViewModel.navigatorFilter + navigatorFilter: workspace?.projectNavigatorViewModel.navigatorFilter, + generalSettings: generalSettings ) return cell } @@ -112,7 +113,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } // If the user has set "Reveal file on selection change" to on or it is forced to reveal, // we need to reveal the item before selecting the row. - if Settings.shared.preferences.general.revealFileOnFocusChange || forcesReveal { + if settingsAccessor.value(GeneralSettings.self).revealFileOnFocusChange || forcesReveal { reveal(item) } let row = outlineView.row(forItem: item) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 1e6f31a257..0e8a49bfc1 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -45,41 +45,31 @@ final class ProjectNavigatorViewController: NSViewController { var workspaceNavigator: WorkspaceNavigator = NoOpWorkspaceNavigator() weak var activeEditorState: (any ActiveEditorState)? - var iconColor: GeneralSettings.FileIconStyle = .color { - willSet { - if newValue != iconColor { - outlineView?.reloadData() - } - } - } + /// The settings store, pushed in from `ProjectNavigatorOutlineView`. AppKit controllers cannot + /// read the SwiftUI environment, so the representable that owns this one hands it down. + var settingsAccessor: SettingsAccessing = DefaultSettingsReader() - // These three drive `CEWorkspaceFile.labelFileName()`, which is read when a cell is built. - // Cells are only built by `outlineView(_:viewFor:)`, so without a reload a preference change - // leaves every visible label showing the text it was born with — the same reason `iconColor` - // and `rowHeight` reload below. - var fileExtensionsVisibility: GeneralSettings.FileExtensionsVisibility = .showAll { - willSet { - if newValue != fileExtensionsVisibility { - outlineView?.reloadData() - } - } - } - - var shownFileExtensions: GeneralSettings.FileExtensions = .default { + /// The general settings, by value — the source for cell construction and the four fields that + /// require a reload when they change. + /// + /// `fileIconStyle` colours the icons; `fileExtensionsVisibility`, `shownFileExtensions` and + /// `hiddenFileExtensions` drive `CEWorkspaceFile.labelFileName(_:)`, which is read when a cell is + /// built. Cells are only built by `outlineView(_:viewFor:)`, so without a reload a preference + /// change leaves every visible label showing the text it was born with — the same reason + /// `rowHeight` reloads below. + var generalSettings: GeneralSettings = .init() { willSet { - if newValue != shownFileExtensions { + if newValue.fileIconStyle != generalSettings.fileIconStyle + || newValue.fileExtensionsVisibility != generalSettings.fileExtensionsVisibility + || newValue.shownFileExtensions != generalSettings.shownFileExtensions + || newValue.hiddenFileExtensions != generalSettings.hiddenFileExtensions { outlineView?.reloadData() } } } - var hiddenFileExtensions: GeneralSettings.FileExtensions = .default { - willSet { - if newValue != hiddenFileExtensions { - outlineView?.reloadData() - } - } - } + /// The icon-colouring preference, kept as a name of its own because it reads as one. + var iconColor: GeneralSettings.FileIconStyle { generalSettings.fileIconStyle } var rowHeight: Double = 22 { willSet { @@ -208,7 +198,7 @@ final class ProjectNavigatorViewController: NSViewController { } else { outlineView.expandItem(item) } - } else if Settings[\.navigation].navigationStyle == .openInTabs { + } else if settingsAccessor.value(NavigationSettings.self).navigationStyle == .openInTabs { workspaceNavigator.open(file: item, asTemporary: false) } } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift index 2e6db5761f..630f1b3598 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift @@ -39,7 +39,7 @@ struct SourceControlNavigatorHistoryView: View { branchName: sourceControlManager.currentBranch?.name, maxCount: nil, fileLocalPath: nil, - showMergeCommits: Settings.shared.preferences.sourceControl.git.showMergeCommitsPerFileLog + showMergeCommits: showMergeCommitsPerFileLog ) await MainActor.run { commitHistory = commits diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift index ec76867e53..2cdfa5787a 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift @@ -21,11 +21,16 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { private let windowManager: WorkspaceWindowManaging private let languageServices: LanguageServicesProvider + /// The settings store, so the standalone hosting root below can inject the settings seam. + private let settingsStore: AppSettingsStore + init( lspService: any LSPServiceProtocol, windowManager: WorkspaceWindowManaging, - languageServices: LanguageServicesProvider + languageServices: LanguageServicesProvider, + settingsStore: AppSettingsStore ) { + self.settingsStore = settingsStore self.lspService = lspService self.windowManager = windowManager self.languageServices = languageServices @@ -36,7 +41,7 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { } func makeWindowContentView(for document: CodeFileDocument) -> NSView { - NSHostingView(rootView: SettingsInjector { + NSHostingView(rootView: SettingsInjector(store: settingsStore) { WindowCodeFileView(codeFile: document) .environment(\.languageServices, languageServices) }) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift index 33877fb19d..f8ac1b02cb 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Files/CEWorkspaceFile+Presentation.swift @@ -48,8 +48,10 @@ extension CEWorkspaceFile { /// silently failed twice over: the raw value for `.txt` was `"text"`, so a user /// entering `txt` never matched, and any extension absent from the `FileType` enum /// fell back to `.txt` and so reported itself as `"text"`. - func labelFileName() -> String { - let prefs = Settings.shared.preferences.general + /// - Parameter prefs: The general settings, passed in by value. This used to read the settings + /// singleton; an extension on a domain type has no injection channel, so its one caller-visible + /// dependency became a parameter instead. + func labelFileName(_ prefs: GeneralSettings) -> String { switch prefs.fileExtensionsVisibility { case .hideAll: return self.fileName(typeHidden: true) @@ -62,8 +64,8 @@ extension CEWorkspaceFile { } } - func validateFileName(for newName: String) -> Bool { - guard newName != labelFileName() && + func validateFileName(for newName: String, prefs: GeneralSettings) -> Bool { + guard newName != labelFileName(prefs) && !newName.isEmpty && newName.isValidFilename && !FileManager.default.fileExists( diff --git a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift index e978a2487a..ec266970be 100644 --- a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift +++ b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift @@ -16,7 +16,7 @@ import CodeEditCore public final class RegistryManager: RegistryManaging { let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "RegistryManager") - let installPath = Settings.shared.baseURL.appending(path: "Language Servers") + let installPath: URL /// The URL of where the registry.json file will be downloaded from let registryURL = URL( @@ -60,8 +60,10 @@ public final class RegistryManager: RegistryManaging { eventBus: EventBus, errorNotifier: ErrorNotifying, shellClient: ShellClientProtocol, - settingsAccessor: SettingsAccessing + settingsAccessor: SettingsAccessing, + installPath: URL ) { + self.installPath = installPath self.eventBus = eventBus self.errorNotifier = errorNotifier self.shellClient = shellClient diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SettingsData.swift b/CodeEditModules/Sources/CodeEditSettings/Models/SettingsData.swift deleted file mode 100644 index d3207231ea..0000000000 --- a/CodeEditModules/Sources/CodeEditSettings/Models/SettingsData.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// Settings.swift -// CodeEditModules/Settings -// -// Created by Lukas Pistrol on 01.04.22. -// - -import SwiftUI -import Foundation - -/// # Settings -/// -/// The model structure of settings for `CodeEdit` -/// -/// A `JSON` representation is persisted in `~/Library/Application Support/CodeEdit/preference.json`. -/// - Attention: Don't use `UserDefaults` for persisting user accessible settings. -/// If a further setting is needed, extend the struct like ``GeneralSettings``, -/// ``ThemeSettings``, or ``TerminalSettings`` does. -/// -/// - Note: Also make sure to implement the ``init(from:)`` initializer, decoding -/// all properties with -/// [`decodeIfPresent`](https://developer.apple.com/documentation/swift/keyeddecodingcontainer/2921389-decodeifpresent) -/// and providing a default value. Otherwise all settings get overridden. -public struct SettingsData: Codable, Hashable { - - /// The general global settings - public var general: GeneralSettings = .init() - - /// The global settings for accounts - public var accounts: AccountsSettings = .init() - - /// The global settings for themes - public var navigation: NavigationSettings = .init() - - /// The global settings for themes - public var theme: ThemeSettings = .init() - - /// The global settings for text editing - public var textEditing: TextEditingSettings = .init() - - /// The global settings for the terminal emulator - public var terminal: TerminalSettings = .init() - - /// The global settings for source control - public var sourceControl: SourceControlSettings = .init() - - /// The global settings for keybindings - public var keybindings: KeybindingsSettings = .init() - - /// Search Settings - public var search: SearchSettings = .init() - - /// Language Server Settings - public var languageServers: LanguageServerSettings = .init() - - /// Developer settings for CodeEdit developers - public var developerSettings: DeveloperSettings = .init() - - /// Default initializer - public init() {} - - /// Explicit decoder init for setting default values when key is not present in `JSON` - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.general = try container.decodeIfPresent(GeneralSettings.self, forKey: .general) ?? .init() - self.accounts = try container.decodeIfPresent(AccountsSettings.self, forKey: .accounts) ?? .init() - self.navigation = try container.decodeIfPresent(NavigationSettings.self, forKey: .navigation) ?? .init() - self.theme = try container.decodeIfPresent(ThemeSettings.self, forKey: .theme) ?? .init() - self.terminal = try container.decodeIfPresent(TerminalSettings.self, forKey: .terminal) ?? .init() - self.textEditing = try container.decodeIfPresent(TextEditingSettings.self, forKey: .textEditing) ?? .init() - self.search = try container.decodeIfPresent(SearchSettings.self, forKey: .search) ?? .init() - self.sourceControl = try container.decodeIfPresent( - SourceControlSettings.self, - forKey: .sourceControl - ) ?? .init() - self.keybindings = try container.decodeIfPresent( - KeybindingsSettings.self, - forKey: .keybindings - ) ?? .init() - self.languageServers = try container.decodeIfPresent( - LanguageServerSettings.self, forKey: .languageServers - ) ?? .init() - self.developerSettings = try container.decodeIfPresent( - DeveloperSettings.self, forKey: .developerSettings - ) ?? .init() - } -} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/AppSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Store/AppSettings.swift deleted file mode 100644 index 70b639212c..0000000000 --- a/CodeEditModules/Sources/CodeEditSettings/Store/AppSettings.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// AppSettings.swift -// CodeEdit -// -// Created by Wouter Hennen on 12/04/2023. -// - -import Foundation -import SwiftUI - -@propertyWrapper -public struct AppSettings: DynamicProperty where T: Equatable { - - var settings: Environment - - let keyPath: WritableKeyPath - - public init(_ keyPath: WritableKeyPath) { - self.keyPath = keyPath - let settingsKeyPath = (\EnvironmentValues.settings).appending(path: keyPath) - self.settings = Environment(settingsKeyPath) - } - - public var wrappedValue: T { - get { - Settings.shared.preferences[keyPath: keyPath] - } - nonmutating set { - Settings.shared.preferences[keyPath: keyPath] = newValue - } - } - - public var projectedValue: Binding { - Binding { - Settings.shared.preferences[keyPath: keyPath] - } set: { - Settings.shared.preferences[keyPath: keyPath] = $0 - } - } -} - -public struct SettingsDataEnvironmentKey: EnvironmentKey { - nonisolated(unsafe) public static var defaultValue: SettingsData = .init() -} - -public extension EnvironmentValues { - /// The app-wide settings model. Views read individual settings through this value (usually via - /// the ``AppSettings`` property wrapper) so they update whenever a setting changes. - var settings: SettingsDataEnvironmentKey.Value { - get { self[SettingsDataEnvironmentKey.self] } - set { self[SettingsDataEnvironmentKey.self] = newValue } - } -} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift deleted file mode 100644 index 37d78f3206..0000000000 --- a/CodeEditModules/Sources/CodeEditSettings/Store/Settings.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// Settings.swift -// CodeEditModules/Settings -// -// Created by Lukas Pistrol on 01.04.22. -// - -import Foundation -import SwiftUI -import Combine - -/// The Preferences View Model. Accessible via the singleton "``SettingsModel/shared``". -/// -/// **Usage:** -/// ```swift -/// @StateObject -/// private var prefs: SettingsModel = .shared -/// ``` -public final class Settings: ObservableObject { - - /// The publicly available singleton instance of ``SettingsModel`` - nonisolated(unsafe) public static let shared: Settings = .init() - - private var store: SettingsStore - private var storeTask: AnyCancellable! - private var revisionTask: AnyCancellable! - - private init() { - self.store = SettingsStore() - self.preferences = .init() - self.preferences = loadSettings() - - self.storeTask = self.$preferences.throttle(for: 2, scheduler: RunLoop.main, latest: true).sink { - try? self.savePreferences($0) - } - // Bumped on the `willSet` emission, i.e. before SwiftUI runs the update pass that the same - // change schedules, so a body evaluated for this change already sees the new revision. - self.revisionTask = self.$preferences.dropFirst().sink { [weak self] _ in - self?.revision &+= 1 - } - } - - public static subscript(_ path: WritableKeyPath, suite: Settings = .shared) -> T { - get { - suite.preferences[keyPath: path] - } - set { - suite.preferences[keyPath: path] = newValue - } - } - - /// Published instance of the ``Settings`` model. - /// - /// Changes are saved automatically. - @Published public var preferences: SettingsData - - /// A counter incremented once per change to ``preferences``. - /// - /// This is the settings seam's **invalidation signal**. Views reach settings through - /// ``SettingsValue``, whose only environment dependency would otherwise be - /// ``EnvironmentValues/settingsAccessor`` — an existential holding a stateless store that never - /// compares unequal to itself. Re-rendering on a settings change would then rest on SwiftUI - /// treating a rewritten non-`Equatable` existential as a change, which is unspecified. - /// An `Int` is `Equatable`, so an injector publishing it into - /// ``EnvironmentValues/settingsRevision`` makes the invalidation explicit and precise: it - /// changes exactly when settings change, and never otherwise. - /// - /// Deliberately not derived from `SettingsData` itself — the seam types must not name the - /// app-wide aggregate. - @Published public private(set) var revision: Int = 0 - - /// Load and construct ``Settings`` model from - /// `~/Library/Application Support/CodeEdit/settings.json` - /// - /// Builds a ``SettingsStore`` from the file on disk (or an empty one if it is absent or - /// unreadable) and populates ``SettingsData`` from it section by section, so sections nothing - /// here decodes are preserved verbatim in ``store`` for the next save. - private func loadSettings() -> SettingsData { - if !filemanager.fileExists(atPath: settingsURL.path) { - try? filemanager.createDirectory(at: baseURL, withIntermediateDirectories: false) - self.store = SettingsStore() - return .init() - } - - guard let json = try? Data(contentsOf: settingsURL), - let loadedStore = try? SettingsStore(data: json) - else { - self.store = SettingsStore() - return .init() - } - self.store = loadedStore - - var data = SettingsData() - data.general = store[GeneralSettings.self] - data.accounts = store[AccountsSettings.self] - data.navigation = store[NavigationSettings.self] - data.theme = store[ThemeSettings.self] - data.textEditing = store[TextEditingSettings.self] - data.terminal = store[TerminalSettings.self] - data.sourceControl = store[SourceControlSettings.self] - data.keybindings = store[KeybindingsSettings.self] - data.search = store[SearchSettings.self] - data.languageServers = store[LanguageServerSettings.self] - data.developerSettings = store[DeveloperSettings.self] - return data - } - - /// Save ``Settings`` model to - /// `~/Library/Application Support/CodeEdit/settings.json` - /// - /// Writes each ``SettingsData`` field back into ``store`` before encoding, so sections the - /// store holds but ``SettingsData`` has no field for (an unknown or disabled extension's - /// configuration) are re-emitted unchanged. - private func savePreferences(_ data: SettingsData) throws { - store[GeneralSettings.self] = data.general - store[AccountsSettings.self] = data.accounts - store[NavigationSettings.self] = data.navigation - store[ThemeSettings.self] = data.theme - store[TextEditingSettings.self] = data.textEditing - store[TerminalSettings.self] = data.terminal - store[SourceControlSettings.self] = data.sourceControl - store[KeybindingsSettings.self] = data.keybindings - store[SearchSettings.self] = data.search - store[LanguageServerSettings.self] = data.languageServers - store[DeveloperSettings.self] = data.developerSettings - - let prettyJSON = try store.encoded() - try prettyJSON.write(to: settingsURL, options: .atomic) - } - - /// Default instance of the `FileManager` - private let filemanager = FileManager.default - - /// The base URL of settings. - /// - /// Points to `~/Library/Application Support/CodeEdit/` - public var baseURL: URL { - filemanager - .homeDirectoryForCurrentUser - .appending(path: "Library/Application Support/CodeEdit", directoryHint: .isDirectory) - } - - /// The URL of the `settings.json` settings file. - /// - /// Points to `~/Library/Application Support/CodeEdit/settings.json` - private var settingsURL: URL { - baseURL - .appending(path: "settings") - .appendingPathExtension("json") - } -} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsLocation.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsLocation.swift new file mode 100644 index 0000000000..d839216dde --- /dev/null +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsLocation.swift @@ -0,0 +1,30 @@ +// +// SettingsLocation.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 10/08/26. +// + +import Foundation + +/// Where CodeEdit's user-editable configuration lives on disk. +/// +/// Namespaced constants, not a service: a filesystem path has no state, no lifetime and nothing to +/// inject. It was previously reachable only through `Settings.shared.baseURL`, which made every +/// consumer of the *path* a consumer of the *singleton*. Splitting it out is what let that singleton +/// be deleted without inventing an injection channel for a constant. +public enum SettingsLocation { + /// `~/Library/Application Support/CodeEdit/` + public static var baseURL: URL { + FileManager.default + .homeDirectoryForCurrentUser + .appending(path: "Library/Application Support/CodeEdit", directoryHint: .isDirectory) + } + + /// `~/Library/Application Support/CodeEdit/settings.json` + public static var settingsFileURL: URL { + baseURL + .appending(path: "settings") + .appendingPathExtension("json") + } +} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index d7462795da..47e2571b16 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -99,7 +99,7 @@ public extension EnvironmentValues { set { self[SettingsAccessorKey.self] = newValue } } - /// Changes once per settings change; see ``Settings/revision``. + /// Changes once per settings change; see the app-side `AppSettingsStore.revision`. /// /// The seam's invalidation signal, kept in its own `Equatable` key rather than folded into /// ``settingsAccessor``. Two keys, two jobs: the accessor answers *what the value is* and is @@ -107,7 +107,7 @@ public extension EnvironmentValues { /// changed*. That separation is what lets a non-observing injection point (`appServices(_:)`) /// supply the accessor without also having to fake a change signal it cannot compute. /// - /// Injected by any view that observes ``Settings``. A subtree that receives an accessor but no + /// Injected by any view that observes the store. A subtree that receives an accessor but no /// revision reads correct values and never re-renders on change — inject both, or neither. var settingsRevision: Int { get { self[SettingsRevisionKey.self] } diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index 88b9d13595..c562581777 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -26,13 +26,36 @@ struct SettingsFormatTests { /// /// The fixture deliberately holds no default values: a fixture of defaults would pass even if /// decoding silently fell back to defaults, which is the failure this guards against. + /// + /// It used to round-trip a `SettingsData`, which is no longer possible: that aggregate is an + /// app-side façade over this store and is not `Codable`. Every section is instead read *and + /// written back* through the store, which is the same work: reading alone would prove nothing, + /// because an unread section is re-emitted as the raw JSON it came in as. Writing the decoded + /// value back replaces the stored JSON with whatever the Swift type produces — exactly the step + /// that could silently drop or rename a key. @Test func fullSettingsRoundTripsUnchanged() throws { let original = try fixture("full-settings") - let decoded = try JSONDecoder().decode(SettingsData.self, from: original) - let reencoded = try JSONEncoder().encode(decoded) + let store = try SettingsStore(data: original) - #expect(try parsed(reencoded) == parsed(original)) + func reencode(_ type: S.Type) { + let decoded = store[S.self] + store[S.self] = decoded + } + + reencode(GeneralSettings.self) + reencode(AccountsSettings.self) + reencode(NavigationSettings.self) + reencode(ThemeSettings.self) + reencode(TextEditingSettings.self) + reencode(TerminalSettings.self) + reencode(SourceControlSettings.self) + reencode(KeybindingsSettings.self) + reencode(SearchSettings.self) + reencode(LanguageServerSettings.self) + reencode(DeveloperSettings.self) + + #expect(try parsed(store.encoded()) == parsed(original)) } /// Section keys must match the JSON keys `SettingsData` already uses, or existing settings diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/AppSettingsStoreTests.swift new file mode 100644 index 0000000000..b1da9fe465 --- /dev/null +++ b/CodeEditTests/App/AppSettingsStoreTests.swift @@ -0,0 +1,119 @@ +// +// AppSettingsStoreTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 09/08/2026. +// + +import Foundation +import Testing +import CodeEditSettings +@testable import CodeEdit + +/// Verifies `AppSettingsStore` reads and writes real, persisted settings rather than answering with +/// section defaults like `DefaultSettingsReader` would. A test that only checked defaults would pass +/// against either implementation and prove nothing. +/// +/// Each test builds its own store over a temporary file. The predecessor of this suite mutated the +/// `Settings.shared` singleton and had to restore what it found, serialize against every other suite +/// that touched settings, and could never assert anything about the file on disk. That is the +/// concrete payoff of moving ownership into the composition root. +@MainActor +struct AppSettingsStoreTests { + + /// A store over a fresh temporary `settings.json` that no other test can see. + private func makeStore(seed: String? = nil) throws -> (AppSettingsStore, URL) { + let directory = URL.temporaryDirectory.appending(path: "AppSettingsStoreTests-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let url = directory.appending(path: "settings.json") + if let seed { + try Data(seed.utf8).write(to: url) + } + return (AppSettingsStore(settingsURL: url), url) + } + + @Test + func readsAValueSeededOnDisk() throws { + // Non-default: `SourceControlGeneral.sourceControlIsEnabled` defaults to `true`, so + // `DefaultSettingsReader` (or a store that never read the file) could not produce `false`. + let (store, _) = try makeStore(seed: #"{"sourceControl":{"general":{"sourceControlIsEnabled":false}}}"#) + + #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) + } + + @Test + func writtenSectionReadsBack() throws { + let (store, _) = try makeStore() + + var section = store.value(SourceControlSettings.self) + #expect(section.general.sourceControlIsEnabled == true, "starts at its default") + section.general.sourceControlIsEnabled = false + store.setValue(section) + + #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) + } + + @Test + func aWriteBumpsTheRevision() throws { + let (store, _) = try makeStore() + let before = store.revision + + var section = store.value(TerminalSettings.self) + section.cursorBlink = !section.cursorBlink + store.setValue(section) + + #expect(store.revision == before + 1) + } + + /// The relaunch guarantee: a write must reach disk on its own, without anyone calling `save`. + /// + /// This is what the `throttle(for: 2, scheduler: RunLoop.main)` pipeline exists for, so the poll + /// window is deliberately several times the interval rather than a fixed sleep of exactly it. + @Test + func aWriteReachesDiskThroughTheThrottle() async throws { + let (store, url) = try makeStore() + + var section = store.value(LanguageServerSettings.self) + section.installedLanguageServers = [ + "round-trip-test": .init(packageName: "round-trip-test", isEnabled: false, version: "9.9.9") + ] + store.setValue(section) + + var attempts = 0 + while !FileManager.default.fileExists(atPath: url.path) && attempts < 120 { + attempts += 1 + try? await Task.sleep(for: .milliseconds(50)) + } + + let reloaded = AppSettingsStore(settingsURL: url) + let readBack = reloaded.value(LanguageServerSettings.self).installedLanguageServers["round-trip-test"] + #expect(readBack?.version == "9.9.9", "a settings write did not survive to disk") + #expect(readBack?.isEnabled == false) + } + + /// A section this build has no field for must survive a save, or a disabled extension loses its + /// configuration the first time anything else is written. + @Test + func unknownSectionsSurviveAWrite() async throws { + let (store, url) = try makeStore(seed: #"{"someFutureFeature":{"keep":"me"}}"#) + + var section = store.value(GeneralSettings.self) + section.fileIconStyle = .monochrome + store.setValue(section) + + var attempts = 0 + while attempts < 120 { + attempts += 1 + try? await Task.sleep(for: .milliseconds(50)) + if let data = try? Data(contentsOf: url), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + object["general"] != nil { + break + } + } + + let data = try Data(contentsOf: url) + let object = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + #expect((object["someFutureFeature"] as? [String: String])?["keep"] == "me") + } +} diff --git a/CodeEditTests/App/LegacySettingsStoreTests.swift b/CodeEditTests/App/LegacySettingsStoreTests.swift deleted file mode 100644 index 8980f0b147..0000000000 --- a/CodeEditTests/App/LegacySettingsStoreTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// LegacySettingsStoreTests.swift -// CodeEditTests -// -// Created by Matthijs Eikelenboom on 09/08/2026. -// - -import Foundation -import Testing -import CodeEditSettings -@testable import CodeEdit - -/// Verifies `LegacySettingsStore` actually reads and writes through to `Settings.shared`, rather -/// than answering with section defaults like `DefaultSettingsReader` would. A test that only -/// checked defaults would pass against either implementation and prove nothing about the bridge. -/// -/// These tests mutate a process-wide singleton, so each one restores what it found. They are -/// `@MainActor` and `.serialized` because `Settings` is an `ObservableObject` with live SwiftUI -/// observers in the test host: mutating `preferences` off the main thread drives an AppKit update -/// from a cooperative-pool thread and trips the Main Thread Checker, and two of them running -/// concurrently would also race on the save/restore of the same field. -@MainActor -@Suite(.serialized) -struct LegacySettingsStoreTests { - @Test - func readsMutatedValueFromTheSingleton() throws { - let originalValue = Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled - defer { - Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = originalValue - } - - // Non-default: `SourceControlGeneral.sourceControlIsEnabled` defaults to `true`, so - // `DefaultSettingsReader` (or an unmapped fallback) would never produce `false` here. - Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = false - - let store: SettingsReading = LegacySettingsStore() - #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) - } - - @Test - func writtenSectionReachesTheSingletonAndReadsBack() throws { - let originalValue = Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled - defer { - Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = originalValue - } - // Start from the default so the assertion below cannot pass on pre-existing state. - Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled = true - - let store: SettingsAccessing = LegacySettingsStore() - var section = store.value(SourceControlSettings.self) - section.general.sourceControlIsEnabled = false - store.setValue(section) - - // Round trip through the seam... - #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false) - // ...and, crucially, into `Settings.shared.preferences` itself, which is what the - // `@Published` → throttle → `savePreferences` pipeline persists. A write that only - // mutated a copy would pass the line above and still lose the value on relaunch. - #expect(Settings.shared.preferences.sourceControl.general.sourceControlIsEnabled == false) - } - - @Test - func writtenSectionIsVisibleToAnIndependentReader() throws { - let original = Settings.shared.preferences.languageServers.installedLanguageServers - defer { - Settings.shared.preferences.languageServers.installedLanguageServers = original - } - Settings.shared.preferences.languageServers.installedLanguageServers = [:] - - let writer: SettingsAccessing = LegacySettingsStore() - var section = writer.value(LanguageServerSettings.self) - section.installedLanguageServers = [ - "round-trip-test": .init(packageName: "round-trip-test", isEnabled: false, version: "9.9.9") - ] - writer.setValue(section) - - // A *different* instance, to prove the value lives in the store and not in the writer. - let reader: SettingsReading = LegacySettingsStore() - let readBack = reader.value(LanguageServerSettings.self).installedLanguageServers["round-trip-test"] - #expect(readBack?.version == "9.9.9") - #expect(readBack?.isEnabled == false) - } -} diff --git a/CodeEditTests/App/SettingsSeamInvalidationTests.swift b/CodeEditTests/App/SettingsSeamInvalidationTests.swift index 478a96e9f9..58c8ca6fb0 100644 --- a/CodeEditTests/App/SettingsSeamInvalidationTests.swift +++ b/CodeEditTests/App/SettingsSeamInvalidationTests.swift @@ -11,12 +11,12 @@ import Testing import CodeEditSettings @testable import CodeEdit -/// Covers the settings seam's *invalidation* half: that a change to `Settings.shared` actually -/// re-renders a view reading through `@SettingsValue`. +/// Covers the settings seam's *invalidation* half: that a change to the store actually re-renders a +/// view reading through `@SettingsValue`. /// /// This is the production path end to end — `SettingsInjector` → `\.settingsRevision` + -/// `LegacySettingsStore` → `@SettingsValue` — not a stand-in. It exists because the read/write -/// tests next door pass just as happily when nothing ever re-renders: they render once. +/// `AppSettingsStore` → `@SettingsValue` — not a stand-in. It exists because the read/write tests +/// next door pass just as happily when nothing ever re-renders: they render once. /// /// **What it does not prove.** It is a guard on the *outcome*, not on the mechanism: it passes with /// `\.settingsRevision` injected and without it. Measured while writing it — with the revision @@ -26,13 +26,23 @@ import CodeEditSettings /// state whenever an ancestor's body reruns. That is the unspecified behaviour the revision key /// replaces with a documented one, so this test cannot distinguish the two and should not be read /// as evidence that it can. -/// -/// `TerminalSettings.cursorBlink` is chosen because no other suite touches it. The suites that -/// mutate `Settings.shared` are not serialized against each other, so sharing a field with -/// `LegacySettingsStoreTests` or `FileExtensionVisibilityTests` would be a real race. @MainActor @Suite(.serialized) struct SettingsSeamInvalidationTests { + + /// A store over a temporary file, so these tests neither read nor overwrite the developer's real + /// `settings.json` — and cannot race any other suite. + private func makeStore() throws -> AppSettingsStore { + let directory = URL.temporaryDirectory.appending(path: "SettingsSeam-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return AppSettingsStore(settingsURL: directory.appending(path: "settings.json")) + } + + private func setCursorBlink(_ value: Bool, on store: AppSettingsStore) { + var section = store.value(TerminalSettings.self) + section.cursorBlink = value + store.setValue(section) + } /// Records the value observed on every body evaluation, so a *missing* re-render is a visible /// absence rather than a stale-but-plausible reading. private final class BodyRecorder { @@ -77,13 +87,13 @@ struct SettingsSeamInvalidationTests { } @Test - func changingSettingsBumpsTheRevision() { - let original = Settings.shared.preferences.terminal.cursorBlink - defer { Settings.shared.preferences.terminal.cursorBlink = original } + func changingSettingsBumpsTheRevision() throws { + let store = try makeStore() + let before = store.revision + + setCursorBlink(!store.value(TerminalSettings.self).cursorBlink, on: store) - let before = Settings.shared.revision - Settings.shared.preferences.terminal.cursorBlink = !original - #expect(Settings.shared.revision == before + 1) + #expect(store.revision == before + 1) } /// Guards the **outcome** users care about: a settings change reaches a view reading through the @@ -96,12 +106,11 @@ struct SettingsSeamInvalidationTests { /// deliver updated values at all — the failure mode that has recurred most on this branch. @Test func settingsChangeReachesAViewThroughTheSeam() async throws { - let original = Settings.shared.preferences.terminal.cursorBlink - defer { Settings.shared.preferences.terminal.cursorBlink = original } + let store = try makeStore() + setCursorBlink(false, on: store) - Settings.shared.preferences.terminal.cursorBlink = false let recorder = BodyRecorder() - let (window, hostingView) = host(SettingsInjector { RevisionProbe(recorder: recorder) }) + let (window, hostingView) = host(SettingsInjector(store: store) { RevisionProbe(recorder: recorder) }) defer { window.contentView = nil } await settle(hostingView) { !recorder.observed.isEmpty } @@ -110,7 +119,7 @@ struct SettingsSeamInvalidationTests { // the one below: defaults can never *change*.) #expect(recorder.observed.last == false) - Settings.shared.preferences.terminal.cursorBlink = true + setCursorBlink(true, on: store) await settle(hostingView) { recorder.observed.last == true } #expect( diff --git a/CodeEditTests/Features/LSP/Registry.swift b/CodeEditTests/Features/LSP/Registry.swift index 75f35e0f75..c34b29f53a 100644 --- a/CodeEditTests/Features/LSP/Registry.swift +++ b/CodeEditTests/Features/LSP/Registry.swift @@ -20,7 +20,10 @@ struct RegistryTests { eventBus: EventBus(), errorNotifier: NoOpErrorNotifier(), shellClient: ShellClient(), - settingsAccessor: RecordingSettingsStore() + settingsAccessor: RecordingSettingsStore(), + // The same path the manager used to read off the settings singleton, so these tests keep + // exercising the real install location. + installPath: SettingsLocation.baseURL.appending(path: "Language Servers") ) // MARK: - Download Tests diff --git a/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift b/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift index 43f8c2a527..b047b8eb93 100644 --- a/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift +++ b/CodeEditTests/Features/LSP/RegistryManagerPersistenceTests.swift @@ -34,7 +34,8 @@ struct RegistryManagerPersistenceTests { eventBus: EventBus(), errorNotifier: NoOpErrorNotifier(), shellClient: ShellClient(), - settingsAccessor: store + settingsAccessor: store, + installPath: URL.temporaryDirectory.appending(path: "RegistryManagerPersistenceTests") ) } diff --git a/CodeEditTests/Features/Tasks/TaskManagerTests.swift b/CodeEditTests/Features/Tasks/TaskManagerTests.swift index 5560f7643d..36ec8168bc 100644 --- a/CodeEditTests/Features/Tasks/TaskManagerTests.swift +++ b/CodeEditTests/Features/Tasks/TaskManagerTests.swift @@ -37,7 +37,9 @@ class TaskManagerTests { @Test func executeTaskInZsh() async throws { - Settings.shared.preferences.terminal.shell = .zsh + // Deliberately configures no shell. The shell preference reaches a task through + // `TerminalEmulatorView`'s `@SettingsValue`, which this headless test never constructs, so + // the singleton write that used to stand here changed nothing about what ran. let task = CETask(name: "Test Task", command: "echo 'Hello World'") tasksConfiguration.tasks.append(task) @@ -56,7 +58,7 @@ class TaskManagerTests { @Test func executeTaskInBash() async throws { - Settings.shared.preferences.terminal.shell = .bash + // See `executeTaskInZsh` — the shell preference never reached this path. let task = CETask(name: "Test Task", command: "echo 'Hello World'") tasksConfiguration.tasks.append(task) diff --git a/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift b/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift index d07a50632b..f8234009ee 100644 --- a/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift +++ b/CodeEditTests/Features/Workspace/FileExtensionVisibilityTests.swift @@ -10,77 +10,75 @@ import CodeEditCore import CodeEditSettings @testable import CodeEdit -/// Covers `CEWorkspaceFile.labelFileName()`, which had no tests despite driving every -/// Project Navigator row label. Mutates the `Settings.shared` singleton, so the original -/// general settings are restored in `tearDown`. +/// Covers `CEWorkspaceFile.labelFileName(_:)`, which had no tests despite driving every +/// Project Navigator row label. +/// +/// It used to mutate the `Settings.shared` singleton and restore it in `tearDown`; the settings it +/// reads are now a parameter, so each case builds the exact `GeneralSettings` it means and no +/// process-wide state is touched. final class FileExtensionVisibilityTests: XCTestCase { - private var original: GeneralSettings! - - override func setUp() { - super.setUp() - original = Settings.shared.preferences.general - } - - override func tearDown() { - Settings.shared.preferences.general = original - original = nil - super.tearDown() + private func settings( + _ visibility: GeneralSettings.FileExtensionsVisibility, + shown: [String] = [], + hidden: [String] = [] + ) -> GeneralSettings { + var settings = GeneralSettings() + settings.fileExtensionsVisibility = visibility + settings.shownFileExtensions.extensions = shown + settings.hiddenFileExtensions.extensions = hidden + return settings } - private func label(_ filename: String) -> String { - CEWorkspaceFile(url: URL(filePath: "/tmp/\(filename)")).labelFileName() + private func label(_ filename: String, _ settings: GeneralSettings) -> String { + CEWorkspaceFile(url: URL(filePath: "/tmp/\(filename)")).labelFileName(settings) } func testShowAllKeepsEveryExtension() { - Settings.shared.preferences.general.fileExtensionsVisibility = .showAll - XCTAssertEqual(label("notes.txt"), "notes.txt") - XCTAssertEqual(label("Model.swift"), "Model.swift") + let settings = settings(.showAll) + XCTAssertEqual(label("notes.txt", settings), "notes.txt") + XCTAssertEqual(label("Model.swift", settings), "Model.swift") } func testHideAllStripsEveryExtension() { - Settings.shared.preferences.general.fileExtensionsVisibility = .hideAll - XCTAssertEqual(label("notes.txt"), "notes") - XCTAssertEqual(label("Model.swift"), "Model") + let settings = settings(.hideAll) + XCTAssertEqual(label("notes.txt", settings), "notes") + XCTAssertEqual(label("Model.swift", settings), "Model") } func testShowOnlyKeepsListedAndStripsTheRest() { - Settings.shared.preferences.general.fileExtensionsVisibility = .showOnly - Settings.shared.preferences.general.shownFileExtensions.extensions = ["swift"] - XCTAssertEqual(label("Model.swift"), "Model.swift") - XCTAssertEqual(label("notes.txt"), "notes") + let settings = settings(.showOnly, shown: ["swift"]) + XCTAssertEqual(label("Model.swift", settings), "Model.swift") + XCTAssertEqual(label("notes.txt", settings), "notes") } func testHideOnlyStripsListedAndKeepsTheRest() { - Settings.shared.preferences.general.fileExtensionsVisibility = .hideOnly - Settings.shared.preferences.general.hiddenFileExtensions.extensions = ["swift"] - XCTAssertEqual(label("Model.swift"), "Model") - XCTAssertEqual(label("notes.txt"), "notes.txt") + let settings = settings(.hideOnly, hidden: ["swift"]) + XCTAssertEqual(label("Model.swift", settings), "Model") + XCTAssertEqual(label("notes.txt", settings), "notes.txt") } /// Regression for 2715e319. Matching used to compare `FileType.rawValue`, whose value for /// `.txt` was the string `"text"` — so entering `txt` never matched anything. func testTxtIsMatchableByItsRealExtension() { - Settings.shared.preferences.general.fileExtensionsVisibility = .showOnly - Settings.shared.preferences.general.shownFileExtensions.extensions = ["txt"] - XCTAssertEqual(label("notes.txt"), "notes.txt") - XCTAssertEqual(label("Model.swift"), "Model") + let settings = settings(.showOnly, shown: ["txt"]) + XCTAssertEqual(label("notes.txt", settings), "notes.txt") + XCTAssertEqual(label("Model.swift", settings), "Model") } /// Regression for 2715e319. Extensions absent from the old `FileType` enum all fell back to /// `.txt` and reported themselves as `"text"`, so the preference could never match them. func testExtensionsAbsentFromTheOldEnumAreMatchable() { - Settings.shared.preferences.general.fileExtensionsVisibility = .hideOnly - Settings.shared.preferences.general.hiddenFileExtensions.extensions = ["toml"] - XCTAssertEqual(label("Config.toml"), "Config") - XCTAssertEqual(label("notes.txt"), "notes.txt") + let settings = settings(.hideOnly, hidden: ["toml"]) + XCTAssertEqual(label("Config.toml", settings), "Config") + XCTAssertEqual(label("notes.txt", settings), "notes.txt") } func testExtensionlessNamesAreUnaffected() { for mode in [GeneralSettings.FileExtensionsVisibility.hideAll, .showAll] { - Settings.shared.preferences.general.fileExtensionsVisibility = mode - XCTAssertEqual(label("LICENSE"), "LICENSE", "mode \(mode)") - XCTAssertEqual(label("Makefile"), "Makefile", "mode \(mode)") + let settings = settings(mode) + XCTAssertEqual(label("LICENSE", settings), "LICENSE", "mode \(mode)") + XCTAssertEqual(label("Makefile", settings), "Makefile", "mode \(mode)") } } } From 4b3c0c75e6045b4795e4b05bd32913ed8b81ac93 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 16:43:25 +0200 Subject: [PATCH 258/335] Fix: Configure settings-backed singletons at composition-root construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installing them from applicationDidFinishLaunching could be beaten by application(_:open:) at cold start, so opening a folder from Finder built a workspace window against an unconfigured ThemeModel — a debug trap, and a release crash on ThemeModel.shared.themes.first!. Also preserve an unreadable settings.json as settings.json.corrupt- before falling back to an empty store, so the first write cannot destroy settings that were recoverable by hand. --- CodeEdit/App/AppDelegate.swift | 9 --- CodeEdit/App/AppDependencies.swift | 33 +++++++++++ .../Settings/AppSettingsStore.swift | 33 +++++++++++ CodeEditTests/App/AppSettingsStoreTests.swift | 55 +++++++++++++++++++ .../App/SettingsInstallOrderTests.swift | 48 ++++++++++++++++ 5 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 CodeEditTests/App/SettingsInstallOrderTests.swift diff --git a/CodeEdit/App/AppDelegate.swift b/CodeEdit/App/AppDelegate.swift index 97c0828c6d..60e9d50bd2 100644 --- a/CodeEdit/App/AppDelegate.swift +++ b/CodeEdit/App/AppDelegate.swift @@ -35,19 +35,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var cancellables = Set() - /// Hands the settings store to the three pre-existing singletons that cannot take it through - /// `init`. Composition-root privilege: nothing else may do this. - private func installSettingsStore() { - ThemeModel.shared.configure(settings: dependencies.settingsAccessor) - FeedbackModel.shared.settingsAccessor = dependencies.settingsAccessor - SearchSettingsModel.shared.configure(settings: dependencies.settingsAccessor) - } - func applicationDidFinishLaunching(_ notification: Notification) { CodeFileDocument.isAutoSaveOnProvider = { [settings = dependencies.settingsAccessor] in settings.value(GeneralSettings.self).isAutoSaveOn } - installSettingsStore() enableWindowSizeSaveOnQuit() dependencies.settingsAccessor.value(GeneralSettings.self).appAppearance.applyAppearance() checkForFilesToOpen() diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index 4727fb4a82..a6886b9b4c 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -90,6 +90,39 @@ final class AppDependencies { private(set) lazy var languageServicesProvider: LanguageServicesProvider = AppLanguageServicesProvider(lspService: lspService) + // MARK: - Wiring that must not wait for a delegate callback + + init() { + installSettingsStore() + } + + /// Hands the settings store to the three pre-existing singletons that cannot take it through + /// their own `init`, before anything can read them. + /// + /// Deliberately here and not in an `AppDelegate` callback. It first lived in + /// `applicationDidFinishLaunching`, which `application(_:open urls:)` can beat: launching by + /// double-clicking a folder in Finder, or via a `codeedit://` URL at cold start, opens a + /// workspace window before that callback runs. `ThemeModel` would then still hold + /// `DefaultSettingsReader` — a debug trap, and in release a crash, because + /// `CodeEditWindowController` force-unwraps `ThemeModel.shared.themes.first!` on an array that + /// never loaded. The singleton these replaced was immune to that ordering; this is. + /// + /// `AppDelegate` holds `dependencies` as a non-lazy stored property, so this runs while the + /// delegate itself is being initialized — before *any* delegate callback, not merely before the + /// one that happened to be a problem. Moving it to `applicationWillFinishLaunching` would fix + /// today's ordering and leave the next earlier callback free to reintroduce it. + /// + /// The cost is that `settingsStore` and `ThemeModel` are built eagerly rather than on first use, + /// which front-loads reading `settings.json` and the theme files. Both are read by the first + /// window anyway, so this moves the work earlier rather than adding it. + private func installSettingsStore() { + ThemeModel.shared.configure(settings: settingsAccessor) + FeedbackModel.shared.settingsAccessor = settingsAccessor + SearchSettingsModel.shared.configure(settings: settingsAccessor) + } + + // MARK: - Command-interface adapters, continued + private(set) lazy var codeFileDocumentDelegate: CodeFileDocumentDelegate = AppCodeFileDocumentDelegate( lspService: lspService, diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift b/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift index e0f9b66241..55cb906815 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift @@ -86,6 +86,19 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { // MARK: - Persistence /// Builds the store from `settings.json`, or an empty one when the file is absent or unreadable. + /// + /// The two failure cases are **not** equivalent and are deliberately handled differently. A file + /// that is *absent* means a first launch, and an empty store is the correct answer. A file that + /// is *present but unreadable* means the user has settings that something here failed to parse — + /// a bug in this code, a half-written file, a manual edit with a stray comma. Answering with an + /// empty store is still the only way to keep launching, but the first subsequent write would + /// then persist that emptiness over the original, destroying settings that were very likely + /// recoverable by hand. + /// + /// So the original is copied aside first, and the copy is what makes the fallback survivable. + /// The alternative considered — refusing to save until the user explicitly re-saves — was + /// rejected: it turns one silent failure into another (every later change is dropped with no + /// indication), and it still loses the file the moment anything does write. private static func loadStore(at url: URL) -> SettingsStore { let fileManager = FileManager.default @@ -100,11 +113,31 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { guard let json = try? Data(contentsOf: url), let loaded = try? SettingsStore(data: json) else { + preserveUnreadableFile(at: url) return SettingsStore() } return loaded } + /// Copies an unreadable `settings.json` to `settings.json.corrupt-` before anything + /// can overwrite it. + /// + /// Copied rather than moved, so the app still finds a file where it expects one, and so a + /// failure to copy cannot itself destroy the original. `copyItem` is used rather than re-reading + /// the bytes because the file may have failed to *read*, not merely to parse. + private static func preserveUnreadableFile(at url: URL) { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + // Colon-free: legal on APFS, but a filename with colons reads as a path separator in the + // Finder and in plenty of shell tooling, and this file exists to be found and inspected. + formatter.dateFormat = "yyyy-MM-dd'T'HH-mm-ss'Z'" + + let backup = url.appendingPathExtension("corrupt-\(formatter.string(from: Date()))") + guard !FileManager.default.fileExists(atPath: backup.path) else { return } + try? FileManager.default.copyItem(at: url, to: backup) + } + /// Writes every section — including the ones nothing here decodes — to `settings.json`. /// /// The write is `.atomic` so an interrupted save cannot leave a truncated file where the user's diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/AppSettingsStoreTests.swift index b1da9fe465..cae67ab57b 100644 --- a/CodeEditTests/App/AppSettingsStoreTests.swift +++ b/CodeEditTests/App/AppSettingsStoreTests.swift @@ -91,6 +91,61 @@ struct AppSettingsStoreTests { #expect(readBack?.isEnabled == false) } + /// An unparseable `settings.json` must be copied aside **before** the empty store it falls back + /// to can be written over it. + /// + /// The fallback itself is not the bug — the app has to launch. Losing the original is, and it is + /// silent: the user sees settings reset to defaults and has nothing left to recover from. + @Test + func anUnreadableFileIsPreservedBeforeTheFirstWrite() async throws { + let corruptContents = #"{"general": {"fileIconStyle": "colo"# // truncated mid-write + let (store, url) = try makeStore(seed: corruptContents) + + // Loading fell back to defaults, as it must to keep launching. + #expect(store.value(GeneralSettings.self) == GeneralSettings()) + + // The copy must exist *before* any write, not be made on the way out. + let backups = try backupFiles(besides: url) + #expect(backups.count == 1, "the unreadable file was not copied aside") + #expect(try String(contentsOf: try #require(backups.first), encoding: .utf8) == corruptContents) + + // And the original is still where the app expects it, so the copy is a copy, not a move. + #expect(FileManager.default.fileExists(atPath: url.path)) + + // Now let a write land and confirm the preserved copy is untouched by it. + var section = store.value(GeneralSettings.self) + section.fileIconStyle = .monochrome + store.setValue(section) + await settle { (try? Data(contentsOf: url))?.count != corruptContents.utf8.count } + + let after = try backupFiles(besides: url) + #expect(after.count == 1) + #expect(try String(contentsOf: try #require(after.first), encoding: .utf8) == corruptContents) + } + + /// A *missing* file is the first-launch case: an empty store is correct and nothing is copied. + @Test + func anAbsentFileIsNotTreatedAsCorruption() throws { + let (store, url) = try makeStore() + + #expect(store.value(GeneralSettings.self) == GeneralSettings()) + #expect(try backupFiles(besides: url).isEmpty, "a first launch must not leave a corrupt-copy") + } + + private func backupFiles(besides url: URL) throws -> [URL] { + try FileManager.default + .contentsOfDirectory(at: url.deletingLastPathComponent(), includingPropertiesForKeys: nil) + .filter { $0.lastPathComponent.contains(".corrupt-") } + } + + private func settle(until: () -> Bool) async { + var attempts = 0 + while !until() && attempts < 120 { + attempts += 1 + try? await Task.sleep(for: .milliseconds(50)) + } + } + /// A section this build has no field for must survive a save, or a disabled extension loses its /// configuration the first time anything else is written. @Test diff --git a/CodeEditTests/App/SettingsInstallOrderTests.swift b/CodeEditTests/App/SettingsInstallOrderTests.swift new file mode 100644 index 0000000000..1eeabf482a --- /dev/null +++ b/CodeEditTests/App/SettingsInstallOrderTests.swift @@ -0,0 +1,48 @@ +// +// SettingsInstallOrderTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 10/08/2026. +// + +import Testing +import CodeEditSettings +@testable import CodeEdit + +/// Guards the *consequence* of the settings store reaching `ThemeModel` too late. +/// +/// `ThemeModel.shared` is configured from `AppDependencies.init`, which runs while `AppDelegate`'s +/// non-lazy `dependencies` property is initialized — before any delegate callback. It previously ran +/// in `applicationDidFinishLaunching`, which `application(_:open urls:)` can beat: opening a folder +/// from Finder at cold start builds a workspace window first, and `CodeEditWindowController` +/// force-unwraps `ThemeModel.shared.themes.first!`. On an unconfigured model that array is empty and +/// the app crashes in release. +/// +/// **What this proves and does not.** The ordering itself is structural — it holds because +/// `dependencies` is a stored `let`, not because of anything asserted here — and this test cannot +/// distinguish it from the old ordering, since the app host has fully launched by the time any test +/// runs. What it does guard is the invariant that made the misordering fatal rather than merely +/// wrong: that by the time anything can open a window, the themes are loaded and that force-unwrap +/// is safe. A future change that leaves `ThemeModel` unconfigured at window-open time fails here +/// instead of in a user's crash log. +@MainActor +struct SettingsInstallOrderTests { + + @Test + func themesAreLoadedBeforeAnyWindowCanOpen() { + #expect( + !ThemeModel.shared.themes.isEmpty, + "CodeEditWindowController force-unwraps ThemeModel.shared.themes.first!" + ) + } + + /// The model reads through a real store, not `DefaultSettingsReader`. + /// + /// Asserted through behaviour rather than by inspecting the accessor: a configured model has + /// resolved a selected theme from the store, which a defaults-only reader cannot produce + /// alongside a loaded theme list. + @Test + func themeModelResolvedASelectionFromTheStore() { + #expect(ThemeModel.shared.selectedTheme != nil) + } +} From 0b1c5b281c6e27565c9dd83f665f121feaf43add Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 16:54:52 +0200 Subject: [PATCH 259/335] Refactor: Move SourceControlSettings into CESourceControl --- .../AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift | 1 + CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift | 1 + CodeEditModules/Package.swift | 2 +- .../Models => CESourceControl}/SourceControlSettings.swift | 1 + .../Tests/CodeEditSettingsTests/SettingsFormatTests.swift | 1 + CodeEditTests/App/AppSettingsStoreTests.swift | 1 + CodeEditTests/App/SettingsValueWriteTests.swift | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) rename CodeEditModules/Sources/{CodeEditSettings/Models => CESourceControl}/SourceControlSettings.swift (99%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift index e9d7c42a8d..1d08429e9f 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift @@ -6,6 +6,7 @@ // import CELSP +import CESourceControl import Foundation import CodeEditSettings diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift index 43bfba884e..cd397fe512 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift @@ -5,6 +5,7 @@ // Created by Lukas Pistrol on 01.04.22. // +import CESourceControl import CodeEditSettings /// # SettingsData diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index c6aa9e86ef..559dbfd265 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -121,7 +121,7 @@ let package = Package( .testTarget(name: "CodeEditCoreTests", dependencies: ["CodeEditCore"]), .testTarget( name: "CodeEditSettingsTests", - dependencies: ["CodeEditSettings"], + dependencies: ["CodeEditSettings", "CESourceControl"], resources: [.copy("Fixtures")] ), .testTarget(name: "CodeEditUIUnitTests", dependencies: ["CodeEditUI"]), diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift b/CodeEditModules/Sources/CESourceControl/SourceControlSettings.swift similarity index 99% rename from CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlSettings.swift index 2856cabee0..c5fa4834e4 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlSettings.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlSettings.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/04/08. // +import CodeEditSettings import Foundation /// The global settings for source control diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index c562581777..2ccfd0bee0 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import CESourceControl @testable import CodeEditSettings struct SettingsFormatTests { diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/AppSettingsStoreTests.swift index cae67ab57b..4347d631cb 100644 --- a/CodeEditTests/App/AppSettingsStoreTests.swift +++ b/CodeEditTests/App/AppSettingsStoreTests.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 09/08/2026. // +import CESourceControl import Foundation import Testing import CodeEditSettings diff --git a/CodeEditTests/App/SettingsValueWriteTests.swift b/CodeEditTests/App/SettingsValueWriteTests.swift index 13d488a99e..0cb82d1433 100644 --- a/CodeEditTests/App/SettingsValueWriteTests.swift +++ b/CodeEditTests/App/SettingsValueWriteTests.swift @@ -6,6 +6,7 @@ // import AppKit +import CESourceControl import SwiftUI import Testing import CodeEditSettings From 5336c3dce152544ef967b5c7aec5f83db7254c19 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 17:02:42 +0200 Subject: [PATCH 260/335] Refactor: Move TerminalSettings into CETerminal --- .../TerminalSettings/TerminalSettingsView.swift | 1 + .../Settings/Search/SettingsSearchKeys.swift | 1 + .../AuxiliaryWindows/Settings/SettingsData.swift | 1 + CodeEditModules/Package.swift | 2 +- .../Models => CETerminal}/TerminalSettings.swift | 15 +++++++++++++++ .../Store/CodableDefault+Providers.swift | 14 -------------- .../SettingsFormatTests.swift | 1 + CodeEditTests/App/AppSettingsStoreTests.swift | 1 + .../App/SettingsSeamInvalidationTests.swift | 1 + 9 files changed, 22 insertions(+), 15 deletions(-) rename CodeEditModules/Sources/{CodeEditSettings/Models => CETerminal}/TerminalSettings.swift (88%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift index 212df2de82..d496d8eebb 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/TerminalSettings/TerminalSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CETerminal import CodeEditSettings struct TerminalSettingsView: View { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift index 1d08429e9f..24502dee99 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift @@ -7,6 +7,7 @@ import CELSP import CESourceControl +import CETerminal import Foundation import CodeEditSettings diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift index cd397fe512..5d41efab00 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift @@ -6,6 +6,7 @@ // import CESourceControl +import CETerminal import CodeEditSettings /// # SettingsData diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 559dbfd265..145bea846a 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -121,7 +121,7 @@ let package = Package( .testTarget(name: "CodeEditCoreTests", dependencies: ["CodeEditCore"]), .testTarget( name: "CodeEditSettingsTests", - dependencies: ["CodeEditSettings", "CESourceControl"], + dependencies: ["CodeEditSettings", "CESourceControl", "CETerminal"], resources: [.copy("Fixtures")] ), .testTarget(name: "CodeEditUIUnitTests", dependencies: ["CodeEditUI"]), diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift b/CodeEditModules/Sources/CETerminal/TerminalSettings.swift similarity index 88% rename from CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift rename to CodeEditModules/Sources/CETerminal/TerminalSettings.swift index 560cdee682..cfaeb0c5fc 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/TerminalSettings.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalSettings.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditSettings import Foundation /// The global settings for the terminal emulator @@ -97,3 +98,17 @@ public struct TerminalSettings: SettingsSection { } } } + +// MARK: - Defaults + +public enum DefaultTerminalShell: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = TerminalSettings.Shell.system +} + +public enum DefaultTerminalCursorStyle: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = TerminalSettings.CursorStyle.block +} + +public enum DefaultTerminalFont: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = TerminalSettings.Font() +} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift index 92bf356b81..cf62a1884a 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift @@ -17,20 +17,6 @@ public enum DefaultFalse: DefaultValueProvider { public static let defaultValue = false } -// MARK: - Terminal Defaults - -public enum DefaultTerminalShell: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = TerminalSettings.Shell.system -} - -public enum DefaultTerminalCursorStyle: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = TerminalSettings.CursorStyle.block -} - -public enum DefaultTerminalFont: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = TerminalSettings.Font() -} - // MARK: - Navigation Defaults public enum DefaultNavigationStyle: DefaultValueProvider { diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index 2ccfd0bee0..c241043681 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation import CESourceControl +import CETerminal @testable import CodeEditSettings struct SettingsFormatTests { diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/AppSettingsStoreTests.swift index 4347d631cb..c736e3dabb 100644 --- a/CodeEditTests/App/AppSettingsStoreTests.swift +++ b/CodeEditTests/App/AppSettingsStoreTests.swift @@ -6,6 +6,7 @@ // import CESourceControl +import CETerminal import Foundation import Testing import CodeEditSettings diff --git a/CodeEditTests/App/SettingsSeamInvalidationTests.swift b/CodeEditTests/App/SettingsSeamInvalidationTests.swift index 58c8ca6fb0..274c46ca6f 100644 --- a/CodeEditTests/App/SettingsSeamInvalidationTests.swift +++ b/CodeEditTests/App/SettingsSeamInvalidationTests.swift @@ -6,6 +6,7 @@ // import AppKit +import CETerminal import SwiftUI import Testing import CodeEditSettings From 6a616db1a2e5b2251c0c2d976579afa8fd352efe Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 17:06:11 +0200 Subject: [PATCH 261/335] Refactor: Move LanguageServerSettings into CELSP --- CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift | 1 + CodeEditModules/Package.swift | 2 +- .../Models => CELSP}/LanguageServerSettings.swift | 7 +++++++ .../CodeEditSettings/Store/CodableDefault+Providers.swift | 6 ------ .../Tests/CodeEditSettingsTests/SettingsFormatTests.swift | 1 + CodeEditTests/App/AppSettingsStoreTests.swift | 1 + 6 files changed, 11 insertions(+), 7 deletions(-) rename CodeEditModules/Sources/{CodeEditSettings/Models => CELSP}/LanguageServerSettings.swift (80%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift index 5d41efab00..1f2810c717 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsData.swift @@ -5,6 +5,7 @@ // Created by Lukas Pistrol on 01.04.22. // +import CELSP import CESourceControl import CETerminal import CodeEditSettings diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 145bea846a..8071ae1b56 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -121,7 +121,7 @@ let package = Package( .testTarget(name: "CodeEditCoreTests", dependencies: ["CodeEditCore"]), .testTarget( name: "CodeEditSettingsTests", - dependencies: ["CodeEditSettings", "CESourceControl", "CETerminal"], + dependencies: ["CodeEditSettings", "CELSP", "CESourceControl", "CETerminal"], resources: [.copy("Fixtures")] ), .testTarget(name: "CodeEditUIUnitTests", dependencies: ["CodeEditUI"]), diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift b/CodeEditModules/Sources/CELSP/LanguageServerSettings.swift similarity index 80% rename from CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift rename to CodeEditModules/Sources/CELSP/LanguageServerSettings.swift index 69c7ee9531..a6bb5fcb88 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/LanguageServerSettings.swift +++ b/CodeEditModules/Sources/CELSP/LanguageServerSettings.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 2/2/25. // +import CodeEditSettings import Foundation public struct LanguageServerSettings: SettingsSection { @@ -31,3 +32,9 @@ public struct LanguageServerSettings: SettingsSection { } } } + +// MARK: - Defaults + +public enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue: [String: LanguageServerSettings.Installed] = [:] +} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift index cf62a1884a..352cafb455 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift @@ -5,8 +5,6 @@ // Created by Matthijs Eikelenboom on 07.04.26. // -import AppKit - // MARK: - Bool Defaults public enum DefaultTrue: DefaultValueProvider { @@ -33,10 +31,6 @@ public enum DefaultEmptyStringDictionary: DefaultValueProvider { public static let defaultValue: [String: String] = [:] } -public enum DefaultEmptyLanguageServerDictionary: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue: [String: LanguageServerSettings.Installed] = [:] -} - // MARK: - Account Defaults public enum DefaultGitAccounts: DefaultValueProvider { diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index c241043681..b9fad69d5c 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import CELSP import CESourceControl import CETerminal @testable import CodeEditSettings diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/AppSettingsStoreTests.swift index c736e3dabb..d6967486b0 100644 --- a/CodeEditTests/App/AppSettingsStoreTests.swift +++ b/CodeEditTests/App/AppSettingsStoreTests.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 09/08/2026. // +import CELSP import CESourceControl import CETerminal import Foundation From 154414a541008a9fbb8deb205d2b2483e876f4df Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 19:21:57 +0200 Subject: [PATCH 262/335] Docs: Retitle the manifest MARKs and fix stale settings docs Package.swift's Kernel/Shared substrate/Features MARKs no longer matched the measured graph now that CodeEditSettings holds the seam, Codable machinery, and theme. Split into kernel / shared substrate / editor substrate / app-linked services / features, verified against actual import counts. ARCHITECTURE.md's hub-heuristic watch item and settings section still described Settings.shared and LegacySettingsStore, both removed earlier in this phase; re-counted CodeEditSettings's AppKit/SwiftUI imports and rewrote the settings-access bullets to describe AppSettingsStore. Consumer-count litmus (task 9, step 2) found no unambiguous single-consumer eviction: KeyboardShortcutWrapper looked like one but is also used internally by KeybindingsSettings, so it stays. --- CodeEditModules/Package.swift | 8 +++++++- docs/ARCHITECTURE.md | 37 +++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 8071ae1b56..479dbe481a 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -47,6 +47,10 @@ let package = Package( dependencies: [.product(name: "CodeEditSymbols", package: "CodeEditSymbols")], resources: [.process("Resources")] ), + .target(name: "CodeEditSettings", dependencies: ["CodeEditCore"]), + + // MARK: - Editor substrate + // CodeFileDocument + editor-framework bridging; consumed only by CEEditor and CELSP. .target( name: "CodeEditDocument", dependencies: [ @@ -57,7 +61,9 @@ let package = Package( .product(name: "TextStory", package: "TextStory") ] ), - .target(name: "CodeEditSettings", dependencies: ["CodeEditCore"]), + + // MARK: - App-linked services + // Zero package-internal consumers; the app target links these directly. .target(name: "ShellClient", dependencies: ["CodeEditCore"]), .target(name: "CEWorkspaceFileManager", dependencies: ["CodeEditCore"]), diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7bfca640e1..cd5b35215b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -17,10 +17,11 @@ CodeEdit.xcworkspace ├── Sources/ │ ├── CodeEditCore — pure types, EventBus, command interfaces (no UI/IO, zero deps) │ ├── CodeEditUI — shared presentation atoms (→ CodeEditSymbols only) + │ ├── CodeEditSettings — settings seam + store + theme (UI pages stay app-side) │ ├── CodeEditDocument — CodeFileDocument + editor-framework bridging protocols - │ ├── CodeEditSettings — settings model + store (UI pages stay app-side) - │ ├── ShellClient — Process adapter - │ ├── CEWorkspaceFileManager — FileManager + FSEvents workspace tree + │ │ (consumed only by CEEditor and CELSP) + │ ├── ShellClient — Process adapter (app-linked; no package-internal consumer) + │ ├── CEWorkspaceFileManager — FileManager + FSEvents workspace tree (app-linked, ditto) │ └── CEEditor, CESearch, CENotifications, CELSP, CESourceControl, CETerminal │ — one target per feature └── Tests/ — CodeEditCoreTests, CodeEditUIUnitTests, CESearchTests, @@ -114,8 +115,10 @@ where it is visible to everyone. Acyclicity itself needs no rule — SwiftPM enf **Hub heuristic.** Any target both depended on by three or more others *and* itself depending on three or more is a hub under review. 2022's `AppPreferences` was exactly this and would have been -flagged years before it became fatal. `CodeEditSettings` is the current watch item: four dependents, -and it imports `AppKit` in 3 files and `SwiftUI` in 9. +flagged years before it became fatal. `CodeEditSettings` is the current watch item: four dependents +(`CEEditor`, `CELSP`, `CESourceControl`, `CETerminal`) but only one dependency (`CodeEditCore`), so +it stays a well-formed shared substrate rather than a hub — and it imports `AppKit` in 1 file and +`SwiftUI` in 7. ## Where does my code go? @@ -216,22 +219,26 @@ never by naming the app-wide `SettingsData` aggregate. Three roles, pick by cons Access is **section-granular**: `value(_:)`/`setValue(_:)` deal in whole `SettingsSection` values, so a caller changing one field reads its section, mutates it and writes it back. -- **`@AppSettings` is app-target only.** It reads the `Settings.shared` singleton directly and is - what ~30 app-target files still use. Feature packages must not use it; new app-target code - should prefer the seam. +- **`@AppSettings` is app-target only.** It resolves through the same `settingsAccessor`/ + `settingsRevision` environment as `SettingsValue`, addressing a section field through the + app-wide `SettingsData` façade instead of naming one section directly. There is no + `Settings.shared` singleton any more — `AppSettingsStore` (owned by `AppDependencies`) is the + concrete accessor, injected like everything else. `@AppSettings` is what ~30 app-target files + still use; feature packages must not use it, and new app-target code should prefer the seam. - **`@Environment` does not cross an `NSHostingView`/`NSHostingController` boundary.** A new standalone hosting root must be given `.appServices(_:)` or wrapped in `SettingsInjector`, or its subtree falls back to `DefaultSettingsReader` — plausible defaults, and **writes discarded**. That fallback `assertionFailure`s outside SwiftUI previews precisely because it is otherwise silent. - **Invalidation is explicit.** `SettingsValue` also depends on the `Equatable` - `\.settingsRevision` environment key, fed from `Settings.revision`. Rewriting the accessor is not - a re-render signal: it is a stateless value behind an existential. Any injection point that - *observes* `Settings` supplies the revision (`SettingsInjector`, `CodeEditApp`); `appServices(_:)` - observes nothing, so it supplies the accessor only. -- **`LegacySettingsStore` is a stopgap.** It is the concrete accessor today, bridging to - `Settings.shared` so writes reach the existing throttled save pipeline. A section-keyed store - replaces it in a later slice. + `\.settingsRevision` environment key, fed from `AppSettingsStore.revision`. Rewriting the + accessor is not a re-render signal: it is a stateless value behind an existential. Any injection + point that *observes* the store supplies the revision (`SettingsInjector`, `CodeEditApp`); + `appServices(_:)` observes nothing, so it supplies the accessor only. +- **`AppSettingsStore` is the concrete accessor.** Section-keyed storage, owned by + `AppDependencies` (no `shared`), driving the same throttled save pipeline `Settings.shared` used + to own. Sections nothing here decodes are held verbatim and re-emitted on save, so a disabled + extension's configuration survives. ## Creating a new feature target From 1e0667963f2d79f1d0c8b8b2afdea86cd363780b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 19:44:13 +0200 Subject: [PATCH 263/335] Fix: Inject the settings store into the menu-bar Commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CodeEditCommands` and `ViewCommands` read settings through `@AppSettings`, which now resolves via `@Environment`. `Commands` content is not part of the view hierarchy — `.commands { }` attaches beside a scene's content, not inside it — so whether `SettingsSceneInjector`'s environment reaches there is undocumented. Menu items that write user settings must not rest on that: if it stopped holding, Font Size and the Jump Bar toggle would become silent no-ops, the Source Control group would show unconditionally, and `DefaultSettingsReader` would trap in debug while the menu bar is built. Both conformers now take `AppSettingsStore` by initializer and observe it. `ObservableObject` observation inside a `Commands` conformer is already load-bearing here — it is what keeps the Show/Hide titles in the same menu current — so this removes an unspecified dependency without adding a new one. Behaviour is unchanged, including the independent 1...288 clamping of the editor and terminal font sizes. --- CodeEdit/App/MenuBar/CodeEditCommands.swift | 21 ++++- CodeEdit/App/MenuBar/ViewCommands.swift | 91 +++++++++++++++------ 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/CodeEdit/App/MenuBar/CodeEditCommands.swift b/CodeEdit/App/MenuBar/CodeEditCommands.swift index fad981919b..ef0b65ca7b 100644 --- a/CodeEdit/App/MenuBar/CodeEditCommands.swift +++ b/CodeEdit/App/MenuBar/CodeEditCommands.swift @@ -6,19 +6,32 @@ // import SwiftUI -import CodeEditSettings struct CodeEditCommands: Commands { let dependencies: AppDependencies - @AppSettings(\.sourceControl.general.sourceControlIsEnabled) - private var sourceControlIsEnabled + /// The settings store, taken from `dependencies` rather than from the environment. + /// + /// See ``ViewCommands/settingsStore`` for why: `Commands` content is not part of the view + /// hierarchy, so `@Environment` — and with it `@AppSettings` — cannot be relied on here. Without + /// a real store this menu would build with `DefaultSettingsReader`, trapping in debug and showing + /// the Source Control group unconditionally in release. + @ObservedObject private var settingsStore: AppSettingsStore + + init(dependencies: AppDependencies) { + self.dependencies = dependencies + self.settingsStore = dependencies.settingsStore + } + + private var sourceControlIsEnabled: Bool { + SettingsData(accessor: settingsStore).sourceControl.general.sourceControlIsEnabled + } var body: some Commands { Group { // SwiftUI limits to 9 items in an initializer, so we have to group every 9 items. MainCommands() FileCommands(windowManager: dependencies.workspaceWindowManager) - ViewCommands() + ViewCommands(settingsStore: settingsStore) FindCommands() NavigateCommands() TasksCommands() diff --git a/CodeEdit/App/MenuBar/ViewCommands.swift b/CodeEdit/App/MenuBar/ViewCommands.swift index 277133f9f5..ca77461039 100644 --- a/CodeEdit/App/MenuBar/ViewCommands.swift +++ b/CodeEdit/App/MenuBar/ViewCommands.swift @@ -6,18 +6,23 @@ // import SwiftUI -import CodeEditSettings -import Combine struct ViewCommands: Commands { - @AppSettings(\.textEditing.font.size) - var editorFontSize - @AppSettings(\.terminal.font.size) - var terminalFontSize - @AppSettings(\.general.showEditorJumpBar) - var showEditorJumpBar - @AppSettings(\.general.dimEditorsWithoutFocus) - var dimEditorsWithoutFocus + + /// The settings store, handed in by `CodeEditCommands` rather than read from the environment. + /// + /// `Commands` content is **not** part of the view hierarchy: `.commands { }` attaches to a + /// `Scene` beside its content, so whether the `.environment` values `SettingsSceneInjector` + /// applies to that content also reach here is undocumented SwiftUI behaviour. Menu items that + /// read *and write* user settings must not rest on it — if it ever stopped holding, Font Size + /// and the Jump Bar toggle would become silent no-ops and `DefaultSettingsReader` would trap + /// while the menu bar is built. + /// + /// Observed, not merely held: the menu reflects settings state (the Jump Bar item's title, the + /// Dim-editors check mark), so it has to re-evaluate when they change. `ObservableObject` + /// observation inside a `Commands` conformer is already load-bearing here — it is how + /// ``UpdatingWindowController`` keeps the Show/Hide titles below current. + @ObservedObject private var settingsStore: AppSettingsStore @FocusedBinding(\.navigationSplitViewVisibility) var navigationSplitViewVisibility @@ -27,6 +32,48 @@ struct ViewCommands: Commands { @UpdatingWindowController var windowController: CodeEditWindowController? + init(settingsStore: AppSettingsStore) { + self.settingsStore = settingsStore + } + + /// A fresh façade over the store. Stateless, so building one per access is free. + private var settings: SettingsData { + SettingsData(accessor: settingsStore) + } + + /// The same read-modify-write `AppSettings`' `projectedValue` performs, without the environment. + private func binding(_ keyPath: WritableKeyPath) -> Binding { + Binding { + settings[keyPath: keyPath] + } set: { newValue in + var settings = SettingsData(accessor: settingsStore) + settings[keyPath: keyPath] = newValue + } + } + + /// Nudges the editor and terminal font sizes together, each clamped independently so one already + /// at the limit does not stop the other from moving. Both bounds match the Text Editing and + /// Terminal settings pages. + private func adjustFontSizes(by delta: Double) { + var settings = SettingsData(accessor: settingsStore) + + let editorSize = settings.textEditing.font.size + if (delta > 0 && editorSize < 288) || (delta < 0 && editorSize > 1) { + settings.textEditing.font.size = editorSize + delta + } + + let terminalSize = settings.terminal.font.size + if (delta > 0 && terminalSize < 288) || (delta < 0 && terminalSize > 1) { + settings.terminal.font.size = terminalSize + delta + } + } + + private func resetFontSizes() { + var settings = SettingsData(accessor: settingsStore) + settings.textEditing.font.size = 12 + settings.terminal.font.size = 12 + } + var body: some Commands { CommandGroup(after: .toolbar) { Button("Show Command Palette") { @@ -41,30 +88,19 @@ struct ViewCommands: Commands { Menu("Font Size") { Button("Increase") { - if editorFontSize < 288 { - editorFontSize += 1 - } - if terminalFontSize < 288 { - terminalFontSize += 1 - } + adjustFontSizes(by: 1) } .keyboardShortcut("+") Button("Decrease") { - if editorFontSize > 1 { - editorFontSize -= 1 - } - if terminalFontSize > 1 { - terminalFontSize -= 1 - } + adjustFontSizes(by: -1) } .keyboardShortcut("-") Divider() Button("Reset") { - editorFontSize = 12 - terminalFontSize = 12 + resetFontSizes() } .keyboardShortcut("0", modifiers: [.command, .control]) } @@ -81,11 +117,12 @@ struct ViewCommands: Commands { Divider() - Button("\(showEditorJumpBar ? "Hide" : "Show") Jump Bar") { - showEditorJumpBar.toggle() + Button("\(settings.general.showEditorJumpBar ? "Hide" : "Show") Jump Bar") { + var settings = SettingsData(accessor: settingsStore) + settings.general.showEditorJumpBar.toggle() } - Toggle("Dim editors without focus", isOn: $dimEditorsWithoutFocus) + Toggle("Dim editors without focus", isOn: binding(\.general.dimEditorsWithoutFocus)) Divider() From 7ed38fcd819f34e88465706685cb9db9c1ea8555 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 19:44:28 +0200 Subject: [PATCH 264/335] Fix: Preserve a section this build cannot decode before replacing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `.corrupt-` copy only fired on a whole-file load failure, so a file that loads fine but holds one undecodable section had no backup. A hand-edited `"theme": []` reads as defaults, and the first theme change persists those defaults over it. The raw JSON was already retained and re-emitted verbatim, so the loss is narrower than it looks: it happens only on the first write of that same section. `SettingsStore` now announces exactly that moment through `willReplaceUndecodableSection`, before the assignment and while the original is still on disk, and `AppSettingsStore` answers it with the same copy-aside the whole-file path uses — once per store, since the copy covers the whole file. The hook is on the write rather than the read deliberately: a read loses nothing, and every intervening save re-emits the raw value, so the file is still intact when the handler runs. Four tests cover it, asserting the original survives rather than that the section reads as defaults — the latter passes against the broken behaviour. Blanking the report block fails three of them. --- .../Settings/AppSettingsStore.swift | 19 ++++++++ .../Store/SettingsStore.swift | 41 ++++++++++++++-- .../SettingsFormatTests.swift | 42 +++++++++++++++++ CodeEditTests/App/AppSettingsStoreTests.swift | 47 +++++++++++++++++++ 4 files changed, 145 insertions(+), 4 deletions(-) diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift b/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift index 55cb906815..df71a3f2fa 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift @@ -50,10 +50,25 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { /// temporary file instead of the user's real `settings.json`. private let settingsURL: URL + /// One copy-aside per store, however many sections turn out to be undecodable: the copy is of the + /// whole file, so the first one already contains every one of them. + private var hasPreservedOriginal = false + init(settingsURL: URL = SettingsLocation.settingsFileURL) { self.settingsURL = settingsURL self.store = Self.loadStore(at: settingsURL) + // The whole-file copy above only covers a file that failed to *load*. A file that loads fine + // but holds one section this build cannot decode is the same loss at a smaller scale: the + // section reads as defaults, and the first write of it replaces the user's JSON with values + // they never chose. `SettingsStore` announces exactly that moment, before it happens and + // while the original is still on disk, so the same copy-aside applies. + self.store.willReplaceUndecodableSection = { [weak self] _ in + guard let self, !self.hasPreservedOriginal else { return } + self.hasPreservedOriginal = true + Self.preserveUnreadableFile(at: settingsURL) + } + self.saveTask = saveRequests .throttle(for: 2, scheduler: RunLoop.main, latest: true) .sink { [weak self] in @@ -122,6 +137,10 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { /// Copies an unreadable `settings.json` to `settings.json.corrupt-` before anything /// can overwrite it. /// + /// Called from two places: a whole file that failed to load, and — via + /// `SettingsStore.willReplaceUndecodableSection` — the first write that would replace a single + /// section this build could not decode. + /// /// Copied rather than moved, so the app still finds a file where it expects one, and so a /// failure to copy cannot itself destroy the original. `copyItem` is used rather than re-reading /// the bytes because the file may have failed to *read*, not merely to parse. diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift index ba7a629711..0e10929a23 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift @@ -14,6 +14,11 @@ import Foundation /// store decoded only known keys and wrote only known keys, so a file written by a newer build lost /// keys when an older build saved. /// +/// A section that is present but *undecodable* is read as its defaults, but is likewise held and +/// re-emitted verbatim — so a hand-edit this build cannot parse survives every save until something +/// writes that same section back. That write is announced through +/// ``willReplaceUndecodableSection`` so the owner can preserve the original first. +/// /// Not `@MainActor`: it is constructed and used from the main actor in practice, but isolating it /// would break the deliberately-nonisolated ``SettingsAccessing`` conformance added in Task 3. public final class SettingsStore { @@ -23,6 +28,24 @@ public final class SettingsStore { private let decoder = JSONDecoder() private let encoder = JSONEncoder() + /// Called with the section key immediately before a write replaces a stored value that this + /// build could **not** decode. + /// + /// This is the store's one destructive moment, and the only one. A section that fails to decode + /// reads as `S()`, but the raw JSON stays in `sections` and ``encoded()`` re-emits it verbatim, + /// so the user's original survives every save — right up until something writes *that* section + /// back, which replaces it with values that were never theirs. Nothing else in this type can lose + /// data. + /// + /// Deliberately a callback rather than a policy: this target has no business knowing where the + /// file lives or what "preserve" means. The owner (`AppSettingsStore`) copies the file aside. + /// Called synchronously and before the replacement, so the handler still sees the original both + /// in this store and on disk. + public var willReplaceUndecodableSection: ((String) -> Void)? + + /// Keys already reported, so a section written repeatedly reports once. + private var reportedUndecodableSections: Set = [] + /// An empty store, with every section at its defaults. public init() { self.sections = [:] @@ -37,10 +60,7 @@ public final class SettingsStore { /// undecodable. public subscript(_ type: S.Type) -> S { get { - guard let raw = sections[S.settingsKey], - let data = try? encoder.encode(raw), - let decoded = try? decoder.decode(S.self, from: data) - else { + guard let raw = sections[S.settingsKey], let decoded = decode(raw, as: S.self) else { return S() } return decoded @@ -52,10 +72,23 @@ public final class SettingsStore { assertionFailure("Section '\(S.settingsKey)' failed to round-trip through JSONValue") return } + // Report *before* the assignment: this is the one point at which a value the user wrote + // by hand, and this build could not read, stops existing. + if let existing = sections[S.settingsKey], + decode(existing, as: S.self) == nil, + reportedUndecodableSections.insert(S.settingsKey).inserted { + willReplaceUndecodableSection?(S.settingsKey) + } sections[S.settingsKey] = raw } } + /// Decodes one stored section, or `nil` when this build cannot read it. + private func decode(_ raw: JSONValue, as type: S.Type) -> S? { + guard let data = try? encoder.encode(raw) else { return nil } + return try? decoder.decode(S.self, from: data) + } + /// Every section read from disk, including ones nothing is registered to decode. public func encoded() throws -> Data { let data = try encoder.encode(sections) diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index b9fad69d5c..825af5f97c 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -126,6 +126,48 @@ struct SettingsFormatTests { #expect(reloaded[TerminalSettings.self].cursorBlink == true, "untouched field survived") } + /// A section this build cannot decode must survive a save untouched for as long as nothing + /// writes it — reading it as defaults must not make those defaults the stored value. + @Test + func anUndecodableSectionIsReEmittedVerbatim() throws { + let original = Data(#"{"theme":["hand","edited"],"general":{"fileIconStyle":"monochrome"}}"#.utf8) + let store = try SettingsStore(data: original) + + #expect(store[ThemeSettings.self] == ThemeSettings(), "an unreadable section reads as defaults") + + // A write to an *unrelated* section must not take the broken one down with it. + var general = store[GeneralSettings.self] + general.fileIconStyle = .color + store[GeneralSettings.self] = general + + let after = try parsed(store.encoded()) + #expect(after["theme"] as? NSArray == ["hand", "edited"] as NSArray) + } + + /// Writing a section whose stored value could not be decoded is the one destructive operation in + /// the store, and it must announce itself before it happens. + /// + /// The handler is what lets `AppSettingsStore` copy the file aside first. Asserting on the values + /// instead would prove nothing: they are the same defaults either way. + @Test + func replacingAnUndecodableSectionIsAnnouncedOnce() throws { + let store = try SettingsStore(data: Data(#"{"theme":[],"general":{}}"#.utf8)) + + var announced: [String] = [] + store.willReplaceUndecodableSection = { announced.append($0) } + + // A decodable section is replaced silently — nothing of the user's is lost. + store[GeneralSettings.self] = store[GeneralSettings.self] + #expect(announced.isEmpty) + + store[ThemeSettings.self] = store[ThemeSettings.self] + #expect(announced == ["theme"]) + + // The original is gone now, so a repeat write has nothing left to announce. + store[ThemeSettings.self] = store[ThemeSettings.self] + #expect(announced == ["theme"]) + } + /// Loading and saving through `Settings` must not drop sections it has no field for. /// /// This is the guarantee extensions depend on: a user who disables an extension must not lose diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/AppSettingsStoreTests.swift index d6967486b0..a78bf7f4e5 100644 --- a/CodeEditTests/App/AppSettingsStoreTests.swift +++ b/CodeEditTests/App/AppSettingsStoreTests.swift @@ -126,6 +126,53 @@ struct AppSettingsStoreTests { #expect(try String(contentsOf: try #require(after.first), encoding: .utf8) == corruptContents) } + /// A file that loads but holds **one** undecodable section must be copied aside before the write + /// that replaces that section, and the copy must still hold the user's original text. + /// + /// The scenario is a hand-edit: `"theme": []` is valid JSON but not a `ThemeSettings`, so the + /// section reads as defaults and the next theme change persists those defaults over it. The + /// assertion is deliberately on the *original bytes surviving somewhere*, not on the values read + /// back — asserting defaults would pass against the unprotected implementation too. + @Test + func anUndecodableSectionIsPreservedBeforeTheWriteThatReplacesIt() throws { + let seed = #"{"theme":[],"general":{"fileIconStyle":"monochrome"}}"# + let (store, url) = try makeStore(seed: seed) + + // The rest of the file is fine, so nothing is copied on load. + #expect(store.value(GeneralSettings.self).fileIconStyle == .monochrome) + #expect(try backupFiles(besides: url).isEmpty, "a readable file must not be copied on load") + + // Reading the broken section falls back to defaults, but changes nothing on disk. + #expect(store.value(ThemeSettings.self) == ThemeSettings()) + #expect(try backupFiles(besides: url).isEmpty, "a read is not destructive and needs no copy") + + // The write is the destructive moment; the copy must already exist when it lands. + var theme = store.value(ThemeSettings.self) + theme.matchAppearance = !theme.matchAppearance + store.setValue(theme) + + let backups = try backupFiles(besides: url) + #expect(backups.count == 1, "the undecodable section was replaced with no copy of the original") + #expect(try String(contentsOf: try #require(backups.first), encoding: .utf8) == seed) + } + + /// A second write of the same broken section must not pile up copies — one per store is enough, + /// because the copy is of the whole file. + @Test + func onlyOneCopyIsMadeHoweverManySectionsAreReplaced() throws { + let (store, url) = try makeStore(seed: #"{"theme":[],"terminal":"nope"}"#) + + var theme = store.value(ThemeSettings.self) + theme.matchAppearance = !theme.matchAppearance + store.setValue(theme) + + var terminal = store.value(TerminalSettings.self) + terminal.cursorBlink = !terminal.cursorBlink + store.setValue(terminal) + + #expect(try backupFiles(besides: url).count == 1) + } + /// A *missing* file is the first-launch case: an empty store is correct and nothing is copied. @Test func anAbsentFileIsNotTreatedAsCorruption() throws { From 2d9ff588fef316a69476aaeb93d8ed197c644ddd Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 19:44:37 +0200 Subject: [PATCH 265/335] Docs: Correct the settings-seam comments `SettingsInjector` claimed `CodeEditApp` cannot reach the store through the delegate adaptor during init; it does exactly that. The real reason it cannot observe the store is that a property wrapper cannot be attached to a value obtained from another one. `AppSettings`' "~50 call sites" and ARCHITECTURE.md's "~30 files" disagreed and were both approximate. Measured: 47 declarations across 30 app-target files. Both documents also now record that neither wrapper works in a `Commands` conformer, and ARCHITECTURE.md records the undecodable-section preservation. --- .../AuxiliaryWindows/Settings/AppSettings.swift | 8 ++++++-- .../Settings/SettingsInjector.swift | 11 ++++++++--- docs/ARCHITECTURE.md | 15 ++++++++++++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift index ab41fa6da2..270a69bf00 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift @@ -16,12 +16,16 @@ import CodeEditSettings /// for invalidation — so a view can use either without a difference in behaviour. The distinction /// that remains is what they may name: `SettingsValue` names one section and works inside feature /// packages, `AppSettings` names the app-wide aggregate and therefore cannot. New code should prefer -/// `SettingsValue`; this wrapper exists so its ~50 existing call sites did not all have to move in -/// one change. +/// `SettingsValue`; this wrapper exists so its existing declarations — 47 of them, across 30 +/// app-target files — did not all have to move in one change. /// /// **Only valid inside a `View`.** It used to read a singleton, so it also worked in models and /// AppKit types; it no longer does, and such a use resolves to `DefaultSettingsReader`, which traps /// in debug. Non-view types take a ``SettingsReading``/``SettingsAccessing`` by initializer instead. +/// +/// A `Commands` conformer counts as a non-view type here: `.commands { }` is attached beside a +/// scene's content rather than inside it, so the environment is not documented to reach it. See +/// `CodeEditCommands`, which is handed the store by initializer. @propertyWrapper struct AppSettings: DynamicProperty where T: Equatable { diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index 0244d06bd8..c3a8fe0242 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -40,9 +40,14 @@ struct SettingsInjector: View { /// The scene-level counterpart of ``SettingsInjector``. /// /// The app's scenes are not inside any hosting root, so they need their own injector — and it must -/// be a `Scene`, since a `View` cannot wrap one. `CodeEditApp` cannot observe the store itself: it -/// reaches it through the `NSApplicationDelegateAdaptor`, which is unavailable until every stored -/// property is initialized, so the observation lives here instead. +/// be a `Scene`, since a `View` cannot wrap one. `CodeEditApp` can *reach* the store (it does, in +/// `init`, through the `NSApplicationDelegateAdaptor`), but it cannot `@ObservedObject` it: the +/// store is not one of its stored properties, and a property wrapper cannot be attached to a value +/// obtained from another one. So the observation lives here instead. +/// +/// Note what this does **not** cover: `.commands { }` is attached beside a scene's content, not +/// inside it, so these `.environment` values are not documented to reach `Commands` conformers. +/// `CodeEditCommands` therefore takes the store by initializer — see its documentation. struct SettingsSceneInjector: Scene { /// Observed, not merely held: this scene's job is to re-inject `revision` when it changes. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cd5b35215b..e9eba07058 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -223,8 +223,14 @@ so a caller changing one field reads its section, mutates it and writes it back. `settingsRevision` environment as `SettingsValue`, addressing a section field through the app-wide `SettingsData` façade instead of naming one section directly. There is no `Settings.shared` singleton any more — `AppSettingsStore` (owned by `AppDependencies`) is the - concrete accessor, injected like everything else. `@AppSettings` is what ~30 app-target files - still use; feature packages must not use it, and new app-target code should prefer the seam. + concrete accessor, injected like everything else. `@AppSettings` is what 30 app-target files + still use (47 declarations); feature packages must not use it, and new app-target code should + prefer the seam. +- **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's + content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies + reaches menu-bar code. `CodeEditCommands`/`ViewCommands` are handed `AppSettingsStore` by + initializer and `@ObservedObject` it — reads, writes and menu invalidation all stop depending on + undocumented behaviour. - **`@Environment` does not cross an `NSHostingView`/`NSHostingController` boundary.** A new standalone hosting root must be given `.appServices(_:)` or wrapped in `SettingsInjector`, or its subtree falls back to `DefaultSettingsReader` — plausible defaults, and **writes discarded**. @@ -238,7 +244,10 @@ so a caller changing one field reads its section, mutates it and writes it back. - **`AppSettingsStore` is the concrete accessor.** Section-keyed storage, owned by `AppDependencies` (no `shared`), driving the same throttled save pipeline `Settings.shared` used to own. Sections nothing here decodes are held verbatim and re-emitted on save, so a disabled - extension's configuration survives. + extension's configuration survives. The same holds for a section that is present but + *undecodable*: it reads as defaults but is re-emitted unchanged, and the one write that would + replace it is announced through `SettingsStore.willReplaceUndecodableSection` so the file is + copied to `settings.json.corrupt-` first. ## Creating a new feature target From 32ff8024fb42648534d2c1f0b694b9156fa870de Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Mon, 10 Aug 2026 19:48:19 +0200 Subject: [PATCH 266/335] Docs: Correct the @AppSettings figures Measured 46 declarations across 29 files. The previous figure counted the explanatory comment in CodeEditCommands.swift as a declaration. --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e9eba07058..0790c9cbba 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -223,8 +223,8 @@ so a caller changing one field reads its section, mutates it and writes it back. `settingsRevision` environment as `SettingsValue`, addressing a section field through the app-wide `SettingsData` façade instead of naming one section directly. There is no `Settings.shared` singleton any more — `AppSettingsStore` (owned by `AppDependencies`) is the - concrete accessor, injected like everything else. `@AppSettings` is what 30 app-target files - still use (47 declarations); feature packages must not use it, and new app-target code should + concrete accessor, injected like everything else. `@AppSettings` is what 29 app-target files + still use (46 declarations); feature packages must not use it, and new app-target code should prefer the seam. - **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies From e4bd8ece8a2b1fd2843e95f4a0df1f9e98d19f48 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 12 Aug 2026 22:41:52 +0200 Subject: [PATCH 267/335] Refactor: Key the panel tab bar's layout state by tab id --- .../WorkspacePanel/WorkspacePanelTabBar.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift index f146dc9187..9d91030c98 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift @@ -19,9 +19,9 @@ struct WorkspacePanelTabBar: View { var position: GeneralSettings.SidebarTabBarPosition - @State private var tabLocations: [Tab: CGRect] = [:] - @State private var tabWidth: [Tab: CGFloat] = [:] - @State private var tabOffsets: [Tab: CGFloat] = [:] + @State private var tabLocations: [Tab.ID: CGRect] = [:] + @State private var tabWidth: [Tab.ID: CGFloat] = [:] + @State private var tabOffsets: [Tab.ID: CGFloat] = [:] /// The tab currently being dragged. /// @@ -79,8 +79,8 @@ struct WorkspacePanelTabBar: View { ForEach(items) { tab in makeIcon(tab: tab, size: size) .offset( - x: (position == .top) ? (tabOffsets[tab] ?? 0) : 0, - y: (position == .side) ? (tabOffsets[tab] ?? 0) : 0 + x: (position == .top) ? (tabOffsets[tab.id] ?? 0) : 0, + y: (position == .side) ? (tabOffsets[tab.id] ?? 0) : 0 ) .background(makeTabItemGeometryReader(tab: tab)) .simultaneousGesture(makeAreaTabDragGesture(tab: tab)) @@ -129,12 +129,12 @@ struct WorkspacePanelTabBar: View { let currentLocation = (position == .top) ? value.location.x : value.location.y guard let startLocation = draggingStartLocation, let currentIndex = items.firstIndex(of: tab), - let currentTabWidth = tabWidth[tab], + let currentTabWidth = tabWidth[tab.id], let lastLocation = draggingLastLocation else { return } let dragDifference = currentLocation - lastLocation - tabOffsets[tab] = currentLocation - startLocation + tabOffsets[tab.id] = currentLocation - startLocation // Check for swaps between adjacent tabs // Left tab @@ -214,8 +214,8 @@ struct WorkspacePanelTabBar: View { // Get info about the tab to swap with let swapTab = items[swapIndex] - guard let swapTabLocation = tabLocations[swapTab], - let swapTabWidth = tabWidth[swapTab] + guard let swapTabLocation = tabLocations[swapTab.id], + let swapTabWidth = tabWidth[swapTab.id] else { return } let isWithinBounds: Bool @@ -233,7 +233,7 @@ struct WorkspacePanelTabBar: View { if isWithinBounds { let changing = swapTabWidth - 1 draggingStartLocation! += direction == .previous ? -changing : changing - tabOffsets[tab]! += direction == .previous ? changing : -changing + tabOffsets[tab.id]! += direction == .previous ? changing : -changing items.swapAt(currentIndex, swapIndex) } } @@ -279,14 +279,14 @@ struct WorkspacePanelTabBar: View { Rectangle() .foregroundColor(.clear) .onAppear { - self.tabWidth[tab] = (position == .top) ? geometry.size.width : geometry.size.height - self.tabLocations[tab] = geometry.frame(in: .global) + self.tabWidth[tab.id] = (position == .top) ? geometry.size.width : geometry.size.height + self.tabLocations[tab.id] = geometry.frame(in: .global) } .onChange(of: geometry.frame(in: .global)) { _, newFrame in - self.tabLocations[tab] = newFrame + self.tabLocations[tab.id] = newFrame } .onChange(of: geometry.size.width) { _, newWidth in - self.tabWidth[tab] = newWidth + self.tabWidth[tab.id] = newWidth } } } From 397f96ce568a2f7e5740b029468a7974f6387578 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 12 Aug 2026 22:46:41 +0200 Subject: [PATCH 268/335] Feat: Add WorkspacePanelContribution --- .../WorkspacePanelContribution.swift | 35 +++++++++++++++++++ .../WorkspacePanelContributionTests.swift | 33 +++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift create mode 100644 CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift diff --git a/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift b/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift new file mode 100644 index 0000000000..4a5aa3923a --- /dev/null +++ b/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift @@ -0,0 +1,35 @@ +// +// WorkspacePanelContribution.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import SwiftUI + +/// One tab contributed to a workspace panel — the navigator, inspector or utility area. +/// +/// A contribution is a value, not a case: first-party features, the app shell itself and installed +/// extensions all vend these, and the panel that renders them cannot tell which is which. That is +/// deliberate — it is what lets a new contribution source arrive without the panel code changing. +/// +/// Lives in `CodeEditUI` because it needs SwiftUI and nothing else. It cannot live in +/// `CodeEditCore`, whose charter forbids UI imports. +public protocol WorkspacePanelContribution: Identifiable { + /// Stable across launches, and unique within a panel. + /// + /// Extension-provided ids must be namespaced by their provider so two extensions cannot collide. + var id: String { get } + + /// Shown in the tab's tooltip and accessibility label. + var title: String { get } + + /// An SF Symbol name for the tab. + var systemImage: String { get } + + /// The tab's content. + /// + /// Type-erased because a panel holds a heterogeneous list. Erasure happens once per tab, not per + /// row, and one tab is visible at a time. + var content: AnyView { get } +} diff --git a/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift b/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift new file mode 100644 index 0000000000..93262bb500 --- /dev/null +++ b/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift @@ -0,0 +1,33 @@ +// +// WorkspacePanelContributionTests.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import Testing +import SwiftUI +@testable import CodeEditUI + +private struct StubContribution: WorkspacePanelContribution { + let id: String + let title: String + let systemImage: String + var content: AnyView { AnyView(Color.clear) } +} + +struct WorkspacePanelContributionTests { + + /// A heterogeneous list is the whole point: first-party and extension contributions differ in + /// type but must live in one array. + @Test + func contributionsAreAddressableAsAHeterogeneousList() { + let items: [any WorkspacePanelContribution] = [ + StubContribution(id: "a", title: "Alpha", systemImage: "a.circle"), + StubContribution(id: "b", title: "Beta", systemImage: "b.circle") + ] + + #expect(items.map(\.id) == ["a", "b"]) + #expect(items.first?.title == "Alpha") + } +} From 2e01895145e0c1f1f850ddbb1c992605aae9f315 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 12 Aug 2026 22:56:19 +0200 Subject: [PATCH 269/335] Refactor: Assemble workspace panel tabs from contributions --- CodeEdit/App/MenuBar/TasksCommands.swift | 2 +- CodeEdit/App/MenuBar/ViewCommands.swift | 4 +- .../CodeEditWindowController.swift | 4 +- .../InspectorArea/InspectorAreaView.swift | 23 ++----- .../InspectorAreaViewModel.swift | 9 +-- .../InspectorContributions.swift | 30 +++++++++ .../InspectorArea/InspectorTab.swift | 63 ------------------ .../NavigatorArea/NavigatorAreaView.swift | 15 +---- .../NavigatorAreaViewModel.swift | 9 +-- .../NavigatorContributions.swift | 31 +++++++++ .../NavigatorArea/NavigatorTab.swift | 63 ------------------ .../StartTaskToolbarButton.swift | 2 +- .../StartTaskToolbarItem.swift | 2 +- .../UtilityAreaContributions.swift | 30 +++++++++ .../UtilityArea/UtilityAreaTab.swift | 49 -------------- .../UtilityArea/UtilityAreaView.swift | 6 +- .../UtilityArea/UtilityAreaViewModel.swift | 10 ++- .../ExtensionPanelContribution.swift | 64 +++++++++++++++++++ .../WorkspacePanel/PanelContributions.swift | 50 +++++++++++++++ .../WorkspacePanel/WorkspacePanelTabBar.swift | 52 +++++++-------- .../WorkspacePanel/WorkspacePanelView.swift | 23 ++++--- .../PanelContributionsTests.swift | 50 +++++++++++++++ 22 files changed, 325 insertions(+), 266 deletions(-) create mode 100644 CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift delete mode 100644 CodeEdit/WorkspaceWindow/InspectorArea/InspectorTab.swift create mode 100644 CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift delete mode 100644 CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorTab.swift create mode 100644 CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift delete mode 100644 CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTab.swift create mode 100644 CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift create mode 100644 CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift create mode 100644 CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift diff --git a/CodeEdit/App/MenuBar/TasksCommands.swift b/CodeEdit/App/MenuBar/TasksCommands.swift index b540b286c7..aff8e18145 100644 --- a/CodeEdit/App/MenuBar/TasksCommands.swift +++ b/CodeEdit/App/MenuBar/TasksCommands.swift @@ -101,7 +101,7 @@ struct TasksCommands: Commands { // Open the utility area utilityAreaModel.isCollapsed.toggle() } - utilityAreaModel.selectedTab = .debugConsole // Switch to the correct tab + utilityAreaModel.selectedTabID = "debugConsole" // Switch to the correct tab taskManager?.taskShowingOutput = taskManager?.selectedTaskID // Switch to the selected task } diff --git a/CodeEdit/App/MenuBar/ViewCommands.swift b/CodeEdit/App/MenuBar/ViewCommands.swift index ca77461039..a1fb41f5e5 100644 --- a/CodeEdit/App/MenuBar/ViewCommands.swift +++ b/CodeEdit/App/MenuBar/ViewCommands.swift @@ -198,9 +198,9 @@ extension ViewCommands { var body: some View { Menu("Navigators", content: { - ForEach(Array(model.tabItems.prefix(9).enumerated()), id: \.element) { index, tab in + ForEach(Array(model.tabItems.prefix(9).enumerated()), id: \.element.id) { index, tab in Button(tab.title) { - model.setNavigatorTab(tab: tab) + model.selectedTabID = tab.id } .keyboardShortcut(KeyEquivalent(Character(String(index + 1)))) } diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index e07cb64179..7a2235a9fd 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -186,10 +186,10 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } if let navigatorViewModel = navigatorSidebarViewModel, - let searchTab = navigatorViewModel.tabItems.first(where: { $0 == .search }) { + navigatorViewModel.tabItems.contains(where: { $0.id == "search" }) { DispatchQueue.main.async { self.workspace?.searchState.shouldFocusSearchField = true - navigatorViewModel.setNavigatorTab(tab: searchTab) + navigatorViewModel.selectedTabID = "search" } } } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift index 9c68e68656..0db1d30ca4 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift @@ -23,29 +23,16 @@ struct InspectorAreaView: View { } private func updateTabs() { - var tabs: [InspectorTab] = [.file, .gitHistory] - - if showInternalDevelopmentInspector { - tabs.append(.internalDevelopment) - } - - viewModel.tabItems = tabs + 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() + viewModel.tabItems = inspectorContributions( + extensionManager: extensionManager, + showInternalDevelopment: showInternalDevelopmentInspector + ) } var body: some View { WorkspacePanelView( viewModel: viewModel, - selectedTab: $viewModel.selectedTab, + selectedTabID: $viewModel.selectedTabID, tabItems: $viewModel.tabItems, sidebarPosition: sidebarPosition ) diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift index a36f8f0490..e7d0749a79 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift @@ -6,13 +6,10 @@ // import Foundation +import CodeEditUI class InspectorAreaViewModel: ObservableObject { - @Published var selectedTab: InspectorTab? = .file + @Published var selectedTabID: String? = "file" /// The tab bar items in the Inspector - @Published var tabItems: [InspectorTab] = [] - - func setInspectorTab(tab newTab: InspectorTab) { - selectedTab = newTab - } + @Published var tabItems: [any WorkspacePanelContribution] = [] } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift new file mode 100644 index 0000000000..27ba17520d --- /dev/null +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift @@ -0,0 +1,30 @@ +// +// InspectorContributions.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import CodeEditUI +import SwiftUI + +struct FileInspectorContribution: WorkspacePanelContribution { + let id = "file" + let title = "File Inspector" + let systemImage = "doc" + var content: AnyView { AnyView(FileInspectorView()) } +} + +struct GitHistoryInspectorContribution: WorkspacePanelContribution { + let id = "gitHistory" + let title = "History Inspector" + let systemImage = "clock" + var content: AnyView { AnyView(HistoryInspectorView()) } +} + +struct InternalDevelopmentInspectorContribution: WorkspacePanelContribution { + let id = "internalDevelopment" + let title = "Internal Development" + let systemImage = "hammer" + var content: AnyView { AnyView(InternalDevelopmentInspectorView()) } +} diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorTab.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorTab.swift deleted file mode 100644 index 284c3fdfc9..0000000000 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorTab.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// InspectorTab.swift -// CodeEdit -// -// Created by Wouter Hennen on 02/06/2023. -// - -import SwiftUI -import CodeEditKit -import ExtensionFoundation - -enum InspectorTab: WorkspacePanelTab { - case file - case gitHistory - case internalDevelopment - case uiExtension(endpoint: AppExtensionIdentity, data: ResolvedSidebar.SidebarStore) - - var systemImage: String { - switch self { - case .file: - return "doc" - case .gitHistory: - return "clock" - case .internalDevelopment: - return "hammer" - case .uiExtension(_, let data): - return data.icon ?? "e.square" - } - } - - var id: String { - if case let .uiExtension(endpoint, data) = self { - return endpoint.bundleIdentifier + data.sceneID - } - return title - } - - var title: String { - switch self { - case .file: - return "File Inspector" - case .gitHistory: - return "History Inspector" - case .internalDevelopment: - return "Internal Development" - case .uiExtension(_, let data): - return data.help ?? data.sceneID - } - } - - var body: some View { - switch self { - case .file: - FileInspectorView() - case .gitHistory: - HistoryInspectorView() - case .internalDevelopment: - InternalDevelopmentInspectorView() - case let .uiExtension(endpoint, data): - ExtensionSceneView(with: endpoint, sceneID: data.sceneID) - } - } -} diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift index 45929ceb0b..d94d581343 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift @@ -18,24 +18,13 @@ struct NavigatorAreaView: View { init(viewModel: NavigatorAreaViewModel) { 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() + viewModel.tabItems = navigatorContributions(extensionManager: extensionManager) } var body: some View { WorkspacePanelView( viewModel: viewModel, - selectedTab: $viewModel.selectedTab, + selectedTabID: $viewModel.selectedTabID, tabItems: $viewModel.tabItems, sidebarPosition: sidebarPosition ) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift index 0133d81414..c73e0cc7c1 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift @@ -6,13 +6,10 @@ // import Foundation +import CodeEditUI class NavigatorAreaViewModel: ObservableObject { - @Published var selectedTab: NavigatorTab? = .project + @Published var selectedTabID: String? = "project" /// The tab bar items in the Navigator - @Published var tabItems: [NavigatorTab] = [] - - func setNavigatorTab(tab newTab: NavigatorTab) { - selectedTab = newTab - } + @Published var tabItems: [any WorkspacePanelContribution] = [] } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift new file mode 100644 index 0000000000..4b9fe51bd6 --- /dev/null +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift @@ -0,0 +1,31 @@ +// +// NavigatorContributions.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import CodeEditUI +import SwiftUI + +/// The project navigator is shell chrome: it has no owning package and stays app-side permanently. +struct ProjectNavigatorContribution: WorkspacePanelContribution { + let id = "project" + let title = "Project" + let systemImage = "folder" + var content: AnyView { AnyView(ProjectNavigatorView()) } +} + +struct SourceControlNavigatorContribution: WorkspacePanelContribution { + let id = "sourceControl" + let title = "Source Control" + let systemImage = "vault" + var content: AnyView { AnyView(SourceControlNavigatorView()) } +} + +struct FindNavigatorContribution: WorkspacePanelContribution { + let id = "search" + let title = "Search" + let systemImage = "magnifyingglass" + var content: AnyView { AnyView(FindNavigatorTab()) } +} diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorTab.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorTab.swift deleted file mode 100644 index 27f5a721a2..0000000000 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorTab.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// NavigatorTab.swift -// CodeEdit -// -// Created by Wouter Hennen on 02/06/2023. -// - -import SwiftUI -import CodeEditKit -import ExtensionFoundation - -enum NavigatorTab: WorkspacePanelTab { - case project - case sourceControl - case search - case uiExtension(endpoint: AppExtensionIdentity, data: ResolvedSidebar.SidebarStore) - - var systemImage: String { - switch self { - case .project: - return "folder" - case .sourceControl: - return "vault" - case .search: - return "magnifyingglass" - case .uiExtension(_, let data): - return data.icon ?? "e.square" - } - } - - var id: String { - if case let .uiExtension(endpoint, data) = self { - return endpoint.bundleIdentifier + data.sceneID - } - return title - } - - var title: String { - switch self { - case .project: - return "Project" - case .sourceControl: - return "Source Control" - case .search: - return "Search" - case .uiExtension(_, let data): - return data.help ?? data.sceneID - } - } - - var body: some View { - switch self { - case .project: - ProjectNavigatorView() - case .sourceControl: - SourceControlNavigatorView() - case .search: - FindNavigatorTab() - case let .uiExtension(endpoint, data): - ExtensionSceneView(with: endpoint, sceneID: data.sceneID) - } - } -} diff --git a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift index 46e3939a02..263353aece 100644 --- a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift +++ b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift @@ -28,7 +28,7 @@ struct StartTaskToolbarButton: View { if utilityAreaCollapsed { commandManager?.executeCommand("open.drawer") } - utilityAreaModel.selectedTab = .debugConsole + utilityAreaModel.selectedTabID = "debugConsole" taskManager.taskShowingOutput = taskManager.selectedTaskID } label: { Label("Start", systemImage: "play.fill") diff --git a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift index e8706bccb6..2c6d2f315a 100644 --- a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift +++ b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift @@ -43,7 +43,7 @@ final class StartTaskToolbarItem: NSToolbarItem { if utilityAreaCollapsed { commandManager.executeCommand("open.drawer") } - utilityAreaModel?.selectedTab = .debugConsole + utilityAreaModel?.selectedTabID = "debugConsole" taskManager.taskShowingOutput = taskManager.selectedTaskID } } diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift new file mode 100644 index 0000000000..163ce657b1 --- /dev/null +++ b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift @@ -0,0 +1,30 @@ +// +// UtilityAreaContributions.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import CodeEditUI +import SwiftUI + +struct TerminalUtilityContribution: WorkspacePanelContribution { + let id = "terminal" + let title = "Terminal" + let systemImage = "terminal" + var content: AnyView { AnyView(UtilityAreaTerminalView()) } +} + +struct DebugConsoleUtilityContribution: WorkspacePanelContribution { + let id = "debugConsole" + let title = "Debug Console" + let systemImage = "ladybug" + var content: AnyView { AnyView(UtilityAreaDebugView()) } +} + +struct OutputUtilityContribution: WorkspacePanelContribution { + let id = "output" + let title = "Output" + let systemImage = "list.bullet.indent" + var content: AnyView { AnyView(UtilityAreaOutputView()) } +} diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTab.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTab.swift deleted file mode 100644 index 2056b6d7d7..0000000000 --- a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaTab.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// UtilityAreaTab.swift -// CodeEdit -// -// Created by Wouter Hennen on 02/06/2023. -// - -import SwiftUI - -enum UtilityAreaTab: WorkspacePanelTab, CaseIterable { - var id: Self { self } - - case terminal - case debugConsole - case output - - var title: String { - switch self { - case .terminal: - return "Terminal" - case .debugConsole: - return "Debug Console" - case .output: - return "Output" - } - } - - var systemImage: String { - switch self { - case .terminal: - return "terminal" - case .debugConsole: - return "ladybug" - case .output: - return "list.bullet.indent" - } - } - - var body: some View { - switch self { - case .terminal: - UtilityAreaTerminalView() - case .debugConsole: - UtilityAreaDebugView() - case .output: - UtilityAreaOutputView() - } - } -} diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift index 6d35448169..52068ae591 100644 --- a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift +++ b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift @@ -8,12 +8,13 @@ import SwiftUI struct UtilityAreaView: View { + @ObservedObject private var extensionManager = ExtensionManager.shared @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel var body: some View { WorkspacePanelView( viewModel: utilityAreaViewModel, - selectedTab: $utilityAreaViewModel.selectedTab, + selectedTabID: $utilityAreaViewModel.selectedTabID, tabItems: $utilityAreaViewModel.tabItems, sidebarPosition: .side, darkDivider: true @@ -21,5 +22,8 @@ struct UtilityAreaView: View { .accessibilityElement(children: .contain) .accessibilityLabel("Utility Area") .accessibilityIdentifier("UtilityArea") + .onAppear { + utilityAreaViewModel.tabItems = utilityAreaContributions(extensionManager: extensionManager) + } } } diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift index a0a6f1ee27..88e3776016 100644 --- a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift +++ b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift @@ -6,6 +6,7 @@ // import CodeEditCore +import CodeEditUI import CETerminal import SwiftUI @@ -14,7 +15,7 @@ import SwiftUI /// A model class to host and manage data for the Utility area. class UtilityAreaViewModel: ObservableObject { - @Published var selectedTab: UtilityAreaTab? = .terminal + @Published var selectedTabID: String? = "terminal" @Published var terminals: [UtilityAreaTerminal] = [] @@ -32,8 +33,11 @@ class UtilityAreaViewModel: ObservableObject { /// The current height of the drawer. Zero if hidden @Published var currentHeight: Double = 0 - /// The tab bar items for the UtilityAreaView - @Published var tabItems: [UtilityAreaTab] = UtilityAreaTab.allCases + /// The tab bar items for the UtilityAreaView. + /// + /// Seeded by ``UtilityAreaView`` on appear: assembly is `@MainActor`, and a property default is + /// evaluated in this non-isolated class's `init`. + @Published var tabItems: [any WorkspacePanelContribution] = [] /// The tab bar view model for UtilityAreaTabView @Published var tabViewModel = UtilityAreaTabViewModel() diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift new file mode 100644 index 0000000000..21358400dc --- /dev/null +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift @@ -0,0 +1,64 @@ +// +// ExtensionPanelContribution.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import CodeEditKit +import CodeEditUI +import ExtensionFoundation +import SwiftUI + +/// The workspace panel a contribution is assembled for. +enum PanelKind { + case navigator, inspector, utilityArea +} + +/// The sole ExtensionKit-aware contribution. +/// +/// One type, constructed once per discovered extension scene. Keeping ExtensionKit's vocabulary +/// here — rather than in the panels — is what lets another contribution source arrive later without +/// the panel code changing. +struct ExtensionPanelContribution: WorkspacePanelContribution { + let endpoint: AppExtensionIdentity + let data: ResolvedSidebar.SidebarStore + + /// Note the `.` separator: `bundleIdentifier + sceneID` concatenated without one can collide + /// between two extensions. Selection is not persisted, so the format change costs nothing. + var id: String { endpoint.bundleIdentifier + "." + data.sceneID } + var title: String { data.help ?? data.sceneID } + var systemImage: String { data.icon ?? "e.square" } + var content: AnyView { AnyView(ExtensionSceneView(with: endpoint, sceneID: data.sceneID)) } +} + +/// Collects every installed extension's scenes that target the given panel. +@MainActor +func extensionContributions( + for kind: PanelKind, + from extensionManager: ExtensionManager +) -> [any WorkspacePanelContribution] { + let sidebarKind: ResolvedSidebar.Kind + switch kind { + case .navigator: + sidebarKind = .navigator + case .inspector: + sidebarKind = .inspector + case .utilityArea: + // `ResolvedSidebar.Kind` in CodeEditKit declares exactly `case navigator, inspector` + // (`ResolvedSidebar.swift:14`), so an extension cannot declare a utility-area tab at all. + // Adding a kind is a change to CodeEditKit, a separate published repo, and is out of scope. + return [] + } + + return extensionManager + .extensions + .flatMap { ext in + ext.availableFeatures.compactMap { feature -> (any WorkspacePanelContribution)? in + if case .sidebarItem(let data) = feature, data.kind == sidebarKind { + return ExtensionPanelContribution(endpoint: ext.endpoint, data: data) + } + return nil + } + } +} diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift new file mode 100644 index 0000000000..4a6f876b82 --- /dev/null +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift @@ -0,0 +1,50 @@ +// +// PanelContributions.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import CodeEditUI + +@MainActor +func navigatorContributions( + extensionManager: ExtensionManager +) -> [any WorkspacePanelContribution] { + var items: [any WorkspacePanelContribution] = [ + ProjectNavigatorContribution(), + SourceControlNavigatorContribution(), + FindNavigatorContribution() + ] + items += extensionContributions(for: .navigator, from: extensionManager) + return items +} + +@MainActor +func inspectorContributions( + extensionManager: ExtensionManager, + showInternalDevelopment: Bool +) -> [any WorkspacePanelContribution] { + var items: [any WorkspacePanelContribution] = [ + FileInspectorContribution(), + GitHistoryInspectorContribution() + ] + if showInternalDevelopment { + items.append(InternalDevelopmentInspectorContribution()) + } + items += extensionContributions(for: .inspector, from: extensionManager) + return items +} + +@MainActor +func utilityAreaContributions( + extensionManager: ExtensionManager +) -> [any WorkspacePanelContribution] { + var items: [any WorkspacePanelContribution] = [ + TerminalUtilityContribution(), + DebugConsoleUtilityContribution(), + OutputUtilityContribution() + ] + items += extensionContributions(for: .utilityArea, from: extensionManager) + return items +} diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift index 9d91030c98..e4f07f62df 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift @@ -7,26 +7,22 @@ import SwiftUI import CodeEditSettings +import CodeEditUI -protocol WorkspacePanelTab: View, Identifiable, Hashable { - var title: String { get } - var systemImage: String { get } -} - -struct WorkspacePanelTabBar: View { - @Binding var items: [Tab] - @Binding var selection: Tab? +struct WorkspacePanelTabBar: View { + @Binding var items: [any WorkspacePanelContribution] + @Binding var selectionID: String? var position: GeneralSettings.SidebarTabBarPosition - @State private var tabLocations: [Tab.ID: CGRect] = [:] - @State private var tabWidth: [Tab.ID: CGFloat] = [:] - @State private var tabOffsets: [Tab.ID: CGFloat] = [:] + @State private var tabLocations: [String: CGRect] = [:] + @State private var tabWidth: [String: CGFloat] = [:] + @State private var tabOffsets: [String: CGFloat] = [:] - /// The tab currently being dragged. + /// The id of the tab currently being dragged. /// /// It will be `nil` when there is no tab dragged currently. - @State private var draggingTab: Tab? + @State private var draggingTabID: String? /// The start location of dragging. /// @@ -51,7 +47,7 @@ struct WorkspacePanelTabBar: View { GeometryReader { proxy in iconsView(size: proxy.size) .frame(maxWidth: .infinity, maxHeight: .infinity) - .animation(.default, value: items) + .animation(.default, value: items.map(\.id)) } .clipped() .frame(maxWidth: .infinity, idealHeight: 27) @@ -63,7 +59,7 @@ struct WorkspacePanelTabBar: View { iconsView(size: proxy.size) .padding(.vertical, 5) .frame(maxWidth: .infinity, maxHeight: .infinity) - .animation(.default, value: items) + .animation(.default, value: items.map(\.id)) } .clipped() .frame(idealWidth: 40, maxHeight: .infinity) @@ -76,7 +72,7 @@ struct WorkspacePanelTabBar: View { ? AnyLayout(HStackLayout(spacing: 0)) : AnyLayout(VStackLayout(spacing: 0)) layout { - ForEach(items) { tab in + ForEach(items, id: \.id) { tab in makeIcon(tab: tab, size: size) .offset( x: (position == .top) ? (tabOffsets[tab.id] ?? 0) : 0, @@ -92,21 +88,21 @@ struct WorkspacePanelTabBar: View { } private func makeIcon( - tab: Tab, + tab: any WorkspacePanelContribution, scale: Image.Scale = .medium, size: CGSize ) -> some View { Button { - selection = tab + selectionID = tab.id } label: { getSafeImage(named: tab.systemImage, accessibilityDescription: tab.title) .font(.system(size: 12.5)) - .symbolVariant(tab == selection ? .fill : .none) + .symbolVariant(tab.id == selectionID ? .fill : .none) .help(tab.title) } .buttonStyle( .icon( - isActive: tab == selection, + isActive: tab.id == selectionID, size: CGSize( width: position == .side ? 40 : 24, height: position == .side ? 28 : size.height @@ -118,17 +114,17 @@ struct WorkspacePanelTabBar: View { .accessibilityLabel(tab.title) } - private func makeAreaTabDragGesture(tab: Tab) -> some Gesture { + private func makeAreaTabDragGesture(tab: any WorkspacePanelContribution) -> some Gesture { DragGesture(minimumDistance: 2, coordinateSpace: .global) .onChanged({ value in - if draggingTab != tab { + if draggingTabID != tab.id { initializeDragGesture(value: value, for: tab) } // Get the current cursor location let currentLocation = (position == .top) ? value.location.x : value.location.y guard let startLocation = draggingStartLocation, - let currentIndex = items.firstIndex(of: tab), + let currentIndex = items.firstIndex(where: { $0.id == tab.id }), let currentTabWidth = tabWidth[tab.id], let lastLocation = draggingLastLocation else { return } @@ -169,13 +165,13 @@ struct WorkspacePanelTabBar: View { tabOffsets = [:] } DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - draggingTab = nil + draggingTabID = nil } }) } - private func initializeDragGesture(value: DragGesture.Value, for tab: Tab) { - draggingTab = tab + private func initializeDragGesture(value: DragGesture.Value, for tab: any WorkspacePanelContribution) { + draggingTabID = tab.id let initialLocation = position == .top ? value.startLocation.x : value.startLocation.y draggingStartLocation = initialLocation draggingLastLocation = initialLocation @@ -188,7 +184,7 @@ struct WorkspacePanelTabBar: View { // swiftlint:disable:next function_parameter_count private func swapTab( - tab: Tab, + tab: any WorkspacePanelContribution, currentIndex: Int, currentLocation: CGFloat, dragDifference: CGFloat, @@ -274,7 +270,7 @@ struct WorkspacePanelTabBar: View { ) } - private func makeTabItemGeometryReader(tab: Tab) -> some View { + private func makeTabItemGeometryReader(tab: any WorkspacePanelContribution) -> some View { GeometryReader { geometry in Rectangle() .foregroundColor(.clear) diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift index 033e6957f5..02839dc1cf 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift @@ -9,10 +9,10 @@ import SwiftUI import CodeEditSettings import CodeEditUI -struct WorkspacePanelView: View { +struct WorkspacePanelView: View { @ObservedObject var viewModel: ViewModel - @Binding var selectedTab: Tab? - @Binding var tabItems: [Tab] + @Binding var selectedTabID: String? + @Binding var tabItems: [any WorkspacePanelContribution] @Environment(\.colorScheme) private var colorScheme @@ -22,22 +22,27 @@ struct WorkspacePanelView: init( viewModel: ViewModel, - selectedTab: Binding, - tabItems: Binding<[Tab]>, + selectedTabID: Binding, + tabItems: Binding<[any WorkspacePanelContribution]>, sidebarPosition: GeneralSettings.SidebarTabBarPosition, darkDivider: Bool = false ) { self.viewModel = viewModel - self._selectedTab = selectedTab + self._selectedTabID = selectedTabID self._tabItems = tabItems self.sidebarPosition = sidebarPosition self.darkDivider = darkDivider } + private var selectedTab: (any WorkspacePanelContribution)? { + guard let selectedTabID else { return nil } + return tabItems.first { $0.id == selectedTabID } + } + var body: some View { VStack(spacing: 0) { if let selection = selectedTab { - selection + selection.content } else { CEContentUnavailableView("No Selection") } @@ -45,7 +50,7 @@ struct WorkspacePanelView: .safeAreaInset(edge: .leading, spacing: 0) { if sidebarPosition == .side { HStack(spacing: 0) { - WorkspacePanelTabBar(items: $tabItems, selection: $selectedTab, position: sidebarPosition) + WorkspacePanelTabBar(items: $tabItems, selectionID: $selectedTabID, position: sidebarPosition) Divider() .overlay(Color(nsColor: darkDivider && colorScheme == .dark ? .black : .clear)) } @@ -55,7 +60,7 @@ struct WorkspacePanelView: if sidebarPosition == .top { VStack(spacing: 0) { Divider() - WorkspacePanelTabBar(items: $tabItems, selection: $selectedTab, position: sidebarPosition) + WorkspacePanelTabBar(items: $tabItems, selectionID: $selectedTabID, position: sidebarPosition) Divider() } } else if !darkDivider { diff --git a/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift b/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift new file mode 100644 index 0000000000..bd746f8b87 --- /dev/null +++ b/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift @@ -0,0 +1,50 @@ +// +// PanelContributionsTests.swift +// CodeEditTests +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import Testing +import CodeEditUI +@testable import CodeEdit + +@MainActor +struct PanelContributionsTests { + + /// Ids must be unique within a panel — a duplicate would make selection ambiguous and silently + /// break the tab bar's layout state, which is keyed by id. + @Test + func navigatorContributionIdsAreUnique() { + let items = navigatorContributions(extensionManager: ExtensionManager()) + #expect(Set(items.map(\.id)).count == items.count) + } + + /// Order is the contract: first-party tabs keep their existing positions. + @Test + func navigatorKeepsItsFirstPartyOrder() { + let items = navigatorContributions(extensionManager: ExtensionManager()) + #expect(items.map(\.id).prefix(3) == ["project", "sourceControl", "search"]) + } + + /// The inspector's developer tab is the only conditional contribution. + @Test + func inspectorIncludesTheDeveloperTabOnlyWhenEnabled() { + let disabled = inspectorContributions( + extensionManager: ExtensionManager(), showInternalDevelopment: false + ) + let enabled = inspectorContributions( + extensionManager: ExtensionManager(), showInternalDevelopment: true + ) + + #expect(!disabled.map(\.id).contains("internalDevelopment")) + #expect(enabled.map(\.id).contains("internalDevelopment")) + #expect(enabled.count == disabled.count + 1) + } + + @Test + func utilityAreaKeepsItsFirstPartyOrder() { + let items = utilityAreaContributions(extensionManager: ExtensionManager()) + #expect(items.map(\.id) == ["terminal", "debugConsole", "output"]) + } +} From f2433e0c482ee25f6b7e63ebc86b1ec26b82b546 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 12 Aug 2026 23:07:11 +0200 Subject: [PATCH 270/335] Test: Cover panel tab parity and make tab ids compile-checked --- CodeEdit/App/MenuBar/TasksCommands.swift | 2 +- .../CodeEditWindowController.swift | 8 +- .../InspectorAreaViewModel.swift | 2 +- .../InspectorContributions.swift | 6 +- .../NavigatorAreaViewModel.swift | 2 +- .../NavigatorContributions.swift | 6 +- .../StartTaskToolbarButton.swift | 2 +- .../StartTaskToolbarItem.swift | 2 +- .../UtilityAreaContributions.swift | 6 +- .../UtilityArea/UtilityAreaView.swift | 4 - .../UtilityArea/UtilityAreaViewModel.swift | 14 +++- .../WorkspacePanel/PanelContributions.swift | 20 +++++ .../PanelContributionsTests.swift | 79 +++++++++++++++++-- 13 files changed, 120 insertions(+), 33 deletions(-) diff --git a/CodeEdit/App/MenuBar/TasksCommands.swift b/CodeEdit/App/MenuBar/TasksCommands.swift index aff8e18145..b7d44904de 100644 --- a/CodeEdit/App/MenuBar/TasksCommands.swift +++ b/CodeEdit/App/MenuBar/TasksCommands.swift @@ -101,7 +101,7 @@ struct TasksCommands: Commands { // Open the utility area utilityAreaModel.isCollapsed.toggle() } - utilityAreaModel.selectedTabID = "debugConsole" // Switch to the correct tab + utilityAreaModel.selectedTabID = PanelTabID.debugConsole // Switch to the correct tab taskManager?.taskShowingOutput = taskManager?.selectedTaskID // Switch to the selected task } diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index 7a2235a9fd..d457f19d45 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -39,7 +39,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs // Window-UI models: window-scoped state, owned here (1:1 with the workspace). let statusBarViewModel = StatusBarViewModel() - let utilityAreaModel = UtilityAreaViewModel() + let utilityAreaModel = UtilityAreaViewModel( + tabItems: utilityAreaContributions(extensionManager: .shared) + ) let openQuicklyViewModel: OpenQuicklyViewModel let commandsPaletteState: QuickActionsViewModel let notificationPanel: NotificationPanelViewModel @@ -186,10 +188,10 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } if let navigatorViewModel = navigatorSidebarViewModel, - navigatorViewModel.tabItems.contains(where: { $0.id == "search" }) { + navigatorViewModel.tabItems.contains(where: { $0.id == PanelTabID.search }) { DispatchQueue.main.async { self.workspace?.searchState.shouldFocusSearchField = true - navigatorViewModel.selectedTabID = "search" + navigatorViewModel.selectedTabID = PanelTabID.search } } } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift index e7d0749a79..013a0648a9 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaViewModel.swift @@ -9,7 +9,7 @@ import Foundation import CodeEditUI class InspectorAreaViewModel: ObservableObject { - @Published var selectedTabID: String? = "file" + @Published var selectedTabID: String? = PanelTabID.file /// The tab bar items in the Inspector @Published var tabItems: [any WorkspacePanelContribution] = [] } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift index 27ba17520d..bd232605e9 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift @@ -9,21 +9,21 @@ import CodeEditUI import SwiftUI struct FileInspectorContribution: WorkspacePanelContribution { - let id = "file" + let id = PanelTabID.file let title = "File Inspector" let systemImage = "doc" var content: AnyView { AnyView(FileInspectorView()) } } struct GitHistoryInspectorContribution: WorkspacePanelContribution { - let id = "gitHistory" + let id = PanelTabID.gitHistory let title = "History Inspector" let systemImage = "clock" var content: AnyView { AnyView(HistoryInspectorView()) } } struct InternalDevelopmentInspectorContribution: WorkspacePanelContribution { - let id = "internalDevelopment" + let id = PanelTabID.internalDevelopment let title = "Internal Development" let systemImage = "hammer" var content: AnyView { AnyView(InternalDevelopmentInspectorView()) } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift index c73e0cc7c1..f546edeba3 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaViewModel.swift @@ -9,7 +9,7 @@ import Foundation import CodeEditUI class NavigatorAreaViewModel: ObservableObject { - @Published var selectedTabID: String? = "project" + @Published var selectedTabID: String? = PanelTabID.project /// The tab bar items in the Navigator @Published var tabItems: [any WorkspacePanelContribution] = [] } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift index 4b9fe51bd6..90e85d1b00 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift @@ -10,21 +10,21 @@ import SwiftUI /// The project navigator is shell chrome: it has no owning package and stays app-side permanently. struct ProjectNavigatorContribution: WorkspacePanelContribution { - let id = "project" + let id = PanelTabID.project let title = "Project" let systemImage = "folder" var content: AnyView { AnyView(ProjectNavigatorView()) } } struct SourceControlNavigatorContribution: WorkspacePanelContribution { - let id = "sourceControl" + let id = PanelTabID.sourceControl let title = "Source Control" let systemImage = "vault" var content: AnyView { AnyView(SourceControlNavigatorView()) } } struct FindNavigatorContribution: WorkspacePanelContribution { - let id = "search" + let id = PanelTabID.search let title = "Search" let systemImage = "magnifyingglass" var content: AnyView { AnyView(FindNavigatorTab()) } diff --git a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift index 263353aece..aff3ce4450 100644 --- a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift +++ b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarButton.swift @@ -28,7 +28,7 @@ struct StartTaskToolbarButton: View { if utilityAreaCollapsed { commandManager?.executeCommand("open.drawer") } - utilityAreaModel.selectedTabID = "debugConsole" + utilityAreaModel.selectedTabID = PanelTabID.debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID } label: { Label("Start", systemImage: "play.fill") diff --git a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift index 2c6d2f315a..ac87b5472d 100644 --- a/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift +++ b/CodeEdit/WorkspaceWindow/TaskToolbarItems/StartTaskToolbarItem.swift @@ -43,7 +43,7 @@ final class StartTaskToolbarItem: NSToolbarItem { if utilityAreaCollapsed { commandManager.executeCommand("open.drawer") } - utilityAreaModel?.selectedTabID = "debugConsole" + utilityAreaModel?.selectedTabID = PanelTabID.debugConsole taskManager.taskShowingOutput = taskManager.selectedTaskID } } diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift index 163ce657b1..916f68d18d 100644 --- a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift +++ b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaContributions.swift @@ -9,21 +9,21 @@ import CodeEditUI import SwiftUI struct TerminalUtilityContribution: WorkspacePanelContribution { - let id = "terminal" + let id = PanelTabID.terminal let title = "Terminal" let systemImage = "terminal" var content: AnyView { AnyView(UtilityAreaTerminalView()) } } struct DebugConsoleUtilityContribution: WorkspacePanelContribution { - let id = "debugConsole" + let id = PanelTabID.debugConsole let title = "Debug Console" let systemImage = "ladybug" var content: AnyView { AnyView(UtilityAreaDebugView()) } } struct OutputUtilityContribution: WorkspacePanelContribution { - let id = "output" + let id = PanelTabID.output let title = "Output" let systemImage = "list.bullet.indent" var content: AnyView { AnyView(UtilityAreaOutputView()) } diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift index 52068ae591..52e8941e12 100644 --- a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift +++ b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaView.swift @@ -8,7 +8,6 @@ import SwiftUI struct UtilityAreaView: View { - @ObservedObject private var extensionManager = ExtensionManager.shared @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel var body: some View { @@ -22,8 +21,5 @@ struct UtilityAreaView: View { .accessibilityElement(children: .contain) .accessibilityLabel("Utility Area") .accessibilityIdentifier("UtilityArea") - .onAppear { - utilityAreaViewModel.tabItems = utilityAreaContributions(extensionManager: extensionManager) - } } } diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift index 88e3776016..87f40f2f0c 100644 --- a/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift +++ b/CodeEdit/WorkspaceWindow/UtilityArea/UtilityAreaViewModel.swift @@ -15,7 +15,7 @@ import SwiftUI /// A model class to host and manage data for the Utility area. class UtilityAreaViewModel: ObservableObject { - @Published var selectedTabID: String? = "terminal" + @Published var selectedTabID: String? = PanelTabID.terminal @Published var terminals: [UtilityAreaTerminal] = [] @@ -35,13 +35,19 @@ class UtilityAreaViewModel: ObservableObject { /// The tab bar items for the UtilityAreaView. /// - /// Seeded by ``UtilityAreaView`` on appear: assembly is `@MainActor`, and a property default is - /// evaluated in this non-isolated class's `init`. - @Published var tabItems: [any WorkspacePanelContribution] = [] + /// Injected rather than defaulted: assembly is `@MainActor`, and a property-default expression + /// is evaluated in this non-isolated class's `init`. The owner supplies it, so the list is + /// populated before the first body evaluation — the utility area never paints "No Selection". + @Published var tabItems: [any WorkspacePanelContribution] /// The tab bar view model for UtilityAreaTabView @Published var tabViewModel = UtilityAreaTabViewModel() + /// - Parameter tabItems: The panel's tabs. Defaults to none, for tests that do not exercise them. + init(tabItems: [any WorkspacePanelContribution] = []) { + self.tabItems = tabItems + } + // MARK: - State Restoration func restoreFromState(_ statePersistence: any WorkspaceStatePersisting) { diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift index 4a6f876b82..9448bd535e 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift @@ -7,6 +7,26 @@ import CodeEditUI +/// The ids of the first-party panel tabs. +/// +/// Selecting a tab means assigning its id, so these exist to keep every call site that does so +/// compile-checked. A bare string typo is silent: guarded sites become a no-op, unguarded ones +/// select an id no contribution has and the panel renders "No Selection". Extension-provided ids +/// are dynamic and deliberately absent. +enum PanelTabID { + static let project = "project" + static let sourceControl = "sourceControl" + static let search = "search" + + static let file = "file" + static let gitHistory = "gitHistory" + static let internalDevelopment = "internalDevelopment" + + static let terminal = "terminal" + static let debugConsole = "debugConsole" + static let output = "output" +} + @MainActor func navigatorContributions( extensionManager: ExtensionManager diff --git a/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift b/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift index bd746f8b87..ae9107020c 100644 --- a/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift +++ b/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift @@ -9,22 +9,56 @@ import Testing import CodeEditUI @testable import CodeEdit +/// Parity tests for the three panels' first-party tabs. +/// +/// Every expectation is a literal, never derived from `PanelTabID` or from the contribution types +/// themselves — a test that computes its expectation the way the code does would pass through any +/// rename. These titles and SF Symbols are the ones the deleted `NavigatorTab`, `InspectorTab` and +/// `UtilityAreaTab` enums shipped; changing one is a silent visual regression, so it must fail here. +/// +/// Only the first-party prefix is asserted: a developer machine may have real extensions installed, +/// which legitimately append tabs. @MainActor struct PanelContributionsTests { - /// Ids must be unique within a panel — a duplicate would make selection ambiguous and silently - /// break the tab bar's layout state, which is keyed by id. + // MARK: - Navigator + @Test - func navigatorContributionIdsAreUnique() { + func navigatorTabsKeepTheirIdsAndOrder() { let items = navigatorContributions(extensionManager: ExtensionManager()) - #expect(Set(items.map(\.id)).count == items.count) + #expect(items.prefix(3).map(\.id) == ["project", "sourceControl", "search"]) } - /// Order is the contract: first-party tabs keep their existing positions. @Test - func navigatorKeepsItsFirstPartyOrder() { + func navigatorTabsKeepTheirTitles() { let items = navigatorContributions(extensionManager: ExtensionManager()) - #expect(items.map(\.id).prefix(3) == ["project", "sourceControl", "search"]) + #expect(items.prefix(3).map(\.title) == ["Project", "Source Control", "Search"]) + } + + @Test + func navigatorTabsKeepTheirSymbols() { + let items = navigatorContributions(extensionManager: ExtensionManager()) + #expect(items.prefix(3).map(\.systemImage) == ["folder", "vault", "magnifyingglass"]) + } + + // MARK: - Inspector + + @Test + func inspectorTabsKeepTheirIdsAndOrder() { + let items = inspectorContributions(extensionManager: ExtensionManager(), showInternalDevelopment: true) + #expect(items.prefix(3).map(\.id) == ["file", "gitHistory", "internalDevelopment"]) + } + + @Test + func inspectorTabsKeepTheirTitles() { + let items = inspectorContributions(extensionManager: ExtensionManager(), showInternalDevelopment: true) + #expect(items.prefix(3).map(\.title) == ["File Inspector", "History Inspector", "Internal Development"]) + } + + @Test + func inspectorTabsKeepTheirSymbols() { + let items = inspectorContributions(extensionManager: ExtensionManager(), showInternalDevelopment: true) + #expect(items.prefix(3).map(\.systemImage) == ["doc", "clock", "hammer"]) } /// The inspector's developer tab is the only conditional contribution. @@ -42,9 +76,38 @@ struct PanelContributionsTests { #expect(enabled.count == disabled.count + 1) } + // MARK: - Utility area + + /// `ResolvedSidebar.Kind` has no utility-area case, so this list is always exactly three tabs. @Test - func utilityAreaKeepsItsFirstPartyOrder() { + func utilityAreaTabsKeepTheirIdsAndOrder() { let items = utilityAreaContributions(extensionManager: ExtensionManager()) #expect(items.map(\.id) == ["terminal", "debugConsole", "output"]) } + + @Test + func utilityAreaTabsKeepTheirTitles() { + let items = utilityAreaContributions(extensionManager: ExtensionManager()) + #expect(items.map(\.title) == ["Terminal", "Debug Console", "Output"]) + } + + @Test + func utilityAreaTabsKeepTheirSymbols() { + let items = utilityAreaContributions(extensionManager: ExtensionManager()) + #expect(items.map(\.systemImage) == ["terminal", "ladybug", "list.bullet.indent"]) + } + + // MARK: - Selection call sites + + /// The four `selectedTabID` call sites assign these constants; if one drifts from the + /// contribution that owns it, selection silently stops resolving. + @Test + func selectionConstantsMatchTheContributionsTheyName() { + #expect(PanelTabID.search == "search") + #expect(PanelTabID.debugConsole == "debugConsole") + #expect(navigatorContributions(extensionManager: ExtensionManager()) + .contains { $0.id == PanelTabID.search }) + #expect(utilityAreaContributions(extensionManager: ExtensionManager()) + .contains { $0.id == PanelTabID.debugConsole }) + } } From 20a1d8945bf96a17992e188f9ac26df0bd2ae4af Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 12 Aug 2026 23:15:16 +0200 Subject: [PATCH 271/335] Refactor: Let CESearch vend its own navigator tab --- .../NavigatorArea/FindNavigatorTab.swift | 29 ----------- .../NavigatorContributions.swift | 7 --- .../WorkspacePanel/PanelContributions.swift | 5 +- CodeEditModules/Package.swift | 2 +- .../CESearch/FindNavigatorContribution.swift | 48 +++++++++++++++++++ 5 files changed, 53 insertions(+), 38 deletions(-) delete mode 100644 CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigatorTab.swift create mode 100644 CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigatorTab.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigatorTab.swift deleted file mode 100644 index a909b05882..0000000000 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/FindNavigatorTab.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FindNavigatorTab.swift -// CodeEdit -// -// Created by Matthijs Eikelenboom on 05/07/2026. -// - -import SwiftUI -import CodeEditSettings -import CESearch - -/// App-side wrapper for the Search package's find navigator: reads Settings -/// (which the package cannot import) and passes them down as configuration. -struct FindNavigatorTab: View { - @AppSettings(\.general.projectNavigatorSize) - var projectNavigatorSize - - @AppSettings(\.general.findNavigatorDetail) - var findNavigatorDetail - - var body: some View { - FindNavigatorView( - configuration: FindNavigatorConfiguration( - rowHeight: projectNavigatorSize.rowHeight, - matchDetailLineLimit: findNavigatorDetail.rawValue - ) - ) - } -} diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift index 90e85d1b00..7145cce54f 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift @@ -22,10 +22,3 @@ struct SourceControlNavigatorContribution: WorkspacePanelContribution { let systemImage = "vault" var content: AnyView { AnyView(SourceControlNavigatorView()) } } - -struct FindNavigatorContribution: WorkspacePanelContribution { - let id = PanelTabID.search - let title = "Search" - let systemImage = "magnifyingglass" - var content: AnyView { AnyView(FindNavigatorTab()) } -} diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift index 9448bd535e..f25167485b 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 12/08/26. // +import CESearch import CodeEditUI /// The ids of the first-party panel tabs. @@ -16,7 +17,9 @@ import CodeEditUI enum PanelTabID { static let project = "project" static let sourceControl = "sourceControl" - static let search = "search" + /// Owned by `CESearch.FindNavigatorContribution`, which vends the tab this id selects — kept + /// as one source of truth rather than duplicated as a literal. + static let search = FindNavigatorContribution.tabID static let file = "file" static let gitHistory = "gitHistory" diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 479dbe481a..00a75f0962 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -104,7 +104,7 @@ let package = Package( ] ), .target(name: "CENotifications", dependencies: ["CodeEditCore", "CodeEditUI"]), - .target(name: "CESearch", dependencies: ["CodeEditCore", "CodeEditUI"]), + .target(name: "CESearch", dependencies: ["CodeEditCore", "CodeEditUI", "CodeEditSettings"]), .target( name: "CESourceControl", dependencies: [ diff --git a/CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift b/CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift new file mode 100644 index 0000000000..7c28381adb --- /dev/null +++ b/CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift @@ -0,0 +1,48 @@ +// +// FindNavigatorContribution.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 12/08/26. +// + +import CodeEditSettings +import CodeEditUI +import SwiftUI + +/// CESearch's navigator tab. +/// +/// The package vends this itself; there is no app-side wrapper. Settings are read here through the +/// seam rather than passed down from the app, which is what the wrapper this replaced existed to do +/// before feature packages could read settings. +public struct FindNavigatorContribution: WorkspacePanelContribution { + /// The single source of truth for this tab's id. The app-side `PanelTabID.search` references + /// this constant so there is exactly one place the id is defined, even though ownership of the + /// id now sits with the feature that owns the tab. + public static let tabID = "search" + + public let id = FindNavigatorContribution.tabID + public let title = "Search" + public let systemImage = "magnifyingglass" + + public init() {} + + public var content: AnyView { AnyView(FindNavigatorContentView()) } +} + +/// Reads the settings the find navigator needs, so the contribution itself stays a plain value. +private struct FindNavigatorContentView: View { + @SettingsValue(GeneralSettings.self, \.projectNavigatorSize) + private var projectNavigatorSize + + @SettingsValue(GeneralSettings.self, \.findNavigatorDetail) + private var findNavigatorDetail + + var body: some View { + FindNavigatorView( + configuration: FindNavigatorConfiguration( + rowHeight: projectNavigatorSize.rowHeight, + matchDetailLineLimit: findNavigatorDetail.rawValue + ) + ) + } +} From 0b33142440fd4ca5c48b0820f8f1dd63d62ebc71 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 12 Aug 2026 23:23:35 +0200 Subject: [PATCH 272/335] Docs: Document the workspace panel contribution seam --- docs/ARCHITECTURE.md | 61 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0790c9cbba..57df8e8074 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -115,10 +115,12 @@ where it is visible to everyone. Acyclicity itself needs no rule — SwiftPM enf **Hub heuristic.** Any target both depended on by three or more others *and* itself depending on three or more is a hub under review. 2022's `AppPreferences` was exactly this and would have been -flagged years before it became fatal. `CodeEditSettings` is the current watch item: four dependents -(`CEEditor`, `CELSP`, `CESourceControl`, `CETerminal`) but only one dependency (`CodeEditCore`), so -it stays a well-formed shared substrate rather than a hub — and it imports `AppKit` in 1 file and -`SwiftUI` in 7. +flagged years before it became fatal. `CodeEditSettings` is the current watch item: five dependents +as of the panel-contributions work (`CEEditor`, `CELSP`, `CESearch`, `CESourceControl`, `CETerminal` +— up from four; `CESearch` joined when `FindNavigatorContribution` started reading its own settings +instead of taking them from an app-side wrapper) but only one dependency (`CodeEditCore`), so it +stays a well-formed shared substrate rather than a hub — and it imports `AppKit` in 1 file and +`SwiftUI` in 7 (both unchanged; re-measured, not carried forward). ## Where does my code go? @@ -128,7 +130,13 @@ Work through these in order; the first match wins. [recipe](#creating-a-new-feature-target)). Features start as targets; the app target is not the default. Exception: *shell chrome* that composes multiple features around the concrete `Workspace` hub — navigator/inspector/utility areas, the status bar — stays - app-side, because its interface would effectively be "the whole app". + app-side, because its interface would effectively be "the whole app". **This exemption is + about the panel, not any one tab inside it.** `NavigatorAreaView` hosts a tab bar over many + features' tabs and has no single owner, so it stays app-side; a *tab* is one feature's own + UI and belongs in that feature's package — `CESearch` owns `FindNavigatorContribution` for + exactly this reason (see [Panel tab contributions](#panel-tab-contributions)). The only + permanent app-side tab is `ProjectNavigatorContribution`, because the project navigator has + no owning package to move to, not because it is a tab. 2. **A type, protocol, event, or command interface needed by two or more features?** → `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI/IO imports, no external dependencies). Events (facts, e.g. `TaskNotificationEvent`) and command interfaces @@ -204,6 +212,49 @@ Grouping is **purpose-first**: (the presentation-state split), and views issue commands through protocol-typed environment keys. +## Panel tab contributions + +The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, +`InspectorTab`, `UtilityAreaTab`). Each panel is a list of `WorkspacePanelContribution` values — +a tab is a value, not a case — assembled by one function per panel in +`CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`: first-party entries named +directly, conditional ones (`InternalDevelopmentInspectorContribution`) as a plain `if`, then +extension-provided ones appended through a single adapter call. The panel that renders the list +cannot tell a first-party tab from an extension's — that indistinguishability is the point; it is +what lets a new contribution source arrive without the panel code changing. + +`WorkspacePanelContribution` lives in `CodeEditUI` +(`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`), not `CodeEditCore`: a +contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forbids UI imports +outright. `CodeEditUI`'s own charter (rule 2) is satisfied too — the protocol needs SwiftUI and +nothing else. + +A contribution's owner follows the same placement rule as everything else in +[Where does my code go?](#where-does-my-code-go): a feature that owns a tab vends its own +contribution from its package, reading whatever it needs (including settings, through the seam) +directly rather than having the app assemble it. `CESearch`'s `FindNavigatorContribution` +(`CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift`) replaced an app-side +`FindNavigatorTab` wrapper that existed only to shuttle settings values down — once the feature +could read its own settings, the wrapper had no reason to exist. The tab's id is now owned by the +same package (`FindNavigatorContribution.tabID`); the app's `PanelTabID.search` references it +rather than duplicating the literal, so there is exactly one source of truth even though the app +still needs a compile-checked constant to select the tab by. + +`ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) +is the one contribution that stays app-side permanently — not because it is a tab (see the +[chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no +owning package to move to. `SourceControlNavigatorContribution` in the same file is app-side only +until `SourceControlNavigatorView` is packaged — an out-of-scope follow-up, not a charter +exception. + +Extensions are the third contribution source. `ExtensionPanelContribution` +(`CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift`) is the **only** app +file, outside the pre-existing extension-management UI under `AuxiliaryWindows/Extensions/`, that +may name `AppExtensionIdentity` or `ResolvedSidebar`. Confining ExtensionKit's vocabulary to this +one adapter is what let the app-side panel-contribution list above be written without an +ExtensionKit import in sight, and is what would let a second, non-ExtensionKit contribution source +arrive later without touching the panels. + ## Reading and writing settings Feature packages reach settings through the **settings seam** in `CodeEditSettings` From f4d1dd33c10280fbf2d41caad7b240537bd9bfbe Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 13 Aug 2026 16:09:23 +0200 Subject: [PATCH 273/335] Fix: Isolate WorkspacePanelContribution.content to the main actor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every contribution vends a SwiftUI view, and View's members are main-actor isolated, but the requirement was nonisolated. A conformer in a Swift 6 target therefore warned when constructing its own content view — FindNavigatorContribution in CESearch hit exactly that. The nine app-side conformers have the same shape but sit in the Swift 5 app target, which does not report it. Only content is isolated; id, title and systemImage stay plain values read from non-isolated positions. No conformer or test needed changing. --- .../Sources/CodeEditUI/WorkspacePanelContribution.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift b/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift index 4a5aa3923a..cf9974be80 100644 --- a/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift +++ b/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift @@ -31,5 +31,11 @@ public protocol WorkspacePanelContribution: Identifiable { /// /// Type-erased because a panel holds a heterogeneous list. Erasure happens once per tab, not per /// row, and one tab is visible at a time. - var content: AnyView { get } + /// + /// Main-actor isolated because every contribution vends a SwiftUI view, and `View`'s members are + /// main-actor isolated. Left nonisolated, a conformer in a Swift 6 target warns when it + /// constructs its own content view — the app target simply doesn't report it, being Swift 5. + /// Only this requirement is isolated: `id`, `title` and `systemImage` are plain values read from + /// non-isolated positions. + @MainActor var content: AnyView { get } } From b46e21aaec39762be6e1c96e13b9f72595788f0b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 13:47:37 +0200 Subject: [PATCH 274/335] Feat: Let WorkspaceNavigator open a file by URL --- .../Adapters/AppWorkspaceNavigator.swift | 11 +++++++++++ .../Infrastructure/WorkspaceNavigator.swift | 12 ++++++++++++ .../Workspace/AppWorkspaceNavigatorTests.swift | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift index 9e2d174f54..909efe54f7 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift @@ -25,6 +25,17 @@ final class AppWorkspaceNavigator: WorkspaceNavigator { _ = windowManager.openFileInWorkspace(url: file.url, asTemporary: asTemporary) } + @MainActor + func open(fileAt url: URL, asTemporary: Bool) { + guard let ceFile = windowManager.workspace(containing: url)?.workspaceFileManager.getFile( + url.absolutePath, + createIfNotFound: true + ) else { + return + } + open(file: ceFile, asTemporary: asTemporary) + } + @MainActor func reveal(file: CEWorkspaceFile) { windowManager.workspace(containing: file.url)?.revealRequests.send(file) diff --git a/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift index e18904548b..cc240ea0c5 100644 --- a/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift +++ b/CodeEditModules/Sources/CodeEditCore/Infrastructure/WorkspaceNavigator.swift @@ -16,6 +16,16 @@ public protocol WorkspaceNavigator: AnyObject { @MainActor func open(file: CEWorkspaceFile, asTemporary: Bool) + /// Resolve `url` to a workspace file and open it in the active editor. + /// + /// For callers that hold a URL rather than a `CEWorkspaceFile` — a package cannot resolve one + /// without depending on the workspace file manager, and expressing intent is the point of this + /// interface. + /// - Parameter asTemporary: open as a temporary (preview) tab, replaced by the next + /// temporary open, rather than a pinned tab. + @MainActor + func open(fileAt url: URL, asTemporary: Bool) + /// Highlight `file` in the project navigator without opening it. @MainActor func reveal(file: CEWorkspaceFile) @@ -31,6 +41,8 @@ public final class NoOpWorkspaceNavigator: WorkspaceNavigator { @MainActor public func open(file: CEWorkspaceFile, asTemporary: Bool) {} @MainActor + public func open(fileAt url: URL, asTemporary: Bool) {} + @MainActor public func reveal(file: CEWorkspaceFile) {} @MainActor public func closeTab(file: CEWorkspaceFile) {} diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index 0608913209..0862262331 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -51,6 +51,24 @@ struct AppWorkspaceNavigatorTests { #expect(mock.opened.first?.asTemporary == true) } + @MainActor + @Test + func openFileAtURLResolvesThroughWorkspaceFileManagerAndPreservesTemporaryFlag() throws { + let workspace = try TestWorkspaceFactory.make() + let fileURL = workspace.fileURL.appending(path: "example.swift") + try "// example".write(to: fileURL, atomically: true, encoding: .utf8) + + let mock = MockWindowManager() + mock.stubbedWorkspace = workspace + let navigator = AppWorkspaceNavigator(windowManager: mock) + + navigator.open(fileAt: fileURL, asTemporary: true) + + #expect(mock.opened.count == 1) + #expect(mock.opened.first?.url == fileURL) + #expect(mock.opened.first?.asTemporary == true) + } + @MainActor @Test func revealSendsRevealRequestOnCorrectWorkspace() throws { From bee0268c68f89a50798ad53bd208e296976ed0d3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 13:55:12 +0200 Subject: [PATCH 275/335] Refactor: Drop the workspace file manager from the source control navigator --- .../Changes/SourceControlNavigatorChangesList.swift | 11 +---------- .../GitChangedFileLabel.swift | 13 ++----------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift index 6af32edcfe..7b082e7a3d 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift @@ -7,7 +7,6 @@ import CESourceControl import AppKit -import CEWorkspaceFileManager import SwiftUI import CodeEditCore @@ -16,9 +15,6 @@ struct SourceControlNavigatorChangesList: View { @Environment(\.workspaceNavigator) private var workspaceNavigator - @Environment(\.workspaceFileManager) - private var workspaceFileManager - @State var selection = Set() var body: some View { @@ -79,11 +75,6 @@ struct SourceControlNavigatorChangesList: View { } private func openGitFile(_ file: GitChangedFile) { - guard let ceFile = workspaceFileManager?.getFile(file.ceFileKey, createIfNotFound: true) else { - return - } - DispatchQueue.main.async { - workspaceNavigator.open(file: ceFile, asTemporary: true) - } + workspaceNavigator.open(fileAt: file.fileURL, asTemporary: true) } } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift index 81b69f0068..cb850d0280 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift @@ -8,7 +8,6 @@ import CESourceControl import SwiftUI import ShellClient -import CEWorkspaceFileManager import CodeEditCore import CodeEditSettings import CodeEditUI @@ -16,9 +15,6 @@ import CodeEditUI struct GitChangedFileLabel: View { @EnvironmentObject private var sourceControlManager: SourceControlManager - @Environment(\.workspaceFileManager) - private var workspaceFileManager - let file: GitChangedFile var body: some View { @@ -27,13 +23,8 @@ struct GitChangedFileLabel: View { .lineLimit(1) .truncationMode(.middle) } icon: { - if let ceFile = workspaceFileManager?.getFile(file.ceFileKey, createIfNotFound: true) { - Image(nsImage: ceFile.nsIcon) - .renderingMode(.template) - } else { - FileIcon.generic.image - .renderingMode(.template) - } + FileIcon.spec(for: file.fileURL).image + .renderingMode(.template) } } } From e864634702d0ffde12172b978916742bb6c24f9c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 13:58:32 +0200 Subject: [PATCH 276/335] Refactor: Drop the remaining workspace file manager use from the changes list --- .../SourceControlNavigatorChangesList.swift | 4 +++- .../GitChangedFileListView.swift | 18 +----------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift index 7b082e7a3d..ce106d4e19 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift @@ -75,6 +75,8 @@ struct SourceControlNavigatorChangesList: View { } private func openGitFile(_ file: GitChangedFile) { - workspaceNavigator.open(fileAt: file.fileURL, asTemporary: true) + DispatchQueue.main.async { + workspaceNavigator.open(fileAt: file.fileURL, asTemporary: true) + } } } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift index 0ed134fdf2..5ec805a3a8 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift @@ -8,7 +8,6 @@ import CESourceControl import SwiftUI import CodeEditSettings -import CEWorkspaceFileManager import CodeEditCore import CodeEditUI @@ -18,9 +17,6 @@ struct GitChangedFileListView: View { private var fileIconStyle @EnvironmentObject private var sourceControlManager: SourceControlManager - @Environment(\.workspaceFileManager) - private var workspaceFileManager - @Binding private var changedFile: GitChangedFile @State private var staged: Bool @@ -65,21 +61,9 @@ struct GitChangedFileListView: View { } private var listItemTint: Color { - if let ceFile = workspaceFileManager?.getFile(changedFile.ceFileKey, createIfNotFound: true) { - iconForegroundColor(ceFile) - } else { - iconForegroundColor(nil) - } - } - - private func iconForegroundColor(_ file: CEWorkspaceFile?) -> Color { switch fileIconStyle { case .color: - if let file { - return file.iconColor - } else { - return FileIcon.generic.color - } + return FileIcon.spec(for: changedFile.fileURL).color case .monochrome: return Color("CoolGray") } From f9f78670f3a7e28e692608a3ecdcddf64387bad2 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 14:04:42 +0200 Subject: [PATCH 277/335] Refactor: Read source control view settings through the seam --- .../InspectorArea/HistoryInspector/HistoryInspectorView.swift | 2 +- .../SourceControlNavigator/GitChangedFileListView.swift | 2 +- .../History/SourceControlNavigatorHistoryView.swift | 2 +- .../Repository/SourceControlNavigatorRepositoryItem.swift | 2 +- .../SourceControlNavigator/SourceControlNavigatorView.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift index 37f0c0e9f2..7d212fd538 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift @@ -11,7 +11,7 @@ import CodeEditUI import CodeEditCore struct HistoryInspectorView: View { - @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) + @SettingsValue(SourceControlSettings.self, \.git.showMergeCommitsPerFileLog) var showMergeCommitsPerFileLog @EnvironmentObject private var sourceControlManager: SourceControlManager diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift index 5ec805a3a8..fd7562c084 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift @@ -13,7 +13,7 @@ import CodeEditUI /// A view to display a changed file's information in a list view. Optionally displays the staged status. struct GitChangedFileListView: View { - @AppSettings(\.general.fileIconStyle) + @SettingsValue(GeneralSettings.self, \.fileIconStyle) private var fileIconStyle @EnvironmentObject private var sourceControlManager: SourceControlManager diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift index 630f1b3598..9b75064ec2 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift @@ -19,7 +19,7 @@ struct SourceControlNavigatorHistoryView: View { case error(error: Error) } - @AppSettings(\.sourceControl.git.showMergeCommitsPerFileLog) + @SettingsValue(SourceControlSettings.self, \.git.showMergeCommitsPerFileLog) var showMergeCommitsPerFileLog @EnvironmentObject var sourceControlManager: SourceControlManager diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift index 073566df12..4d00752d3a 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift @@ -9,7 +9,7 @@ import SwiftUI import CodeEditSettings struct SourceControlNavigatorRepositoryItem: View { - @AppSettings(\.general.fileIconStyle) + @SettingsValue(GeneralSettings.self, \.fileIconStyle) var fileIconStyle let item: RepoOutlineGroupItem diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift index b0da5c2d0b..f8850a054a 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift @@ -14,7 +14,7 @@ struct SourceControlNavigatorView: View { @EnvironmentObject private var sourceControlManager: SourceControlManager @EnvironmentObject private var sourceControlViewModel: SourceControlViewModel - @AppSettings(\.sourceControl.general.fetchRefreshServerStatus) + @SettingsValue(SourceControlSettings.self, \.general.fetchRefreshServerStatus) var fetchRefreshServerStatus var body: some View { From f63918db88da330e970356741fe311104e0050e9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 14:15:36 +0200 Subject: [PATCH 278/335] Refactor: Move the source control views into CESourceControl --- CodeEdit/Utils/Date+Formatted.swift | 28 --------- .../CodeEditSplitViewController.swift | 7 ++- .../InspectorArea/InspectorAreaView.swift | 12 +++- .../InspectorContributions.swift | 7 --- .../NavigatorArea/NavigatorAreaView.swift | 8 ++- .../NavigatorContributions.swift | 7 --- .../WorkspacePanel/PanelContributions.swift | 23 ++++++-- .../GitHistoryInspectorContribution.swift | 32 +++++++++++ .../HistoryInspectorItemView.swift | 0 .../HistoryInspectorModel.swift | 16 +++--- .../HistoryInspectorView.swift | 20 ++++--- .../HistoryInspector/HistoryPopoverView.swift | 0 ...rceControlNavigatorChangesCommitView.swift | 1 - .../SourceControlNavigatorChangesList.swift | 9 +-- .../SourceControlNavigatorChangesView.swift | 7 ++- .../SourceControlNavigatorNoRemotesView.swift | 1 - .../SourceControlNavigatorSyncView.swift | 1 - .../GitChangedFileLabel.swift | 20 +------ .../GitChangedFileListView.swift | 1 - .../History/CommitDetailsHeaderView.swift | 0 .../History/CommitDetailsView.swift | 1 - .../History/CommitListItemView.swift | 0 .../History/Date+RelativeStringToNow.swift | 39 +++++++++++++ .../SourceControlNavigatorHistoryView.swift | 1 - .../History/String+MD5.swift | 0 .../Repository/RepoOutlineGroupItem.swift | 0 ...SourceControlNavigatorRepositoryItem.swift | 5 +- ...lNavigatorRepositoryView+contextMenu.swift | 0 ...gatorRepositoryView+outlineGroupData.swift | 0 ...SourceControlNavigatorRepositoryView.swift | 1 - .../SourceControlNavigatorContribution.swift | 34 +++++++++++ .../SourceControlNavigatorToolbarBottom.swift | 1 - .../SourceControlNavigatorView.swift | 20 +++++-- .../CommitFormattingTests.swift | 57 +++++++++++++++++++ .../Utils/UnitTests_Extensions.swift | 41 ------------- 35 files changed, 250 insertions(+), 150 deletions(-) create mode 100644 CodeEditModules/Sources/CESourceControl/HistoryInspector/GitHistoryInspectorContribution.swift rename {CodeEdit/WorkspaceWindow/InspectorArea => CodeEditModules/Sources/CESourceControl}/HistoryInspector/HistoryInspectorItemView.swift (100%) rename {CodeEdit/WorkspaceWindow/InspectorArea => CodeEditModules/Sources/CESourceControl}/HistoryInspector/HistoryInspectorModel.swift (80%) rename {CodeEdit/WorkspaceWindow/InspectorArea => CodeEditModules/Sources/CESourceControl}/HistoryInspector/HistoryInspectorView.swift (76%) rename {CodeEdit/WorkspaceWindow/InspectorArea => CodeEditModules/Sources/CESourceControl}/HistoryInspector/HistoryPopoverView.swift (100%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift (99%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift (90%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift (90%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift (97%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift (99%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/GitChangedFileLabel.swift (62%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/GitChangedFileListView.swift (99%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/History/CommitDetailsHeaderView.swift (100%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/History/CommitDetailsView.swift (99%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/History/CommitListItemView.swift (100%) create mode 100644 CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift (99%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/History/String+MD5.swift (100%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift (100%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift (86%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift (100%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift (100%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift (99%) create mode 100644 CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorContribution.swift rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift (99%) rename {CodeEdit/WorkspaceWindow/NavigatorArea => CodeEditModules/Sources/CESourceControl}/SourceControlNavigator/SourceControlNavigatorView.swift (81%) create mode 100644 CodeEditModules/Tests/CESourceControlTests/CommitFormattingTests.swift diff --git a/CodeEdit/Utils/Date+Formatted.swift b/CodeEdit/Utils/Date+Formatted.swift index 54ada62530..7607b231cb 100644 --- a/CodeEdit/Utils/Date+Formatted.swift +++ b/CodeEdit/Utils/Date+Formatted.swift @@ -9,34 +9,6 @@ import Foundation extension Date { - /// Returns a formatted & localized string of a relative duration compared to the current date & time - /// when the date is in `today` or `yesterday`. Otherwise it returns a formatted date in `short` - /// format. The time is omitted. - /// - Parameter locale: The locale. Defaults to `Locale.current` - /// - Returns: A localized formatted string - func relativeStringToNow(locale: Locale = .current) -> String { - if Calendar.current.isDateInToday(self) || - Calendar.current.isDateInYesterday(self) { - var style = RelativeFormatStyle( - presentation: .named, - unitsStyle: .abbreviated, - locale: .current, - calendar: .current, - capitalizationContext: .standalone - ) - - style.locale = locale - - return self.formatted(style) - } - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .none - formatter.locale = locale - - return formatter.string(from: self) - } - static var logFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss.SSSS" diff --git a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift index c1a9e55be1..74aa40b802 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift @@ -117,7 +117,7 @@ final class CodeEditSplitViewController: NSSplitViewController { activeEditorState: AppActiveEditorState ) -> NSSplitViewItem { makeNavigator(view: SettingsInjector(store: dependencies.settingsStore) { - NavigatorAreaView(viewModel: navigatorViewModel) + NavigatorAreaView(viewModel: navigatorViewModel, navigator: dependencies.workspaceNavigator) .environment(\.workspace, workspace) .environmentObject(workspace.editorManager) .environmentObject(workspace.projectNavigatorViewModel) @@ -171,7 +171,10 @@ final class CodeEditSplitViewController: NSSplitViewController { fileEditorOverrides: AppFileEditorOverrides ) -> NSSplitViewItem { makeInspector(view: SettingsInjector(store: dependencies.settingsStore) { - InspectorAreaView(viewModel: InspectorAreaViewModel()) + InspectorAreaView( + viewModel: InspectorAreaViewModel(), + activeEditorState: activeEditorState + ) .environmentObject(workspace.editorManager) .environmentObject(workspace.sourceControlManager) .environment(\.workspaceFileManager, workspace.workspaceFileManager) diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift index 0db1d30ca4..6a766a776e 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings struct InspectorAreaView: View { @@ -18,14 +19,21 @@ struct InspectorAreaView: View { @AppSettings(\.developerSettings.showInternalDevelopmentInspector) var showInternalDevelopmentInspector - init(viewModel: InspectorAreaViewModel) { + /// The active-file read-model the history inspector follows. Taken by `init` rather than read + /// from `\.activeEditorState`: `updateTabs()` builds the contributions, and a view's + /// environment is not populated at the point the tabs first need it. + private let activeEditorState: ActiveEditorState + + init(viewModel: InspectorAreaViewModel, activeEditorState: ActiveEditorState) { self.viewModel = viewModel + self.activeEditorState = activeEditorState } private func updateTabs() { viewModel.tabItems = inspectorContributions( extensionManager: extensionManager, - showInternalDevelopment: showInternalDevelopmentInspector + showInternalDevelopment: showInternalDevelopmentInspector, + activeEditorState: activeEditorState ) } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift index bd232605e9..872f4ac06d 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/InspectorContributions.swift @@ -15,13 +15,6 @@ struct FileInspectorContribution: WorkspacePanelContribution { var content: AnyView { AnyView(FileInspectorView()) } } -struct GitHistoryInspectorContribution: WorkspacePanelContribution { - let id = PanelTabID.gitHistory - let title = "History Inspector" - let systemImage = "clock" - var content: AnyView { AnyView(HistoryInspectorView()) } -} - struct InternalDevelopmentInspectorContribution: WorkspacePanelContribution { let id = PanelTabID.internalDevelopment let title = "Internal Development" diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift index d94d581343..6a159a4470 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings struct NavigatorAreaView: View { @@ -15,10 +16,13 @@ struct NavigatorAreaView: View { @AppSettings(\.general.navigatorTabBarPosition) var sidebarPosition: GeneralSettings.SidebarTabBarPosition - init(viewModel: NavigatorAreaViewModel) { + init(viewModel: NavigatorAreaViewModel, navigator: WorkspaceNavigator) { self.viewModel = viewModel - viewModel.tabItems = navigatorContributions(extensionManager: extensionManager) + viewModel.tabItems = navigatorContributions( + extensionManager: extensionManager, + navigator: navigator + ) } var body: some View { diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift index 7145cce54f..4e83e4f153 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift @@ -15,10 +15,3 @@ struct ProjectNavigatorContribution: WorkspacePanelContribution { let systemImage = "folder" var content: AnyView { AnyView(ProjectNavigatorView()) } } - -struct SourceControlNavigatorContribution: WorkspacePanelContribution { - let id = PanelTabID.sourceControl - let title = "Source Control" - let systemImage = "vault" - var content: AnyView { AnyView(SourceControlNavigatorView()) } -} diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift index f25167485b..83b5da5e9d 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift @@ -6,6 +6,8 @@ // import CESearch +import CESourceControl +import CodeEditCore import CodeEditUI /// The ids of the first-party panel tabs. @@ -16,13 +18,15 @@ import CodeEditUI /// are dynamic and deliberately absent. enum PanelTabID { static let project = "project" - static let sourceControl = "sourceControl" + /// Owned by `CESourceControl.SourceControlNavigatorContribution`. + static let sourceControl = SourceControlNavigatorContribution.tabID /// Owned by `CESearch.FindNavigatorContribution`, which vends the tab this id selects — kept /// as one source of truth rather than duplicated as a literal. static let search = FindNavigatorContribution.tabID static let file = "file" - static let gitHistory = "gitHistory" + /// Owned by `CESourceControl.GitHistoryInspectorContribution`. + static let gitHistory = GitHistoryInspectorContribution.tabID static let internalDevelopment = "internalDevelopment" static let terminal = "terminal" @@ -30,13 +34,19 @@ enum PanelTabID { static let output = "output" } +/// The `navigator` and `activeEditorState` parameters below default to the no-op implementations +/// `CodeEditCore` vends — the same defaults the `\.workspaceNavigator` and `\.activeEditorState` +/// environment keys carry, and for the same reason: a caller with no workspace (previews, tests) +/// legitimately has nothing to pass. The real values come from the composition root via +/// `NavigatorAreaView` / `InspectorAreaView`. @MainActor func navigatorContributions( - extensionManager: ExtensionManager + extensionManager: ExtensionManager, + navigator: WorkspaceNavigator = NoOpWorkspaceNavigator() ) -> [any WorkspacePanelContribution] { var items: [any WorkspacePanelContribution] = [ ProjectNavigatorContribution(), - SourceControlNavigatorContribution(), + SourceControlNavigatorContribution(navigator: navigator), FindNavigatorContribution() ] items += extensionContributions(for: .navigator, from: extensionManager) @@ -46,11 +56,12 @@ func navigatorContributions( @MainActor func inspectorContributions( extensionManager: ExtensionManager, - showInternalDevelopment: Bool + showInternalDevelopment: Bool, + activeEditorState: ActiveEditorState = NoOpActiveEditorState() ) -> [any WorkspacePanelContribution] { var items: [any WorkspacePanelContribution] = [ FileInspectorContribution(), - GitHistoryInspectorContribution() + GitHistoryInspectorContribution(activeEditorState: activeEditorState) ] if showInternalDevelopment { items.append(InternalDevelopmentInspectorContribution()) diff --git a/CodeEditModules/Sources/CESourceControl/HistoryInspector/GitHistoryInspectorContribution.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/GitHistoryInspectorContribution.swift new file mode 100644 index 0000000000..5492e7a4a5 --- /dev/null +++ b/CodeEditModules/Sources/CESourceControl/HistoryInspector/GitHistoryInspectorContribution.swift @@ -0,0 +1,32 @@ +// +// GitHistoryInspectorContribution.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 14/08/26. +// + +import CodeEditCore +import CodeEditUI +import SwiftUI + +/// CESourceControl's inspector tab. +/// +/// Takes the workspace's active-file read-model as an initialiser parameter: the history it shows +/// follows the editor selection, and `\.activeEditorState` is an app-shell environment key. +public struct GitHistoryInspectorContribution: WorkspacePanelContribution { + /// The single source of truth for this tab's id. The app-side `PanelTabID.gitHistory` + /// references this constant so the id is defined in exactly one place. + public static let tabID = "gitHistory" + + public let id = GitHistoryInspectorContribution.tabID + public let title = "History Inspector" + public let systemImage = "clock" + + private let activeEditorState: ActiveEditorState + + public init(activeEditorState: ActiveEditorState) { + self.activeEditorState = activeEditorState + } + + public var content: AnyView { AnyView(HistoryInspectorView(activeEditorState: activeEditorState)) } +} diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorItemView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorItemView.swift rename to CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorItemView.swift diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorModel.swift similarity index 80% rename from CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift rename to CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorModel.swift index 9358bda363..0cd7f01790 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorModel.swift +++ b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorModel.swift @@ -5,11 +5,14 @@ // Created by Nanashi Li on 2022/04/18. // -import CESourceControl import Foundation import CodeEditSettings import CodeEditCore +/// Main-actor isolated: it is created and driven entirely by `HistoryInspectorView`, and this +/// target compiles under Swift 6 strict concurrency, where passing a non-`Sendable` model into a +/// `Task` from the view is an error rather than the warning it was app-side. +@MainActor final class HistoryInspectorModel: ObservableObject { /// The settings store. Assigned by `HistoryInspectorView` from the environment, alongside the /// source-control manager — this model is created by a view and configured the same way. @@ -40,7 +43,7 @@ final class HistoryInspectorModel: ObservableObject { func updateCommitHistory() async { guard let sourceControlManager, let fileURL else { - await setCommitHistory([]) + commitHistory = [] return } @@ -53,14 +56,9 @@ final class HistoryInspectorModel: ObservableObject { fileLocalPath: fileURL, showMergeCommits: settingsAccessor.value(SourceControlSettings.self).git.showMergeCommitsPerFileLog ) - await setCommitHistory(commitHistory) + self.commitHistory = commitHistory } catch { - await setCommitHistory([]) + self.commitHistory = [] } } - - @MainActor - private func setCommitHistory(_ history: [GitCommit]) { - self.commitHistory = history - } } diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift similarity index 76% rename from CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift rename to CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift index 7d212fd538..0a16e542c6 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift @@ -4,20 +4,21 @@ // // Created by Nanashi Li on 2022/03/24. // -import CESourceControl import SwiftUI import CodeEditSettings import CodeEditUI import CodeEditCore -struct HistoryInspectorView: View { +public struct HistoryInspectorView: View { @SettingsValue(SourceControlSettings.self, \.git.showMergeCommitsPerFileLog) var showMergeCommitsPerFileLog @EnvironmentObject private var sourceControlManager: SourceControlManager - @Environment(\.activeEditorState) - private var activeEditorState + /// The active-file read-model, injected rather than read from the environment: the + /// `\.activeEditorState` key is declared in the app shell and this view now ships in + /// `CESourceControl`. + private let activeEditorState: ActiveEditorState @Environment(\.settingsAccessor) private var settingsAccessor @@ -26,13 +27,14 @@ struct HistoryInspectorView: View { @State var selection: GitCommit? - /// Initialize with GitClient - /// - Parameter gitClient: a GitClient - init() { + /// - Parameter activeEditorState: the workspace's active-file read-model; the history shown + /// follows its selection. + public init(activeEditorState: ActiveEditorState) { + self.activeEditorState = activeEditorState self.model = .init() } - var body: some View { + public var body: some View { Group { if model.sourceControlManager != nil { VStack { @@ -49,7 +51,7 @@ struct HistoryInspectorView: View { } } } else { - NoSelectionInspectorView() + CEContentUnavailableView("No Selection") } } .onReceive(activeEditorState.selectedFilePublisher) { file in diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryPopoverView.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryPopoverView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/InspectorArea/HistoryInspector/HistoryPopoverView.swift rename to CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryPopoverView.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift index 516a8fc8a0..4c37c944f1 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesCommitView.swift @@ -5,7 +5,6 @@ // Created by Albert Vinizhanau on 10/19/23. // -import CESourceControl import SwiftUI import CodeEditUI diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift similarity index 90% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift index ce106d4e19..2fe5d9b83f 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesList.swift @@ -5,15 +5,16 @@ // Created by Austin Condiff on 11/18/23. // -import CESourceControl import AppKit import SwiftUI import CodeEditCore struct SourceControlNavigatorChangesList: View { @EnvironmentObject var sourceControlManager: SourceControlManager - @Environment(\.workspaceNavigator) - private var workspaceNavigator + + /// Threaded from `SourceControlNavigatorView` rather than read from the environment: the + /// environment key lives in the app shell, and this view now ships in `CESourceControl`. + let navigator: WorkspaceNavigator @State var selection = Set() @@ -76,7 +77,7 @@ struct SourceControlNavigatorChangesList: View { private func openGitFile(_ file: GitChangedFile) { DispatchQueue.main.async { - workspaceNavigator.open(fileAt: file.fileURL, asTemporary: true) + navigator.open(fileAt: file.fileURL, asTemporary: true) } } } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift similarity index 90% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift index 639de7cf83..0894448d95 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorChangesView.swift @@ -5,13 +5,16 @@ // Created by Nanashi Li on 2022/05/20. // -import CESourceControl import SwiftUI +import CodeEditCore import CodeEditUI struct SourceControlNavigatorChangesView: View { @EnvironmentObject var sourceControlManager: SourceControlManager + /// Passed down to the changes list, which needs it to open a changed file. + let navigator: WorkspaceNavigator + var hasRemotes: Bool { !sourceControlManager.remotes.isEmpty } @@ -50,7 +53,7 @@ struct SourceControlNavigatorChangesView: View { Divider() } if hasChanges { - SourceControlNavigatorChangesList() + SourceControlNavigatorChangesList(navigator: navigator) } else { CEContentUnavailableView("No Changes") } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift similarity index 97% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift index a27d82af98..187c8c468f 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorNoRemotesView.swift @@ -5,7 +5,6 @@ // Created by Austin Condiff on 11/17/23. // -import CESourceControl import SwiftUI struct SourceControlNavigatorNoRemotesView: View { diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift index 209335ec7c..1c80b2e435 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Changes/SourceControlNavigatorSyncView.swift @@ -5,7 +5,6 @@ // Created by Albert Vinizhanau on 10/20/23. // -import CESourceControl import SwiftUI struct SourceControlNavigatorSyncView: View { diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/GitChangedFileLabel.swift similarity index 62% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/GitChangedFileLabel.swift index cb850d0280..094e8263ee 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileLabel.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/GitChangedFileLabel.swift @@ -5,16 +5,11 @@ // Created by Khan Winter on 8/23/24. // -import CESourceControl import SwiftUI -import ShellClient import CodeEditCore -import CodeEditSettings import CodeEditUI struct GitChangedFileLabel: View { - @EnvironmentObject private var sourceControlManager: SourceControlManager - let file: GitChangedFile var body: some View { @@ -29,6 +24,9 @@ struct GitChangedFileLabel: View { } } +// The label reads nothing but `file`, so the preview needs no environment. Building a +// `SourceControlManager` here previously forced a `ShellClient` import that this target — and this +// view — has no other use for. #Preview { Group { GitChangedFileLabel(file: GitChangedFile( @@ -37,12 +35,6 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: nil )) - .environmentObject(SourceControlManager( - workspaceURL: URL(filePath: "/Users/CodeEdit"), - shellClient: ShellClient(), - eventBus: EventBus(), - settingsReader: DefaultSettingsReader() - )) GitChangedFileLabel(file: GitChangedFile( status: .none, @@ -50,11 +42,5 @@ struct GitChangedFileLabel: View { fileURL: URL(filePath: "/Users/CodeEdit/app.jsx"), originalFilename: "app2.jsx" )) - .environmentObject(SourceControlManager( - workspaceURL: URL(filePath: "/Users/CodeEdit"), - shellClient: ShellClient(), - eventBus: EventBus(), - settingsReader: DefaultSettingsReader() - )) }.padding() } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/GitChangedFileListView.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/GitChangedFileListView.swift index fd7562c084..16f6792e39 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/GitChangedFileListView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/GitChangedFileListView.swift @@ -5,7 +5,6 @@ // Created by Nanashi Li on 2022/05/20. // -import CESourceControl import SwiftUI import CodeEditSettings import CodeEditCore diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitDetailsHeaderView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsHeaderView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitDetailsHeaderView.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitDetailsView.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitDetailsView.swift index 649f726b8d..d35c322d32 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitDetailsView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitDetailsView.swift @@ -5,7 +5,6 @@ // Created by Austin Condiff on 12/27/23. // -import CESourceControl import SwiftUI import CodeEditUI import CodeEditCore diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitListItemView.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/CommitListItemView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/CommitListItemView.swift diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift new file mode 100644 index 0000000000..8540c547c7 --- /dev/null +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift @@ -0,0 +1,39 @@ +// +// Date+RelativeStringToNow.swift +// CodeEditModules/CodeEditUtils +// +// Created by Lukas Pistrol on 20.04.22. +// + +import Foundation + +extension Date { + + /// Returns a formatted & localized string of a relative duration compared to the current date & time + /// when the date is in `today` or `yesterday`. Otherwise it returns a formatted date in `short` + /// format. The time is omitted. + /// - Parameter locale: The locale. Defaults to `Locale.current` + /// - Returns: A localized formatted string + func relativeStringToNow(locale: Locale = .current) -> String { + if Calendar.current.isDateInToday(self) || + Calendar.current.isDateInYesterday(self) { + var style = RelativeFormatStyle( + presentation: .named, + unitsStyle: .abbreviated, + locale: .current, + calendar: .current, + capitalizationContext: .standalone + ) + + style.locale = locale + + return self.formatted(style) + } + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .none + formatter.locale = locale + + return formatter.string(from: self) + } +} diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift index 9b75064ec2..1a5e2647e9 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/SourceControlNavigatorHistoryView.swift @@ -5,7 +5,6 @@ // Created by Austin Condiff on 12/27/2023. // -import CESourceControl import SwiftUI import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/String+MD5.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/String+MD5.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/History/String+MD5.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/String+MD5.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/RepoOutlineGroupItem.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift similarity index 86% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift index 4d00752d3a..5dbf33db05 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift @@ -58,7 +58,10 @@ struct SourceControlNavigatorRepositoryItem: View { } } .opacity(controlActiveState == .inactive ? 0.5 : 1) - .foregroundStyle(fileIconStyle == .color ? item.imageColor : Color.coolGray) + // `Color.coolGray` is an app-target asset symbol, which a package cannot see. Named + // lookup against the main bundle resolves the same asset and is what the sibling + // `GitChangedFileListView` — and `CEEditor` — already do for this colour. + .foregroundStyle(fileIconStyle == .color ? item.imageColor : Color("CoolGray")) }) .padding(.leading, 1) .padding(.vertical, -1) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift similarity index 100% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift index e680e75980..9dd8f90fb3 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView.swift @@ -5,7 +5,6 @@ // Created by Nanashi Li on 2022/05/20. // -import CESourceControl import SwiftUI import CodeEditUI import CodeEditCore diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorContribution.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorContribution.swift new file mode 100644 index 0000000000..71109cf22d --- /dev/null +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorContribution.swift @@ -0,0 +1,34 @@ +// +// SourceControlNavigatorContribution.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 14/08/26. +// + +import CodeEditCore +import CodeEditUI +import SwiftUI + +/// CESourceControl's navigator tab. +/// +/// The package vends this itself; there is no app-side wrapper. The one dependency the tab cannot +/// resolve for itself — the command interface for opening a changed file — arrives as an +/// initialiser parameter from the composition root rather than through an environment key the +/// package does not own. +public struct SourceControlNavigatorContribution: WorkspacePanelContribution { + /// The single source of truth for this tab's id. The app-side `PanelTabID.sourceControl` + /// references this constant so the id is defined in exactly one place. + public static let tabID = "sourceControl" + + public let id = SourceControlNavigatorContribution.tabID + public let title = "Source Control" + public let systemImage = "vault" + + private let navigator: WorkspaceNavigator + + public init(navigator: WorkspaceNavigator) { + self.navigator = navigator + } + + public var content: AnyView { AnyView(SourceControlNavigatorView(navigator: navigator)) } +} diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift similarity index 99% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift index a41876d61a..69c776c273 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorToolbarBottom.swift @@ -5,7 +5,6 @@ // Created by Nanashi Li on 2022/05/20. // -import CESourceControl import SwiftUI import CodeEditUI diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift similarity index 81% rename from CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift index f8850a054a..712ab66622 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/SourceControlNavigator/SourceControlNavigatorView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift @@ -5,21 +5,29 @@ // Created by Nanashi Li on 2022/05/20. // -import CESourceControl import SwiftUI +import CodeEditCore import CodeEditSettings import CodeEditUI -struct SourceControlNavigatorView: View { +public struct SourceControlNavigatorView: View { @EnvironmentObject private var sourceControlManager: SourceControlManager @EnvironmentObject private var sourceControlViewModel: SourceControlViewModel @SettingsValue(SourceControlSettings.self, \.general.fetchRefreshServerStatus) var fetchRefreshServerStatus - var body: some View { + /// The command interface used to open a changed file. Injected rather than read from the + /// environment, so the app shell stays the only place that knows where it comes from. + private let navigator: WorkspaceNavigator + + public init(navigator: WorkspaceNavigator) { + self.navigator = navigator + } + + public var body: some View { VStack(spacing: 0) { - SourceControlNavigatorTabs() + SourceControlNavigatorTabs(navigator: navigator) .environmentObject(sourceControlManager) .environmentObject(sourceControlViewModel) .task { @@ -47,6 +55,8 @@ struct SourceControlNavigatorTabs: View { @EnvironmentObject var sourceControlManager: SourceControlManager @State private var selectedSection: Int = 0 + let navigator: WorkspaceNavigator + var body: some View { if sourceControlManager.isGitRepository { SegmentedControl( @@ -59,7 +69,7 @@ struct SourceControlNavigatorTabs: View { .padding(.horizontal, 8) Divider() if selectedSection == 0 { - SourceControlNavigatorChangesView() + SourceControlNavigatorChangesView(navigator: navigator) } if selectedSection == 1 { SourceControlNavigatorHistoryView() diff --git a/CodeEditModules/Tests/CESourceControlTests/CommitFormattingTests.swift b/CodeEditModules/Tests/CESourceControlTests/CommitFormattingTests.swift new file mode 100644 index 0000000000..70de789041 --- /dev/null +++ b/CodeEditModules/Tests/CESourceControlTests/CommitFormattingTests.swift @@ -0,0 +1,57 @@ +// +// CommitFormattingTests.swift +// CESourceControlTests +// +// Created by Matthijs Eikelenboom on 14/08/26. +// + +import Foundation +import XCTest +@testable import CESourceControl + +/// Covers the two formatting helpers the commit list relies on. They moved here with +/// `CommitListItemView` — `relativeStringToNow` renders a commit's age, `md5` builds the Gravatar +/// hash for its author — and these assertions are the ones they shipped with app-side. +final class CommitFormattingTests: XCTestCase { + + // MARK: - Date + relative string + + func testRelativeDateStringMinutes() throws { + let date = Date.now.addingTimeInterval(-61) + let string = date.relativeStringToNow(locale: Locale(identifier: "en_US")) + + XCTAssertEqual("1 min. ago", string) + } + + func testRelativeDateStringHours() throws { + let date = Date.now.addingTimeInterval(-3_601) + let string = date.relativeStringToNow(locale: Locale(identifier: "en_US")) + + XCTAssertEqual("1 hr. ago", string) + } + + func testRelativeDateStringDays() throws { + let date = Date.now.addingTimeInterval(-86_400) + let string = date.relativeStringToNow(locale: Locale(identifier: "en_US")) + + XCTAssertEqual("yesterday", string) + } + + // MARK: - String + MD5 + + func testMD5GenerationCaseSensitive() throws { + let testString = "CodeEdit" + let md5 = testString.md5(caseSensitive: true) + + let result = "8ba8c8fd0442f7bae4d441e2a3fda706" + XCTAssertEqual(result, md5) + } + + func testMD5Generation() throws { + let testString = "CodeEdit" + let md5 = testString.md5(caseSensitive: false) + + let result = "4cdf122ff382a2d929eddc1a63473ec1" + XCTAssertEqual(result, md5) + } +} diff --git a/CodeEditTests/Utils/UnitTests_Extensions.swift b/CodeEditTests/Utils/UnitTests_Extensions.swift index 4cfbd2b2f3..1404c80454 100644 --- a/CodeEditTests/Utils/UnitTests_Extensions.swift +++ b/CodeEditTests/Utils/UnitTests_Extensions.swift @@ -58,47 +58,6 @@ final class CodeEditUtilsExtensionsUnitTests: XCTestCase { XCTAssertEqual(alpha, color.alphaComponent) } - // MARK: - DATE + FORMATTED - - func testRelativeDateStringMinutes() throws { - let date = Date.now.addingTimeInterval(-61) - let string = date.relativeStringToNow(locale: Locale(identifier: "en_US")) - - XCTAssertEqual("1 min. ago", string) - } - - func testRelativeDateStringHours() throws { - let date = Date.now.addingTimeInterval(-3_601) - let string = date.relativeStringToNow(locale: Locale(identifier: "en_US")) - - XCTAssertEqual("1 hr. ago", string) - } - - func testRelativeDateStringDays() throws { - let date = Date.now.addingTimeInterval(-86_400) - let string = date.relativeStringToNow(locale: Locale(identifier: "en_US")) - - XCTAssertEqual("yesterday", string) - } - - // MARK: - STRING + MD5 - - func testMD5GenerationCaseSensitive() throws { - let testString = "CodeEdit" - let md5 = testString.md5(caseSensitive: true) - - let result = "8ba8c8fd0442f7bae4d441e2a3fda706" - XCTAssertEqual(result, md5) - } - - func testMD5Generation() throws { - let testString = "CodeEdit" - let md5 = testString.md5(caseSensitive: false) - - let result = "4cdf122ff382a2d929eddc1a63473ec1" - XCTAssertEqual(result, md5) - } - // MARK: - STRING + VALID FILE NAME func testValidFileName() { From 7b2e5151a4cb15725a0c88d6cf88fc9ecae49cee Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 14:19:42 +0200 Subject: [PATCH 279/335] Refactor: Require the panel contribution dependencies explicitly --- .../WorkspacePanel/PanelContributions.swift | 14 +++--- .../PanelContributionsTests.swift | 50 +++++++++++++++---- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift index 83b5da5e9d..8b93fc54a9 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift @@ -34,15 +34,15 @@ enum PanelTabID { static let output = "output" } -/// The `navigator` and `activeEditorState` parameters below default to the no-op implementations -/// `CodeEditCore` vends — the same defaults the `\.workspaceNavigator` and `\.activeEditorState` -/// environment keys carry, and for the same reason: a caller with no workspace (previews, tests) -/// legitimately has nothing to pass. The real values come from the composition root via -/// `NavigatorAreaView` / `InspectorAreaView`. +/// The `navigator` and `activeEditorState` parameters below are deliberately **required**. A +/// default would let a forgotten injection compile and then fail silently at runtime — a no-op +/// navigator means clicking a changed file does nothing, which no gate can see. Callers with no +/// workspace (tests) pass `CodeEditCore`'s no-op types explicitly, which is a choice rather than an +/// accident. @MainActor func navigatorContributions( extensionManager: ExtensionManager, - navigator: WorkspaceNavigator = NoOpWorkspaceNavigator() + navigator: WorkspaceNavigator ) -> [any WorkspacePanelContribution] { var items: [any WorkspacePanelContribution] = [ ProjectNavigatorContribution(), @@ -57,7 +57,7 @@ func navigatorContributions( func inspectorContributions( extensionManager: ExtensionManager, showInternalDevelopment: Bool, - activeEditorState: ActiveEditorState = NoOpActiveEditorState() + activeEditorState: ActiveEditorState ) -> [any WorkspacePanelContribution] { var items: [any WorkspacePanelContribution] = [ FileInspectorContribution(), diff --git a/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift b/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift index ae9107020c..30d96cf794 100644 --- a/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift +++ b/CodeEditTests/WorkspaceWindow/PanelContributionsTests.swift @@ -6,6 +6,7 @@ // import Testing +import CodeEditCore import CodeEditUI @testable import CodeEdit @@ -25,19 +26,28 @@ struct PanelContributionsTests { @Test func navigatorTabsKeepTheirIdsAndOrder() { - let items = navigatorContributions(extensionManager: ExtensionManager()) + let items = navigatorContributions( + extensionManager: ExtensionManager(), + navigator: NoOpWorkspaceNavigator() + ) #expect(items.prefix(3).map(\.id) == ["project", "sourceControl", "search"]) } @Test func navigatorTabsKeepTheirTitles() { - let items = navigatorContributions(extensionManager: ExtensionManager()) + let items = navigatorContributions( + extensionManager: ExtensionManager(), + navigator: NoOpWorkspaceNavigator() + ) #expect(items.prefix(3).map(\.title) == ["Project", "Source Control", "Search"]) } @Test func navigatorTabsKeepTheirSymbols() { - let items = navigatorContributions(extensionManager: ExtensionManager()) + let items = navigatorContributions( + extensionManager: ExtensionManager(), + navigator: NoOpWorkspaceNavigator() + ) #expect(items.prefix(3).map(\.systemImage) == ["folder", "vault", "magnifyingglass"]) } @@ -45,19 +55,31 @@ struct PanelContributionsTests { @Test func inspectorTabsKeepTheirIdsAndOrder() { - let items = inspectorContributions(extensionManager: ExtensionManager(), showInternalDevelopment: true) + let items = inspectorContributions( + extensionManager: ExtensionManager(), + showInternalDevelopment: true, + activeEditorState: NoOpActiveEditorState() + ) #expect(items.prefix(3).map(\.id) == ["file", "gitHistory", "internalDevelopment"]) } @Test func inspectorTabsKeepTheirTitles() { - let items = inspectorContributions(extensionManager: ExtensionManager(), showInternalDevelopment: true) + let items = inspectorContributions( + extensionManager: ExtensionManager(), + showInternalDevelopment: true, + activeEditorState: NoOpActiveEditorState() + ) #expect(items.prefix(3).map(\.title) == ["File Inspector", "History Inspector", "Internal Development"]) } @Test func inspectorTabsKeepTheirSymbols() { - let items = inspectorContributions(extensionManager: ExtensionManager(), showInternalDevelopment: true) + let items = inspectorContributions( + extensionManager: ExtensionManager(), + showInternalDevelopment: true, + activeEditorState: NoOpActiveEditorState() + ) #expect(items.prefix(3).map(\.systemImage) == ["doc", "clock", "hammer"]) } @@ -65,10 +87,14 @@ struct PanelContributionsTests { @Test func inspectorIncludesTheDeveloperTabOnlyWhenEnabled() { let disabled = inspectorContributions( - extensionManager: ExtensionManager(), showInternalDevelopment: false + extensionManager: ExtensionManager(), + showInternalDevelopment: false, + activeEditorState: NoOpActiveEditorState() ) let enabled = inspectorContributions( - extensionManager: ExtensionManager(), showInternalDevelopment: true + extensionManager: ExtensionManager(), + showInternalDevelopment: true, + activeEditorState: NoOpActiveEditorState() ) #expect(!disabled.map(\.id).contains("internalDevelopment")) @@ -105,8 +131,12 @@ struct PanelContributionsTests { func selectionConstantsMatchTheContributionsTheyName() { #expect(PanelTabID.search == "search") #expect(PanelTabID.debugConsole == "debugConsole") - #expect(navigatorContributions(extensionManager: ExtensionManager()) - .contains { $0.id == PanelTabID.search }) + #expect( + navigatorContributions( + extensionManager: ExtensionManager(), + navigator: NoOpWorkspaceNavigator() + ).contains { $0.id == PanelTabID.search } + ) #expect(utilityAreaContributions(extensionManager: ExtensionManager()) .contains { $0.id == PanelTabID.debugConsole }) } From 90798999fc10deff2e59753392ad494c34700754 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 14:24:44 +0200 Subject: [PATCH 280/335] Docs: Record the source control view relocation --- docs/ARCHITECTURE.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 57df8e8074..c32dcdeb1d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -243,9 +243,22 @@ still needs a compile-checked constant to select the tab by. `ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) is the one contribution that stays app-side permanently — not because it is a tab (see the [chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no -owning package to move to. `SourceControlNavigatorContribution` in the same file is app-side only -until `SourceControlNavigatorView` is packaged — an out-of-scope follow-up, not a charter -exception. +owning package to move to. + +`CESourceControl` is the second feature package to own a panel tab, after `CESearch`. +`SourceControlNavigatorContribution` and `GitHistoryInspectorContribution` +(`CodeEditModules/Sources/CESourceControl/SourceControlNavigator/` and `.../HistoryInspector/`) +vend `SourceControlNavigatorView` and `HistoryInspectorView` from the package; the app-side +`WorkspaceWindow/NavigatorArea/SourceControlNavigator/` and +`WorkspaceWindow/InspectorArea/HistoryInspector/` groups no longer exist. The relocation is the +shape a future one follows: whatever the view needs that the package cannot see becomes a +**required** initialiser parameter on the contribution — `WorkspaceNavigator` for the navigator +tab, `ActiveEditorState` for the inspector tab — rather than an environment key moved down with +it; both are non-optional so a missing injection at the call site +(`PanelContributions.swift`) is a compile error, not a silently no-op tab. Each contribution still +owns its own `tabID` constant, and the app's `PanelTabID.sourceControl` / `.gitHistory` reference +it rather than duplicating the literal, same as `PanelTabID.search`. A third relocation onto this +shape is expected. Extensions are the third contribution source. `ExtensionPanelContribution` (`CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift`) is the **only** app From 2db596b88bce8b71e520985600c83f26e24e67c5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 14 Aug 2026 14:36:21 +0200 Subject: [PATCH 281/335] Fix: Apply final review fixes for the source-control view relocation Collapse AppWorkspaceNavigator.open(fileAt:) to delegate straight to openFileInWorkspace (sorted, nearest-workspace resolution) instead of pre-probing via the unsorted workspace(containing:), which could silently drop resolvable URLs and mutate the wrong workspace's file map as a side effect. Also drop the accidental public on HistoryInspectorView/SourceControlNavigatorView (only their Contribution wrappers need it) and fix a stale file-header path in Date+RelativeStringToNow.swift. --- .../Adapters/AppWorkspaceNavigator.swift | 8 +------- .../HistoryInspectorView.swift | 6 +++--- .../History/Date+RelativeStringToNow.swift | 2 +- .../SourceControlNavigatorView.swift | 6 +++--- .../AppWorkspaceNavigatorTests.swift | 19 +++++++++++++++++++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift index 909efe54f7..d52f247db4 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppWorkspaceNavigator.swift @@ -27,13 +27,7 @@ final class AppWorkspaceNavigator: WorkspaceNavigator { @MainActor func open(fileAt url: URL, asTemporary: Bool) { - guard let ceFile = windowManager.workspace(containing: url)?.workspaceFileManager.getFile( - url.absolutePath, - createIfNotFound: true - ) else { - return - } - open(file: ceFile, asTemporary: asTemporary) + _ = windowManager.openFileInWorkspace(url: url, asTemporary: asTemporary) } @MainActor diff --git a/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift index 0a16e542c6..e78c1308f2 100644 --- a/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift @@ -9,7 +9,7 @@ import CodeEditSettings import CodeEditUI import CodeEditCore -public struct HistoryInspectorView: View { +struct HistoryInspectorView: View { @SettingsValue(SourceControlSettings.self, \.git.showMergeCommitsPerFileLog) var showMergeCommitsPerFileLog @@ -29,12 +29,12 @@ public struct HistoryInspectorView: View { /// - Parameter activeEditorState: the workspace's active-file read-model; the history shown /// follows its selection. - public init(activeEditorState: ActiveEditorState) { + init(activeEditorState: ActiveEditorState) { self.activeEditorState = activeEditorState self.model = .init() } - public var body: some View { + var body: some View { Group { if model.sourceControlManager != nil { VStack { diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift index 8540c547c7..fc03744963 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/History/Date+RelativeStringToNow.swift @@ -1,6 +1,6 @@ // // Date+RelativeStringToNow.swift -// CodeEditModules/CodeEditUtils +// CodeEdit // // Created by Lukas Pistrol on 20.04.22. // diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift index 712ab66622..47bb7281dc 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/SourceControlNavigatorView.swift @@ -10,7 +10,7 @@ import CodeEditCore import CodeEditSettings import CodeEditUI -public struct SourceControlNavigatorView: View { +struct SourceControlNavigatorView: View { @EnvironmentObject private var sourceControlManager: SourceControlManager @EnvironmentObject private var sourceControlViewModel: SourceControlViewModel @@ -21,11 +21,11 @@ public struct SourceControlNavigatorView: View { /// environment, so the app shell stays the only place that knows where it comes from. private let navigator: WorkspaceNavigator - public init(navigator: WorkspaceNavigator) { + init(navigator: WorkspaceNavigator) { self.navigator = navigator } - public var body: some View { + var body: some View { VStack(spacing: 0) { SourceControlNavigatorTabs(navigator: navigator) .environmentObject(sourceControlManager) diff --git a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift index 0862262331..ba8be53ae3 100644 --- a/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift +++ b/CodeEditTests/Features/Workspace/AppWorkspaceNavigatorTests.swift @@ -69,6 +69,25 @@ struct AppWorkspaceNavigatorTests { #expect(mock.opened.first?.asTemporary == true) } + /// Regression test for the case the old unsorted `workspace(containing:)` probe could miss: + /// `open(fileAt:)` must still open the file via `openFileInWorkspace` (the sorted, + /// nearest-workspace path) even when the unsorted probe would have returned nil. + @MainActor + @Test + func openFileAtURLOpensEvenWhenWorkspaceContainingProbeMisses() throws { + let mock = MockWindowManager() + // Deliberately leave `stubbedWorkspace` nil so `workspace(containing:)` returns nil, + // simulating the unsorted probe missing a URL that the sorted path would still resolve. + let navigator = AppWorkspaceNavigator(windowManager: mock) + let fileURL = URL(fileURLWithPath: "/tmp/unmatched-by-probe.swift") + + navigator.open(fileAt: fileURL, asTemporary: false) + + #expect(mock.opened.count == 1) + #expect(mock.opened.first?.url == fileURL) + #expect(mock.opened.first?.asTemporary == false) + } + @MainActor @Test func revealSendsRevealRequestOnCorrectWorkspace() throws { From 96c35f2f72b8ef2e91c6a6dd000587d39301a035 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 15 Aug 2026 14:13:44 +0200 Subject: [PATCH 282/335] Refactor: Move the accounts settings into CESourceControl --- .../AccountsSettings/AccountSelectionView.swift | 1 + .../AccountsSettingsAccountLink.swift | 1 + .../AccountsSettingsDetailsView.swift | 1 + .../AccountsSettings/AccountsSettingsView.swift | 1 + .../SourceControlAccount+Icon.swift | 1 + .../AccountsSettings.swift | 17 ++++++++++++++++- .../SourceControlAccount.swift | 0 .../Store/CodableDefault+Providers.swift | 8 -------- 8 files changed, 21 insertions(+), 9 deletions(-) rename CodeEditModules/Sources/{CodeEditSettings/Models => CESourceControl}/AccountsSettings.swift (56%) rename CodeEditModules/Sources/{CodeEditSettings/Models => CESourceControl}/SourceControlAccount.swift (100%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift index a0b867286d..32113b7b07 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CESourceControl import CodeEditSettings struct AccountSelectionView: View { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift index a824116528..e6ee9f7463 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsAccountLink.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CESourceControl import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift index 95c33791eb..07461a7a63 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CESourceControl import CodeEditSettings struct AccountsSettingsDetailsView: View { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift index 1fc762f053..bcbe837890 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CESourceControl import CodeEditSettings struct AccountsSettingsView: View { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift index 710e08155c..b60e8f329c 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/SourceControlAccount+Icon.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CESourceControl import CodeEditSettings extension SourceControlAccount.Provider { diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift b/CodeEditModules/Sources/CESourceControl/AccountsSettings.swift similarity index 56% rename from CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift rename to CodeEditModules/Sources/CESourceControl/AccountsSettings.swift index f0106981d9..29059ace09 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/AccountsSettings.swift +++ b/CodeEditModules/Sources/CESourceControl/AccountsSettings.swift @@ -5,9 +5,14 @@ // Created by Nanashi Li on 2022/04/08. // +import CodeEditSettings import Foundation -/// The global settings for source control accounts +/// The user's source-control accounts. +/// +/// Lives in `CESourceControl` rather than with the app-wide settings models because its entire +/// contents are source control: `sourceControlAccounts` holds `[SourceControlAccount]` and an SSH +/// key, and nothing else. The general-sounding name is historical. public struct AccountsSettings: SettingsSection { /// The top-level key this section occupies in `settings.json`. @@ -28,3 +33,13 @@ public struct AccountsSettings: SettingsSection { public init() {} } } + +// MARK: - Defaults + +public enum DefaultGitAccounts: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue = AccountsSettings.GitAccounts() +} + +public enum DefaultEmptySourceControlAccounts: DefaultValueProvider { + nonisolated(unsafe) public static let defaultValue: [SourceControlAccount] = [] +} diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/SourceControlAccount.swift b/CodeEditModules/Sources/CESourceControl/SourceControlAccount.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditSettings/Models/SourceControlAccount.swift rename to CodeEditModules/Sources/CESourceControl/SourceControlAccount.swift diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift index 352cafb455..d3183fd972 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/CodableDefault+Providers.swift @@ -33,14 +33,6 @@ public enum DefaultEmptyStringDictionary: DefaultValueProvider { // MARK: - Account Defaults -public enum DefaultGitAccounts: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue = AccountsSettings.GitAccounts() -} - -public enum DefaultEmptySourceControlAccounts: DefaultValueProvider { - nonisolated(unsafe) public static let defaultValue: [SourceControlAccount] = [] -} - public enum DefaultEmptyString: DefaultValueProvider { public static let defaultValue = "" } From 2641484d1db8ce724e61042611f9226862b7d61b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 15 Aug 2026 14:16:50 +0200 Subject: [PATCH 283/335] Docs: Give CodeEditCore's purity rule the justification it actually earns --- docs/ARCHITECTURE.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c32dcdeb1d..517297f4f7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -91,8 +91,26 @@ Three checks are enforced in CI. Each one blocks a specific failure documented i [History](#history-why-the-2022-module-split-failed) — none is enforced on principle alone. 1. **`CodeEditCore` purity.** Zero dependencies, local or external. No `SwiftUI`, `AppKit`, or - `Cocoa` import. Blocks 2022's `WorkspaceClient → TabBar`. Keep it platform-free too: it is the one - target that would port to iPadOS unchanged. + `Cocoa` import. Blocks 2022's `WorkspaceClient → TabBar`. The two constraints earn their keep + separately: **zero local dependencies** is the acyclicity guarantee — it makes Core a sink, so + every "A and B both need X" resolves downward, which is the direct fix for the "no kernel + existed" failure above. **No UI frameworks** keeps the placement question answerable — without + it, Core becomes the place everything shared goes, which is what `AppPreferences` was and one of + the two documented causes of the 2022 collapse. + + The friction this produces is usually the rule working. Three worked examples already in this + codebase: `FileIcon` is keyed on `URL` rather than a domain type, so it needs neither + `CodeEditCore` nor a charter exception (see rule 2 below); `WorkspacePanelContribution` was shaped + to need only SwiftUI, so it lives in `CodeEditUI` + (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); and fuzzy matching's + concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` + (below). + + The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry + `NSFont.Weight`, which forces them out of Core. That is the rule flagging a presentation type + inside a settings model, not the rule obstructing a reasonable design — storing the weight as a + `Double` and converting at the presentation boundary would make both structs Core-eligible with + no rule change. 2. **`CodeEditUI` purity.** No local target dependencies; external `CodeEditSymbols` only. Blocks 2022's `CodeEditUI → Git`. This is why `FileIcon` is keyed on `URL` rather than on a domain type — a deliberate consequence, not an accident. From 206e9db959a234cdde2d091e290ff3b639f9611f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 16:58:55 +0200 Subject: [PATCH 284/335] Refactor: Move Theme into CodeEditCore --- .../Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeModel+Export.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeModel.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeRepository.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift | 1 + .../Settings/Pages/ThemeSettings/ThemeSettingsView.swift | 1 + CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift | 1 + .../Domain/Theme}/Loopable.swift | 0 .../{CodeEditSettings => CodeEditCore/Domain/Theme}/Theme.swift | 0 .../Sources/CodeEditSettings/Models/Theme+Color.swift | 1 + .../Sources/CodeEditSettings/Models/ThemeSettings.swift | 1 + .../Sources/CodeEditSettings/Store/Environment+Theme.swift | 1 + 14 files changed, 12 insertions(+) rename CodeEditModules/Sources/{CodeEditSettings => CodeEditCore/Domain/Theme}/Loopable.swift (100%) rename CodeEditModules/Sources/{CodeEditSettings => CodeEditCore/Domain/Theme}/Theme.swift (100%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift index 97eba1c21c..ca8d1c4894 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+CRUD.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import UniformTypeIdentifiers diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift index ca899c6176..b3d44d0b6b 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel+Export.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import UniformTypeIdentifiers diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift index d938c3d999..ec97ed78af 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import UniformTypeIdentifiers diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeRepository.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeRepository.swift index 2967e4ce22..5a8eb1b857 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeRepository.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeRepository.swift @@ -6,6 +6,7 @@ // import Foundation +import CodeEditCore import CodeEditSettings /// Handles all file I/O operations for themes. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index e1ac3fb8c2..d83da02d73 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import CodeEditUI diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift index f943c40b40..6afea06be9 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsColorPreview.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings struct ThemeSettingsColorPreview: View { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index b06c0b5114..c79837bc38 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings struct ThemeSettingsThemeDetails: View { diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 9fa7fb1ebd..a18b30a878 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import CodeEditUI diff --git a/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift b/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift index 2875a081bc..d5b8d8924a 100644 --- a/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift +++ b/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 10.07.26. // +import CodeEditCore import CodeEditSettings import CodeEditSourceEditor import AppKit diff --git a/CodeEditModules/Sources/CodeEditSettings/Loopable.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Theme/Loopable.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditSettings/Loopable.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Theme/Loopable.swift diff --git a/CodeEditModules/Sources/CodeEditSettings/Theme.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Theme/Theme.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditSettings/Theme.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/Theme/Theme.swift diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift b/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift index d74de8717e..d1644ca00a 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift @@ -5,6 +5,7 @@ // Created by Lukas Pistrol on 31.03.22. // +import CodeEditCore import SwiftUI public extension Theme.Attributes { diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift index 1458b45039..2c446e0ea3 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift @@ -5,6 +5,7 @@ // Created by Nanashi Li on 2022/04/08. // +import CodeEditCore import Foundation /// A dictionary containing the keys and associated ``Theme/Attributes`` of overridden properties diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift index 9bcf47f46c..32bfac50f5 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift @@ -5,6 +5,7 @@ // Created by Matthijs Eikelenboom on 10.07.26. // +import CodeEditCore import SwiftUI private struct CurrentThemeKey: EnvironmentKey { From de9a79a8ade22cb10b46c17fcc1dab911d0b180a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 17:06:03 +0200 Subject: [PATCH 285/335] Feat: Add ActiveTheme to CodeEditCore --- .../Infrastructure/ActiveTheme.swift | 39 ++++++++ .../CodeEditCoreTests/ActiveThemeTests.swift | 94 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift create mode 100644 CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift new file mode 100644 index 0000000000..e95e75cb4a --- /dev/null +++ b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift @@ -0,0 +1,39 @@ +// +// ActiveTheme.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/08/26. +// + +import Combine + +/// The themes currently in effect, published to whatever is rendering with them. +/// +/// **Holds only the themes in effect — keep it that way.** An `ObservableObject` invalidates every +/// observer on any published change, and the observers here are the code editor and the terminal. +/// Adding frequently-changing state would re-render both for changes they do not care about. +/// Theme *management* — the list, selection UI, add/edit state — stays in the app-side `ThemeModel`. +/// +/// Lives in `CodeEditCore` because `ObservableObject` is Combine, not SwiftUI: Core's charter forbids +/// only `SwiftUI`, `AppKit` and `Cocoa`. `FindReplaceQuery` is the existing precedent. +public final class ActiveTheme: ObservableObject { + + /// The theme in effect, or `nil` before any theme has loaded. + @Published public private(set) var current: Theme? + + /// The theme to use where a dark appearance is forced independently of `current` — the terminal + /// does this when its own `darkAppearance` setting is on. + @Published public private(set) var dark: Theme? + + public init() {} + + /// Publishes only when something actually changed. + /// + /// `@Published` fires on every assignment regardless of equality, so an unguarded write would + /// re-render both observers for nothing. The guard lives here rather than at the call site so no + /// writer can bypass it. + public func update(current: Theme?, dark: Theme?) { + if self.current != current { self.current = current } + if self.dark != dark { self.dark = dark } + } +} diff --git a/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift b/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift new file mode 100644 index 0000000000..5b564a6be5 --- /dev/null +++ b/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift @@ -0,0 +1,94 @@ +// +// ActiveThemeTests.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/08/26. +// + +import Combine +import Testing +@testable import CodeEditCore + +@MainActor +struct ActiveThemeTests { + + /// A change must reach observers — this is the whole reason the type exists. + @Test + func publishesWhenTheCurrentThemeChanges() { + let active = ActiveTheme() + var emissions = 0 + let token = active.objectWillChange.sink { _ in emissions += 1 } + + active.update(current: Self.makeTheme(name: "Solarized"), dark: nil) + + #expect(emissions == 1) + #expect(active.current?.name == "Solarized") + token.cancel() + } + + /// Assigning an equal value must NOT publish. `@Published` fires on every set regardless of + /// equality, and both observers are expensive views — without this guard, any repeated write + /// re-renders the editor and the terminal for nothing. + @Test + func doesNotPublishWhenAssignedAnEqualValue() { + let theme = Self.makeTheme(name: "Solarized") + let active = ActiveTheme() + active.update(current: theme, dark: nil) + + var emissions = 0 + let token = active.objectWillChange.sink { _ in emissions += 1 } + + active.update(current: theme, dark: nil) + + #expect(emissions == 0) + token.cancel() + } + + /// A fresh holder is already `nil`; setting `nil` again must be a no-op too. + @Test + func doesNotPublishWhenSettingNilOnAFreshHolder() { + let active = ActiveTheme() + var emissions = 0 + let token = active.objectWillChange.sink { _ in emissions += 1 } + + active.update(current: nil, dark: nil) + + #expect(emissions == 0) + token.cancel() + } + + // MARK: - Fixture + + private static func attr() -> Theme.Attributes { + Theme.Attributes(color: "#000000") + } + + private static func makeTheme(name: String) -> Theme { + let editor = Theme.EditorColors( + text: attr(), insertionPoint: attr(), invisibles: attr(), background: attr(), + lineHighlight: attr(), selection: attr(), keywords: attr(), commands: attr(), + types: attr(), attributes: attr(), variables: attr(), values: attr(), + numbers: attr(), strings: attr(), characters: attr(), comments: attr() + ) + let terminal = Theme.TerminalColors( + text: attr(), boldText: attr(), cursor: attr(), background: attr(), selection: attr(), + black: attr(), red: attr(), green: attr(), yellow: attr(), blue: attr(), magenta: attr(), + cyan: attr(), white: attr(), brightBlack: attr(), brightRed: attr(), brightGreen: attr(), + brightYellow: attr(), brightBlue: attr(), brightMagenta: attr(), brightCyan: attr(), + brightWhite: attr() + ) + return Theme( + editor: editor, + terminal: terminal, + author: "Test", + license: "MIT", + metadataDescription: "Test theme", + distributionURL: "", + isBundled: false, + name: name, + displayName: name, + appearance: .dark, + version: "1.0" + ) + } +} From 39879ce52e98edf3b91967e288a6f2995ef4a513 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 17:16:01 +0200 Subject: [PATCH 286/335] Refactor: Deliver the active theme through an observable object --- .../Pages/ThemeSettings/ThemeModel.swift | 16 +++++++++ .../CodeEditWindowController.swift | 2 +- CodeEdit/WorkspaceWindow/WorkspaceView.swift | 7 ++-- .../Sources/CEEditor/Views/CodeFileView.swift | 5 ++- .../Views/TerminalEmulatorView.swift | 14 ++++---- .../Store/Environment+Theme.swift | 34 ------------------- 6 files changed, 30 insertions(+), 48 deletions(-) delete mode 100644 CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift index ec97ed78af..c93b89cd3b 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift @@ -33,6 +33,12 @@ final class ThemeModel: ObservableObject { settingsAccessor.setValue(section) } + /// The themes in effect, handed to the editor and the terminal as an `@EnvironmentObject`. + /// + /// Kept in sync from the `didSet` observers on ``selectedTheme`` and ``selectedDarkTheme``. + /// Theme *management* state stays here; only the two themes in effect cross into rendering. + let activeTheme = ActiveTheme() + /// Default instance of the `FileManager` let filemanager = FileManager.default @@ -80,6 +86,9 @@ final class ThemeModel: ObservableObject { /// Used for auto-switching theme to match macOS system appearance @Published var selectedDarkTheme: Theme? { didSet { + // Synchronous on purpose: the async hop below defers a *settings write*. Deferring the + // in-memory update too would delay the re-render this holder exists to deliver. + publishActiveTheme() DispatchQueue.main.async { self.updateThemeSettings { $0.selectedDarkTheme = self.selectedDarkTheme?.name ?? "Broken" } } @@ -98,12 +107,19 @@ final class ThemeModel: ObservableObject { /// The currently selected ``Theme``. @Published var selectedTheme: Theme? { didSet { + // Synchronous on purpose — see ``selectedDarkTheme``. + publishActiveTheme() DispatchQueue.main.async { self.updateThemeSettings { $0.selectedTheme = self.selectedTheme?.name } } } } + /// Pushes the current selection into ``activeTheme``, which publishes only on a real change. + private func publishActiveTheme() { + activeTheme.update(current: selectedTheme, dark: selectedDarkTheme) + } + @Published var previousTheme: Theme? /// Only themes where ``Theme/appearance`` == ``Theme/ThemeType/dark`` diff --git a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift index d457f19d45..329ce4cb5d 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditWindowController.swift @@ -223,7 +223,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs .environment(\.workspaceFileProvider, workspace.workspaceFileManager) .environment(\.filePreview) { file in AnyView(FilePreviewView(item: file)) } .environment(\.languageServices, dependencies.languageServicesProvider) - .environment(\.currentTheme, ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first!) + .environmentObject(ThemeModel.shared.activeTheme) panel.contentView = NSHostingView( rootView: SettingsInjector(store: dependencies.settingsStore) { contentView } diff --git a/CodeEdit/WorkspaceWindow/WorkspaceView.swift b/CodeEdit/WorkspaceWindow/WorkspaceView.swift index 97ade329b1..2286b3fd05 100644 --- a/CodeEdit/WorkspaceWindow/WorkspaceView.swift +++ b/CodeEdit/WorkspaceWindow/WorkspaceView.swift @@ -60,8 +60,6 @@ struct WorkspaceView: View { } .edgesIgnoringSafeArea(.top) .frame(maxWidth: .infinity, maxHeight: .infinity) - .environment(\.currentTheme, themeModel.selectedTheme ?? themeModel.themes.first!) - .environment(\.currentDarkTheme, themeModel.selectedDarkTheme) .overlay(alignment: .top) { utilityArea(proxy: proxy) } @@ -143,6 +141,11 @@ struct WorkspaceView: View { _ = handleDrop(providers: providers) return true } + // Outermost on purpose. An `.overlay`/`.background` closure is a *sibling* of the view it + // decorates, so it does not see an environment applied further in: injected on the split + // view alone, the utility area's terminal would miss it entirely — and `@EnvironmentObject` + // traps rather than degrading to nil, the way the retired theme environment key did. + .environmentObject(themeModel.activeTheme) .accessibilityElement(children: .contain) .accessibilityLabel("workspace area") } diff --git a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift index 8efc3470f8..9c1d37d5a2 100644 --- a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift @@ -69,8 +69,7 @@ struct CodeFileView: View { @EnvironmentObject var undoRegistration: UndoManagerRegistration - @Environment(\.currentTheme) - private var injectedTheme + @EnvironmentObject private var activeTheme: ActiveTheme @State private var treeSitter = TreeSitterClient() @@ -113,7 +112,7 @@ struct CodeFileView: View { } private var currentTheme: Theme { - injectedTheme! + activeTheme.current! } @Environment(\.edgeInsets) diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift index 4c07e89012..c66d6aa3d9 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import SwiftTerm @@ -31,10 +32,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { @SettingsValue(ThemeSettings.self, \.matchAppearance) private var themeMatchAppearance - @Environment(\.currentTheme) - private var currentTheme - @Environment(\.currentDarkTheme) - private var currentDarkTheme + @EnvironmentObject private var activeTheme: ActiveTheme private var font: NSFont { if terminalSettings.useTextEditorFont { @@ -85,7 +83,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the mapped array of `SwiftTerm.Color` objects of ANSI Colors private var colors: [SwiftTerm.Color] { - guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { + guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return [] } return selectedTheme.terminal.ansiColors.map { color in @@ -95,7 +93,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the `cursor` color of the selected theme private var cursorColor: NSColor { - guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { + guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return NSColor(.accentColor) } return NSColor(selectedTheme.terminal.cursor.swiftColor) @@ -103,7 +101,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the `selection` color of the selected theme private var selectionColor: NSColor { - guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { + guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return NSColor(.accentColor) } return NSColor(selectedTheme.terminal.selection.swiftColor) @@ -111,7 +109,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { /// Returns the `text` color of the selected theme private var textColor: NSColor { - guard let selectedTheme = useDarkTheme ? currentDarkTheme : currentTheme else { + guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return NSColor(.primary) } return NSColor(selectedTheme.terminal.text.swiftColor) diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift b/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift deleted file mode 100644 index 32bfac50f5..0000000000 --- a/CodeEditModules/Sources/CodeEditSettings/Store/Environment+Theme.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Environment+Theme.swift -// CodeEditSettings -// -// Created by Matthijs Eikelenboom on 10.07.26. -// - -import CodeEditCore -import SwiftUI - -private struct CurrentThemeKey: EnvironmentKey { - static let defaultValue: Theme? = nil -} - -private struct CurrentDarkThemeKey: EnvironmentKey { - static let defaultValue: Theme? = nil -} - -public extension EnvironmentValues { - /// The theme currently active in the editor, following the user's selection - /// and the app's light/dark appearance. - var currentTheme: Theme? { - get { self[CurrentThemeKey.self] } - set { self[CurrentThemeKey.self] = newValue } - } - - /// The user's saved dark-appearance theme, independent of `currentTheme`. - /// Lets a view force dark colors (e.g. a terminal's "always dark" setting) - /// without following the editor's active light/dark theme. - var currentDarkTheme: Theme? { - get { self[CurrentDarkThemeKey.self] } - set { self[CurrentDarkThemeKey.self] = newValue } - } -} From a25c773d4a67bd57cdab87d32e4e8ada44e62924 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 17:19:39 +0200 Subject: [PATCH 287/335] Fix: Inject the active theme into the single-file document window --- CodeEdit/App/AppDependencies.swift | 3 ++- .../Settings/Pages/ThemeSettings/ThemeModel.swift | 7 ++++++- .../Adapters/AppCodeFileDocumentDelegate.swift | 10 +++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index a6886b9b4c..a7c4754edb 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -128,6 +128,7 @@ final class AppDependencies { lspService: lspService, windowManager: workspaceWindowManager, languageServices: languageServicesProvider, - settingsStore: settingsStore + settingsStore: settingsStore, + activeTheme: ThemeModel.shared.activeTheme ) } diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift index c93b89cd3b..6cb5280ec1 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift @@ -116,8 +116,13 @@ final class ThemeModel: ObservableObject { } /// Pushes the current selection into ``activeTheme``, which publishes only on a real change. + /// + /// The `?? themes.first` carries over the fallback both former injection sites applied: if no + /// theme matches the current appearance, any loaded theme beats none. It is deliberately not + /// `themes.first!` — ``ActiveTheme/current`` is already optional, so the write site has no + /// reason to trap. ``selectedDarkTheme`` gets no fallback because the old injection had none. private func publishActiveTheme() { - activeTheme.update(current: selectedTheme, dark: selectedDarkTheme) + activeTheme.update(current: selectedTheme ?? themes.first, dark: selectedDarkTheme) } @Published var previousTheme: Theme? diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift index 2cdfa5787a..337197b8fa 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift @@ -8,6 +8,7 @@ import CELSP import AppKit import CEEditor +import CodeEditCore import SwiftUI import CodeEditTextView import CodeEditDocument @@ -24,13 +25,19 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { /// The settings store, so the standalone hosting root below can inject the settings seam. private let settingsStore: AppSettingsStore + /// The themes in effect. `CodeFileView` reads these as an `@EnvironmentObject`, which traps when + /// missing — and the hosting root below is standalone, so nothing above it can supply them. + private let activeTheme: ActiveTheme + init( lspService: any LSPServiceProtocol, windowManager: WorkspaceWindowManaging, languageServices: LanguageServicesProvider, - settingsStore: AppSettingsStore + settingsStore: AppSettingsStore, + activeTheme: ActiveTheme ) { self.settingsStore = settingsStore + self.activeTheme = activeTheme self.lspService = lspService self.windowManager = windowManager self.languageServices = languageServices @@ -44,6 +51,7 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { NSHostingView(rootView: SettingsInjector(store: settingsStore) { WindowCodeFileView(codeFile: document) .environment(\.languageServices, languageServices) + .environmentObject(activeTheme) }) } From 922c6f8da4c6668fbf4f79dcae4391e763f7c18c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 17:35:08 +0200 Subject: [PATCH 288/335] Fix: Publish theme edits made to the already-active theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ActiveTheme.update(current:dark:)` guarded its assignments behind `!=`. But `Theme` is `Equatable` by *name* — `Theme.==` compares `id`, which is `name` — because `themes.firstIndex(of:)` relies on that to locate a theme and update it in place. An edited copy of the active theme therefore compared equal to it and the guard dropped the write, leaving the holder on a stale struct: editing a colour of the currently active theme updated the settings preview while the open editor and terminal kept the old colours. Assign unconditionally instead. That is what the retired code did — it re-derived the injected value on every `ThemeModel` publish — and the write frequency is human-scale, a theme switch or a colour edit. Changing `Theme` equality to compare by value was rejected: it would break theme editing. The two tests asserting non-publication encoded the removed behaviour and are deleted; a regression test covers a same-name, different-colours update, asserting both that it publishes and that the new colour is stored. The surviving emission-count assertion becomes "at least one", since an unconditional `update` writes both `@Published` properties. --- .../Pages/ThemeSettings/ThemeModel.swift | 5 ++- .../Infrastructure/ActiveTheme.swift | 15 ++++--- .../CodeEditCoreTests/ActiveThemeTests.swift | 44 +++++++++---------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift index 6cb5280ec1..95c0fce2e3 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeModel.swift @@ -115,7 +115,10 @@ final class ThemeModel: ObservableObject { } } - /// Pushes the current selection into ``activeTheme``, which publishes only on a real change. + /// Pushes the current selection into ``activeTheme``, which assigns and publishes unconditionally. + /// + /// Unconditional on purpose: ``Theme`` equality is by name, so any `!=` guard would drop colour + /// edits made to the theme that is already active. See ``ActiveTheme/update(current:dark:)``. /// /// The `?? themes.first` carries over the fallback both former injection sites applied: if no /// theme matches the current appearance, any loaded theme beats none. It is deliberately not diff --git a/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift index e95e75cb4a..98d6250a1f 100644 --- a/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift +++ b/CodeEditModules/Sources/CodeEditCore/Infrastructure/ActiveTheme.swift @@ -27,13 +27,16 @@ public final class ActiveTheme: ObservableObject { public init() {} - /// Publishes only when something actually changed. + /// Assigns unconditionally, publishing on every call. /// - /// `@Published` fires on every assignment regardless of equality, so an unguarded write would - /// re-render both observers for nothing. The guard lives here rather than at the call site so no - /// writer can bypass it. + /// **Do not reinstate an equality guard here.** One was tried and removed: ``Theme`` is + /// `Equatable` by *name* (`Theme.==` compares `id`, which is `name`), because + /// `themes.firstIndex(of:)` relies on that to find a theme to update in place. A `!=` guard + /// therefore reads "same theme" for an edited copy of the active theme and silently swallows + /// colour changes, leaving this holder on a stale struct forever. Writes are human-scale — a + /// theme switch or a colour edit — so the redundant publishes cost nothing worth guarding. public func update(current: Theme?, dark: Theme?) { - if self.current != current { self.current = current } - if self.dark != dark { self.dark = dark } + self.current = current + self.dark = dark } } diff --git a/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift b/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift index 5b564a6be5..b71f0da1db 100644 --- a/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift +++ b/CodeEditModules/Tests/CodeEditCoreTests/ActiveThemeTests.swift @@ -13,6 +13,9 @@ import Testing struct ActiveThemeTests { /// A change must reach observers — this is the whole reason the type exists. + /// + /// The count is asserted as "at least one": `update` assigns both `@Published` properties + /// unconditionally, so one call emits twice. Only *reaching* observers is the contract. @Test func publishesWhenTheCurrentThemeChanges() { let active = ActiveTheme() @@ -21,39 +24,31 @@ struct ActiveThemeTests { active.update(current: Self.makeTheme(name: "Solarized"), dark: nil) - #expect(emissions == 1) + #expect(emissions >= 1) #expect(active.current?.name == "Solarized") token.cancel() } - /// Assigning an equal value must NOT publish. `@Published` fires on every set regardless of - /// equality, and both observers are expensive views — without this guard, any repeated write - /// re-renders the editor and the terminal for nothing. + /// Editing a colour of the *active* theme must reach observers, and must be stored. + /// + /// ``Theme`` is `Equatable` by name, so an edited copy of the active theme compares *equal* to + /// it. An equality guard in `update` therefore dropped this write entirely: the editor and the + /// terminal kept rendering the old colours while the settings preview showed the new ones. @Test - func doesNotPublishWhenAssignedAnEqualValue() { - let theme = Self.makeTheme(name: "Solarized") + func publishesAndStoresAThemeEditedUnderTheSameName() { + let original = Self.makeTheme(name: "Solarized", editorText: "#000000") + let edited = Self.makeTheme(name: "Solarized", editorText: "#FF00FF") let active = ActiveTheme() - active.update(current: theme, dark: nil) + active.update(current: original, dark: nil) var emissions = 0 let token = active.objectWillChange.sink { _ in emissions += 1 } - active.update(current: theme, dark: nil) + active.update(current: edited, dark: nil) - #expect(emissions == 0) - token.cancel() - } - - /// A fresh holder is already `nil`; setting `nil` again must be a no-op too. - @Test - func doesNotPublishWhenSettingNilOnAFreshHolder() { - let active = ActiveTheme() - var emissions = 0 - let token = active.objectWillChange.sink { _ in emissions += 1 } - - active.update(current: nil, dark: nil) - - #expect(emissions == 0) + #expect(original == edited, "Precondition: Theme equality is by name, not by value.") + #expect(emissions >= 1) + #expect(active.current?.editor.text.color == "#FF00FF") token.cancel() } @@ -63,9 +58,10 @@ struct ActiveThemeTests { Theme.Attributes(color: "#000000") } - private static func makeTheme(name: String) -> Theme { + private static func makeTheme(name: String, editorText: String = "#000000") -> Theme { let editor = Theme.EditorColors( - text: attr(), insertionPoint: attr(), invisibles: attr(), background: attr(), + text: Theme.Attributes(color: editorText), + insertionPoint: attr(), invisibles: attr(), background: attr(), lineHighlight: attr(), selection: attr(), keywords: attr(), commands: attr(), types: attr(), attributes: attr(), variables: attr(), values: attr(), numbers: attr(), strings: attr(), characters: attr(), comments: attr() From d44b9b705604b16201a732647c1c96f8f1d905cb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 18:41:30 +0200 Subject: [PATCH 289/335] Refactor: Convert theme colours through CodeEditUI's hex helper --- .../ThemeSettings/Theme+SwiftColor.swift | 27 +++++++++ CodeEditModules/Package.swift | 1 + .../CEEditor/Models/Theme+EditorTheme.swift | 59 ++++++------------- .../CEEditor/Models/Theme+NSColor.swift | 27 +++++++++ .../Views/TerminalEmulatorView.swift | 7 ++- .../CodeEditSettings/Models/Theme+Color.swift | 31 ---------- .../Store => CodeEditUI}/Color+HEX.swift | 2 +- 7 files changed, 79 insertions(+), 75 deletions(-) create mode 100644 CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/Theme+SwiftColor.swift create mode 100644 CodeEditModules/Sources/CEEditor/Models/Theme+NSColor.swift delete mode 100644 CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift rename CodeEditModules/Sources/{CodeEditSettings/Store => CodeEditUI}/Color+HEX.swift (99%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/Theme+SwiftColor.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/Theme+SwiftColor.swift new file mode 100644 index 0000000000..c2f160104f --- /dev/null +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/Theme+SwiftColor.swift @@ -0,0 +1,27 @@ +// +// Theme+SwiftColor.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 15/08/2026. +// + +import CodeEditCore +import CodeEditUI +import SwiftUI + +extension Theme.Attributes { + /// The attribute's color as a SwiftUI `Color`; setting it stores the new value as a hex string. + /// + /// Deliberately app-target-private: `Theme` lives in `CodeEditCore`, which may not import SwiftUI, + /// and `CodeEditUI` — which owns the hex conversion — may not import `CodeEditCore`. Each module + /// that needs a typed color keeps its own adapter over the shared `String`-keyed helper. It stays + /// settable because the theme settings detail view binds `$…swiftColor` straight into pickers. + var swiftColor: Color { + get { + Color(hex: color) + } + set { + self.color = newValue.hexString + } + } +} diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 00a75f0962..ef84c183bf 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -119,6 +119,7 @@ let package = Package( dependencies: [ "CodeEditCore", "CodeEditSettings", + "CodeEditUI", .product(name: "SwiftTerm", package: "SwiftTerm") ] ), diff --git a/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift b/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift index d5b8d8924a..e23bc851ce 100644 --- a/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift +++ b/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift @@ -6,50 +6,29 @@ // import CodeEditCore -import CodeEditSettings import CodeEditSourceEditor import AppKit public extension Theme.EditorColors { - /// Bridges the settings theme's editor colors to a source editor `EditorTheme`, converting in both directions. + /// Bridges the settings theme's editor colors to a source editor `EditorTheme`. var editorTheme: EditorTheme { - get { - .init( - text: .init(color: text.nsColor), - insertionPoint: insertionPoint.nsColor, - invisibles: .init(color: invisibles.nsColor), - background: background.nsColor, - lineHighlight: lineHighlight.nsColor, - selection: selection.nsColor, - keywords: .init(color: keywords.nsColor), - commands: .init(color: commands.nsColor), - types: .init(color: types.nsColor), - attributes: .init(color: attributes.nsColor), - variables: .init(color: variables.nsColor), - values: .init(color: values.nsColor), - numbers: .init(color: numbers.nsColor), - strings: .init(color: strings.nsColor), - characters: .init(color: characters.nsColor), - comments: .init(color: comments.nsColor) - ) - } - set { - self.text.nsColor = newValue.text.color - self.insertionPoint.nsColor = newValue.insertionPoint - self.invisibles.nsColor = newValue.invisibles.color - self.background.nsColor = newValue.background - self.lineHighlight.nsColor = newValue.lineHighlight - self.selection.nsColor = newValue.selection - self.keywords.nsColor = newValue.keywords.color - self.commands.nsColor = newValue.commands.color - self.types.nsColor = newValue.types.color - self.attributes.nsColor = newValue.attributes.color - self.variables.nsColor = newValue.variables.color - self.values.nsColor = newValue.values.color - self.numbers.nsColor = newValue.numbers.color - self.strings.nsColor = newValue.strings.color - self.characters.nsColor = newValue.characters.color - self.comments.nsColor = newValue.comments.color - } + .init( + text: .init(color: text.nsColor), + insertionPoint: insertionPoint.nsColor, + invisibles: .init(color: invisibles.nsColor), + background: background.nsColor, + lineHighlight: lineHighlight.nsColor, + selection: selection.nsColor, + keywords: .init(color: keywords.nsColor), + commands: .init(color: commands.nsColor), + types: .init(color: types.nsColor), + attributes: .init(color: attributes.nsColor), + variables: .init(color: variables.nsColor), + values: .init(color: values.nsColor), + numbers: .init(color: numbers.nsColor), + strings: .init(color: strings.nsColor), + characters: .init(color: characters.nsColor), + comments: .init(color: comments.nsColor) + ) } } diff --git a/CodeEditModules/Sources/CEEditor/Models/Theme+NSColor.swift b/CodeEditModules/Sources/CEEditor/Models/Theme+NSColor.swift new file mode 100644 index 0000000000..2ddbe43d1f --- /dev/null +++ b/CodeEditModules/Sources/CEEditor/Models/Theme+NSColor.swift @@ -0,0 +1,27 @@ +// +// Theme+NSColor.swift +// CEEditor +// +// Created by Matthijs Eikelenboom on 15/08/2026. +// + +import AppKit +import CodeEditCore +import CodeEditUI + +extension Theme.Attributes { + /// The attribute's color as an AppKit `NSColor`; setting it stores the new value as a hex string. + /// + /// Deliberately module-private: `Theme` lives in `CodeEditCore`, which may not import AppKit, and + /// `CodeEditUI` — which owns the hex conversion — may not import `CodeEditCore`. Each module that + /// needs a typed color keeps its own adapter over the shared `String`-keyed helper. `CEEditor` is + /// the only module that reads theme colors as `NSColor`, so this duplicates nothing. + var nsColor: NSColor { + get { + NSColor(hex: color) + } + set { + self.color = newValue.hexString + } + } +} diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift index c66d6aa3d9..d75231783e 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift @@ -8,6 +8,7 @@ import SwiftUI import CodeEditCore import CodeEditSettings +import CodeEditUI import SwiftTerm /// # TerminalEmulatorView @@ -96,7 +97,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return NSColor(.accentColor) } - return NSColor(selectedTheme.terminal.cursor.swiftColor) + return NSColor(hex: selectedTheme.terminal.cursor.color) } /// Returns the `selection` color of the selected theme @@ -104,7 +105,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return NSColor(.accentColor) } - return NSColor(selectedTheme.terminal.selection.swiftColor) + return NSColor(hex: selectedTheme.terminal.selection.color) } /// Returns the `text` color of the selected theme @@ -112,7 +113,7 @@ public struct TerminalEmulatorView: NSViewRepresentable { guard let selectedTheme = useDarkTheme ? activeTheme.dark : activeTheme.current else { return NSColor(.primary) } - return NSColor(selectedTheme.terminal.text.swiftColor) + return NSColor(hex: selectedTheme.terminal.text.color) } /// Returns the `background` color of the selected theme diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift b/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift deleted file mode 100644 index d1644ca00a..0000000000 --- a/CodeEditModules/Sources/CodeEditSettings/Models/Theme+Color.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Theme+Color.swift -// CodeEditSettings -// -// Created by Lukas Pistrol on 31.03.22. -// - -import CodeEditCore -import SwiftUI - -public extension Theme.Attributes { - /// The attribute's color as a SwiftUI `Color`; setting it stores the new value as a hex string. - var swiftColor: Color { - get { - Color(hex: color) - } - set { - self.color = newValue.hexString - } - } - - /// The attribute's color as an AppKit `NSColor`; setting it stores the new value as a hex string. - var nsColor: NSColor { - get { - NSColor(hex: color) - } - set { - self.color = newValue.hexString - } - } -} diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/Color+HEX.swift b/CodeEditModules/Sources/CodeEditUI/Color+HEX.swift similarity index 99% rename from CodeEditModules/Sources/CodeEditSettings/Store/Color+HEX.swift rename to CodeEditModules/Sources/CodeEditUI/Color+HEX.swift index 436c78d660..3e8a80cf74 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/Color+HEX.swift +++ b/CodeEditModules/Sources/CodeEditUI/Color+HEX.swift @@ -1,6 +1,6 @@ // // Color+HEX.swift -// CodeEditSettings +// CodeEditUI // // Created by Lukas Pistrol on 23.03.22. // From 8cc2ddd0e27e42266fdd100b7e892eae0075c777 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 20:26:53 +0200 Subject: [PATCH 290/335] Refactor: Move the settings store into CodeEditSettings as PersistentSettingsStore The store was already an ObservableObject; it lived app-side only because nothing needed it elsewhere. Moving it into the package is what lets feature packages observe it directly, which the next commits depend on. Compiling it under the package's Swift 6 strict-concurrency mode required no changes, so the nonisolated SettingsAccessing conformance and the mutate-then- publish ordering in setValue are both preserved exactly. Adds tests for two behaviours that were previously asserted only in doc comments: that a section no type decodes survives a save by this build (what third-party extension settings will rely on), and that observers read the new value rather than the one being replaced. --- CodeEdit/App/AppDependencies.swift | 2 +- CodeEdit/App/MenuBar/CodeEditCommands.swift | 3 +- CodeEdit/App/MenuBar/ViewCommands.swift | 5 +- .../Feedback/FeedbackView.swift | 5 +- .../Feedback/FeedbackWindowController.swift | 3 +- .../Settings/SettingsInjector.swift | 8 +- .../AppCodeFileDocumentDelegate.swift | 11 +-- .../Store/PersistentSettingsStore.swift | 15 ++-- .../Store/SettingsStore.swift | 2 +- .../Store/SettingsValue.swift | 2 +- .../SettingsFormatTests.swift | 2 +- .../SettingsStoreTests.swift | 90 +++++++++++++++++++ ...ift => PersistentSettingsStoreTests.swift} | 14 +-- .../App/SettingsSeamInvalidationTests.swift | 8 +- 14 files changed, 132 insertions(+), 38 deletions(-) rename CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift => CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift (94%) create mode 100644 CodeEditModules/Tests/CodeEditSettingsTests/SettingsStoreTests.swift rename CodeEditTests/App/{AppSettingsStoreTests.swift => PersistentSettingsStoreTests.swift} (95%) diff --git a/CodeEdit/App/AppDependencies.swift b/CodeEdit/App/AppDependencies.swift index a7c4754edb..e97b2ea3fd 100644 --- a/CodeEdit/App/AppDependencies.swift +++ b/CodeEdit/App/AppDependencies.swift @@ -34,7 +34,7 @@ final class AppDependencies { /// /// Concrete (not `SettingsAccessing`) because the injectors also need its `revision` publisher /// to observe. Consumers that only read or write settings take ``settingsAccessor`` instead. - private(set) lazy var settingsStore = AppSettingsStore() + private(set) lazy var settingsStore = PersistentSettingsStore() /// Feature-side settings access, read and write. The same object as ``settingsStore``, narrowed /// to the seam's protocol so nothing outside the composition root names the concrete type. diff --git a/CodeEdit/App/MenuBar/CodeEditCommands.swift b/CodeEdit/App/MenuBar/CodeEditCommands.swift index ef0b65ca7b..c1069c858d 100644 --- a/CodeEdit/App/MenuBar/CodeEditCommands.swift +++ b/CodeEdit/App/MenuBar/CodeEditCommands.swift @@ -5,6 +5,7 @@ // Created by Wouter Hennen on 11/03/2023. // +import CodeEditSettings import SwiftUI struct CodeEditCommands: Commands { @@ -16,7 +17,7 @@ struct CodeEditCommands: Commands { /// hierarchy, so `@Environment` — and with it `@AppSettings` — cannot be relied on here. Without /// a real store this menu would build with `DefaultSettingsReader`, trapping in debug and showing /// the Source Control group unconditionally in release. - @ObservedObject private var settingsStore: AppSettingsStore + @ObservedObject private var settingsStore: PersistentSettingsStore init(dependencies: AppDependencies) { self.dependencies = dependencies diff --git a/CodeEdit/App/MenuBar/ViewCommands.swift b/CodeEdit/App/MenuBar/ViewCommands.swift index a1fb41f5e5..0846ba3281 100644 --- a/CodeEdit/App/MenuBar/ViewCommands.swift +++ b/CodeEdit/App/MenuBar/ViewCommands.swift @@ -5,6 +5,7 @@ // Created by Wouter Hennen on 13/03/2023. // +import CodeEditSettings import SwiftUI struct ViewCommands: Commands { @@ -22,7 +23,7 @@ struct ViewCommands: Commands { /// Dim-editors check mark), so it has to re-evaluate when they change. `ObservableObject` /// observation inside a `Commands` conformer is already load-bearing here — it is how /// ``UpdatingWindowController`` keeps the Show/Hide titles below current. - @ObservedObject private var settingsStore: AppSettingsStore + @ObservedObject private var settingsStore: PersistentSettingsStore @FocusedBinding(\.navigationSplitViewVisibility) var navigationSplitViewVisibility @@ -32,7 +33,7 @@ struct ViewCommands: Commands { @UpdatingWindowController var windowController: CodeEditWindowController? - init(settingsStore: AppSettingsStore) { + init(settingsStore: PersistentSettingsStore) { self.settingsStore = settingsStore } diff --git a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift index afa9946c97..102a657fb4 100644 --- a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift +++ b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackView.swift @@ -5,8 +5,9 @@ // Created by Nanashi Li on 2022/04/14. // -import SwiftUI +import CodeEditSettings import CodeEditUI +import SwiftUI struct FeedbackView: View { @ObservedObject private var feedbackModel: FeedbackModel = .shared @@ -211,7 +212,7 @@ struct FeedbackView: View { } } - func showWindow(settingsStore: AppSettingsStore) { + func showWindow(settingsStore: PersistentSettingsStore) { FeedbackWindowController( view: self, size: NSSize(width: 1028, height: 762), diff --git a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift index f975690086..aa67d73454 100644 --- a/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift +++ b/CodeEdit/AuxiliaryWindows/Feedback/FeedbackWindowController.swift @@ -5,10 +5,11 @@ // Created by Nanashi Li on 2022/04/14. // +import CodeEditSettings import SwiftUI final class FeedbackWindowController: NSWindowController, NSToolbarDelegate { - convenience init(view: T, size: NSSize, settingsStore: AppSettingsStore) { + convenience init(view: T, size: NSSize, settingsStore: PersistentSettingsStore) { let hostingController = NSHostingController(rootView: SettingsInjector(store: settingsStore) { view }) let window = NSWindow(contentViewController: hostingController) self.init(window: window) diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index c3a8fe0242..76cd3b375d 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -16,11 +16,11 @@ import CodeEditSettings struct SettingsInjector: View { /// Observed, not merely held: this view's job is to re-inject `revision` when it changes. - @ObservedObject var store: AppSettingsStore + @ObservedObject var store: PersistentSettingsStore @ViewBuilder var content: Content - init(store: AppSettingsStore, @ViewBuilder content: () -> Content) { + init(store: PersistentSettingsStore, @ViewBuilder content: () -> Content) { self.store = store self.content = content() } @@ -51,11 +51,11 @@ struct SettingsInjector: View { struct SettingsSceneInjector: Scene { /// Observed, not merely held: this scene's job is to re-inject `revision` when it changes. - @ObservedObject var store: AppSettingsStore + @ObservedObject var store: PersistentSettingsStore var content: Content - init(store: AppSettingsStore, @SceneBuilder content: () -> Content) { + init(store: PersistentSettingsStore, @SceneBuilder content: () -> Content) { self.store = store self.content = content() } diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift index 337197b8fa..daa5047b71 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift @@ -5,13 +5,14 @@ // Created by Matthijs Eikelenboom. // -import CELSP import AppKit import CEEditor +import CELSP import CodeEditCore -import SwiftUI -import CodeEditTextView import CodeEditDocument +import CodeEditSettings +import CodeEditTextView +import SwiftUI /// App-side implementation of ``CodeFileDocumentDelegate``. Bridges a packaged /// `CodeFileDocument` back to the app's `Workspace` undo registry, Settings-injected @@ -23,7 +24,7 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { private let languageServices: LanguageServicesProvider /// The settings store, so the standalone hosting root below can inject the settings seam. - private let settingsStore: AppSettingsStore + private let settingsStore: PersistentSettingsStore /// The themes in effect. `CodeFileView` reads these as an `@EnvironmentObject`, which traps when /// missing — and the hosting root below is standalone, so nothing above it can supply them. @@ -33,7 +34,7 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { lspService: any LSPServiceProtocol, windowManager: WorkspaceWindowManaging, languageServices: LanguageServicesProvider, - settingsStore: AppSettingsStore, + settingsStore: PersistentSettingsStore, activeTheme: ActiveTheme ) { self.settingsStore = settingsStore diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift b/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift similarity index 94% rename from CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift rename to CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift index df71a3f2fa..4da1098529 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/AppSettingsStore.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift @@ -1,5 +1,5 @@ // -// AppSettingsStore.swift +// PersistentSettingsStore.swift // CodeEdit // // Created by Matthijs Eikelenboom on 10/08/26. @@ -7,7 +7,6 @@ import Combine import Foundation -import CodeEditSettings /// The app's single settings store: owns the on-disk state, the save pipeline and the seam's /// invalidation signal. @@ -19,7 +18,7 @@ import CodeEditSettings /// Not `@MainActor`: it conforms to ``SettingsAccessing``, which is deliberately nonisolated so it /// can be an `EnvironmentKey` value (see that protocol's documentation). Main-thread use is asserted /// at the write entry point instead. -final class AppSettingsStore: ObservableObject, SettingsAccessing { +public final class PersistentSettingsStore: ObservableObject, SettingsAccessing { /// Section-keyed storage. Sections nothing here decodes are held verbatim and re-emitted on /// save, so a disabled extension's configuration survives. @@ -41,10 +40,10 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { /// rewritten non-`Equatable` existential as a change, which is unspecified. An `Int` is /// `Equatable`, so an injector publishing it into ``EnvironmentValues/settingsRevision`` makes /// the invalidation explicit and precise. - @Published private(set) var revision: Int = 0 + @Published public private(set) var revision: Int = 0 /// `~/Library/Application Support/CodeEdit/` — the folder settings and adjacent app data live in. - var baseURL: URL { SettingsLocation.baseURL } + public var baseURL: URL { SettingsLocation.baseURL } /// The file this store loads from and saves to. Injectable so a test can point a store at a /// temporary file instead of the user's real `settings.json`. @@ -54,7 +53,7 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { /// whole file, so the first one already contains every one of them. private var hasPreservedOriginal = false - init(settingsURL: URL = SettingsLocation.settingsFileURL) { + public init(settingsURL: URL = SettingsLocation.settingsFileURL) { self.settingsURL = settingsURL self.store = Self.loadStore(at: settingsURL) @@ -78,11 +77,11 @@ final class AppSettingsStore: ObservableObject, SettingsAccessing { // MARK: - SettingsAccessing - func value(_ type: S.Type) -> S { + public func value(_ type: S.Type) -> S { store[S.self] } - func setValue(_ value: S) { + public func setValue(_ value: S) { // `SettingsAccessing` is deliberately nonisolated (see the protocol's docs), so the compiler // cannot enforce this. A write bumps `revision`, whose `@Published` change drives AppKit // through SwiftUI observers — off the main thread that corrupts AppKit state rather than diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift index 0e10929a23..31f8095a4e 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsStore.swift @@ -38,7 +38,7 @@ public final class SettingsStore { /// data. /// /// Deliberately a callback rather than a policy: this target has no business knowing where the - /// file lives or what "preserve" means. The owner (`AppSettingsStore`) copies the file aside. + /// file lives or what "preserve" means. The owner (`PersistentSettingsStore`) copies the file aside. /// Called synchronously and before the replacement, so the handler still sees the original both /// in this store and on disk. public var willReplaceUndecodableSection: ((String) -> Void)? diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index 47e2571b16..0281528e40 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -99,7 +99,7 @@ public extension EnvironmentValues { set { self[SettingsAccessorKey.self] = newValue } } - /// Changes once per settings change; see the app-side `AppSettingsStore.revision`. + /// Changes once per settings change; see `PersistentSettingsStore.revision`. /// /// The seam's invalidation signal, kept in its own `Equatable` key rather than folded into /// ``settingsAccessor``. Two keys, two jobs: the accessor answers *what the value is* and is diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift index 825af5f97c..a1a14e1bc8 100644 --- a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsFormatTests.swift @@ -147,7 +147,7 @@ struct SettingsFormatTests { /// Writing a section whose stored value could not be decoded is the one destructive operation in /// the store, and it must announce itself before it happens. /// - /// The handler is what lets `AppSettingsStore` copy the file aside first. Asserting on the values + /// The handler is what lets `PersistentSettingsStore` copy the file aside first. Asserting on the values /// instead would prove nothing: they are the same defaults either way. @Test func replacingAnUndecodableSectionIsAnnouncedOnce() throws { diff --git a/CodeEditModules/Tests/CodeEditSettingsTests/SettingsStoreTests.swift b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsStoreTests.swift new file mode 100644 index 0000000000..3bd64aea16 --- /dev/null +++ b/CodeEditModules/Tests/CodeEditSettingsTests/SettingsStoreTests.swift @@ -0,0 +1,90 @@ +// +// SettingsStoreTests.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 16/08/26. +// + +import Testing +import Foundation +@testable import CodeEditSettings + +/// Guards the two store behaviours nothing else pins. +/// +/// Both were previously asserted only in doc comments, and both are load-bearing: the first is what +/// third-party extension settings will rely on, the second is what makes a settings change visible +/// to a view body re-evaluated by the change itself. +struct SettingsStoreTests { + + // MARK: - Preservation + + /// A section nothing in this build decodes must survive a save by this build. + /// + /// This is the guarantee an extension's configuration depends on: a newer build writes a key, + /// an older build loads and saves, and the key is still there afterwards. + @Test + func preservesSectionsNoTypeClaims() throws { + let raw = Data(#"{"general":{"revealFileOnFocusChange":true},"com.example.ext":{"enabled":true}}"#.utf8) + + let store = try SettingsStore(data: raw) + store[GeneralSettings.self] = store[GeneralSettings.self] + + let object = try #require( + JSONSerialization.jsonObject(with: try store.encoded()) as? [String: Any] + ) + let extensionSection = try #require( + object["com.example.ext"] as? [String: Any], + "a section no type decodes must survive a write by this build" + ) + #expect(extensionSection["enabled"] as? Bool == true, "its contents must survive verbatim") + } + + /// An undecodable section is likewise held verbatim, rather than being dropped or defaulted on + /// disk, until something writes that same section back. + @Test + func preservesASectionThisBuildCannotDecode() throws { + let raw = Data(#"{"general":"this is not an object","com.example.ext":{"enabled":true}}"#.utf8) + + let store = try SettingsStore(data: raw) + _ = store[GeneralSettings.self] + + let object = try #require( + JSONSerialization.jsonObject(with: try store.encoded()) as? [String: Any] + ) + #expect( + object["general"] as? String == "this is not an object", + "reading an undecodable section must not replace it on disk" + ) + } + + // MARK: - Ordering + + /// Observers must see the **new** value, not the value being replaced. + /// + /// `PersistentSettingsStore` mutates its backing store before bumping `revision`, so a view body + /// re-evaluated by the publish already reads the new value. Inverting that order would make + /// every settings change appear one edit behind. + @MainActor + @Test + func publishesOnlyAfterTheNewValueIsReadable() throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("settings-\(UUID().uuidString).json") + defer { try? FileManager.default.removeItem(at: url) } + + let store = PersistentSettingsStore(settingsURL: url) + + var section = store.value(GeneralSettings.self) + section.revealFileOnFocusChange.toggle() + let expected = section.revealFileOnFocusChange + + var observed: Bool? + let token = store.objectWillChange.sink { _ in + observed = store.value(GeneralSettings.self).revealFileOnFocusChange + } + defer { token.cancel() } + + store.setValue(section) + + #expect(observed == expected, "an observer must read the new value, not the one replaced") + } +} diff --git a/CodeEditTests/App/AppSettingsStoreTests.swift b/CodeEditTests/App/PersistentSettingsStoreTests.swift similarity index 95% rename from CodeEditTests/App/AppSettingsStoreTests.swift rename to CodeEditTests/App/PersistentSettingsStoreTests.swift index a78bf7f4e5..b069b5a930 100644 --- a/CodeEditTests/App/AppSettingsStoreTests.swift +++ b/CodeEditTests/App/PersistentSettingsStoreTests.swift @@ -1,5 +1,5 @@ // -// AppSettingsStoreTests.swift +// PersistentSettingsStoreTests.swift // CodeEditTests // // Created by Matthijs Eikelenboom on 09/08/2026. @@ -13,7 +13,7 @@ import Testing import CodeEditSettings @testable import CodeEdit -/// Verifies `AppSettingsStore` reads and writes real, persisted settings rather than answering with +/// Verifies `PersistentSettingsStore` reads and writes real, persisted settings rather than answering with /// section defaults like `DefaultSettingsReader` would. A test that only checked defaults would pass /// against either implementation and prove nothing. /// @@ -22,17 +22,17 @@ import CodeEditSettings /// that touched settings, and could never assert anything about the file on disk. That is the /// concrete payoff of moving ownership into the composition root. @MainActor -struct AppSettingsStoreTests { +struct PersistentSettingsStoreTests { /// A store over a fresh temporary `settings.json` that no other test can see. - private func makeStore(seed: String? = nil) throws -> (AppSettingsStore, URL) { - let directory = URL.temporaryDirectory.appending(path: "AppSettingsStoreTests-\(UUID().uuidString)") + private func makeStore(seed: String? = nil) throws -> (PersistentSettingsStore, URL) { + let directory = URL.temporaryDirectory.appending(path: "PersistentSettingsStoreTests-\(UUID().uuidString)") try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let url = directory.appending(path: "settings.json") if let seed { try Data(seed.utf8).write(to: url) } - return (AppSettingsStore(settingsURL: url), url) + return (PersistentSettingsStore(settingsURL: url), url) } @Test @@ -88,7 +88,7 @@ struct AppSettingsStoreTests { try? await Task.sleep(for: .milliseconds(50)) } - let reloaded = AppSettingsStore(settingsURL: url) + let reloaded = PersistentSettingsStore(settingsURL: url) let readBack = reloaded.value(LanguageServerSettings.self).installedLanguageServers["round-trip-test"] #expect(readBack?.version == "9.9.9", "a settings write did not survive to disk") #expect(readBack?.isEnabled == false) diff --git a/CodeEditTests/App/SettingsSeamInvalidationTests.swift b/CodeEditTests/App/SettingsSeamInvalidationTests.swift index 274c46ca6f..da639f333a 100644 --- a/CodeEditTests/App/SettingsSeamInvalidationTests.swift +++ b/CodeEditTests/App/SettingsSeamInvalidationTests.swift @@ -16,7 +16,7 @@ import CodeEditSettings /// view reading through `@SettingsValue`. /// /// This is the production path end to end — `SettingsInjector` → `\.settingsRevision` + -/// `AppSettingsStore` → `@SettingsValue` — not a stand-in. It exists because the read/write tests +/// `PersistentSettingsStore` → `@SettingsValue` — not a stand-in. It exists because the read/write tests /// next door pass just as happily when nothing ever re-renders: they render once. /// /// **What it does not prove.** It is a guard on the *outcome*, not on the mechanism: it passes with @@ -33,13 +33,13 @@ struct SettingsSeamInvalidationTests { /// A store over a temporary file, so these tests neither read nor overwrite the developer's real /// `settings.json` — and cannot race any other suite. - private func makeStore() throws -> AppSettingsStore { + private func makeStore() throws -> PersistentSettingsStore { let directory = URL.temporaryDirectory.appending(path: "SettingsSeam-\(UUID().uuidString)") try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - return AppSettingsStore(settingsURL: directory.appending(path: "settings.json")) + return PersistentSettingsStore(settingsURL: directory.appending(path: "settings.json")) } - private func setCursorBlink(_ value: Bool, on store: AppSettingsStore) { + private func setCursorBlink(_ value: Bool, on store: PersistentSettingsStore) { var section = store.value(TerminalSettings.self) section.cursorBlink = value store.setValue(section) From fbe8b8efb5a57209c3dc14deb36901266033e9c5 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 20:30:20 +0200 Subject: [PATCH 291/335] Refactor: Inject the settings store as an environment object Additive: nothing reads the object yet, so a missing injection cannot fail here. Both injectors are the only places the accessor is injected, and all thirteen app-side hosting roots wrap in one of them, so injecting the object in the same two places gives identical coverage by construction rather than by reachability analysis. --- CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index 76cd3b375d..69d1e24782 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -27,6 +27,10 @@ struct SettingsInjector: View { var body: some View { content + // The store as an observed object. SwiftUI subscribes to it directly, so this one + // injection carries both the value and the change signal that the two `.environment` + // keys below need two keys to express. + .environmentObject(store) .environment(\.settingsAccessor, store) // The seam's invalidation signal. Rewriting the accessor above is *not* enough: it is a // stable instance behind an existential, so whether SwiftUI treats the rewrite as a @@ -62,6 +66,7 @@ struct SettingsSceneInjector: Scene { var body: some Scene { content + .environmentObject(store) .environment(\.settingsAccessor, store) .environment(\.settingsRevision, store.revision) } From 31aeeeb6c6f63be75bf1ea2896cb0dfca7170eb3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 20:37:12 +0200 Subject: [PATCH 292/335] Refactor: Observe the settings store instead of an accessor and revision key SettingsValue and AppSettings now hold an @EnvironmentObject rather than a pair of environment keys, one carrying the value and one an Int used only to force re-evaluation. SwiftUI subscribes to the store itself, so the counter and the 'inject both, or neither' hazard both stop existing. ProjectNavigatorViewController's settingsAccessor loses its defaulting stand-in and becomes optional behind a guard, so a controller reached before its representable pushes a store fails loudly instead of reporting defaults. DefaultSettingsReader is kept, contrary to the plan: it had a second role the spec missed. Besides backing the deleted environment key it is the stand-in four singletons hold between construction and configure(), where its trapping behaviour is still what is wanted. SettingsValue can no longer be tested through a protocol double, since @EnvironmentObject cannot carry an existential; the two view-level write tests use a real store on a temporary file. RecordingSettingsStore still serves the initializer-injected consumers. --- CodeEdit/App/Environment+AppCommands.swift | 14 --- .../Settings/AppSettings.swift | 27 ++--- .../Settings/SettingsInjector.swift | 15 +-- .../ProjectNavigatorOutlineView.swift | 7 +- ...ViewController+NSOutlineViewDelegate.swift | 2 +- .../ProjectNavigatorViewController.swift | 17 ++- .../HistoryInspectorView.swift | 5 +- .../Store/PersistentSettingsStore.swift | 15 +-- .../Store/SettingsValue.swift | 105 ++++++------------ .../App/SettingsValueWriteTests.swift | 40 +++---- 10 files changed, 97 insertions(+), 150 deletions(-) diff --git a/CodeEdit/App/Environment+AppCommands.swift b/CodeEdit/App/Environment+AppCommands.swift index 0c81de709b..54f292ab0c 100644 --- a/CodeEdit/App/Environment+AppCommands.swift +++ b/CodeEdit/App/Environment+AppCommands.swift @@ -107,13 +107,6 @@ extension View { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) - // Accessor only — deliberately *not* `\.settingsRevision`. `dependencies` observes - // nothing, so any revision from here would be frozen, and this modifier is applied - // closer to the leaf than `SettingsInjector` is: it would overwrite a live signal with - // a dead one. The revision comes from the observing injectors instead - // (`SettingsInjector`, `CodeEditApp`), and the accessor injected here is equivalent to - // theirs, so overwriting it changes nothing. - .environment(\.settingsAccessor, dependencies.settingsAccessor) } } @@ -131,12 +124,5 @@ extension Scene { .environment(\.workspaceFileOpener, dependencies.workspaceFileOpener) .environment(\.workspaceNavigator, dependencies.workspaceNavigator) .environment(\.languageServices, dependencies.languageServicesProvider) - // Accessor only — deliberately *not* `\.settingsRevision`. `dependencies` observes - // nothing, so any revision from here would be frozen, and this modifier is applied - // closer to the leaf than `SettingsInjector` is: it would overwrite a live signal with - // a dead one. The revision comes from the observing injectors instead - // (`SettingsInjector`, `CodeEditApp`), and the accessor injected here is equivalent to - // theirs, so overwriting it changes nothing. - .environment(\.settingsAccessor, dependencies.settingsAccessor) } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift b/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift index 270a69bf00..97e4f3b551 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/AppSettings.swift @@ -11,17 +11,17 @@ import CodeEditSettings /// Reads and writes one setting inside a SwiftUI view, addressed through the app-wide /// ``SettingsData`` aggregate. /// -/// The legacy app-side counterpart of ``SettingsValue``. Both now resolve the same way — through -/// ``EnvironmentValues/settingsAccessor`` for the value and ``EnvironmentValues/settingsRevision`` -/// for invalidation — so a view can use either without a difference in behaviour. The distinction +/// The legacy app-side counterpart of ``SettingsValue``. Both now resolve the same way — by +/// observing the injected `PersistentSettingsStore` — so a view can use either without a difference +/// in behaviour. The distinction /// that remains is what they may name: `SettingsValue` names one section and works inside feature /// packages, `AppSettings` names the app-wide aggregate and therefore cannot. New code should prefer /// `SettingsValue`; this wrapper exists so its existing declarations — 47 of them, across 30 /// app-target files — did not all have to move in one change. /// /// **Only valid inside a `View`.** It used to read a singleton, so it also worked in models and -/// AppKit types; it no longer does, and such a use resolves to `DefaultSettingsReader`, which traps -/// in debug. Non-view types take a ``SettingsReading``/``SettingsAccessing`` by initializer instead. +/// AppKit types; it no longer does, and such a use traps at runtime because no store was injected. +/// Non-view types take a ``SettingsReading``/``SettingsAccessing`` by initializer instead. /// /// A `Commands` conformer counts as a non-view type here: `.commands { }` is attached beside a /// scene's content rather than inside it, so the environment is not documented to reach it. See @@ -29,30 +29,21 @@ import CodeEditSettings @propertyWrapper struct AppSettings: DynamicProperty where T: Equatable { - @Environment(\.settingsAccessor) - private var accessor - - /// Not a source of data — a source of *invalidation*. See ``EnvironmentValues/settingsRevision``. - @Environment(\.settingsRevision) - private var revision + @EnvironmentObject private var store: PersistentSettingsStore private let keyPath: WritableKeyPath init(_ keyPath: WritableKeyPath) { + self._store = EnvironmentObject() self.keyPath = keyPath } var wrappedValue: T { - get { - // Read, not merely declared: an unread `@Environment` is a dependency SwiftUI does not - // document itself as tracking, and being tracked is this property's entire purpose. - _ = revision - return SettingsData(accessor: accessor)[keyPath: keyPath] - } + get { SettingsData(accessor: store)[keyPath: keyPath] } nonmutating set { // `SettingsData` is a stateless façade, so this "local" mutation writes straight through // to the accessor — the section is read, the field replaced, the section written back. - var settings = SettingsData(accessor: accessor) + var settings = SettingsData(accessor: store) settings[keyPath: keyPath] = newValue } } diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift index 69d1e24782..8cc71b09c1 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsInjector.swift @@ -27,17 +27,10 @@ struct SettingsInjector: View { var body: some View { content - // The store as an observed object. SwiftUI subscribes to it directly, so this one - // injection carries both the value and the change signal that the two `.environment` - // keys below need two keys to express. + // One injection carries both the value and the change signal. SwiftUI subscribes to + // the object itself, which is what the retired pair of environment keys — an accessor + // plus an `Int` revision — needed two keys and a hand-maintained counter to express. .environmentObject(store) - .environment(\.settingsAccessor, store) - // The seam's invalidation signal. Rewriting the accessor above is *not* enough: it is a - // stable instance behind an existential, so whether SwiftUI treats the rewrite as a - // change is unspecified — and `.appServices(_:)`, applied closer to the leaf in - // `CodeEditSplitViewController`, overwrites it with the same instance anyway. - // `settingsRevision` is `Equatable` and lives in its own key, so neither can defeat it. - .environment(\.settingsRevision, store.revision) } } @@ -67,7 +60,5 @@ struct SettingsSceneInjector: Scene { var body: some Scene { content .environmentObject(store) - .environment(\.settingsAccessor, store) - .environment(\.settingsRevision, store.revision) } } diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index 285a0b2dd3..c6d4df5072 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -27,15 +27,14 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { @AppSettings(\.general) private var generalSettings - @Environment(\.settingsAccessor) - private var settingsAccessor + @EnvironmentObject private var settingsStore: PersistentSettingsStore typealias NSViewControllerType = ProjectNavigatorViewController func makeNSViewController(context: Context) -> ProjectNavigatorViewController { let controller = ProjectNavigatorViewController() controller.generalSettings = generalSettings - controller.settingsAccessor = settingsAccessor + controller.settingsAccessor = settingsStore controller.activeEditorState = activeEditorState controller.workspaceNavigator = workspaceNavigator @@ -53,7 +52,7 @@ struct ProjectNavigatorOutlineView: NSViewControllerRepresentable { } func updateNSViewController(_ nsViewController: ProjectNavigatorViewController, context: Context) { - nsViewController.settingsAccessor = settingsAccessor + nsViewController.settingsAccessor = settingsStore nsViewController.generalSettings = generalSettings nsViewController.rowHeight = generalSettings.projectNavigatorSize.rowHeight /// if the window becomes active from background, it will restore the selection to outline view. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift index 7b2e180d13..853a46e24c 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+NSOutlineViewDelegate.swift @@ -113,7 +113,7 @@ extension ProjectNavigatorViewController: NSOutlineViewDelegate { } // If the user has set "Reveal file on selection change" to on or it is forced to reveal, // we need to reveal the item before selecting the row. - if settingsAccessor.value(GeneralSettings.self).revealFileOnFocusChange || forcesReveal { + if settings.value(GeneralSettings.self).revealFileOnFocusChange || forcesReveal { reveal(item) } let row = outlineView.row(forItem: item) diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 0e8a49bfc1..2244e245b2 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -47,7 +47,20 @@ final class ProjectNavigatorViewController: NSViewController { /// The settings store, pushed in from `ProjectNavigatorOutlineView`. AppKit controllers cannot /// read the SwiftUI environment, so the representable that owns this one hands it down. - var settingsAccessor: SettingsAccessing = DefaultSettingsReader() + /// + /// Optional with no default rather than a defaulting reader: a controller reached before its + /// representable has pushed a store is a wiring bug, and a stand-in value would answer with + /// plausible defaults and hide it. Read through ``settings``. + var settingsAccessor: SettingsAccessing? + + /// The pushed-in store, or a loud failure in debug. + var settings: SettingsAccessing { + guard let settingsAccessor else { + assertionFailure("ProjectNavigatorViewController used before a settings store was pushed in") + return DefaultSettingsReader() + } + return settingsAccessor + } /// The general settings, by value — the source for cell construction and the four fields that /// require a reload when they change. @@ -198,7 +211,7 @@ final class ProjectNavigatorViewController: NSViewController { } else { outlineView.expandItem(item) } - } else if settingsAccessor.value(NavigationSettings.self).navigationStyle == .openInTabs { + } else if settings.value(NavigationSettings.self).navigationStyle == .openInTabs { workspaceNavigator.open(file: item, asTemporary: false) } } diff --git a/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift index e78c1308f2..37855c87ac 100644 --- a/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift +++ b/CodeEditModules/Sources/CESourceControl/HistoryInspector/HistoryInspectorView.swift @@ -20,8 +20,7 @@ struct HistoryInspectorView: View { /// `CESourceControl`. private let activeEditorState: ActiveEditorState - @Environment(\.settingsAccessor) - private var settingsAccessor + @EnvironmentObject private var settingsStore: PersistentSettingsStore @ObservedObject private var model: HistoryInspectorModel @@ -62,7 +61,7 @@ struct HistoryInspectorView: View { .task { // The model is created by this view, so this view configures it — the same shape as // `setWorkspace` below. - model.settingsAccessor = settingsAccessor + model.settingsAccessor = settingsStore await model.setWorkspace(sourceControlManager: sourceControlManager) await model.setFile(url: activeEditorState.selectedFile?.url.path()) } diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift b/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift index 4da1098529..f160b04e69 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift @@ -33,13 +33,14 @@ public final class PersistentSettingsStore: ObservableObject, SettingsAccessing /// A counter incremented once per change to any section. /// - /// This is the settings seam's **invalidation signal**. Views reach settings through - /// ``SettingsValue`` or `AppSettings`, whose only other environment dependency is - /// ``EnvironmentValues/settingsAccessor`` — an existential holding a store that never compares - /// unequal to itself. Re-rendering on a settings change would then rest on SwiftUI treating a - /// rewritten non-`Equatable` existential as a change, which is unspecified. An `Int` is - /// `Equatable`, so an injector publishing it into ``EnvironmentValues/settingsRevision`` makes - /// the invalidation explicit and precise. + /// Kept for **AppKit** consumers, which cannot observe an `ObservableObject` and instead watch + /// this counter to know when to reload. + /// + /// SwiftUI no longer needs it: views observe this store directly through ``SettingsValue``, so + /// the change signal is the object's own. It previously had to be a separate `Equatable` + /// environment key, because the accessor was injected as an existential that never compares + /// unequal to itself — leaving re-render to depend on SwiftUI treating a rewritten + /// non-`Equatable` value as a change, which is unspecified. @Published public private(set) var revision: Int = 0 /// `~/Library/Application Support/CodeEdit/` — the folder settings and adjacent app data live in. diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index 0281528e40..89874caed0 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -13,23 +13,30 @@ import SwiftUI /// Feature packages depend on this rather than on the concrete store, so they never name the /// app-wide aggregate and never reach a singleton. /// +/// This is the seam for **non-SwiftUI** consumers — models, services, AppKit controllers — which take +/// it by initializer. SwiftUI views observe ``PersistentSettingsStore`` directly through +/// ``SettingsValue`` instead, because `@EnvironmentObject` cannot carry a protocol existential. +/// /// Deliberately neither `Sendable` nor `@MainActor`: conforming types include a store class whose -/// methods cannot be actor-isolated without breaking this conformance, and `EnvironmentKey` -/// requires a nonisolated static default. +/// methods cannot be actor-isolated without breaking this conformance. The other former reason — +/// that `EnvironmentKey` requires a nonisolated static default — no longer applies, since that key +/// is gone. What still blocks isolation is the Swift 5 app target, where annotating this protocol +/// cascades into its callers. public protocol SettingsReading { /// The current value of `type`, or its defaults if the section is absent. func value(_ type: S.Type) -> S } -/// A reader that always answers with defaults, and **discards every write**. The environment's -/// fallback, so a preview with no store configured still renders. +/// A reader that answers with defaults and **discards every write**, trapping in debug. +/// +/// This was the environment key's fallback. That key is gone — a view with no store now traps +/// immediately — but the type survives for its *other* role: the stand-in that pre-existing +/// singletons (`ThemeModel`, `FeedbackModel`, `SearchSettingsModel`, `HistoryInspectorModel`) hold +/// between construction and `configure(_:)`. Being used at all still means a wiring bug, so both +/// methods `assertionFailure` unless `XCODE_RUNNING_FOR_PREVIEWS` is set. /// -/// The discarding write is the dangerous half: a view whose subtree never received a real store -/// (most easily by sitting behind an `NSHostingView`/`NSHostingController` boundary, which -/// `@Environment` does not cross) will read plausible defaults and *appear* to save, losing the -/// user's change with no error. Reaching this type outside a `#Preview` is therefore treated as a -/// wiring bug: both methods `assertionFailure` unless `XCODE_RUNNING_FOR_PREVIEWS` is set, so the -/// bug is loud in debug and unchanged in release. +/// The discarding write is the dangerous half: a consumer that never received a real store reads +/// plausible defaults and *appears* to save, losing the user's change with no error. public struct DefaultSettingsReader: SettingsAccessing { /// Previews legitimately render with no store configured; everywhere else, reaching this type /// is a wiring bug worth a debug trap. @@ -40,14 +47,12 @@ public struct DefaultSettingsReader: SettingsAccessing { public init() {} /// Answers with defaults. Reads are only *misleading*, not destructive, so they trap in debug - /// but the value is still returned — a preview or a mis-wired subtree keeps rendering. + /// but the value is still returned — a preview or a mis-wired consumer keeps working. public func value(_ type: S.Type) -> S { if !Self.isRunningInPreviews { assertionFailure( - "Read of '\(S.settingsKey)' fell back to defaults: this view subtree never received " - + "a settings accessor. A standalone NSHostingView/NSHostingController root needs " - + "`.appServices(_:)` or `SettingsInjector` — `@Environment` does not cross a " - + "hosting boundary." + "Read of '\(S.settingsKey)' fell back to defaults: this consumer never received a " + + "settings store. Singletons receive one through `configure(_:)` at launch." ) } return S() @@ -57,10 +62,8 @@ public struct DefaultSettingsReader: SettingsAccessing { public func setValue(_ value: S) { if !Self.isRunningInPreviews { assertionFailure( - "Write to '\(S.settingsKey)' was discarded: this view subtree never received a " - + "settings accessor. A standalone NSHostingView/NSHostingController root needs " - + "`.appServices(_:)` or `SettingsInjector` — `@Environment` does not cross a " - + "hosting boundary." + "Write to '\(S.settingsKey)' was discarded: this consumer never received a settings " + + "store. Singletons receive one through `configure(_:)` at launch." ) } } @@ -79,42 +82,6 @@ public struct SnapshotSettingsReader: SettingsReading { } } -public struct SettingsAccessorKey: EnvironmentKey { - /// Defaults are a legitimate value here — a preview with no store configured should render. - nonisolated(unsafe) public static let defaultValue: SettingsAccessing = DefaultSettingsReader() -} - -public struct SettingsRevisionKey: EnvironmentKey { - /// `0` forever: a subtree with no injector has no settings to change under it. - public static let defaultValue: Int = 0 -} - -public extension EnvironmentValues { - /// The settings accessor for the current view tree. - /// - /// Typed as ``SettingsAccessing`` rather than ``SettingsReading`` so that ``SettingsValue`` can - /// vend a `Binding` from the same value it reads through. - var settingsAccessor: SettingsAccessing { - get { self[SettingsAccessorKey.self] } - set { self[SettingsAccessorKey.self] = newValue } - } - - /// Changes once per settings change; see `PersistentSettingsStore.revision`. - /// - /// The seam's invalidation signal, kept in its own `Equatable` key rather than folded into - /// ``settingsAccessor``. Two keys, two jobs: the accessor answers *what the value is* and is - /// legitimately a stable, stateless instance, while the revision answers *whether anything - /// changed*. That separation is what lets a non-observing injection point (`appServices(_:)`) - /// supply the accessor without also having to fake a change signal it cannot compute. - /// - /// Injected by any view that observes the store. A subtree that receives an accessor but no - /// revision reads correct values and never re-renders on change — inject both, or neither. - var settingsRevision: Int { - get { self[SettingsRevisionKey.self] } - set { self[SettingsRevisionKey.self] = newValue } - } -} - /// Reads and writes one property of one settings section inside a SwiftUI view. /// /// ```swift @@ -128,34 +95,32 @@ public extension EnvironmentValues { /// /// The key path is a `WritableKeyPath` even for read-only uses: every settings field is a `var`, so /// requiring it costs read-only call sites nothing and keeps one property wrapper for both jobs. +/// +/// **A missing injection traps.** This replaced a pair of environment *keys* — one for the value, +/// one for an `Int` invalidation signal — whose failure modes were both silent: a subtree given +/// neither read plausible defaults and discarded writes, and a subtree given the value but not the +/// signal read correctly and never re-rendered. `@EnvironmentObject` makes both unrepresentable. +/// SwiftUI subscribes to the object itself, so there is no second key to forget and nothing to keep +/// in sync. @propertyWrapper public struct SettingsValue: DynamicProperty { - @Environment(\.settingsAccessor) - private var accessor - - /// Not a source of data — a source of *invalidation*. See ``EnvironmentValues/settingsRevision``. - @Environment(\.settingsRevision) - private var revision + @EnvironmentObject private var store: PersistentSettingsStore private let keyPath: WritableKeyPath public init(_ section: S.Type, _ keyPath: WritableKeyPath) { + self._store = EnvironmentObject() self.keyPath = keyPath } public var wrappedValue: Value { - get { - // Read, not merely declared: an unread `@Environment` is a dependency SwiftUI does not - // document itself as tracking, and being tracked is this property's entire purpose. - _ = revision - return accessor.value(S.self)[keyPath: keyPath] - } - // Read-modify-write of the whole section: the accessor is section-granular, and this is the + get { store.value(S.self)[keyPath: keyPath] } + // Read-modify-write of the whole section: the store is section-granular, and this is the // only way to change one field without naming the settings aggregate. nonmutating set { - var section = accessor.value(S.self) + var section = store.value(S.self) section[keyPath: keyPath] = newValue - accessor.setValue(section) + store.setValue(section) } } diff --git a/CodeEditTests/App/SettingsValueWriteTests.swift b/CodeEditTests/App/SettingsValueWriteTests.swift index 0cb82d1433..a5e28a9750 100644 --- a/CodeEditTests/App/SettingsValueWriteTests.swift +++ b/CodeEditTests/App/SettingsValueWriteTests.swift @@ -67,11 +67,20 @@ struct SettingsValueWriteTests { /// /// `refreshStatusLocally` is the untouched sibling: the probes never write it, so it is what /// distinguishes a read-modify-write from a section rebuilt at its defaults. - private func makeSeededStore() -> RecordingSettingsStore { - var section = SourceControlSettings() + /// + /// A **real** store on a temporary file, not a protocol double: `@SettingsValue` injects through + /// `@EnvironmentObject`, which cannot carry an existential, so a view-level test cannot + /// substitute a recorder. `RecordingSettingsStore` still serves the initializer-injected + /// consumers, which is where the protocol seam still applies. + private func makeSeededStore() -> PersistentSettingsStore { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("settings-\(UUID().uuidString).json") + let store = PersistentSettingsStore(settingsURL: url) + var section = store.value(SourceControlSettings.self) section.general.sourceControlIsEnabled = false section.general.refreshStatusLocally = false - return RecordingSettingsStore([SourceControlSettings.settingsKey: section]) + store.setValue(section) + return store } /// Hosts `view` long enough for SwiftUI to evaluate its body, then waits until it writes. @@ -80,7 +89,7 @@ struct SettingsValueWriteTests { /// drive the update cycle, and an on-screen window makes the shared app-hosted test process /// talk to the window server, which destabilised the whole test plan when this suite ran /// concurrently with others. - private func render(_ view: some View, until store: RecordingSettingsStore) async { + private func render(_ view: some View, until store: PersistentSettingsStore) async { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 64, height: 64), styleMask: [.borderless], @@ -94,7 +103,8 @@ struct SettingsValueWriteTests { hostingView.layoutSubtreeIfNeeded() var attempts = 0 - while store.writes.isEmpty && attempts < 200 { + while store.value(SourceControlSettings.self).general.sourceControlIsEnabled == false + && attempts < 200 { attempts += 1 try? await Task.sleep(for: .milliseconds(10)) } @@ -106,22 +116,18 @@ struct SettingsValueWriteTests { let observed = ObservedValue() await render( - WrappedValueProbe(observed: observed).environment(\.settingsAccessor, store), + WrappedValueProbe(observed: observed).environmentObject(store), until: store ) // The view read through the injected store, not through defaults. #expect(observed.value == false) - let written = try #require( - store.lastWrite(SourceControlSettings.self), - "@SettingsValue's setter never reached the accessor" - ) - #expect(written.general.sourceControlIsEnabled == true) + let written = store.value(SourceControlSettings.self) + #expect(written.general.sourceControlIsEnabled == true, "the setter never reached the store") // Read-modify-write, not replace-with-defaults: the sibling field the probe never touched // still carries its seeded, non-default value. #expect(written.general.refreshStatusLocally == false) - #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == true) } @Test @@ -130,18 +136,14 @@ struct SettingsValueWriteTests { let observed = ObservedValue() await render( - ProjectedValueProbe(observed: observed).environment(\.settingsAccessor, store), + ProjectedValueProbe(observed: observed).environmentObject(store), until: store ) #expect(observed.value == false) - let written = try #require( - store.lastWrite(SourceControlSettings.self), - "@SettingsValue's projectedValue binding never reached the accessor" - ) - #expect(written.general.sourceControlIsEnabled == true) + let written = store.value(SourceControlSettings.self) + #expect(written.general.sourceControlIsEnabled == true, "the binding never reached the store") #expect(written.general.refreshStatusLocally == false) - #expect(store.value(SourceControlSettings.self).general.sourceControlIsEnabled == true) } } From 0bac8ab14680212243dd40bc56e37177a8cd85d4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sun, 16 Aug 2026 20:43:59 +0200 Subject: [PATCH 293/335] Docs: Record where settings sections live, and update the seam guide The settings section of the architecture guide described the retired pair of environment keys, including an invalidation bullet that no longer applies. Rewrites it around the observed store, and notes the two consequences worth knowing: a missing injection now traps, and a view-level test cannot substitute a protocol double. Adds the placement rule for settings sections, which was unwritten and had to be re-derived by grep twice. It describes all eleven existing sections and requires no moves; what it adds is that Core and CodeEditUI are excluded, and the trigger for moving a section into a feature package. --- .../EditorTabBarTrailingAccessories.swift | 8 ++ .../Store/PersistentSettingsStore.swift | 17 +++-- docs/ARCHITECTURE.md | 73 ++++++++++++++----- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift index 7eb83cdfdf..1fa17ffd79 100644 --- a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift +++ b/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift @@ -112,7 +112,15 @@ struct EditorTabBarTrailingAccessories: View { } struct TabBarTrailingAccessories_Previews: PreviewProvider { + /// A store on a temporary file, never the user's real `settings.json`: this view writes through + /// `@SettingsValue`, and a preview must not be able to persist over real settings. + private static let store = PersistentSettingsStore( + settingsURL: FileManager.default.temporaryDirectory + .appendingPathComponent("preview-settings.json") + ) + static var previews: some View { EditorTabBarTrailingAccessories(codeFile: .constant(nil)) + .environmentObject(store) } } diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift b/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift index f160b04e69..d69bbe17f6 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/PersistentSettingsStore.swift @@ -15,9 +15,11 @@ import Foundation /// `shared`. It replaces `Settings.shared`, which reached the same state from anywhere and could /// therefore never be substituted in a test or a second configuration. /// -/// Not `@MainActor`: it conforms to ``SettingsAccessing``, which is deliberately nonisolated so it -/// can be an `EnvironmentKey` value (see that protocol's documentation). Main-thread use is asserted -/// at the write entry point instead. +/// Not `@MainActor`: it conforms to ``SettingsAccessing``, which is nonisolated. The original reason +/// — that the protocol had to supply a nonisolated `EnvironmentKey` default — is gone with that key. +/// What still blocks isolation is the Swift 5 app target, where annotating the protocol cascades +/// into its callers, so main-thread use is asserted at the write entry point instead. Isolating both +/// belongs with the app-target Swift 6 migration. public final class PersistentSettingsStore: ObservableObject, SettingsAccessing { /// Section-keyed storage. Sections nothing here decodes are held verbatim and re-emitted on @@ -83,10 +85,11 @@ public final class PersistentSettingsStore: ObservableObject, SettingsAccessing } public func setValue(_ value: S) { - // `SettingsAccessing` is deliberately nonisolated (see the protocol's docs), so the compiler - // cannot enforce this. A write bumps `revision`, whose `@Published` change drives AppKit - // through SwiftUI observers — off the main thread that corrupts AppKit state rather than - // failing cleanly. Loud in debug, unchanged in release. + // `SettingsAccessing` is nonisolated (see the protocol's docs), so the compiler cannot + // enforce this yet — the Swift 5 app target is the remaining blocker, not the environment + // key this store used to be injected through. A write publishes to SwiftUI observers and + // bumps `revision` for AppKit ones; off the main thread that corrupts AppKit state rather + // than failing cleanly. Loud in debug, unchanged in release. MainActor.assertIsolated("Settings must be written on the main thread") store[S.self] = value diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 517297f4f7..27016824ef 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -294,36 +294,42 @@ never by naming the app-wide `SettingsData` aggregate. Three roles, pick by cons | Consumer | Use | Why | | --- | --- | --- | -| SwiftUI view | `@SettingsValue(TerminalSettings.self, \.cursorBlink)` | Resolves from the environment; `$`-projects a `Binding` for `Toggle`/`TextField`. | +| SwiftUI view | `@SettingsValue(TerminalSettings.self, \.cursorBlink)` | Observes the injected `PersistentSettingsStore`; `$`-projects a `Binding` for `Toggle`/`TextField`. | | Read-only object (managers, services) | `SettingsReading` by initializer | No environment outside a view; narrow protocol makes read-only visible at the call site. | | Object that also writes | `SettingsAccessing` by initializer | The read+write half; every `SettingsAccessing` satisfies `SettingsReading`. | Access is **section-granular**: `value(_:)`/`setValue(_:)` deal in whole `SettingsSection` values, so a caller changing one field reads its section, mutates it and writes it back. -- **`@AppSettings` is app-target only.** It resolves through the same `settingsAccessor`/ - `settingsRevision` environment as `SettingsValue`, addressing a section field through the - app-wide `SettingsData` façade instead of naming one section directly. There is no - `Settings.shared` singleton any more — `AppSettingsStore` (owned by `AppDependencies`) is the - concrete accessor, injected like everything else. `@AppSettings` is what 29 app-target files - still use (46 declarations); feature packages must not use it, and new app-target code should - prefer the seam. +- **`@AppSettings` is app-target only.** It observes the same store as `SettingsValue`, but + addresses a section field through the app-wide `SettingsData` façade instead of naming one + section directly. There is no `Settings.shared` singleton any more — `PersistentSettingsStore` + (owned by `AppDependencies`) is the concrete store, injected like everything else. `@AppSettings` + is what 29 app-target files still use (46 declarations); feature packages must not use it, and + new app-target code should prefer the seam. - **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies - reaches menu-bar code. `CodeEditCommands`/`ViewCommands` are handed `AppSettingsStore` by + reaches menu-bar code. `CodeEditCommands`/`ViewCommands` are handed `PersistentSettingsStore` by initializer and `@ObservedObject` it — reads, writes and menu invalidation all stop depending on undocumented behaviour. -- **`@Environment` does not cross an `NSHostingView`/`NSHostingController` boundary.** A new - standalone hosting root must be given `.appServices(_:)` or wrapped in `SettingsInjector`, or its - subtree falls back to `DefaultSettingsReader` — plausible defaults, and **writes discarded**. - That fallback `assertionFailure`s outside SwiftUI previews precisely because it is otherwise - silent. -- **Invalidation is explicit.** `SettingsValue` also depends on the `Equatable` - `\.settingsRevision` environment key, fed from `AppSettingsStore.revision`. Rewriting the - accessor is not a re-render signal: it is a stateless value behind an existential. Any injection - point that *observes* the store supplies the revision (`SettingsInjector`, `CodeEditApp`); - `appServices(_:)` observes nothing, so it supplies the accessor only. -- **`AppSettingsStore` is the concrete accessor.** Section-keyed storage, owned by +- **The environment does not cross an `NSHostingView`/`NSHostingController` boundary.** A new + standalone hosting root must be wrapped in `SettingsInjector` (or `SettingsSceneInjector` for a + scene), or every `@SettingsValue` under it **traps**. That is deliberate: this replaced a pair of + environment keys whose failure modes were silent — a subtree given neither read plausible + defaults and discarded writes, and a subtree given the value but not the separate `Int` + invalidation key read correctly and never re-rendered. `@EnvironmentObject` makes both + unrepresentable, because SwiftUI subscribes to the store itself. Those two injectors are the only + places the store is injected, which is what makes the coverage question answerable by grep rather + than by reachability analysis. +- **A protocol double cannot be substituted into a view.** `@EnvironmentObject` cannot carry an + existential, so a view-level test injects a real `PersistentSettingsStore` on a temporary file. + The protocol seam still applies to every initializer-injected consumer, which is where + `RecordingSettingsStore` and `SnapshotSettingsReader` are used. +- **`DefaultSettingsReader` is not the environment's fallback any more** — there is no fallback. It + survives as the stand-in four singletons (`ThemeModel`, `FeedbackModel`, `SearchSettingsModel`, + `HistoryInspectorModel`) hold between construction and `configure(_:)`. It still + `assertionFailure`s outside previews, since reaching it means a real store never arrived. +- **`PersistentSettingsStore` is the concrete store.** Section-keyed storage, owned by `AppDependencies` (no `shared`), driving the same throttled save pipeline `Settings.shared` used to own. Sections nothing here decodes are held verbatim and re-emitted on save, so a disabled extension's configuration survives. The same holds for a section that is present but @@ -331,6 +337,33 @@ so a caller changing one field reads its section, mutates it and writes it back. replace it is announced through `SettingsStore.willReplaceUndecodableSection` so the file is copied to `settings.json.corrupt-` first. +### Where a settings section lives + +A section lives with **its owner**: + +| Readers | Home | Examples | +| --- | --- | --- | +| Exactly one feature package | that package | `TerminalSettings` → `CETerminal`, `LanguageServerSettings` → `CELSP`, `SourceControlSettings`/`AccountsSettings` → `CESourceControl` | +| More than one module, or only the app | `CodeEditSettings` | `TextEditing`, `Theme`, `General`, `Navigation`, `Developer`, `Search`, `Keybindings` | + +**Never `CodeEditCore` or `CodeEditUI`.** A `Codable` config bag has no natural boundary and +accretes — that is precisely how Core became `AppPreferences` the first time, and Core's purity +rationale is that the placement question stays answerable. `CodeEditUI` is excluded mechanically: +it may not depend on a local target, so it cannot see settings types at all. + +**Migration trigger:** when a section's readers collapse to a single feature, it moves with that +feature. That is how the four package-owned sections got where they are. + +App-only sections (`SearchSettings`, `KeybindingsSettings`) stay in `CodeEditSettings` rather than +moving app-side: `SettingsFormatTests` guards the on-disk format for every section in one place +using `Bundle.module` fixtures, and splitting two sections into the app target would split that +guard across two bundle mechanisms to satisfy a boundary nothing enforces. + +Two field-level misplacements are **recorded but not fixed**, because both keys live in users' +`settings.json` and moving a field is a data migration rather than a refactor: +`GeneralSettings.findNavigatorDetail` is read by `CESearch` (a feature-specific field in a shared +section), and `SearchSettings.ignoreGlobPatterns` is dead. + ## Creating a new feature target 1. Create the folder `CodeEditModules/Sources/CE/` and add a target and product for it in From 1669a89694450801d171df6598f9fc1f6f61178f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 13:09:09 +0200 Subject: [PATCH 294/335] Fix: Reselect a panel tab when the selected one goes away MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A panel's tab list is rebuilt at runtime — the inspector rebuilds on a settings change, and any panel's list changes when an extension is enabled or disabled. Nothing reconciled the stored selection against it, so a selection naming a tab that had just gone left the panel reading No Selection until the user clicked something: the id was stale rather than absent, so nothing recovered on its own. Reproducible before this: select the Internal Development inspector tab, then turn that setting off. The rule lives in CodeEditUI as reconcilingSelection(_:) so it is stated once for every panel and can be tested without a view. The panel applies it on appear as well as on change, so a selection that is already stale at first render also recovers. --- .../WorkspacePanel/WorkspacePanelView.swift | 7 ++++ .../WorkspacePanelContribution.swift | 19 ++++++++++ .../WorkspacePanelContributionTests.swift | 38 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift index c7fa18ff2c..bff4daa445 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelView.swift @@ -98,6 +98,13 @@ struct WorkspacePanelView: View { .if(.tahoe) { $0.clipped() } + // The tab list is rebuilt at runtime, so a selection can outlive the tab it names. + .onAppear { + selectedTabID = tabItems.reconcilingSelection(selectedTabID) + } + .onChange(of: tabItems.map(\.id)) { _, _ in + selectedTabID = tabItems.reconcilingSelection(selectedTabID) + } .safeAreaInset(edge: .bottom, spacing: 0) { if #available(macOS 26, *), let selection = selectedTab { selection.bottomView diff --git a/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift b/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift index e030321a5b..f668a5fc25 100644 --- a/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift +++ b/CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift @@ -57,3 +57,22 @@ public extension WorkspacePanelContribution { /// Most tabs have no bottom bar. @MainActor var bottomView: AnyView? { nil } } + +public extension Collection where Element == any WorkspacePanelContribution { + /// The selection that should be in effect for this list, given the one currently stored. + /// + /// Keeps `current` when it still names a tab here, and otherwise falls back to the first — or to + /// `nil` when there are no tabs at all. + /// + /// A panel's tab list is not fixed: the inspector rebuilds it when a setting changes, and any + /// panel's list changes when an extension is enabled or disabled. Without this, a selection + /// pointing at a tab that has just gone away leaves the panel reading "No Selection" until the + /// user clicks something — the stored id is stale rather than absent, so nothing recovers on its + /// own. + func reconcilingSelection(_ current: String?) -> String? { + if let current, contains(where: { $0.id == current }) { + return current + } + return first?.id + } +} diff --git a/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift b/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift index 93262bb500..f6da79a4c2 100644 --- a/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift +++ b/CodeEditModules/Tests/CodeEditUIUnitTests/WorkspacePanelContributionTests.swift @@ -30,4 +30,42 @@ struct WorkspacePanelContributionTests { #expect(items.map(\.id) == ["a", "b"]) #expect(items.first?.title == "Alpha") } + + // MARK: - Selection reconciliation + + /// Built per call rather than held in a `static let`: the element type is not `Sendable`, so + /// shared mutable state would not compile under the package's strict concurrency. + private func twoTabs() -> [any WorkspacePanelContribution] { + [ + StubContribution(id: "a", title: "Alpha", systemImage: "a.circle"), + StubContribution(id: "b", title: "Beta", systemImage: "b.circle") + ] + } + + /// A selection that still names a present tab must be left alone — reconciliation runs whenever + /// the list changes, so a version that "fixed" valid selections would yank the user's tab away + /// every time an unrelated one appeared. + @Test + func keepsASelectionThatStillExists() { + #expect(twoTabs().reconcilingSelection("b") == "b") + } + + /// The bug this exists for: the inspector rebuilds its tabs when a setting changes, and a + /// selection naming the removed tab left the panel on "No Selection" with no way back. + @Test + func fallsBackToTheFirstTabWhenTheSelectionIsGone() { + #expect(twoTabs().reconcilingSelection("removed") == "a") + } + + @Test + func selectsTheFirstTabWhenNothingIsSelected() { + #expect(twoTabs().reconcilingSelection(nil) == "a") + } + + /// No tabs means no selection — not the previous id, which would keep a dead selection alive. + @Test + func answersNilWhenThereAreNoTabs() { + let empty: [any WorkspacePanelContribution] = [] + #expect(empty.reconcilingSelection("a") == nil) + } } From fe5a897d9bb2e8e61e0a40dc4009fb3158f4b83f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 15:06:34 +0200 Subject: [PATCH 295/335] Docs: Bring the architecture guide back in line with the tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six facts had drifted: the test-target count and list, CodeEditSettings still being credited with the theme, three worked examples where there are now four, and the SwiftUI import count used as evidence that target is not drifting — stale evidence being worse than none. The enforcement section told readers to run swiftlint without --strict, which reports violations as warnings and exits 0, so a local run looked clean on a tree CI would reject. It also opened by counting tools where the rules section counts rules, which reads as a contradiction. Documents the two panel additions from the Tahoe merge: bottomView, including why it is a protocol requirement rather than something the panel is handed, and the selection-reconciliation invariant, which is silent when violated and invisible from either the protocol or the view. --- docs/ARCHITECTURE.md | 47 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 27016824ef..09bfdabbb2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,7 +7,7 @@ before you add files will save you a failed check. ## Package topology The workspace contains one app project and one local Swift package holding 12 library targets -and 5 test targets: +and 6 test targets: ``` CodeEdit.xcworkspace @@ -17,15 +17,16 @@ CodeEdit.xcworkspace ├── Sources/ │ ├── CodeEditCore — pure types, EventBus, command interfaces (no UI/IO, zero deps) │ ├── CodeEditUI — shared presentation atoms (→ CodeEditSymbols only) - │ ├── CodeEditSettings — settings seam + store + theme (UI pages stay app-side) + │ ├── CodeEditSettings — settings seam + store (UI pages stay app-side) │ ├── CodeEditDocument — CodeFileDocument + editor-framework bridging protocols │ │ (consumed only by CEEditor and CELSP) │ ├── ShellClient — Process adapter (app-linked; no package-internal consumer) │ ├── CEWorkspaceFileManager — FileManager + FSEvents workspace tree (app-linked, ditto) │ └── CEEditor, CESearch, CENotifications, CELSP, CESourceControl, CETerminal │ — one target per feature - └── Tests/ — CodeEditCoreTests, CodeEditUIUnitTests, CESearchTests, - CELSPTests, CESourceControlTests + └── Tests/ — CodeEditCoreTests, CodeEditSettingsTests, + CodeEditUIUnitTests, CESearchTests, CELSPTests, + CESourceControlTests ``` Each library target publishes a like-named `.library` product, and the app target links the ones @@ -98,13 +99,17 @@ Three checks are enforced in CI. Each one blocks a specific failure documented i it, Core becomes the place everything shared goes, which is what `AppPreferences` was and one of the two documented causes of the 2022 collapse. - The friction this produces is usually the rule working. Three worked examples already in this + The friction this produces is usually the rule working. Four worked examples already in this codebase: `FileIcon` is keyed on `URL` rather than a domain type, so it needs neither `CodeEditCore` nor a charter exception (see rule 2 below); `WorkspacePanelContribution` was shaped to need only SwiftUI, so it lives in `CodeEditUI` - (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); and fuzzy matching's + (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); fuzzy matching's concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` - (below). + (below); and `ActiveTheme` — the active light/dark theme, observed by the editor and terminal — + lives *in* Core, because `ObservableObject` comes from Combine rather than SwiftUI. That last one + is worth remembering: the charter forbids `SwiftUI`/`AppKit`/`Cocoa` specifically, so observation + is available in Core and an "it needs to be observable, therefore it needs SwiftUI" argument is + simply false. The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry `NSFont.Weight`, which forces them out of Core. That is the rule flagging a presentation type @@ -138,7 +143,8 @@ as of the panel-contributions work (`CEEditor`, `CELSP`, `CESearch`, `CESourceCo — up from four; `CESearch` joined when `FindNavigatorContribution` started reading its own settings instead of taking them from an app-side wrapper) but only one dependency (`CodeEditCore`), so it stays a well-formed shared substrate rather than a hub — and it imports `AppKit` in 1 file and -`SwiftUI` in 7 (both unchanged; re-measured, not carried forward). +`SwiftUI` in 4, down from 7 once the theme moved to Core and the colour conversion to `CodeEditUI` +(re-measured 2026-08-16, not carried forward). ## Where does my code go? @@ -247,6 +253,24 @@ contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forb outright. `CodeEditUI`'s own charter (rule 2) is satisfied too — the protocol needs SwiftUI and nothing else. +A contribution vends two views. `content` is the tab itself. `bottomView` is an optional bar pinned +below it — the navigator's filter field and sort controls are the existing examples — defaulted to +`nil` so a tab with no such bar says nothing about it. It is a *requirement* rather than something +the panel is handed deliberately: it arrived from upstream as a `switch` over the retired tab enum, +where every new tab had to remember to add its case, and a package could not contribute one at all +because the switch lived app-side. Vended by the contribution, a tab cannot forget, and where the bar +is placed stays the panel's business — pre-Tahoe it insets the tab's own content, from macOS 26 it +spans the panel below the tab bar. + +**A panel's tab list changes at runtime**, so a stored selection can outlive the tab it names: the +inspector rebuilds its list when a setting changes, and any panel's list changes when an extension is +enabled or disabled. `Collection.reconcilingSelection(_:)` (alongside the protocol) keeps a selection +that still resolves and otherwise falls back to the first tab; `WorkspacePanelView` applies it on +appear and on every change to the list. Without it the panel reads "No Selection" until the user +clicks something, because the stored id is stale rather than absent and nothing recovers on its own. +The rule lives in the package rather than the view so it holds for every panel and can be tested +without one. + A contribution's owner follows the same placement rule as everything else in [Where does my code go?](#where-does-my-code-go): a feature that owns a tab vends its own contribution from its package, reading whatever it needs (including settings, through the seam) @@ -396,7 +420,7 @@ section), and `SearchSettings.ignoreGlobPatterns` is dead. ## Enforcement -Two automated checks keep this document honest; both run on every PR: +Two tools enforce the three [Rules](#rules) above; both run on every PR: - **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`) — includes custom rules that reject UI imports in CodeEditCore and model/feature imports in CodeEditUI. These fire @@ -412,10 +436,13 @@ Two automated checks keep this document honest; both run on every PR: Run both locally from the repo root: ```bash -swiftlint lint --quiet +swiftlint lint --strict --quiet python3 .github/scripts/audit_package_imports.py ``` +`--strict` matters: without it SwiftLint reports violations as warnings and exits 0, so a local run +looks clean and CI fails on the same tree. + ### Known weakness in the CodeEditUI charter (2026-08-05) Both checks constrain **local** targets only. `ui_package_purity` lists sibling module names in From 4e706008bf50b67198d142706fb0311bdf307088 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 16:04:19 +0200 Subject: [PATCH 296/335] Refactor: Stop routing a filesystem write through a domain type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FileDropHandler borrowed CEWorkspaceFile.fileManager to check for and remove an existing destination. That reaches into CodeEditCore for a static in order to mutate the filesystem, which makes the domain layer a conduit for a write. It now holds its own FileManager; no caller outside Core uses that static. Also corrects the architecture guide, which asserted in three places that Core performs no I/O. That was never enforced — the SwiftLint rule covers SwiftUI/AppKit/Cocoa only, and Foundation, which Core needs and 47 of its files import, is itself the I/O surface — and it was not true: CEWorkspaceFile reads the filesystem for children and doesExist. It is now stated as a norm with its actual justification and that exception recorded, rather than as a rule nobody checks. --- .../Workspace/Files/FileDropHandler.swift | 9 +++++++-- docs/ARCHITECTURE.md | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift index 4b8606f1d0..006d2beea4 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift @@ -13,6 +13,11 @@ import CodeEditCore @MainActor final class FileDropHandler { + /// This handler's own file manager. Deliberately not `CEWorkspaceFile.fileManager`: borrowing a + /// static off a domain type to *mutate* the filesystem routes a write through `CodeEditCore`, + /// which holds domain types rather than services. + private let fileManager: FileManager = .default + struct Operation { let source: CEWorkspaceFile let destination: URL @@ -45,11 +50,11 @@ final class FileDropHandler { ?? CEWorkspaceFile(url: URL(fileURLWithPath: url.path)) // Handle existing destination via the supplied confirmation closure - if CEWorkspaceFile.fileManager.fileExists(atPath: destURL.path) { + if fileManager.fileExists(atPath: destURL.path) { guard confirmReplace(url.lastPathComponent) else { continue } - try CEWorkspaceFile.fileManager.removeItem(at: destURL) + try fileManager.removeItem(at: destURL) } operations.append(Operation(source: source, destination: destURL, isCopy: isCopyOperation)) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 09bfdabbb2..1f71b1065e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,7 +15,7 @@ CodeEdit.xcworkspace └── CodeEditModules/ ├── Package.swift — the entire local dependency graph, in one file ├── Sources/ - │ ├── CodeEditCore — pure types, EventBus, command interfaces (no UI/IO, zero deps) + │ ├── CodeEditCore — domain types, EventBus, command interfaces (no UI, zero deps) │ ├── CodeEditUI — shared presentation atoms (→ CodeEditSymbols only) │ ├── CodeEditSettings — settings seam + store (UI pages stay app-side) │ ├── CodeEditDocument — CodeFileDocument + editor-framework bridging protocols @@ -136,6 +136,22 @@ features is genuinely needed, try the three [cycle-resolution moves](#cycle-resolution-playbook) first, then declare the edge in the manifest where it is visible to everyone. Acyclicity itself needs no rule — SwiftPM enforces it. +**Keep I/O out of Core.** A norm, not a gate — and worth being precise about, because the guide +previously implied it was enforced. It is not: the SwiftLint rule forbids `SwiftUI`/`AppKit`/`Cocoa` +and nothing more, and `Foundation` — which Core needs for `URL`, `Data` and `Codable`, and which 47 +of its files import — *is* the I/O surface, so an import check cannot express this. + +The reason to keep it out anyway: Core stays deterministic and testable with no filesystem, and I/O +already has a designated home — rule 4 of [Where does my code go?](#where-does-my-code-go) sends +services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. + +**Known exception, recorded rather than pretended away:** `CEWorkspaceFile` exposes +`static let fileManager = FileManager.default` and uses it for `children` and `doesExist`. Those are +filesystem reads from a domain type. Moving them onto the file-manager service is the pure fix; it is +not worth it today against 294 references. What was worth fixing, and has been, is code *outside* +Core borrowing that static to mutate the filesystem — a write routed through the domain layer. There +is now no such caller. + **Hub heuristic.** Any target both depended on by three or more others *and* itself depending on three or more is a hub under review. 2022's `AppPreferences` was exactly this and would have been flagged years before it became fatal. `CodeEditSettings` is the current watch item: five dependents @@ -162,7 +178,7 @@ Work through these in order; the first match wins. permanent app-side tab is `ProjectNavigatorContribution`, because the project navigator has no owning package to move to, not because it is a tab. 2. **A type, protocol, event, or command interface needed by two or more features?** → - `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI/IO imports, no + `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI imports, no external dependencies). Events (facts, e.g. `TaskNotificationEvent`) and command interfaces (requests with exactly one handler, e.g. `WorkspaceNavigator`) always live here. 3. **A reusable view, style, or view modifier with no feature semantics?** → From 7de56dd221644fc4663af94c27b48c27cae0e10d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 16:17:30 +0200 Subject: [PATCH 297/335] Docs: Correct the property named in the Core I/O exception The known-exception note named a 'children' property that does not exist; the filesystem read is in isEmptyFolder. Found because the identifier sweep behind the previous doc review only checked capitalised names, so lowercase member references were never verified. --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1f71b1065e..6638eded13 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -146,8 +146,8 @@ already has a designated home — rule 4 of [Where does my code go?](#where-does services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. **Known exception, recorded rather than pretended away:** `CEWorkspaceFile` exposes -`static let fileManager = FileManager.default` and uses it for `children` and `doesExist`. Those are -filesystem reads from a domain type. Moving them onto the file-manager service is the pure fix; it is +`static let fileManager = FileManager.default` and uses it for `isEmptyFolder` and `doesExist`. +Those are filesystem reads from a domain type. Moving them onto the file-manager service is the pure fix; it is not worth it today against 294 references. What was worth fixing, and has been, is code *outside* Core borrowing that static to mutate the filesystem — a write routed through the domain layer. There is now no such caller. From b41df2620f9bd7e5bde4937278f63bd1eadb958c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 16:21:56 +0200 Subject: [PATCH 298/335] Refactor: Drop CEWorkspaceFile's FileManager alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static was 'FileManager.default' under another name. Being a let, it could not serve as a substitution seam either, so it bought nothing at two call sites — isEmptyFolder and doesExist — that can name the global directly. It cost three things. Made public when Core became a module, it exported the alias and became the route by which app-side code reached through a domain type to delete a file. It required nonisolated(unsafe) purely because it was a static; inlining removes what was CodeEditCore's only concurrency opt-out. And it made Core appear to own a file-manager dependency it never had. The filesystem reads themselves stay, and remain recorded in the architecture guide as the known exception to keeping I/O out of Core. --- .../Workspace/Files/FileDropHandler.swift | 3 --- .../CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift | 9 ++------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift b/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift index 006d2beea4..c1afc14862 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Files/FileDropHandler.swift @@ -13,9 +13,6 @@ import CodeEditCore @MainActor final class FileDropHandler { - /// This handler's own file manager. Deliberately not `CEWorkspaceFile.fileManager`: borrowing a - /// static off a domain type to *mutate* the filesystem routes a write through `CodeEditCore`, - /// which holds domain types rather than services. private let fileManager: FileManager = .default struct Operation { diff --git a/CodeEditModules/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift index d1d0f1c3cc..4c8d66f991 100644 --- a/CodeEditModules/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift +++ b/CodeEditModules/Sources/CodeEditCore/Domain/Workspace/CEWorkspaceFile.swift @@ -44,7 +44,7 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable /// True if this directory has no contents. (Check ``isFolder`` first.) public var isEmptyFolder: Bool { - (try? Self.fileManager.contentsOfDirectory( + (try? FileManager.default.contentsOfDirectory( at: resolvedURL, includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants @@ -55,7 +55,7 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable public var isRoot: Bool { parent == nil } /// True if the file exists on disk. - public var doesExist: Bool { Self.fileManager.fileExists(atPath: self.url.path) } + public var doesExist: Bool { FileManager.default.fileExists(atPath: self.url.path) } /// The file's UTType. public var contentType: UTType? { url.contentType } @@ -113,11 +113,6 @@ public final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable isFolder ? url : url.deletingLastPathComponent() } - // MARK: Statics - - /// `FileManager.default` is documented thread-safe; the shared instance is only read from here. - nonisolated(unsafe) public static let fileManager = FileManager.default - // MARK: Comparable / Hashable public static func == (lhs: CEWorkspaceFile, rhs: CEWorkspaceFile) -> Bool { From b785a2958bd054dd5d5a1547718dbb696f44d2e0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 16:49:04 +0200 Subject: [PATCH 299/335] Docs: Replace unenforced claims with the scope rule they were reaching for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit of the guide's non-CI-backed assertions found four that were false and one stale. The CI-backed rules were all accurate, which is the pattern: a claim nothing checks drifts, and nothing distinguished those claims typographically from the enforced ones. 'No singletons' was the worst of them — eight exist, and the same document describes how three of them receive their settings store. It was also the wrong target: Apple's own frameworks are full of singletons, and what was removed with Factory was a container, not shared state. Replaced with the rule it was reaching for: four scopes, each with an owner, nothing scoped reached ambiently, and a shared only where the platform constructs the object. The eight are listed with the scope each actually has. Window and document scope were already real and unnamed — window-UI state lives on CodeEditWindowController for a reason, and CodeFileDocument.delegateProvider exists because NSDocument is framework-created. Also: the folder convention holds for the app target but not the packages, which still group by kind and still have UseCases folders; five app-side panel tabs are permanent rather than one; ignoreGlobPatterns is wired up and never consulted rather than dead; and the @AppSettings counts had drifted. --- docs/ARCHITECTURE.md | 89 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6638eded13..8f4b2314a7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -175,8 +175,11 @@ Work through these in order; the first match wins. features' tabs and has no single owner, so it stays app-side; a *tab* is one feature's own UI and belongs in that feature's package — `CESearch` owns `FindNavigatorContribution` for exactly this reason (see [Panel tab contributions](#panel-tab-contributions)). The only - permanent app-side tab is `ProjectNavigatorContribution`, because the project navigator has - no owning package to move to, not because it is a tab. + app-side tabs that stay are the ones with no owning package to move to — + `ProjectNavigatorContribution`, `FileInspectorContribution`, + `InternalDevelopmentInspectorContribution`, `DebugConsoleUtilityContribution` and + `OutputUtilityContribution`. `TerminalUtilityContribution` is the one expected to move, to + `CETerminal`. None of them stays because it is a tab. 2. **A type, protocol, event, or command interface needed by two or more features?** → `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI imports, no external dependencies). Events (facts, e.g. `TaskNotificationEvent`) and command interfaces @@ -228,8 +231,14 @@ a move possible. Rewrite the helper, or mirror it locally, instead. Grouping is **purpose-first**: - Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never - by kind — there are no `Models/`, `Views/`, `ViewModels/`, `Services/`, or `UseCases/` + by kind — the app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` folders. +- **The packages do not yet follow this.** `CEEditor`, `CENotifications`, `CESourceControl`, + `CETerminal`, `CodeEditSettings` and `CodeEditUI` still group by kind (17 such folders), and + `CEEditor/UseCases/` and + `CESourceControl/UseCases/` still carry the retired name even though the types inside were + renamed to doers (`EditorRestorer`, `RepositoryCloner`). Follow the convention in new code; + the existing folders are a pending cleanup, not a counter-precedent. - A feature with roughly ten files or fewer stays flat. - Shell/entry views and the feature's primary models sit at the feature root. - Single-consumer helpers live next to their consumer. @@ -245,13 +254,69 @@ Grouping is **purpose-first**: CodeEditCore, implemented by an app-side adapter. - No custom `Notification.Name`s. `NotificationCenter` is only used to observe platform notifications (NSWindow, NSApplication, NSMenu). -- No singletons and no DI container. `AppDependencies` is the app-scope composition root; - objects receive dependencies through initializers, SwiftUI views through environment keys - (`appServices(_:)`). Only composition roots may hold the whole `AppDependencies`. +- **No DI container.** `AppDependencies` is the app-scope composition root; objects receive + dependencies through initializers, SwiftUI views through environment keys (`appServices(_:)`). + Only composition roots may hold the whole `AppDependencies`. A container — ask for a type, get + an instance — was removed deliberately: it hides who owns a thing and how long it lives, which + is the question this section exists to answer. +- **Scope determines owner; nothing scoped is reached ambiently.** See + [Scopes and ownership](#scopes-and-ownership) below. A `static shared` is legitimate only where + the platform constructs the object and no initialiser parameter is available — today that is + `CodeFileDocument`, which is why `delegateProvider` is a static closure set at launch. Eight + other singletons remain, listed there as known exceptions. - No SwiftUI view observes a service directly — services expose a concrete view-state object (the presentation-state split), and views issue commands through protocol-typed environment keys. +### Scopes and ownership + +Four lifetimes exist. Each has an owner, and a type belongs to the narrowest one that fits. + +| Scope | Owner | Examples | +| --- | --- | --- | +| Process | `AppDependencies` | `eventBus`, `lspService`, `settingsStore`, `workspaceWindowManager` | +| Workspace | `Workspace` | `editorManager`, `workspaceFileManager`, `sourceControlManager`, `taskManager` | +| Window | `CodeEditWindowController` | `utilityAreaModel`, `statusBarViewModel`, `notificationPanel`, `openQuicklyViewModel` | +| Document | `CodeFileDocument` | per-file editing state | + +Window scope is not a subdivision of workspace scope: two windows may show one project, and their +utility areas, status bars and palettes must not be shared. That is why window-UI state lives on +`CodeEditWindowController` and not on `Workspace`. + +Document scope is where the platform pushes back. `NSDocument` subclasses are created by the +document architecture, not by us, so no initialiser parameter is available — hence +`CodeFileDocument.delegateProvider`, a static closure set at launch. That is the shape of a +legitimate exception: the platform owns construction. + +**Note on idiom:** "no singletons" is not the goal and never was. Apple's own frameworks are full of +them (`NSApplication.shared`, `FileManager.default`, `NSDocumentController.shared`). What was +removed was a *container*. A `shared` is a problem here only when it gives ambient access to +something whose lifetime is narrower than the process, or when it hides an owner that could hold it. + +#### Known exceptions (2026-08-16) + +Eight singletons remain. None is load-bearing; each is listed with the scope it actually has. + +| Singleton | True scope | Note | +| --- | --- | --- | +| `ThemeModel` | process | already takes its settings store via `configure(_:)` at launch | +| `FeedbackModel` | process | ditto | +| `SearchSettingsModel` | process | ditto | +| `ExtensionManager` | process | | +| `ExtensionDiscovery` | process | | +| `InternalDevelopmentOutputSource` | process | debug-only | +| `EditorStateRestoration` | process | a GRDB database; `nonisolated(unsafe)` and optional | +| `TerminalCache` | **workspace** | see below | + +The first seven are process-scoped services that simply have not moved to `AppDependencies`; doing +so is mechanical and changes no behaviour. + +`TerminalCache` is different, and is the one worth fixing rather than relocating. It is a +process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. +Eviction is per-terminal only — nothing clears it when a workspace or window closes — so two open +projects share one bag of live terminal views. It works because UUIDs do not collide, but the +lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. + ## Panel tab contributions The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, @@ -299,9 +364,11 @@ rather than duplicating the literal, so there is exactly one source of truth eve still needs a compile-checked constant to select the tab by. `ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) -is the one contribution that stays app-side permanently — not because it is a tab (see the +stays app-side — not because it is a tab (see the [chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no -owning package to move to. +owning package to move to. The same holds for the file inspector, the internal-development +inspector, and the debug and output utility tabs. `TerminalUtilityContribution` is the one that +should move, to `CETerminal`; it has not because it is coupled to app-side utility-area chrome. `CESourceControl` is the second feature package to own a panel tab, after `CESearch`. `SourceControlNavigatorContribution` and `GitHistoryInspectorContribution` @@ -345,7 +412,7 @@ so a caller changing one field reads its section, mutates it and writes it back. addresses a section field through the app-wide `SettingsData` façade instead of naming one section directly. There is no `Settings.shared` singleton any more — `PersistentSettingsStore` (owned by `AppDependencies`) is the concrete store, injected like everything else. `@AppSettings` - is what 29 app-target files still use (46 declarations); feature packages must not use it, and + is what 24 app-target files still use (40 declarations, re-counted 2026-08-16); feature packages must not use it, and new app-target code should prefer the seam. - **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies @@ -402,7 +469,9 @@ guard across two bundle mechanisms to satisfy a boundary nothing enforces. Two field-level misplacements are **recorded but not fixed**, because both keys live in users' `settings.json` and moving a field is a data migration rather than a refactor: `GeneralSettings.findNavigatorDetail` is read by `CESearch` (a feature-specific field in a shared -section), and `SearchSettings.ignoreGlobPatterns` is dead. +section), and `SearchSettings.ignoreGlobPatterns` is wired to its settings page and persisted but +never read by `CESearch` — the control works and has no effect, which is worse than dead code +because nothing looks unused. ## Creating a new feature target From 9adcf64c1f3f007741fb4097892148e41a9d953a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 18:33:08 +0200 Subject: [PATCH 300/335] Refactor: Rename UndoManagerRegistration to UndoManagerRegistry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'Registration' names an act; the type is the thing that holds registrations, and its sibling DocumentRegistry already says so. The existing test named its instance 'registrar', reaching for the agent noun the type did not supply. The property follows: workspace.undoRegistration becomes undoRegistry, which reads correctly at the call site — undoRegistry.manager(forFile:). --- .../WorkspaceWindow/CodeEditSplitViewController.swift | 2 +- .../Workspace/Adapters/AppCodeFileDocumentDelegate.swift | 2 +- CodeEdit/WorkspaceWindow/Workspace/Workspace.swift | 6 +++--- CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift | 8 ++++---- ...anagerRegistration.swift => UndoManagerRegistry.swift} | 6 +++--- CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift | 4 ++-- .../Sources/CEEditor/Views/FilePreviewView.swift | 4 ++-- .../Sources/CEEditor/Views/WindowCodeFileView.swift | 4 ++-- ...strationTests.swift => UndoManagerRegistryTests.swift} | 6 +++--- 9 files changed, 21 insertions(+), 21 deletions(-) rename CodeEditModules/Sources/CEEditor/Models/Restoration/{UndoManagerRegistration.swift => UndoManagerRegistry.swift} (94%) rename CodeEditTests/Features/Editor/{UndoManagerRegistrationTests.swift => UndoManagerRegistryTests.swift} (87%) diff --git a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift index aee2045d03..172c14d7ac 100644 --- a/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift +++ b/CodeEdit/WorkspaceWindow/CodeEditSplitViewController.swift @@ -147,7 +147,7 @@ final class CodeEditSplitViewController: NSSplitViewController { .environmentObject(workspace.taskManager) .environmentObject(workspace.sourceControlManager) .environmentObject(workspace.sourceControlViewModel) - .environmentObject(workspace.undoRegistration) + .environmentObject(workspace.undoRegistry) .environmentObject(notificationPanel) .environment(\.workspaceFileManager, workspace.workspaceFileManager) .environment(\.workspaceFileProvider, workspace.workspaceFileManager) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift index daa5047b71..e236c98833 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift @@ -45,7 +45,7 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { } func undoManager(forFile url: URL) -> CEUndoManager? { - windowManager.workspace(containing: url)?.undoRegistration.managerIfExists(forFile: url) + windowManager.workspace(containing: url)?.undoRegistry.managerIfExists(forFile: url) } func makeWindowContentView(for document: CodeFileDocument) -> NSView { diff --git a/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift index 38e3c9fdd8..8b1caeef93 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Workspace.swift @@ -33,7 +33,7 @@ final class Workspace { let taskManager: TaskManager let workspaceSettingsManager: CEWorkspaceSettings let statePersistence: WorkspaceStatePersistence - let undoRegistration: UndoManagerRegistration + let undoRegistry: UndoManagerRegistry // Navigator-coupled — stays until the Navigator feature is packaged // (consumed by the ProjectNavigator AppKit cluster and by-workspace command paths). @@ -60,7 +60,7 @@ final class Workspace { taskManager: TaskManager, workspaceSettingsManager: CEWorkspaceSettings, statePersistence: WorkspaceStatePersistence, - undoRegistration: UndoManagerRegistration, + undoRegistry: UndoManagerRegistry, projectNavigatorViewModel: ProjectNavigatorViewModel, securityScopedURL: URL? ) { @@ -74,7 +74,7 @@ final class Workspace { self.taskManager = taskManager self.workspaceSettingsManager = workspaceSettingsManager self.statePersistence = statePersistence - self.undoRegistration = undoRegistration + self.undoRegistry = undoRegistry self.projectNavigatorViewModel = projectNavigatorViewModel self.securityScopedURL = securityScopedURL } diff --git a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift index 2fb0f04c73..8bd4841959 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/WorkspaceFactory.swift @@ -47,11 +47,11 @@ enum WorkspaceFactory { workspaceURL: url, eventBus: eventBus ) - let undoRegistration = UndoManagerRegistration() + let undoRegistry = UndoManagerRegistry() // Observer registration - workspaceFileManager.addObserver(undoRegistration) - undoRegistration.editorManager = editorManager + workspaceFileManager.addObserver(undoRegistry) + undoRegistry.editorManager = editorManager let workspace = Workspace( fileURL: url, @@ -64,7 +64,7 @@ enum WorkspaceFactory { taskManager: taskManager, workspaceSettingsManager: workspaceSettingsManager, statePersistence: statePersistence, - undoRegistration: undoRegistration, + undoRegistry: undoRegistry, projectNavigatorViewModel: ProjectNavigatorViewModel(), securityScopedURL: securityScopedURL ) diff --git a/CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift b/CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistry.swift similarity index 94% rename from CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift rename to CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistry.swift index 7723434d5d..2301274d4e 100644 --- a/CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistration.swift +++ b/CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistry.swift @@ -1,5 +1,5 @@ // -// UndoManagerRegistration.swift +// UndoManagerRegistry.swift // CodeEdit // // Created by Khan Winter on 6/27/25. @@ -17,7 +17,7 @@ import CodeEditTextView /// - `CEWorkspaceFile` can be refreshed and reloaded at any point. /// - `CodeFileDocument` is released once there are no editors viewing it. /// Undo stacks need to be retained for the duration of a workspace session, enduring editor closes.. -public final class UndoManagerRegistration: ObservableObject { +public final class UndoManagerRegistry: ObservableObject { private var managerMap: [String: CEUndoManager] = [:] /// Used to check whether a file still has an open document. Wired by `WorkspaceFactory`. @@ -53,7 +53,7 @@ public final class UndoManagerRegistration: ObservableObject { } } -extension UndoManagerRegistration: WorkspaceFileObserver { +extension UndoManagerRegistry: WorkspaceFileObserver { /// Managers need to be cleared when the following is true: /// - The file is not open in any editors /// - The file is updated externally diff --git a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift index 9c1d37d5a2..ce99e008cc 100644 --- a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift @@ -67,7 +67,7 @@ struct CodeFileView: View { @Environment(\.colorScheme) private var colorScheme - @EnvironmentObject var undoRegistration: UndoManagerRegistration + @EnvironmentObject var undoRegistry: UndoManagerRegistry @EnvironmentObject private var activeTheme: ActiveTheme @@ -172,7 +172,7 @@ struct CodeFileView: View { } ), highlightProviders: highlightProviders, - undoManager: undoRegistration.manager(forFile: editorInstance.file), + undoManager: undoRegistry.manager(forFile: editorInstance.file), coordinators: textViewCoordinators ) // This view needs to refresh when the codefile changes. The file URL is too stable. diff --git a/CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift b/CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift index 8daccda23a..aaec562033 100644 --- a/CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift @@ -14,7 +14,7 @@ public struct FilePreviewView: View { @StateObject private var editorInstance: EditorInstance @StateObject private var document: CodeFileDocument - @StateObject private var undoRegistration = UndoManagerRegistration() + @StateObject private var undoRegistry = UndoManagerRegistry() public init(item: CEWorkspaceFile) { self.item = item @@ -38,7 +38,7 @@ public struct FilePreviewView: View { languageServices: languageServices, isEditable: false ) - .environmentObject(undoRegistration) + .environmentObject(undoRegistry) } else { NonTextFileView(fileDocument: document) } diff --git a/CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift b/CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift index 59944c9e70..15d675e1bb 100644 --- a/CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift +++ b/CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift @@ -14,7 +14,7 @@ import SwiftUI /// # Should **not** be used other than in a single file window. public struct WindowCodeFileView: View { @StateObject var editorInstance: EditorInstance - @StateObject var undoRegistration: UndoManagerRegistration = UndoManagerRegistration() + @StateObject var undoRegistry: UndoManagerRegistry = UndoManagerRegistry() var codeFile: CodeFileDocument public init(codeFile: CodeFileDocument) { @@ -33,7 +33,7 @@ public struct WindowCodeFileView: View { public var body: some View { if let utType = codeFile.utType, utType.conforms(to: .text) { CodeFileView(editorInstance: editorInstance, codeFile: codeFile, languageServices: languageServices) - .environmentObject(undoRegistration) + .environmentObject(undoRegistry) } else { NonTextFileView(fileDocument: codeFile) } diff --git a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift b/CodeEditTests/Features/Editor/UndoManagerRegistryTests.swift similarity index 87% rename from CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift rename to CodeEditTests/Features/Editor/UndoManagerRegistryTests.swift index 7da1965e19..ee13265b69 100644 --- a/CodeEditTests/Features/Editor/UndoManagerRegistrationTests.swift +++ b/CodeEditTests/Features/Editor/UndoManagerRegistryTests.swift @@ -1,5 +1,5 @@ // -// UndoManagerRegistrationTests.swift +// UndoManagerRegistryTests.swift // CodeEditTests // // Created by Khan Winter on 7/3/25. @@ -14,8 +14,8 @@ import CodeEditTextView @MainActor @Suite -struct UndoManagerRegistrationTests { - let registrar = UndoManagerRegistration() +struct UndoManagerRegistryTests { + let registrar = UndoManagerRegistry() let file = CEWorkspaceFile(url: URL(filePath: "/fake/dir/file.txt")) let textView = TextView(string: "hello world") From 59194a8e33a07fcb97a20b2d11d59f2e17acfb94 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 18:36:41 +0200 Subject: [PATCH 301/335] Refactor: Group CEEditor by purpose instead of by kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models/, Views/ and UseCases/ told you what kind of type a file held, which is the one thing its declaration already says. Nine groups replace them, each named for what its files are about: Editor, Layout, FileViews, TabBar (with Tabs and Tab), JumpBar, Documents, Restoration, Theme and Adapters. Pure renames — git records 55 files changed, 0 insertions, 0 deletions. Swift ignores directory layout and SwiftPM takes the whole Sources/CEEditor tree, so no manifest or import changes were needed. Two placements worth noting. CEWorkspaceFile+Editor moves to TabBar/Tab/, beside the EditorTabRepresentable protocol it conforms to. UndoManagerRegistry moves to Documents/ rather than Restoration/, where it never belonged: it does no saving, and is a per-session registry keyed by file, sibling to DocumentRegistry. Environment keys are distributed to their subject rather than gathered into an Environment/ group, which would be grouping by kind again. --- .../CEEditor/{Models => Adapters}/AppActiveCursorState.swift | 0 .../CEEditor/{Models => Adapters}/AppActiveEditorState.swift | 0 .../CEEditor/{Models => Adapters}/AppFileEditorOverrides.swift | 0 .../CEEditor/{Models => Adapters}/Environment+ActiveEditor.swift | 0 .../{Models => Adapters}/Environment+WorkspaceFileProvider.swift | 0 .../{Models => Adapters}/Environment+WorkspaceNavigator.swift | 0 .../Sources/CEEditor/{Models => Documents}/DocumentRegistry.swift | 0 .../{Models/Restoration => Documents}/UndoManagerRegistry.swift | 0 .../Sources/CEEditor/{Models => }/Editor/Editor+History.swift | 0 .../Sources/CEEditor/{Models => }/Editor/Editor+TabSwitch.swift | 0 CodeEditModules/Sources/CEEditor/{Models => }/Editor/Editor.swift | 0 .../Sources/CEEditor/{Models => Editor}/EditorInstance.swift | 0 .../Sources/CEEditor/{Models => Editor}/EditorManager.swift | 0 .../Sources/CEEditor/{Views => FileViews}/AnyFileView.swift | 0 .../Sources/CEEditor/{Views => FileViews}/CodeFileView.swift | 0 .../CEEditor/{Views => FileViews}/EditorAreaFileView.swift | 0 .../{Views => FileViews}/Environment+LanguageServices.swift | 0 .../Sources/CEEditor/{Views => FileViews}/FilePreviewView.swift | 0 .../Sources/CEEditor/{Views => FileViews}/ImageFileView.swift | 0 .../Sources/CEEditor/{Views => FileViews}/LoadingFileView.swift | 0 .../Sources/CEEditor/{Views => FileViews}/NonTextFileView.swift | 0 .../Sources/CEEditor/{Views => FileViews}/PDFFileView.swift | 0 .../CEEditor/{Views => FileViews}/WindowCodeFileView.swift | 0 .../CEEditor/JumpBar/{Views => }/EditorJumpBarComponent.swift | 0 .../Sources/CEEditor/JumpBar/{Views => }/EditorJumpBarMenu.swift | 0 .../Sources/CEEditor/JumpBar/{Views => }/EditorJumpBarView.swift | 0 .../Sources/CEEditor/{Views => Layout}/EditorAreaView.swift | 0 .../CEEditor/{Models/EditorLayout => Layout}/EditorLayout.swift | 0 .../Sources/CEEditor/{Views => Layout}/EditorLayoutView.swift | 0 .../Sources/CEEditor/{ => Layout}/Environment+SplitEditor.swift | 0 CodeEditModules/Sources/CEEditor/{ => Layout}/SplitViewData.swift | 0 .../EditorLayout+StateRestoration.swift | 0 .../CEEditor/{UseCases => Restoration}/EditorRestorer.swift | 0 .../{Models => }/Restoration/EditorStateRestoration.swift | 0 .../Sources/CEEditor/TabBar/{Views => }/EditorHistoryMenus.swift | 0 .../CEEditor/TabBar/{Views => }/EditorTabBarAccessory.swift | 0 .../CEEditor/TabBar/{Views => }/EditorTabBarContextMenu.swift | 0 .../Sources/CEEditor/TabBar/{Views => }/EditorTabBarDivider.swift | 0 .../TabBar/{Views => }/EditorTabBarLeadingAccessories.swift | 0 .../TabBar/{Views => }/EditorTabBarTrailingAccessories.swift | 0 .../Sources/CEEditor/TabBar/{Views => }/EditorTabBarView.swift | 0 .../CEEditor/{ => TabBar/Tab}/CEWorkspaceFile+Editor.swift | 0 .../CEEditor/TabBar/{Tabs => }/Tab/EditorFileTabCloseButton.swift | 0 .../CEEditor/TabBar/{Tabs => }/Tab/EditorTabBackground.swift | 0 .../CEEditor/TabBar/{Tabs => }/Tab/EditorTabButtonStyle.swift | 0 .../CEEditor/TabBar/{Tabs => }/Tab/EditorTabCloseButton.swift | 0 .../TabBar/{Tabs/Tab/Models => Tab}/EditorTabFileObserver.swift | 0 .../TabBar/{Tabs/Tab/Models => Tab}/EditorTabRepresentable.swift | 0 .../Sources/CEEditor/TabBar/{Tabs => }/Tab/EditorTabView.swift | 0 .../TabBar/Tabs/{Views => }/EditorTabOnDropDelegate.swift | 0 .../CEEditor/TabBar/Tabs/{Views => }/EditorTabs+DragGesture.swift | 0 .../Sources/CEEditor/TabBar/Tabs/{Views => }/EditorTabs.swift | 0 .../TabBar/Tabs/{Views => }/EditorTabsOverflowShadow.swift | 0 .../Sources/CEEditor/{Models => Theme}/Theme+EditorTheme.swift | 0 .../Sources/CEEditor/{Models => Theme}/Theme+NSColor.swift | 0 55 files changed, 0 insertions(+), 0 deletions(-) rename CodeEditModules/Sources/CEEditor/{Models => Adapters}/AppActiveCursorState.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Adapters}/AppActiveEditorState.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Adapters}/AppFileEditorOverrides.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Adapters}/Environment+ActiveEditor.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Adapters}/Environment+WorkspaceFileProvider.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Adapters}/Environment+WorkspaceNavigator.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Documents}/DocumentRegistry.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models/Restoration => Documents}/UndoManagerRegistry.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => }/Editor/Editor+History.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => }/Editor/Editor+TabSwitch.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => }/Editor/Editor.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Editor}/EditorInstance.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Editor}/EditorManager.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/AnyFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/CodeFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/EditorAreaFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/Environment+LanguageServices.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/FilePreviewView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/ImageFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/LoadingFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/NonTextFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/PDFFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => FileViews}/WindowCodeFileView.swift (100%) rename CodeEditModules/Sources/CEEditor/JumpBar/{Views => }/EditorJumpBarComponent.swift (100%) rename CodeEditModules/Sources/CEEditor/JumpBar/{Views => }/EditorJumpBarMenu.swift (100%) rename CodeEditModules/Sources/CEEditor/JumpBar/{Views => }/EditorJumpBarView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => Layout}/EditorAreaView.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models/EditorLayout => Layout}/EditorLayout.swift (100%) rename CodeEditModules/Sources/CEEditor/{Views => Layout}/EditorLayoutView.swift (100%) rename CodeEditModules/Sources/CEEditor/{ => Layout}/Environment+SplitEditor.swift (100%) rename CodeEditModules/Sources/CEEditor/{ => Layout}/SplitViewData.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models/EditorLayout => Restoration}/EditorLayout+StateRestoration.swift (100%) rename CodeEditModules/Sources/CEEditor/{UseCases => Restoration}/EditorRestorer.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => }/Restoration/EditorStateRestoration.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorHistoryMenus.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorTabBarAccessory.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorTabBarContextMenu.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorTabBarDivider.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorTabBarLeadingAccessories.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorTabBarTrailingAccessories.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Views => }/EditorTabBarView.swift (100%) rename CodeEditModules/Sources/CEEditor/{ => TabBar/Tab}/CEWorkspaceFile+Editor.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs => }/Tab/EditorFileTabCloseButton.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs => }/Tab/EditorTabBackground.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs => }/Tab/EditorTabButtonStyle.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs => }/Tab/EditorTabCloseButton.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs/Tab/Models => Tab}/EditorTabFileObserver.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs/Tab/Models => Tab}/EditorTabRepresentable.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/{Tabs => }/Tab/EditorTabView.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/Tabs/{Views => }/EditorTabOnDropDelegate.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/Tabs/{Views => }/EditorTabs+DragGesture.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/Tabs/{Views => }/EditorTabs.swift (100%) rename CodeEditModules/Sources/CEEditor/TabBar/Tabs/{Views => }/EditorTabsOverflowShadow.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Theme}/Theme+EditorTheme.swift (100%) rename CodeEditModules/Sources/CEEditor/{Models => Theme}/Theme+NSColor.swift (100%) diff --git a/CodeEditModules/Sources/CEEditor/Models/AppActiveCursorState.swift b/CodeEditModules/Sources/CEEditor/Adapters/AppActiveCursorState.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/AppActiveCursorState.swift rename to CodeEditModules/Sources/CEEditor/Adapters/AppActiveCursorState.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/AppActiveEditorState.swift b/CodeEditModules/Sources/CEEditor/Adapters/AppActiveEditorState.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/AppActiveEditorState.swift rename to CodeEditModules/Sources/CEEditor/Adapters/AppActiveEditorState.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/AppFileEditorOverrides.swift b/CodeEditModules/Sources/CEEditor/Adapters/AppFileEditorOverrides.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/AppFileEditorOverrides.swift rename to CodeEditModules/Sources/CEEditor/Adapters/AppFileEditorOverrides.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Environment+ActiveEditor.swift b/CodeEditModules/Sources/CEEditor/Adapters/Environment+ActiveEditor.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Environment+ActiveEditor.swift rename to CodeEditModules/Sources/CEEditor/Adapters/Environment+ActiveEditor.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift b/CodeEditModules/Sources/CEEditor/Adapters/Environment+WorkspaceFileProvider.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceFileProvider.swift rename to CodeEditModules/Sources/CEEditor/Adapters/Environment+WorkspaceFileProvider.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift b/CodeEditModules/Sources/CEEditor/Adapters/Environment+WorkspaceNavigator.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Environment+WorkspaceNavigator.swift rename to CodeEditModules/Sources/CEEditor/Adapters/Environment+WorkspaceNavigator.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/DocumentRegistry.swift b/CodeEditModules/Sources/CEEditor/Documents/DocumentRegistry.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/DocumentRegistry.swift rename to CodeEditModules/Sources/CEEditor/Documents/DocumentRegistry.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistry.swift b/CodeEditModules/Sources/CEEditor/Documents/UndoManagerRegistry.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Restoration/UndoManagerRegistry.swift rename to CodeEditModules/Sources/CEEditor/Documents/UndoManagerRegistry.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Editor/Editor+History.swift b/CodeEditModules/Sources/CEEditor/Editor/Editor+History.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Editor/Editor+History.swift rename to CodeEditModules/Sources/CEEditor/Editor/Editor+History.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift b/CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Editor/Editor+TabSwitch.swift rename to CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Editor/Editor.swift b/CodeEditModules/Sources/CEEditor/Editor/Editor.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Editor/Editor.swift rename to CodeEditModules/Sources/CEEditor/Editor/Editor.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/EditorInstance.swift b/CodeEditModules/Sources/CEEditor/Editor/EditorInstance.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/EditorInstance.swift rename to CodeEditModules/Sources/CEEditor/Editor/EditorInstance.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/EditorManager.swift b/CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/EditorManager.swift rename to CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/AnyFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/AnyFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/AnyFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/AnyFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/CodeFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/CodeFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/CodeFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/EditorAreaFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/EditorAreaFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/EditorAreaFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/EditorAreaFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/Environment+LanguageServices.swift b/CodeEditModules/Sources/CEEditor/FileViews/Environment+LanguageServices.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/Environment+LanguageServices.swift rename to CodeEditModules/Sources/CEEditor/FileViews/Environment+LanguageServices.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift b/CodeEditModules/Sources/CEEditor/FileViews/FilePreviewView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/FilePreviewView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/FilePreviewView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/ImageFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/ImageFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/ImageFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/ImageFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/LoadingFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/LoadingFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/LoadingFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/LoadingFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/NonTextFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/NonTextFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/NonTextFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/NonTextFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/PDFFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/PDFFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/PDFFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/PDFFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift b/CodeEditModules/Sources/CEEditor/FileViews/WindowCodeFileView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/WindowCodeFileView.swift rename to CodeEditModules/Sources/CEEditor/FileViews/WindowCodeFileView.swift diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarComponent.swift rename to CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarMenu.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarMenu.swift rename to CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarMenu.swift diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/JumpBar/Views/EditorJumpBarView.swift rename to CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarView.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift b/CodeEditModules/Sources/CEEditor/Layout/EditorAreaView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/EditorAreaView.swift rename to CodeEditModules/Sources/CEEditor/Layout/EditorAreaView.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift b/CodeEditModules/Sources/CEEditor/Layout/EditorLayout.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout.swift rename to CodeEditModules/Sources/CEEditor/Layout/EditorLayout.swift diff --git a/CodeEditModules/Sources/CEEditor/Views/EditorLayoutView.swift b/CodeEditModules/Sources/CEEditor/Layout/EditorLayoutView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Views/EditorLayoutView.swift rename to CodeEditModules/Sources/CEEditor/Layout/EditorLayoutView.swift diff --git a/CodeEditModules/Sources/CEEditor/Environment+SplitEditor.swift b/CodeEditModules/Sources/CEEditor/Layout/Environment+SplitEditor.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Environment+SplitEditor.swift rename to CodeEditModules/Sources/CEEditor/Layout/Environment+SplitEditor.swift diff --git a/CodeEditModules/Sources/CEEditor/SplitViewData.swift b/CodeEditModules/Sources/CEEditor/Layout/SplitViewData.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/SplitViewData.swift rename to CodeEditModules/Sources/CEEditor/Layout/SplitViewData.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift b/CodeEditModules/Sources/CEEditor/Restoration/EditorLayout+StateRestoration.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/EditorLayout/EditorLayout+StateRestoration.swift rename to CodeEditModules/Sources/CEEditor/Restoration/EditorLayout+StateRestoration.swift diff --git a/CodeEditModules/Sources/CEEditor/UseCases/EditorRestorer.swift b/CodeEditModules/Sources/CEEditor/Restoration/EditorRestorer.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/UseCases/EditorRestorer.swift rename to CodeEditModules/Sources/CEEditor/Restoration/EditorRestorer.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift b/CodeEditModules/Sources/CEEditor/Restoration/EditorStateRestoration.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Restoration/EditorStateRestoration.swift rename to CodeEditModules/Sources/CEEditor/Restoration/EditorStateRestoration.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorHistoryMenus.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorHistoryMenus.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorHistoryMenus.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarAccessory.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarAccessory.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarAccessory.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarContextMenu.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarContextMenu.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarContextMenu.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarDivider.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarDivider.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarDivider.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarLeadingAccessories.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarLeadingAccessories.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarLeadingAccessories.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarTrailingAccessories.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarTrailingAccessories.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarTrailingAccessories.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Views/EditorTabBarView.swift rename to CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarView.swift diff --git a/CodeEditModules/Sources/CEEditor/CEWorkspaceFile+Editor.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/CEWorkspaceFile+Editor.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/CEWorkspaceFile+Editor.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/CEWorkspaceFile+Editor.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorFileTabCloseButton.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabBackground.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabBackground.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabBackground.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabButtonStyle.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabButtonStyle.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabButtonStyle.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabCloseButton.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabCloseButton.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabCloseButton.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabFileObserver.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabFileObserver.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabFileObserver.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabRepresentable.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/Models/EditorTabRepresentable.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabRepresentable.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabView.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Tab/EditorTabView.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorTabView.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabOnDropDelegate.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabOnDropDelegate.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabOnDropDelegate.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabs+DragGesture.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs+DragGesture.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabs+DragGesture.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabs.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabs.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabs.swift diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabsOverflowShadow.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/TabBar/Tabs/Views/EditorTabsOverflowShadow.swift rename to CodeEditModules/Sources/CEEditor/TabBar/Tabs/EditorTabsOverflowShadow.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift b/CodeEditModules/Sources/CEEditor/Theme/Theme+EditorTheme.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Theme+EditorTheme.swift rename to CodeEditModules/Sources/CEEditor/Theme/Theme+EditorTheme.swift diff --git a/CodeEditModules/Sources/CEEditor/Models/Theme+NSColor.swift b/CodeEditModules/Sources/CEEditor/Theme/Theme+NSColor.swift similarity index 100% rename from CodeEditModules/Sources/CEEditor/Models/Theme+NSColor.swift rename to CodeEditModules/Sources/CEEditor/Theme/Theme+NSColor.swift From f83a51482768d3872c10f56240b13a29d5705209 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 18:37:12 +0200 Subject: [PATCH 302/335] Docs: Record CEEditor as the worked example for package folder conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-measured rather than decremented: 10 kind-grouped folders remain across five targets, and CESourceControl/UseCases/ is the last carrying the retired name. Also records the two placements from CEEditor worth reusing — a conformance file belongs beside the protocol it satisfies, and environment keys go with their subject rather than into an Environment/ group, which would be grouping by kind again. --- docs/ARCHITECTURE.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8f4b2314a7..19d6591802 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -233,12 +233,20 @@ Grouping is **purpose-first**: - Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never by kind — the app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` folders. -- **The packages do not yet follow this.** `CEEditor`, `CENotifications`, `CESourceControl`, - `CETerminal`, `CodeEditSettings` and `CodeEditUI` still group by kind (17 such folders), and - `CEEditor/UseCases/` and - `CESourceControl/UseCases/` still carry the retired name even though the types inside were - renamed to doers (`EditorRestorer`, `RepositoryCloner`). Follow the convention in new code; - the existing folders are a pending cleanup, not a counter-precedent. +- **`CEEditor` is the worked example** (2026-08-16). Its `Models/`, `Views/` and `UseCases/` were + replaced by nine groups named for what their files are about — `Editor/`, `Layout/`, + `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, `Documents/`, `Restoration/`, + `Theme/`, `Adapters/`. Pure renames: 55 files, zero content changes, because Swift ignores + directory layout and SwiftPM takes the whole target tree. +- **Five targets still group by kind** — `CENotifications`, `CESourceControl`, `CETerminal`, + `CodeEditSettings` and `CodeEditUI`, 10 such folders — and `CESourceControl/UseCases/` is the + last one carrying the retired name, though the type inside was correctly renamed to a doer + (`RepositoryCloner`). Follow the convention in new code; those folders are a pending cleanup, + not a counter-precedent. +- **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol + it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and + environment keys are distributed to their subject rather than gathered into an `Environment/` + group — which would be grouping by kind again. - A feature with roughly ten files or fewer stays flat. - Shell/entry views and the feature's primary models sit at the feature root. - Single-consumer helpers live next to their consumer. From a9c430858b4b3f2bb01b8f8e6f090be94bad7424 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 19:56:02 +0200 Subject: [PATCH 303/335] Docs: Say which undo manager the document delegate can actually find MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit undoManager(forFile:) reads as though it finds the undo manager for a file. It finds the workspace one or nothing — a standalone single-file window keeps a private registry this cannot reach. Records why that is normally safe (every route to a standalone window tries openFileInWorkspace first, using the same predicate as workspace(containing:), so the two cannot disagree at open time), the ordering that defeats it (loose file first, parent folder opened as a workspace second), and why it is left alone. --- .../Adapters/AppCodeFileDocumentDelegate.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift index e236c98833..c6c09d4ecb 100644 --- a/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift +++ b/CodeEdit/WorkspaceWindow/Workspace/Adapters/AppCodeFileDocumentDelegate.swift @@ -44,6 +44,24 @@ final class AppCodeFileDocumentDelegate: CodeFileDocumentDelegate { self.languageServices = languageServices } + /// The **workspace** undo manager for `url`, or `nil` — never a standalone window's. + /// + /// `CodeFileDocument` calls this to register an external change to an open file as one undo + /// mutation. A file shown in a standalone single-file window has its own registry, private to + /// `WindowCodeFileView`, which this cannot reach: there is no workspace to look it up through. + /// + /// Normally the two cannot disagree, because every route to a standalone window + /// (`DocumentOpener`, `WorkspaceWindowManager`'s new-file path, `AppDelegate`'s open handler) + /// tries `openFileInWorkspace(url:)` first and only falls through when it fails — and that check + /// uses the same predicate as `workspace(containing:)`. So if a workspace holds the file there + /// is no standalone window, and if none does this returns `nil` and nothing is registered. + /// + /// The guard is evaluated once, though. Open a loose file, *then* open its parent folder as a + /// workspace, and the standalone window keeps its private registry while this starts finding + /// the workspace's. An external change then registers onto a stack that window does not read. + /// Editing undo inside the window is unaffected. Left as-is deliberately: migrating a window + /// onto a workspace registry, or letting this search every registry, is more machinery than a + /// three-step ordering edge case earns. func undoManager(forFile url: URL) -> CEUndoManager? { windowManager.workspace(containing: url)?.undoRegistry.managerIfExists(forFile: url) } From 214a0a1ef32db8af4604ad0329cc783d39c404f2 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 20:19:14 +0200 Subject: [PATCH 304/335] Refactor: Group CESourceControl by purpose instead of by kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Views/ was a thirteen-file grab-bag holding three unrelated things: it becomes Operations/ (the eight fetch/pull/push/stash/switch/branch sheets) and Branches/ (the pickers, plus the two Formatter subclasses, whose only consumers are two of those sheets — a Formatters/ group would be kind-grouping two files). UseCases/RepositoryCloner joins Clone/ beside the view it serves, which is the last UseCases/ folder in the package; Clone/ViewModels/ flattens, since five files do not need a subfolder. AccountsSettings, SourceControlSettings and SourceControlAccount move to Settings/ — the account struct is the payload of AccountsSettings.gitAccounts, not a free-standing model — leaving SourceControlManager and its extensions at the root where primary models go. Pure renames: 19 files, zero content changes. Accounts/ is deliberately untouched. It is 58 of the target's 133 files and has three call sites in the entire codebase, with BitBucket unreferenced outside its own subtree — restructuring it before settling what is dead would be moving files that may not survive. --- .../CESourceControl/{Views => Branches}/GitBranchesGroup.swift | 0 .../CESourceControl/{Views => Branches}/RegexFormatter.swift | 0 .../CESourceControl/{Views => Branches}/RemoteBranchPicker.swift | 0 .../CESourceControl/{Views => Branches}/ToolbarBranchPicker.swift | 0 .../{Views => Branches}/TrimWhitespaceFormatter.swift | 0 .../Clone/{ViewModels => }/GitCheckoutBranchViewModel.swift | 0 .../Clone/{ViewModels => }/GitCloneViewModel.swift | 0 .../CESourceControl/{UseCases => Clone}/RepositoryCloner.swift | 0 .../SourceControlAddExistingRemoteView.swift | 0 .../{Views => Operations}/SourceControlFetchView.swift | 0 .../{Views => Operations}/SourceControlNewBranchView.swift | 0 .../{Views => Operations}/SourceControlPullView.swift | 0 .../{Views => Operations}/SourceControlPushView.swift | 0 .../{Views => Operations}/SourceControlRenameBranchView.swift | 0 .../{Views => Operations}/SourceControlStashView.swift | 0 .../{Views => Operations}/SourceControlSwitchView.swift | 0 .../Sources/CESourceControl/{ => Settings}/AccountsSettings.swift | 0 .../CESourceControl/{ => Settings}/SourceControlAccount.swift | 0 .../CESourceControl/{ => Settings}/SourceControlSettings.swift | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename CodeEditModules/Sources/CESourceControl/{Views => Branches}/GitBranchesGroup.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Branches}/RegexFormatter.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Branches}/RemoteBranchPicker.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Branches}/ToolbarBranchPicker.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Branches}/TrimWhitespaceFormatter.swift (100%) rename CodeEditModules/Sources/CESourceControl/Clone/{ViewModels => }/GitCheckoutBranchViewModel.swift (100%) rename CodeEditModules/Sources/CESourceControl/Clone/{ViewModels => }/GitCloneViewModel.swift (100%) rename CodeEditModules/Sources/CESourceControl/{UseCases => Clone}/RepositoryCloner.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlAddExistingRemoteView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlFetchView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlNewBranchView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlPullView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlPushView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlRenameBranchView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlStashView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{Views => Operations}/SourceControlSwitchView.swift (100%) rename CodeEditModules/Sources/CESourceControl/{ => Settings}/AccountsSettings.swift (100%) rename CodeEditModules/Sources/CESourceControl/{ => Settings}/SourceControlAccount.swift (100%) rename CodeEditModules/Sources/CESourceControl/{ => Settings}/SourceControlSettings.swift (100%) diff --git a/CodeEditModules/Sources/CESourceControl/Views/GitBranchesGroup.swift b/CodeEditModules/Sources/CESourceControl/Branches/GitBranchesGroup.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/GitBranchesGroup.swift rename to CodeEditModules/Sources/CESourceControl/Branches/GitBranchesGroup.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/RegexFormatter.swift b/CodeEditModules/Sources/CESourceControl/Branches/RegexFormatter.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/RegexFormatter.swift rename to CodeEditModules/Sources/CESourceControl/Branches/RegexFormatter.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/RemoteBranchPicker.swift b/CodeEditModules/Sources/CESourceControl/Branches/RemoteBranchPicker.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/RemoteBranchPicker.swift rename to CodeEditModules/Sources/CESourceControl/Branches/RemoteBranchPicker.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift b/CodeEditModules/Sources/CESourceControl/Branches/ToolbarBranchPicker.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/ToolbarBranchPicker.swift rename to CodeEditModules/Sources/CESourceControl/Branches/ToolbarBranchPicker.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift b/CodeEditModules/Sources/CESourceControl/Branches/TrimWhitespaceFormatter.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/TrimWhitespaceFormatter.swift rename to CodeEditModules/Sources/CESourceControl/Branches/TrimWhitespaceFormatter.swift diff --git a/CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift b/CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchViewModel.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCheckoutBranchViewModel.swift rename to CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchViewModel.swift diff --git a/CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift b/CodeEditModules/Sources/CESourceControl/Clone/GitCloneViewModel.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Clone/ViewModels/GitCloneViewModel.swift rename to CodeEditModules/Sources/CESourceControl/Clone/GitCloneViewModel.swift diff --git a/CodeEditModules/Sources/CESourceControl/UseCases/RepositoryCloner.swift b/CodeEditModules/Sources/CESourceControl/Clone/RepositoryCloner.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/UseCases/RepositoryCloner.swift rename to CodeEditModules/Sources/CESourceControl/Clone/RepositoryCloner.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlAddExistingRemoteView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlAddExistingRemoteView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlAddExistingRemoteView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlFetchView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlFetchView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlFetchView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlFetchView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlNewBranchView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlNewBranchView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlNewBranchView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlNewBranchView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlPullView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlPullView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlPullView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlPullView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlPushView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlPushView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlPushView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlPushView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlRenameBranchView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlRenameBranchView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlRenameBranchView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlStashView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlStashView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlStashView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlStashView.swift diff --git a/CodeEditModules/Sources/CESourceControl/Views/SourceControlSwitchView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlSwitchView.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/Views/SourceControlSwitchView.swift rename to CodeEditModules/Sources/CESourceControl/Operations/SourceControlSwitchView.swift diff --git a/CodeEditModules/Sources/CESourceControl/AccountsSettings.swift b/CodeEditModules/Sources/CESourceControl/Settings/AccountsSettings.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/AccountsSettings.swift rename to CodeEditModules/Sources/CESourceControl/Settings/AccountsSettings.swift diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlAccount.swift b/CodeEditModules/Sources/CESourceControl/Settings/SourceControlAccount.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/SourceControlAccount.swift rename to CodeEditModules/Sources/CESourceControl/Settings/SourceControlAccount.swift diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlSettings.swift b/CodeEditModules/Sources/CESourceControl/Settings/SourceControlSettings.swift similarity index 100% rename from CodeEditModules/Sources/CESourceControl/SourceControlSettings.swift rename to CodeEditModules/Sources/CESourceControl/Settings/SourceControlSettings.swift From f0d59acc528b7d9cdbba50bb01656c3e69a4824a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 20:19:54 +0200 Subject: [PATCH 305/335] Docs: Record CESourceControl's regrouping and correct the folder count The previous count of 10 was measured with a pattern matching 'Models' but not singular 'Model', missing three folders in CELSP, CESearch and CETerminal. The real figure is 13, and UseCases/ is now gone from every package. Also records why CESourceControl/Accounts/ is excluded from the cleanup rather than pending it: 58 of that target's 133 files, three call sites in the whole codebase, BitBucket unreferenced outside its own subtree. Restructuring it before settling what is dead would move files that may not survive. --- docs/ARCHITECTURE.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 19d6591802..94807e9b7a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -233,16 +233,21 @@ Grouping is **purpose-first**: - Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never by kind — the app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` folders. -- **`CEEditor` is the worked example** (2026-08-16). Its `Models/`, `Views/` and `UseCases/` were - replaced by nine groups named for what their files are about — `Editor/`, `Layout/`, - `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, `Documents/`, `Restoration/`, - `Theme/`, `Adapters/`. Pure renames: 55 files, zero content changes, because Swift ignores - directory layout and SwiftPM takes the whole target tree. -- **Five targets still group by kind** — `CENotifications`, `CESourceControl`, `CETerminal`, - `CodeEditSettings` and `CodeEditUI`, 10 such folders — and `CESourceControl/UseCases/` is the - last one carrying the retired name, though the type inside was correctly renamed to a doer - (`RepositoryCloner`). Follow the convention in new code; those folders are a pending cleanup, - not a counter-precedent. +- **`CEEditor` and `CESourceControl` are the worked examples** (2026-08-16). `CEEditor`'s + `Models/`, `Views/` and `UseCases/` became nine groups named for what their files are about — + `Editor/`, `Layout/`, `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, + `Documents/`, `Restoration/`, `Theme/`, `Adapters/`. `CESourceControl`'s `Views/` grab-bag split + into `Operations/` and `Branches/`, its cloner joined `Clone/`, and its settings types moved to + `Settings/`. Both were pure renames — 55 and 19 files, zero content changes — because Swift + ignores directory layout and SwiftPM takes the whole target tree. +- **`UseCases/` is now gone from every package.** The type-level rename to doers + (`EditorRestorer`, `RepositoryCloner`) had stopped at the folder level; it no longer does. +- **13 kind-grouped folders remain**, in `CELSP`, `CENotifications`, `CESearch`, `CETerminal`, + `CodeEditSettings`, `CodeEditUI`, and inside `CESourceControl/Accounts/`. Follow the convention + in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` + is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 + files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own + subtree. - **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and environment keys are distributed to their subject rather than gathered into an `Environment/` From ffdce3aa6553f49739f0644eed29d6147a5ba68c Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 21:17:45 +0200 Subject: [PATCH 306/335] Refactor: Group CENotifications by purpose instead of by kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four kind-folders held 1, 1, 4 and 3 files. Models/ and Protocols/ were a folder per file, and NotificationManaging — the feature's contract, and at 124 lines larger than the manager implementing it — was filed under a kind rather than sitting beside what it describes. Panel/ takes the view model, its three extensions and all three views: they are one cluster, not three, because every view observes NotificationPanelViewModel, and NotificationBannerView is used only inside NotificationPanelView. No Manager/ group: with six files left at the root, another folder would be structure for its own sake, and the convention keeps a feature this size flat. Pure renames: 9 files, zero content changes. --- .../Sources/CENotifications/{Models => }/CENotification.swift | 0 .../CENotifications/{Protocols => }/NotificationManaging.swift | 0 .../CENotifications/{Views => Panel}/NotificationBannerView.swift | 0 .../CENotifications/{Views => Panel}/NotificationPanelView.swift | 0 .../NotificationPanelViewModel+NotificationHandling.swift | 0 .../NotificationPanelViewModel+TimerManagement.swift | 0 .../NotificationPanelViewModel+Visibility.swift | 0 .../{ViewModels => Panel}/NotificationPanelViewModel.swift | 0 .../{Views => Panel}/NotificationToolbarItem.swift | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename CodeEditModules/Sources/CENotifications/{Models => }/CENotification.swift (100%) rename CodeEditModules/Sources/CENotifications/{Protocols => }/NotificationManaging.swift (100%) rename CodeEditModules/Sources/CENotifications/{Views => Panel}/NotificationBannerView.swift (100%) rename CodeEditModules/Sources/CENotifications/{Views => Panel}/NotificationPanelView.swift (100%) rename CodeEditModules/Sources/CENotifications/{ViewModels => Panel}/NotificationPanelViewModel+NotificationHandling.swift (100%) rename CodeEditModules/Sources/CENotifications/{ViewModels => Panel}/NotificationPanelViewModel+TimerManagement.swift (100%) rename CodeEditModules/Sources/CENotifications/{ViewModels => Panel}/NotificationPanelViewModel+Visibility.swift (100%) rename CodeEditModules/Sources/CENotifications/{ViewModels => Panel}/NotificationPanelViewModel.swift (100%) rename CodeEditModules/Sources/CENotifications/{Views => Panel}/NotificationToolbarItem.swift (100%) diff --git a/CodeEditModules/Sources/CENotifications/Models/CENotification.swift b/CodeEditModules/Sources/CENotifications/CENotification.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/Models/CENotification.swift rename to CodeEditModules/Sources/CENotifications/CENotification.swift diff --git a/CodeEditModules/Sources/CENotifications/Protocols/NotificationManaging.swift b/CodeEditModules/Sources/CENotifications/NotificationManaging.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/Protocols/NotificationManaging.swift rename to CodeEditModules/Sources/CENotifications/NotificationManaging.swift diff --git a/CodeEditModules/Sources/CENotifications/Views/NotificationBannerView.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationBannerView.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/Views/NotificationBannerView.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationBannerView.swift diff --git a/CodeEditModules/Sources/CENotifications/Views/NotificationPanelView.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationPanelView.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/Views/NotificationPanelView.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationPanelView.swift diff --git a/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel+NotificationHandling.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+NotificationHandling.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel+NotificationHandling.swift diff --git a/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel+TimerManagement.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+TimerManagement.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel+TimerManagement.swift diff --git a/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel+Visibility.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel+Visibility.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel+Visibility.swift diff --git a/CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/ViewModels/NotificationPanelViewModel.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationPanelViewModel.swift diff --git a/CodeEditModules/Sources/CENotifications/Views/NotificationToolbarItem.swift b/CodeEditModules/Sources/CENotifications/Panel/NotificationToolbarItem.swift similarity index 100% rename from CodeEditModules/Sources/CENotifications/Views/NotificationToolbarItem.swift rename to CodeEditModules/Sources/CENotifications/Panel/NotificationToolbarItem.swift From a97251861ccfc74691cc023f3047457358bc6e1a Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 21:18:47 +0200 Subject: [PATCH 307/335] Docs: Record CENotifications, and make the folder count reproducible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The count has now been wrong four times running, each because the find pattern was narrower than the claim it supported: it matched Models but not Model, then not Protocols, then not Extensions or singular Service. The real figure is 15. The guide now carries the exact command rather than a number alone, so the next person measures the same thing rather than a smaller one. Extensions/ is called out explicitly — a folder of 'things that are extensions' says nothing about what they extend, and it appears in CodeEditCore, CESearch and CETerminal. --- docs/ARCHITECTURE.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 94807e9b7a..0c06a9fb66 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -242,12 +242,29 @@ Grouping is **purpose-first**: ignores directory layout and SwiftPM takes the whole target tree. - **`UseCases/` is now gone from every package.** The type-level rename to doers (`EditorRestorer`, `RepositoryCloner`) had stopped at the folder level; it no longer does. -- **13 kind-grouped folders remain**, in `CELSP`, `CENotifications`, `CESearch`, `CETerminal`, - `CodeEditSettings`, `CodeEditUI`, and inside `CESourceControl/Accounts/`. Follow the convention +- **`CENotifications` followed** (13 files): `Models/`, `Protocols/`, `ViewModels/` and `Views/` + held 1, 1, 4 and 3 files; a single `Panel/` group now holds the view model and every view that + observes it, and the rest sits flat at the root. +- **15 kind-grouped folders remain**, in `CodeEditCore`, `CodeEditSettings`, `CodeEditUI`, + `CELSP`, `CESearch`, `CETerminal`, and inside `CESourceControl/Accounts/`. Follow the convention in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own subtree. + + Note `Extensions/` counts too (`CodeEditCore`, `CESearch`, `CETerminal`): a folder of "things + that are extensions" says nothing about what they extend. + + Measure with this exact pattern, and widen it rather than trusting a smaller number — four + successive counts here were wrong because the pattern matched `Models` but not `Model`, then not + `Protocols`, then not `Extensions` or singular `Service`: + + ```bash + find CodeEditModules/Sources -type d \ + \( -name Model -o -name Models -o -name View -o -name Views -o -name ViewModel \ + -o -name ViewModels -o -name Service -o -name Services -o -name Protocol \ + -o -name Protocols -o -name UseCase -o -name UseCases -o -name Extensions \) | wc -l + ``` - **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and environment keys are distributed to their subject rather than gathered into an `Environment/` From cf54dbaa8f6eae56da94a02ff5164e6f8c030647 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 21:27:24 +0200 Subject: [PATCH 308/335] Refactor: Give CodeEditCore's extensions a subject, and drop a duplicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extensions/ was the one kind-folder here, and it turned out not to be a grab-bag: six of its eight files share a subject. Paths/ takes the four URL helpers, String+ValidFileName, and String+Escaped — whose escapedDirectory and escapedWhiteSpaces exist to make paths safe as shell arguments, which is why GitClient, GitConfigClient and CETask are its consumers. The remaining two are genuinely unrelated and sit at the root: Collection+subscript_safe and String+SafeOffset. Two files at a root is not a folder waiting to happen; a third would be a signal to find the subject they share, not to re-create Extensions/. Event and EventBus join the Events/ folder they sat beside, so it holds the whole mechanism rather than only the facts. FindReplaceQuery moves to Domain/: it is a query model shared by CEEditor and CESearch, not a seam. Domain/ and Infrastructure/ stay. Normally a layer split is kind-grouping, but here the layer is the purpose — the guide describes this target as domain types plus the event bus and command interfaces, and GitBranch and WorkspaceNavigator are genuinely different kinds of thing. Also deletes a private subscript(safe:) added to WorkspacePanelTabBar during the Tahoe merge. Core already had a public one; that file simply did not import CodeEditCore, so nothing objected. --- .../WorkspacePanel/WorkspacePanelTabBar.swift | 9 +-------- .../{Extensions => }/Collection+subscript_safe.swift | 0 .../FindReplace}/FindReplaceQuery.swift | 0 .../CodeEditCore/Infrastructure/{ => Events}/Event.swift | 0 .../Infrastructure/{ => Events}/EventBus.swift | 0 .../{Extensions => Paths}/String+Escaped.swift | 0 .../{Extensions => Paths}/String+ValidFileName.swift | 0 .../{Extensions => Paths}/URL+AbsolutePath.swift | 0 .../{Extensions => Paths}/URL+ContainsSubPath.swift | 0 .../{Extensions => Paths}/URL+FileName.swift | 0 .../{Extensions => Paths}/URL+ResourceValues.swift | 0 .../{Extensions => }/String+SafeOffset.swift | 0 12 files changed, 1 insertion(+), 8 deletions(-) rename CodeEditModules/Sources/CodeEditCore/{Extensions => }/Collection+subscript_safe.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Infrastructure => Domain/FindReplace}/FindReplaceQuery.swift (100%) rename CodeEditModules/Sources/CodeEditCore/Infrastructure/{ => Events}/Event.swift (100%) rename CodeEditModules/Sources/CodeEditCore/Infrastructure/{ => Events}/EventBus.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => Paths}/String+Escaped.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => Paths}/String+ValidFileName.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => Paths}/URL+AbsolutePath.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => Paths}/URL+ContainsSubPath.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => Paths}/URL+FileName.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => Paths}/URL+ResourceValues.swift (100%) rename CodeEditModules/Sources/CodeEditCore/{Extensions => }/String+SafeOffset.swift (100%) diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift index dbe645e6d4..2996a76dda 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditCore import CodeEditSettings import CodeEditUI @@ -325,11 +326,3 @@ private extension WorkspacePanelTabBar { } } } - -/// Bounds-checked lookup, for the Tahoe tab bar's peek at the following tab when deciding whether to -/// draw a divider. -private extension Collection { - subscript(safe index: Index) -> Element? { - indices.contains(index) ? self[index] : nil - } -} diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift b/CodeEditModules/Sources/CodeEditCore/Collection+subscript_safe.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/Collection+subscript_safe.swift rename to CodeEditModules/Sources/CodeEditCore/Collection+subscript_safe.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift b/CodeEditModules/Sources/CodeEditCore/Domain/FindReplace/FindReplaceQuery.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Infrastructure/FindReplaceQuery.swift rename to CodeEditModules/Sources/CodeEditCore/Domain/FindReplace/FindReplaceQuery.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Infrastructure/Event.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/Event.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Infrastructure/Event.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/Event.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Infrastructure/EventBus.swift b/CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/EventBus.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Infrastructure/EventBus.swift rename to CodeEditModules/Sources/CodeEditCore/Infrastructure/Events/EventBus.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/String+Escaped.swift b/CodeEditModules/Sources/CodeEditCore/Paths/String+Escaped.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/String+Escaped.swift rename to CodeEditModules/Sources/CodeEditCore/Paths/String+Escaped.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/String+ValidFileName.swift b/CodeEditModules/Sources/CodeEditCore/Paths/String+ValidFileName.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/String+ValidFileName.swift rename to CodeEditModules/Sources/CodeEditCore/Paths/String+ValidFileName.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift b/CodeEditModules/Sources/CodeEditCore/Paths/URL+AbsolutePath.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/URL+AbsolutePath.swift rename to CodeEditModules/Sources/CodeEditCore/Paths/URL+AbsolutePath.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift b/CodeEditModules/Sources/CodeEditCore/Paths/URL+ContainsSubPath.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/URL+ContainsSubPath.swift rename to CodeEditModules/Sources/CodeEditCore/Paths/URL+ContainsSubPath.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/URL+FileName.swift b/CodeEditModules/Sources/CodeEditCore/Paths/URL+FileName.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/URL+FileName.swift rename to CodeEditModules/Sources/CodeEditCore/Paths/URL+FileName.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift b/CodeEditModules/Sources/CodeEditCore/Paths/URL+ResourceValues.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/URL+ResourceValues.swift rename to CodeEditModules/Sources/CodeEditCore/Paths/URL+ResourceValues.swift diff --git a/CodeEditModules/Sources/CodeEditCore/Extensions/String+SafeOffset.swift b/CodeEditModules/Sources/CodeEditCore/String+SafeOffset.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditCore/Extensions/String+SafeOffset.swift rename to CodeEditModules/Sources/CodeEditCore/String+SafeOffset.swift From b06af2360f56f3e70a8a6fe13deb35f8a55f0253 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 21:27:49 +0200 Subject: [PATCH 309/335] Docs: Record CodeEditCore's regrouping and why its layer split stays Four targets now follow the convention. CodeEditCore is the exception worth stating rather than leaving for someone to discover: Domain/ and Infrastructure/ look like the kind-grouping this rule forbids, but in that target the layer is the purpose, and the guide already describes it that way. --- docs/ARCHITECTURE.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0c06a9fb66..c91e273184 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -245,8 +245,18 @@ Grouping is **purpose-first**: - **`CENotifications` followed** (13 files): `Models/`, `Protocols/`, `ViewModels/` and `Views/` held 1, 1, 4 and 3 files; a single `Panel/` group now holds the view model and every view that observes it, and the rest sits flat at the root. -- **15 kind-grouped folders remain**, in `CodeEditCore`, `CodeEditSettings`, `CodeEditUI`, - `CELSP`, `CESearch`, `CETerminal`, and inside `CESourceControl/Accounts/`. Follow the convention +- **`CodeEditCore` followed**, with one deliberate exception. Its `Extensions/` became `Paths/` + (the four `URL` helpers, `String+ValidFileName`, and `String+Escaped` — whose escaping exists to + make paths safe as shell arguments), with the two genuinely unrelated helpers at the target root. + `Event`/`EventBus` joined `Events/`, and `FindReplaceQuery` moved to `Domain/`, being a query + model shared by `CEEditor` and `CESearch` rather than a seam. + + **`Domain/` and `Infrastructure/` stay.** A layer split is normally kind-grouping, but in this + target the layer *is* the purpose: `GitBranch` is a fact features share, `WorkspaceNavigator` is + a seam they talk through, and this guide already describes the target in exactly those terms. + Do not "fix" this one. +- **14 kind-grouped folders remain**, in `CodeEditSettings`, `CodeEditUI`, `CELSP`, `CESearch`, + `CETerminal`, and inside `CESourceControl/Accounts/`. Follow the convention in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own From c25dd305de74a1eadee2a2ed67424c08a2ea5a80 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 21:48:10 +0200 Subject: [PATCH 310/335] Refactor: Group CETerminal by purpose instead of by kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model/, Views/, Extensions/ and Tasks/Models/ dissolve. Shell/ is the one grouping that earns a folder: Shell, ShellIntegration and CurrentUser reference each other and serve the terminal views — shell configuration is a different subject from terminal rendering. The five view files stay together deliberately. CEActiveTaskTerminalView, CELocalShellTerminalView and CETerminalView are a three-level inheritance chain onto SwiftTerm's TerminalView, and TerminalEmulatorView is the representable wrapping them; splitting them would separate a base class from its subclasses. The two SwiftTerm adapters share a real purpose but two files do not earn a folder. Pure renames: 13 files, zero content changes. Unchanged by this, and still on the list: Tasks/ living inside CETerminal at all is a deferred design question, and TerminalCache remains a process-global cache holding workspace-window-scoped views. --- .../Sources/CETerminal/Tasks/{Models => }/CEActiveTask.swift | 0 .../Sources/CETerminal/Tasks/{Models => }/CETaskStatus.swift | 0 .../TerminalEmulator/{Views => }/CEActiveTaskTerminalView.swift | 0 .../TerminalEmulator/{Views => }/CELocalShellTerminalView.swift | 0 .../CETerminal/TerminalEmulator/{Views => }/CETerminalView.swift | 0 .../TerminalEmulator/{Extensions => }/LocalProcess+sendText.swift | 0 .../TerminalEmulator/{Model => Shell}/CurrentUser.swift | 0 .../CETerminal/TerminalEmulator/{Model => Shell}/Shell.swift | 0 .../TerminalEmulator/{Model => Shell}/ShellIntegration.swift | 0 .../TerminalEmulator/{Extensions => }/SwiftTerm+Color+Init.swift | 0 .../CETerminal/TerminalEmulator/{Model => }/TerminalCache.swift | 0 .../{Views => }/TerminalEmulatorView+Coordinator.swift | 0 .../TerminalEmulator/{Views => }/TerminalEmulatorView.swift | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename CodeEditModules/Sources/CETerminal/Tasks/{Models => }/CEActiveTask.swift (100%) rename CodeEditModules/Sources/CETerminal/Tasks/{Models => }/CETaskStatus.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Views => }/CEActiveTaskTerminalView.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Views => }/CELocalShellTerminalView.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Views => }/CETerminalView.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Extensions => }/LocalProcess+sendText.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Model => Shell}/CurrentUser.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Model => Shell}/Shell.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Model => Shell}/ShellIntegration.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Extensions => }/SwiftTerm+Color+Init.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Model => }/TerminalCache.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Views => }/TerminalEmulatorView+Coordinator.swift (100%) rename CodeEditModules/Sources/CETerminal/TerminalEmulator/{Views => }/TerminalEmulatorView.swift (100%) diff --git a/CodeEditModules/Sources/CETerminal/Tasks/Models/CEActiveTask.swift b/CodeEditModules/Sources/CETerminal/Tasks/CEActiveTask.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/Tasks/Models/CEActiveTask.swift rename to CodeEditModules/Sources/CETerminal/Tasks/CEActiveTask.swift diff --git a/CodeEditModules/Sources/CETerminal/Tasks/Models/CETaskStatus.swift b/CodeEditModules/Sources/CETerminal/Tasks/CETaskStatus.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/Tasks/Models/CETaskStatus.swift rename to CodeEditModules/Sources/CETerminal/Tasks/CETaskStatus.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/CEActiveTaskTerminalView.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CEActiveTaskTerminalView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/CEActiveTaskTerminalView.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/CELocalShellTerminalView.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CELocalShellTerminalView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/CELocalShellTerminalView.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/CETerminalView.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/CETerminalView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/CETerminalView.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/LocalProcess+sendText.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/LocalProcess+sendText.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/LocalProcess+sendText.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/CurrentUser.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/CurrentUser.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/CurrentUser.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/Shell.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/Shell.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/Shell.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/Shell.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/ShellIntegration.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/ShellIntegration.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/ShellIntegration.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/SwiftTerm+Color+Init.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Extensions/SwiftTerm+Color+Init.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/SwiftTerm+Color+Init.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/TerminalCache.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Model/TerminalCache.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/TerminalCache.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/TerminalEmulatorView+Coordinator.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView+Coordinator.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/TerminalEmulatorView+Coordinator.swift diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/TerminalEmulatorView.swift similarity index 100% rename from CodeEditModules/Sources/CETerminal/TerminalEmulator/Views/TerminalEmulatorView.swift rename to CodeEditModules/Sources/CETerminal/TerminalEmulator/TerminalEmulatorView.swift From e106623b89875c1e9ae2edc42430199cf9fc35a9 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 21:48:31 +0200 Subject: [PATCH 311/335] Docs: Record CETerminal's regrouping Five targets follow the convention now. Ten folders remain, three of them in the excluded CESourceControl/Accounts/. --- docs/ARCHITECTURE.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c91e273184..cac1daaf5b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -255,8 +255,11 @@ Grouping is **purpose-first**: target the layer *is* the purpose: `GitBranch` is a fact features share, `WorkspaceNavigator` is a seam they talk through, and this guide already describes the target in exactly those terms. Do not "fix" this one. -- **14 kind-grouped folders remain**, in `CodeEditSettings`, `CodeEditUI`, `CELSP`, `CESearch`, - `CETerminal`, and inside `CESourceControl/Accounts/`. Follow the convention +- **`CETerminal` followed**: `Shell/` (configuration) is the one subgroup that earned a folder, + while the three-level `CETerminalView` inheritance chain and its representable stay together — + splitting them would separate a base class from its subclasses. +- **10 kind-grouped folders remain**, in `CodeEditSettings`, `CodeEditUI`, `CELSP`, `CESearch`, + and inside `CESourceControl/Accounts/` — three of the ten being the excluded `Accounts/`. Follow the convention in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own From b903dc177803e5a605fae7d0c6ccfe3ad1dacde0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 22:03:12 +0200 Subject: [PATCH 312/335] Refactor: Group CELSP by purpose instead of by kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Utils/ was doing two jobs. SemanticToken+Position and TextView+SemanticTokenRangeProvider join Features/SemanticTokens/ beside the code using them; the other three cross the protocol boundary — URL+LSPURI, TextView+LSPRange, LanguageIdentifier+CodeLanguage — and become Conversions/. Registry/ was inconsistent with itself: PackageManagerProtocol sat at its root while RegistryManaging sat in Protocols/. Both are now at the root, along with the five Model/ types, beside RegistryManager and the template parser. LSPUtil.swift declared LSPCompletionItemsUtil and is renamed to match. 'Util' is the same non-name as 'Utils' — the type at least says what it is for. Service/, LanguageServer/ and Features/ stay: the first two are named after their primary type, and Features/ genuinely means the editor features LSP powers, with SemanticTokens/ and DocumentSync/ carrying the meaning. --- .../LanguageIdentifier+CodeLanguage.swift | 0 .../CELSP/{Utils => Conversions}/TextView+LSPRange.swift | 0 .../Sources/CELSP/{Utils => Conversions}/URL+LSPURI.swift | 0 .../SemanticTokens}/SemanticToken+Position.swift | 0 .../SemanticTokens}/TextView+SemanticTokenRangeProvider.swift | 0 .../CELSP/{LSPUtil.swift => LSPCompletionItemsUtil.swift} | 2 +- .../{Model => }/InstallationMethod+PackageManager.swift | 0 .../Sources/CELSP/Registry/{Model => }/InstallationMethod.swift | 0 .../Sources/CELSP/Registry/{Model => }/PackageManagerType.swift | 0 .../Sources/CELSP/Registry/{Model => }/PackageSource.swift | 0 .../CELSP/Registry/{Model => }/RegistryItem+InstallMethod.swift | 0 .../CELSP/Registry/{Protocols => }/RegistryManaging.swift | 0 12 files changed, 1 insertion(+), 1 deletion(-) rename CodeEditModules/Sources/CELSP/{Utils => Conversions}/LanguageIdentifier+CodeLanguage.swift (100%) rename CodeEditModules/Sources/CELSP/{Utils => Conversions}/TextView+LSPRange.swift (100%) rename CodeEditModules/Sources/CELSP/{Utils => Conversions}/URL+LSPURI.swift (100%) rename CodeEditModules/Sources/CELSP/{Utils => Features/SemanticTokens}/SemanticToken+Position.swift (100%) rename CodeEditModules/Sources/CELSP/{Utils => Features/SemanticTokens}/TextView+SemanticTokenRangeProvider.swift (100%) rename CodeEditModules/Sources/CELSP/{LSPUtil.swift => LSPCompletionItemsUtil.swift} (98%) rename CodeEditModules/Sources/CELSP/Registry/{Model => }/InstallationMethod+PackageManager.swift (100%) rename CodeEditModules/Sources/CELSP/Registry/{Model => }/InstallationMethod.swift (100%) rename CodeEditModules/Sources/CELSP/Registry/{Model => }/PackageManagerType.swift (100%) rename CodeEditModules/Sources/CELSP/Registry/{Model => }/PackageSource.swift (100%) rename CodeEditModules/Sources/CELSP/Registry/{Model => }/RegistryItem+InstallMethod.swift (100%) rename CodeEditModules/Sources/CELSP/Registry/{Protocols => }/RegistryManaging.swift (100%) diff --git a/CodeEditModules/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift b/CodeEditModules/Sources/CELSP/Conversions/LanguageIdentifier+CodeLanguage.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Utils/LanguageIdentifier+CodeLanguage.swift rename to CodeEditModules/Sources/CELSP/Conversions/LanguageIdentifier+CodeLanguage.swift diff --git a/CodeEditModules/Sources/CELSP/Utils/TextView+LSPRange.swift b/CodeEditModules/Sources/CELSP/Conversions/TextView+LSPRange.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Utils/TextView+LSPRange.swift rename to CodeEditModules/Sources/CELSP/Conversions/TextView+LSPRange.swift diff --git a/CodeEditModules/Sources/CELSP/Utils/URL+LSPURI.swift b/CodeEditModules/Sources/CELSP/Conversions/URL+LSPURI.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Utils/URL+LSPURI.swift rename to CodeEditModules/Sources/CELSP/Conversions/URL+LSPURI.swift diff --git a/CodeEditModules/Sources/CELSP/Utils/SemanticToken+Position.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticToken+Position.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Utils/SemanticToken+Position.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/SemanticToken+Position.swift diff --git a/CodeEditModules/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift b/CodeEditModules/Sources/CELSP/Features/SemanticTokens/TextView+SemanticTokenRangeProvider.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Utils/TextView+SemanticTokenRangeProvider.swift rename to CodeEditModules/Sources/CELSP/Features/SemanticTokens/TextView+SemanticTokenRangeProvider.swift diff --git a/CodeEditModules/Sources/CELSP/LSPUtil.swift b/CodeEditModules/Sources/CELSP/LSPCompletionItemsUtil.swift similarity index 98% rename from CodeEditModules/Sources/CELSP/LSPUtil.swift rename to CodeEditModules/Sources/CELSP/LSPCompletionItemsUtil.swift index 740a821041..d1bb89b4ee 100644 --- a/CodeEditModules/Sources/CELSP/LSPUtil.swift +++ b/CodeEditModules/Sources/CELSP/LSPCompletionItemsUtil.swift @@ -1,5 +1,5 @@ // -// LSPUtil.swift +// LSPCompletionItemsUtil.swift // CodeEdit // // Created by Abe Malla on 2/10/24. diff --git a/CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift b/CodeEditModules/Sources/CELSP/Registry/InstallationMethod+PackageManager.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod+PackageManager.swift rename to CodeEditModules/Sources/CELSP/Registry/InstallationMethod+PackageManager.swift diff --git a/CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod.swift b/CodeEditModules/Sources/CELSP/Registry/InstallationMethod.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Registry/Model/InstallationMethod.swift rename to CodeEditModules/Sources/CELSP/Registry/InstallationMethod.swift diff --git a/CodeEditModules/Sources/CELSP/Registry/Model/PackageManagerType.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagerType.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Registry/Model/PackageManagerType.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageManagerType.swift diff --git a/CodeEditModules/Sources/CELSP/Registry/Model/PackageSource.swift b/CodeEditModules/Sources/CELSP/Registry/PackageSource.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Registry/Model/PackageSource.swift rename to CodeEditModules/Sources/CELSP/Registry/PackageSource.swift diff --git a/CodeEditModules/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryItem+InstallMethod.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Registry/Model/RegistryItem+InstallMethod.swift rename to CodeEditModules/Sources/CELSP/Registry/RegistryItem+InstallMethod.swift diff --git a/CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManaging.swift similarity index 100% rename from CodeEditModules/Sources/CELSP/Registry/Protocols/RegistryManaging.swift rename to CodeEditModules/Sources/CELSP/Registry/RegistryManaging.swift From 9fb60caf65331cba5350d24b7551541faf8747e3 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 22:03:26 +0200 Subject: [PATCH 313/335] Docs: Record CELSP's regrouping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six targets follow the convention now. Notes that CELSP/Service matches the kind-folder pattern without being one — it is named after LSPService, the same way Registry/ and LanguageServer/ are named after what they hold. --- docs/ARCHITECTURE.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cac1daaf5b..0ce01f3a01 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -258,8 +258,14 @@ Grouping is **purpose-first**: - **`CETerminal` followed**: `Shell/` (configuration) is the one subgroup that earned a folder, while the three-level `CETerminalView` inheritance chain and its representable stay together — splitting them would separate a base class from its subclasses. -- **10 kind-grouped folders remain**, in `CodeEditSettings`, `CodeEditUI`, `CELSP`, `CESearch`, - and inside `CESourceControl/Accounts/` — three of the ten being the excluded `Accounts/`. Follow the convention +- **`CELSP` followed**: `Utils/` split — two semantic-token helpers joined + `Features/SemanticTokens/`, and the three that cross the protocol boundary became + `Conversions/`. `Registry/`'s `Model/` and `Protocols/` dissolved into its root, where + `PackageManagerProtocol` already sat. +- **8 kind-grouped folders remain.** Three are the excluded `CESourceControl/Accounts/`. + `CELSP/Service` matches the pattern but is named after `LSPService`, like `Registry/` and + `LanguageServer/` — it is not a layer. That leaves `CESearch` (two) and one each in + `CodeEditSettings` and `CodeEditUI` as genuine remaining work. Follow the convention in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own From 6cae630793a339caaac5669936d7f39ae75aa5a0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 22:27:15 +0200 Subject: [PATCH 314/335] Docs: Record why ShellClient stays its own one-file target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The question 'why is this a target for one file' has an answer that was nowhere written down: 19 files across CESourceControl and CELSP depend on ShellClientProtocol and none on the implementation, which only the app target composes. States what the boundary actually buys — a manifest edit is needed to reach the implementation, which is visible in review — rather than claiming the import audit forbids it. It does not: import honesty checks that imports are declared, so a feature that declared the dependency would pass. Records the coherent alternative too, so the question is settled rather than merely answered once. --- docs/ARCHITECTURE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0ce01f3a01..d02d58b174 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -145,6 +145,19 @@ The reason to keep it out anyway: Core stays deterministic and testable with no already has a designated home — rule 4 of [Where does my code go?](#where-does-my-code-go) sends services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. +**`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). Size is the +wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and +`CELSP` — `GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers — and +**none of them imports the implementation**. Only the app target does, six files, composing it at +the root. The abstraction is load-bearing, not ceremonial. + +What the separate target buys, stated precisely: reaching for the implementation from a feature +needs a **manifest edit**, visible in review, rather than an import line inside a file. It is +reviewability, not prevention — import honesty checks that imports are *declared*, so a feature that +declared the dependency would pass the audit. Folding it into the app target is a coherent +alternative (the app is the only consumer, and composing platform adapters is a composition-root +job); it would cost the manifest-level visibility and nothing else demonstrable. + **Known exception, recorded rather than pretended away:** `CEWorkspaceFile` exposes `static let fileManager = FileManager.default` and uses it for `isEmptyFolder` and `doesExist`. Those are filesystem reads from a domain type. Moving them onto the file-manager service is the pure fix; it is From 84d139a46d841a7dc722ea31af70b1a3c5f966c8 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 22:36:40 +0200 Subject: [PATCH 315/335] Refactor: Make CodeEditUI's Styles folder hold only styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two files in Styles/ were not styles. View+actionBar is a plain extension View providing a modifier, and MenuWithButtonStyle declares a View — a menu drawn to resemble a bordered button — not a MenuStyle conformer. Both move to Views/. MenuWithButtonStyle is renamed ButtonStyledMenu: the old name reads as a style type, which is what put it in the wrong folder. One consumer. CodeEditUI is otherwise left alone. Grouping by kind is wrong inside a feature, but this target is a component library with no feature semantics by charter, so Styles/, Views/ and EnvironmentKeys/ are the subject — the same terms SwiftUI itself is documented in. Imposing subjects here would mean several two-file folders; SplitView/ stays the one genuine subsystem. --- .../Settings/Pages/ThemeSettings/ThemeSettingsView.swift | 2 +- .../ButtonStyledMenu.swift} | 4 ++-- .../Sources/CodeEditUI/{Styles => Views}/View+actionBar.swift | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename CodeEditModules/Sources/CodeEditUI/{Styles/MenuWithButtonStyle.swift => Views/ButtonStyledMenu.swift} (92%) rename CodeEditModules/Sources/CodeEditUI/{Styles => Views}/View+actionBar.swift (100%) diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index a18b30a878..9c8d2d58e8 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -44,7 +44,7 @@ struct ThemeSettingsView: View { .disabled(themeModel.selectedTheme == nil) .help("Create a new Theme") - MenuWithButtonStyle(systemImage: "ellipsis", menu: { + ButtonStyledMenu(systemImage: "ellipsis", menu: { Group { Button { themeModel.importTheme() diff --git a/CodeEditModules/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift b/CodeEditModules/Sources/CodeEditUI/Views/ButtonStyledMenu.swift similarity index 92% rename from CodeEditModules/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift rename to CodeEditModules/Sources/CodeEditUI/Views/ButtonStyledMenu.swift index 23195dbc6a..880593355a 100644 --- a/CodeEditModules/Sources/CodeEditUI/Styles/MenuWithButtonStyle.swift +++ b/CodeEditModules/Sources/CodeEditUI/Views/ButtonStyledMenu.swift @@ -1,5 +1,5 @@ // -// MenuWithButtonStyle.swift +// ButtonStyledMenu.swift // CodeEdit // // Created by Tommy Ludwig on 08.09.24. @@ -8,7 +8,7 @@ import SwiftUI /// A menu styled to resemble a bordered button. -public struct MenuWithButtonStyle: View { +public struct ButtonStyledMenu: View { var systemImage: String var menu: () -> MenuView diff --git a/CodeEditModules/Sources/CodeEditUI/Styles/View+actionBar.swift b/CodeEditModules/Sources/CodeEditUI/Views/View+actionBar.swift similarity index 100% rename from CodeEditModules/Sources/CodeEditUI/Styles/View+actionBar.swift rename to CodeEditModules/Sources/CodeEditUI/Views/View+actionBar.swift From ae87f12368a17aaf5e6a99d94aa66375ea50a660 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Wed, 19 Aug 2026 22:36:59 +0200 Subject: [PATCH 316/335] Docs: Record CodeEditUI as the second stated exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its Styles/Views/EnvironmentKeys grouping looks like the kind-grouping this rule forbids. It is not: the rule targets layering inside a feature, and this target is a component library with no feature semantics by charter, so kind is the subject a consumer browses by. Also reframes the remaining count. Of the eight folders, only two are actual work — CESearch's, pending its rebuild. The rest are the excluded Accounts/, the two stated exceptions, and CodeEditSettings waiting on its naming question. --- docs/ARCHITECTURE.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d02d58b174..5d09d8ee77 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -275,10 +275,17 @@ Grouping is **purpose-first**: `Features/SemanticTokens/`, and the three that cross the protocol boundary became `Conversions/`. `Registry/`'s `Model/` and `Protocols/` dissolved into its root, where `PackageManagerProtocol` already sat. -- **8 kind-grouped folders remain.** Three are the excluded `CESourceControl/Accounts/`. - `CELSP/Service` matches the pattern but is named after `LSPService`, like `Registry/` and - `LanguageServer/` — it is not a layer. That leaves `CESearch` (two) and one each in - `CodeEditSettings` and `CodeEditUI` as genuine remaining work. Follow the convention +- **`CodeEditUI` is the second stated exception, and was deliberately left grouped by kind.** + Grouping by kind is wrong *inside a feature*; this target is a component library with no feature + semantics by charter, so there is no domain to group by and `Styles/`, `Views/` and + `EnvironmentKeys/` are the subject — the terms SwiftUI itself is documented in. Consumers browse + it asking "is there a button style for this?". Imposing subjects would yield several two-file + folders; `SplitView/` remains the one genuine subsystem. Two files that were not styles moved out + of `Styles/`, and `MenuWithButtonStyle` — a `View`, not a `MenuStyle` — became `ButtonStyledMenu`. +- **8 kind-grouped folders remain**, but only two are work: `CESearch`'s `Model/` and + `Extensions/`, pending its rebuild. Three are the excluded `CESourceControl/Accounts/`, + `CodeEditUI/Views` is the exception above, `CELSP/Service` is named after `LSPService` rather + than being a layer, and `CodeEditSettings/Models` waits on that target's naming question. Follow the convention in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own From 149df3983f6007a42c3f2b4104a02397e62bde73 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 09:48:15 +0200 Subject: [PATCH 317/335] Docs: Record that the folder-convention sweep covered every target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Names the disposition of all twelve so the question 'did anyone look at X' has an answer. CodeEditDocument and CEWorkspaceFileManager needed nothing — both are flat and under the size where the convention asks for groups — which is why they never appeared in the kind-folder counts. --- docs/ARCHITECTURE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5d09d8ee77..e2af74ed4a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -282,6 +282,11 @@ Grouping is **purpose-first**: it asking "is there a button style for this?". Imposing subjects would yield several two-file folders; `SplitView/` remains the one genuine subsystem. Two files that were not styles moved out of `Styles/`, and `MenuWithButtonStyle` — a `View`, not a `MenuStyle` — became `ButtonStyledMenu`. +- **All 12 library targets have been reviewed** (2026-08-16/20). Six were regrouped; two are stated + exceptions (`CodeEditCore`, `CodeEditUI`); `CodeEditDocument` (5 files) and + `CEWorkspaceFileManager` (7) are correctly flat and need nothing; `ShellClient` is one file by + design. Two are blocked on decisions rather than effort: `CodeEditSettings` on its naming + question, `CESearch` on its rebuild. - **8 kind-grouped folders remain**, but only two are work: `CESearch`'s `Model/` and `Extensions/`, pending its rebuild. Three are the excluded `CESourceControl/Accounts/`, `CodeEditUI/Views` is the exception above, `CELSP/Service` is named after `LSPService` rather From 357e94ecf694220c5e00b3a865f61d2649637347 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 10:39:59 +0200 Subject: [PATCH 318/335] Docs: Settle whether CodeEditDocument and CELSP should exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both stay, and the conclusions depend on each other. CEEditor and CELSP reference each other zero times in either direction; what keeps them apart is LanguageServicesProvider, declared in CodeEditDocument, implemented in CELSP and consumed by CEEditor through an environment key — with a no-op implementation so the editor works with no language service at all. CodeEditDocument is therefore the contract between two independent features, not a leftover holding a document type, and CodeFileDocument's AppKit/SwiftUI imports bar it from Core. CELSP is not editor-internal either: its consumers are the settings UI, the utility area and app lifecycle, and 28 of its 78 files install language servers rather than edit text. Also removes an unused 'import CodeEditDocument' from CELSP's LanguageServerDocument.swift — its only mention of CodeFileDocument is a doc comment. Import honesty checks that imports are declared, not that they are used, so nothing flagged it. --- .../CELSP/LanguageServerDocument.swift | 1 - docs/ARCHITECTURE.md | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CodeEditModules/Sources/CELSP/LanguageServerDocument.swift b/CodeEditModules/Sources/CELSP/LanguageServerDocument.swift index 2554fcb15e..80e66f71b8 100644 --- a/CodeEditModules/Sources/CELSP/LanguageServerDocument.swift +++ b/CodeEditModules/Sources/CELSP/LanguageServerDocument.swift @@ -6,7 +6,6 @@ // import AppKit -import CodeEditDocument import CodeEditLanguages /// A set of properties a language server sets when a document is registered. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e2af74ed4a..bb0bb8f38a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -145,6 +145,31 @@ The reason to keep it out anyway: Core stays deterministic and testable with no already has a designated home — rule 4 of [Where does my code go?](#where-does-my-code-go) sends services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. +**`CodeEditDocument` and `CELSP` both stay their own targets** (asked and settled 2026-08-20). +Neither is a leftover, and the two conclusions depend on each other. + +`CEEditor` and `CELSP` reference each other **zero times, in either direction**. They are siblings. +What keeps them apart is `LanguageServicesProvider`, declared in `CodeEditDocument`, implemented by +`CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key — it +even ships a `NoOpLanguageServicesProvider`, so the editor works with no language service at all. +So `CodeEditDocument` is not "the document type plus some bridging": **it is the contract that keeps +two features independent.** `CodeFileDocument` imports AppKit, SwiftUI and the editor frameworks, so +it cannot live in Core; two features need it, so it cannot live in either. Its own target is forced, +not chosen. + +`CELSP` is not part of the editor either. Its consumers are the settings UI (installing servers), the +utility area (reading logs) and app lifecycle — nothing in `CEEditor` imports it — and **28 of its 78 +files are `Registry/`**: package managers, install steps and source parsers for Cargo, NPM, Pip, Go +and GitHub. That is downloading and installing language servers, not editing text. Folding it into +`CEEditor` would make a ~130-file target mixing the two. + +The abstraction is already sound where it counts: `LanguageServer`, `LSPContentCoordinator`, +`SemanticTokenHighlightProvider` and `LanguageServerDocumentObjects` are all generic over +`LanguageServerDocument`, a protocol requiring only `content`, `languageServerURI` and +`getLanguage()`. Only `LSPService` itself pins the generic to `CodeFileDocument`. Decoupling that +would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type — +breaking its use as an existential for DI — to delete a five-file target. A bad trade. + **`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). Size is the wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and `CELSP` — `GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers — and From 6f439369013af3b25463f224bffb5dabafa0c7ed Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 15:13:50 +0200 Subject: [PATCH 319/335] Docs: Delete the Documentation.docc catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Untouched since 2025-01-05. Of the 34 symbols its landing page linked, 9 no longer exist and 13 moved into package targets — DocC documents one module, so an app-target catalog cannot resolve CEWorkspaceFile, FileIcon or ShellClient any more. Ten of the 34 were still app-target symbols. Repair could not have succeeded. The catalog's model — one app target, one documented module — stopped matching a codebase of twelve library targets and a thin shell, and the only thing it could still document is the shell, which is the part least needing an external-audience explainer. Its section names were the retired Features/ folders, and AppPreferences/ was nine files of tutorial for the god object ARCHITECTURE.md names as a cause of the 2022 collapse. Nothing referenced it: no inbound links, absent from Package.swift and CI, 19 months stale while compiled in the app target's Sources phase. This matches what the CodeEditApp org actually does. All five libraries it publishes — CodeEditSourceEditor, CodeEditTextView, CodeEditKit, CodeEditSymbols, CodeEditLanguages — ship a catalog at Sources//Documentation.docc, because a published library has readers who never open its source. None of CodeEditModules' twelve targets has an external consumer, so none needs one; a catalog is optional anyway, since DocC generates symbol docs from doc comments without it. If a target is ever extracted for publication, that is when it gains a catalog. Four project.pbxproj entries removed with it: the catalog was an explicit file reference in the Sources build phase, not a synchronized group, so deleting the folder alone would have broken the build. Also fixes ThemeSettingsView's header, which still named the deleted ThemePreferencesView. --- CodeEdit.xcodeproj/project.pbxproj | 4 - .../ThemeSettings/ThemeSettingsView.swift | 2 +- Documentation.docc/About/About Window.md | 22 -- .../App Window/Adding New Tab Type.md | 94 --------- Documentation.docc/App Window/App Window.md | 48 ----- .../App Window/InspectorSidebarView.md | 37 ---- .../App Window/NavigatorSidebarView.md | 44 ---- .../App Window/StatusBarView.md | 25 --- Documentation.docc/App Window/TabBarView.md | 23 --- .../App Window/UtilityAreaView.md | 27 --- .../AppPreferences/AppPreferences.md | 37 ---- .../AppPreferences/Create a View.md | 169 ---------------- .../AppPreferences/Getting Started.md | 98 --------- .../Sections/AccountPreferencesView.md | 23 --- .../Sections/GeneralPreferencesView.md | 7 - .../Sections/KeybindingsPreferencesView.md | 7 - .../Sections/SourceControlPreferencesView.md | 14 -- .../Sections/TerminalPreferencesView.md | 7 - .../Sections/TextEditingPreferencesView.md | 7 - .../Sections/ThemePreferencesView.md | 15 -- Documentation.docc/AppPreferences/Themes.md | 191 ------------------ Documentation.docc/CodeEditUI/CodeEditUI.md | 23 --- Documentation.docc/CodeEditUI/HelpButton.md | 13 -- .../Resources/BranchPicker_View.png | Bin 901778 -> 0 bytes .../CodeEditUI/Resources/FontPicker_View.png | Bin 8917 -> 0 bytes .../CodeEditUI/Resources/HelpButton_View.png | Bin 4322 -> 0 bytes .../Resources/SegmentedControl_View.png | Bin 5253 -> 0 bytes .../CodeEditUI/SegmentedControl.md | 20 -- .../CodeEditUI/ToolbarBranchPicker.md | 39 ---- Documentation.docc/Documentation.md | 92 --------- .../FileManagement/FileManagement.md | 10 - Documentation.docc/Git/Git.md | 111 ---------- .../KeyChain/CodeEditKeychain.md | 12 -- .../KeyChain/What is Keychain.md | 66 ------ .../Keybindings/KeybindingManager.md | 31 --- Documentation.docc/Welcome/Welcome Window.md | 12 -- 36 files changed, 1 insertion(+), 1329 deletions(-) delete mode 100644 Documentation.docc/About/About Window.md delete mode 100644 Documentation.docc/App Window/Adding New Tab Type.md delete mode 100644 Documentation.docc/App Window/App Window.md delete mode 100644 Documentation.docc/App Window/InspectorSidebarView.md delete mode 100644 Documentation.docc/App Window/NavigatorSidebarView.md delete mode 100644 Documentation.docc/App Window/StatusBarView.md delete mode 100644 Documentation.docc/App Window/TabBarView.md delete mode 100644 Documentation.docc/App Window/UtilityAreaView.md delete mode 100644 Documentation.docc/AppPreferences/AppPreferences.md delete mode 100644 Documentation.docc/AppPreferences/Create a View.md delete mode 100644 Documentation.docc/AppPreferences/Getting Started.md delete mode 100644 Documentation.docc/AppPreferences/Sections/AccountPreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Sections/GeneralPreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Sections/KeybindingsPreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Sections/SourceControlPreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Sections/TextEditingPreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Sections/ThemePreferencesView.md delete mode 100644 Documentation.docc/AppPreferences/Themes.md delete mode 100644 Documentation.docc/CodeEditUI/CodeEditUI.md delete mode 100644 Documentation.docc/CodeEditUI/HelpButton.md delete mode 100644 Documentation.docc/CodeEditUI/Resources/BranchPicker_View.png delete mode 100644 Documentation.docc/CodeEditUI/Resources/FontPicker_View.png delete mode 100644 Documentation.docc/CodeEditUI/Resources/HelpButton_View.png delete mode 100644 Documentation.docc/CodeEditUI/Resources/SegmentedControl_View.png delete mode 100644 Documentation.docc/CodeEditUI/SegmentedControl.md delete mode 100644 Documentation.docc/CodeEditUI/ToolbarBranchPicker.md delete mode 100644 Documentation.docc/Documentation.md delete mode 100644 Documentation.docc/FileManagement/FileManagement.md delete mode 100644 Documentation.docc/Git/Git.md delete mode 100644 Documentation.docc/KeyChain/CodeEditKeychain.md delete mode 100644 Documentation.docc/KeyChain/What is Keychain.md delete mode 100644 Documentation.docc/Keybindings/KeybindingManager.md delete mode 100644 Documentation.docc/Welcome/Welcome Window.md diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index d9fdfd8d7d..ad322dd3e2 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ 58CE7E4200000003004BE402 /* CETerminal in Frameworks */ = {isa = PBXBuildFile; productRef = 58CE7E4100000003004BE401 /* CETerminal */; }; 58CFC49B2F8BE799009F4AA7 /* CodeEditCore in Frameworks */ = {isa = PBXBuildFile; productRef = 58CFC49A2F8BE799009F4AA7 /* CodeEditCore */; }; 58ED10022FFB0001004BE116 /* CEEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 58ED10012FFB0001004BE116 /* CEEditor */; }; - 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 58F2EACE292FB2B0004A9BDE /* Documentation.docc */; }; 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 58F2EB1D292FB954004A9BDE /* Sparkle */; }; 5AD0C0DE2D00000000000001 /* CodeEditDocument in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000002 /* CodeEditDocument */; }; 5AD0C0DE2D00000000000011 /* CodeEditSettings in Frameworks */ = {isa = PBXBuildFile; productRef = 5AD0C0DE2D00000000000012 /* CodeEditSettings */; }; @@ -122,7 +121,6 @@ 284DC8502978BA2600BF2770 /* .all-contributorsrc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ".all-contributorsrc"; sourceTree = ""; }; 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenWithCodeEdit.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 589F3E342936185400E1A4DA /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; - 58F2EACE292FB2B0004A9BDE /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = ""; }; 6C67413D2C44A28C00AABDF5 /* ProjectNavigatorViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+DataSource.swift"; sourceTree = ""; }; 6C67413F2C44A2A200AABDF5 /* ProjectNavigatorViewController+Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+Delegate.swift"; sourceTree = ""; }; 6C9619262C3F285C009733CE /* CodeEditTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CodeEditTestPlan.xctestplan; sourceTree = ""; }; @@ -264,7 +262,6 @@ B62454602D78A3D4009A86D1 /* CodeEditUITests */, B624544F2D78A3D3009A86D1 /* Configs */, B6FF04772B6C08AC002C2C78 /* DefaultThemes */, - 58F2EACE292FB2B0004A9BDE /* Documentation.docc */, B62454CD2D78A3D8009A86D1 /* OpenWithCodeEdit */, 6C9619262C3F285C009733CE /* CodeEditTestPlan.xctestplan */, 284DC8502978BA2600BF2770 /* .all-contributorsrc */, @@ -573,7 +570,6 @@ buildActionMask = 2147483647; files = ( 6CAAF69429BCD78600A1F48A /* (null) in Sources */, - 58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */, 6CB9144B29BEC7F100BC47F2 /* (null) in Sources */, 6CAAF69229BCC71C00A1F48A /* (null) in Sources */, 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */, diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 9c8d2d58e8..514b93205e 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -1,5 +1,5 @@ // -// ThemePreferencesView.swift +// ThemeSettingsView.swift // CodeEdit // // Created by Lukas Pistrol on 30.03.22. diff --git a/Documentation.docc/About/About Window.md b/Documentation.docc/About/About Window.md deleted file mode 100644 index bf811941cf..0000000000 --- a/Documentation.docc/About/About Window.md +++ /dev/null @@ -1,22 +0,0 @@ -# About Window - -The About Window displays general information about the app and acknowledgements to external dependencies. - -## Topics - -### About View - -- ``AboutView`` -- ``AboutWindow`` - -### Acknowledgements - -- ``AcknowledgementsView`` -- ``AcknowledgementsViewModel`` -- ``AcknowledgementsViewWindowController`` -- ``AcknowledgementRowView`` - -- ``AcknowledgementObject`` -- ``AcknowledgementDependency`` -- ``AcknowledgementPackageState`` -- ``AcknowledgementPin`` diff --git a/Documentation.docc/App Window/Adding New Tab Type.md b/Documentation.docc/App Window/Adding New Tab Type.md deleted file mode 100644 index 6541ddc178..0000000000 --- a/Documentation.docc/App Window/Adding New Tab Type.md +++ /dev/null @@ -1,94 +0,0 @@ -# Adding a New Tab Type - -This article is about how to add a new tab type to `TabBar` - -## Overview - -First of all, each data type to be represented as tab in the UI should conform to -``EditorTabRepresentable`` protocol. For example, this is how it is done for -`FileItem`: - -```swift -final class FileItem: Identifiable, Codable, EditorTabRepresentable { - public var tabID: EditorTabID { - .codeEditor(id) - } - - public var title: String { - self.url.lastPathComponent - } - - public var icon: Image { - Image(systemName: self.systemImage) - } - - public var iconColor: Color { - ... - } - - ... -} -``` - -### Add new item identifier case - -Each new tab type must have new identifier case, for example: -```swift -public enum EditorTabID: Codable, Identifiable, Hashable { - public var id: String { - switch self { - ... - case .gitHistory(let repo): - return "gitHistory_\(repo)" - } - } - - /// Represents Git history - case gitHistory(String) -} -``` - -### Opening and closing new tab types - -Tabs are opened using ``WorkspaceDocument/openTab(item:)`` method. It does a set of common -things for all tabs. But also it calls a private method based on the ``EditorTabID`` of the -item. The private method for your ``EditorTabRepresentable`` MUST persist this item -somewhere (I recommend persisting them in ``WorkspaceSelectionState``). - -The same is for closing tabs using ``WorkspaceDocument/closeTab(item:)`` method. -Closing multiple tabs at once is handled by common functions, so there are no changes -required for them. - -``WorkspaceDocument/close()`` calls `WorkspaceDocument.saveSelectionState()` to persist Workspace Selection State to UserDefaults. - -``WorkspaceDocument/read(from:ofType:)`` calls `WorkspaceDocument.readSelectionState()` to retrieve Workspace Selection State from UserDefaults. - -Also, because previously opened tabs are persisted in UserDefaults, -they should be recovered some way later. To recover new tab types you need to add -a case for ``WorkspaceDocument/read(from:ofType:)`` to let it know how to recover your new tab type. - -If you need to persist something as code editor tabs do for files, then you need to add -functionality to persist changes to ``WorkspaceDocument/close()``. - -Also, you need to add a case for new tab type to -``WorkspaceSelectionState/getItemByTab(id:)``. It will allow to use -``WorkspaceSelectionState/selected`` property and other features. - -### Adding a view for the new tab type - -To add a view for new tab type, you need to add a case for your tab type to -``WorkspaceView/tabContent``: - -```swift -@ViewBuilder var tabContent: some View { - if let tabID = workspace.selectionState.selectedId { - switch tabID { - ... - case .gitHistory: - GitHistoryView(windowController: windowController, workspace: workspace) - } - } else { - noEditor - } -} -``` diff --git a/Documentation.docc/App Window/App Window.md b/Documentation.docc/App Window/App Window.md deleted file mode 100644 index d55cd15512..0000000000 --- a/Documentation.docc/App Window/App Window.md +++ /dev/null @@ -1,48 +0,0 @@ -# App Window - -A collection of all the views that make up the main app window. - -## Topics - -### Window Controller - -- ``CodeEditWindowController`` -- ``WorkspaceView`` - -### Main Content - -- ``EditorAreaView`` -- ``EditorAreaFileView`` -- ``CodeFileView`` -- ``NonTextFileView`` -- ``AnyFileView`` -- ``LoadingFileView`` -- ``ImageFileView`` -- ``PDFFileView`` - -### JumpBar - -- ``JumpBarView`` -- ``JumpBarComponent`` -- ``JumpBarMenu`` -- ``JumpBarMenuItem`` - -### Navigator Sidebar - -- ``NavigatorSidebarView`` - -### Inspector Sidebar - -- ``InspectorAreaView`` - -### Status Bar - -- ``StatusBarView`` - -### Tab Bar - -- ``TabBarView`` - -### Terminal Emulator - -- ``TerminalEmulatorView`` diff --git a/Documentation.docc/App Window/InspectorSidebarView.md b/Documentation.docc/App Window/InspectorSidebarView.md deleted file mode 100644 index 6665c26317..0000000000 --- a/Documentation.docc/App Window/InspectorSidebarView.md +++ /dev/null @@ -1,37 +0,0 @@ -# ``CodeEdit/InspectorAreaView`` - -## Topics - -### Toolbars - -- ``InspectorAreaToolbarTop`` - -### File Inspector - -- ``FileInspectorView`` -- ``FileInspectorModel`` -- ``FileLocation`` -- ``IndentUsing`` -- ``LanguageType`` -- ``LineEndings`` -- ``TextEncoding`` - -### History Inspector - -- ``HistoryInspectorView`` -- ``HistoryInspectorModel`` -- ``HistoryInspectorItemView`` -- ``HistoryInspectorNoHistoryView`` -- ``HistoryPopoverView`` - -### Quick Help Inspector - -- ``QuickHelpInspectorView`` - -### No Selection - -- ``NoSelectionInspectorView`` - -### File List - -- ``FileTypeList`` diff --git a/Documentation.docc/App Window/NavigatorSidebarView.md b/Documentation.docc/App Window/NavigatorSidebarView.md deleted file mode 100644 index 3ead03b1c6..0000000000 --- a/Documentation.docc/App Window/NavigatorSidebarView.md +++ /dev/null @@ -1,44 +0,0 @@ -# ``CodeEdit/NavigatorSidebarView`` - -## Topics - -### Toolbars - -- ``NavigatorSidebarToolbarTop`` -- ``NavigatorSidebarToolbarBottom`` - -### Project Navigator - -- ``ProjectNavigatorView`` -- ``OutlineView`` -- ``OutlineViewController`` -- ``OutlineMenu`` -- ``OutlineTableViewCell`` -- ``OutlineTableViewCellDelegate`` - -### Source Control Navigator - -- ``SourceControlNavigatorView`` -- ``SourceControlModel`` -- ``SourceControlSearchToolbar`` -- ``SourceControlToolbarBottom`` -- ``SourceControlNavigatorRepositoriesView`` -- ``SourceControlNavigatorChangesView`` -- ``SourceControlNavigatorChangedFileView`` - -### Find Navigator - -- ``FindNavigatorView`` -- ``FindNavigatorSearchBar`` -- ``FindNavigatorModeSelector`` -- ``FindNavigatorResultList`` -- ``FindNavigatorListViewController`` -- ``FindNavigatorListMatchCell`` - -### Extension Navigator - -- ``ExtensionNavigatorView`` -- ``ExtensionNavigatorItemView`` -- ``ExtensionNavigatorData`` -- ``ExtensionInstallationView`` -- ``ExtensionInstallationViewModel`` diff --git a/Documentation.docc/App Window/StatusBarView.md b/Documentation.docc/App Window/StatusBarView.md deleted file mode 100644 index c786a4afad..0000000000 --- a/Documentation.docc/App Window/StatusBarView.md +++ /dev/null @@ -1,25 +0,0 @@ -# ``CodeEdit/StatusBarView`` - -## Topics - -### Model - -- ``ImageDimensions`` - -### View Model - -- ``StatusBarViewModel`` - -### View Modifiers - -- ``UpdateStatusBarInfo`` - -### Items - -- ``StatusBarMenuStyle`` -- ``StatusBarBreakpointButton`` -- ``StatusBarIndentSelector`` -- ``StatusBarEncodingSelector`` -- ``StatusBarLineEndSelector`` -- ``StatusBarToggleUtilityAreaButton`` -- ``StatusBarCursorPositionLabel`` diff --git a/Documentation.docc/App Window/TabBarView.md b/Documentation.docc/App Window/TabBarView.md deleted file mode 100644 index 99288ffa7a..0000000000 --- a/Documentation.docc/App Window/TabBarView.md +++ /dev/null @@ -1,23 +0,0 @@ -# ``CodeEdit/TabBarView`` - -## Topics - -### Articles - -- - -### Model - -- ``EditorTabID`` -- ``EditorTabRepresentable`` - -### Components - -- ``EditorTabView`` -- ``TabBarContextMenu`` -- ``EditorTabButtonStyle`` -- ``TabDivider`` -- ``TabBarTopDivider`` -- ``TabBarBottomDivider`` -- ``TabBarAccessoryIcon`` -- ``TabBarXcodeBackground`` diff --git a/Documentation.docc/App Window/UtilityAreaView.md b/Documentation.docc/App Window/UtilityAreaView.md deleted file mode 100644 index 50e5e93988..0000000000 --- a/Documentation.docc/App Window/UtilityAreaView.md +++ /dev/null @@ -1,27 +0,0 @@ -# ``CodeEdit/UtilityAreaView`` - -## Topics - -### Model - -- ``UtilityAreaTab`` - -### View Model - -- ``UtilityAreaViewModel`` -- ``UtilityAreaTabViewModel`` - -### Utility - -- ``UtilityAreaTerminal`` -- ``UtilityAreaTerminalTab`` -- ``UtilityAreaDebugView`` -- ``UtilityAreaOutputView`` - -### Toolbar - -- ``UtilityAreaView`` -- ``UtilityAreaSplitTerminalButton`` -- ``UtilityAreaMaximizeButton`` -- ``UtilityAreaClearButton`` -- ``UtilityAreaFilterTextField`` diff --git a/Documentation.docc/AppPreferences/AppPreferences.md b/Documentation.docc/AppPreferences/AppPreferences.md deleted file mode 100644 index 93875f3ecd..0000000000 --- a/Documentation.docc/AppPreferences/AppPreferences.md +++ /dev/null @@ -1,37 +0,0 @@ -# ``CodeEdit/Settings`` - -## Topics - -### Getting Started - -- -- - -### Settings Model - -- ``SettingsModel`` -- ``SoftwareUpdater`` - -### Settings Section Views - -- ``GeneralSettingsView`` -- ``ThemeSettingsView`` -- ``TextEditingSettingsView`` -- ``TerminalSettingsView`` -- ``LocationsSettingsView`` -- ``KeybindingsSettingsView`` -- ``AccountSettingsView`` -- ``SourceControlSettingsView`` -- ``SettingsPlaceholderView`` - -### Helper Views - -- ``SettingsContent`` -- ``SettingsSection`` -- ``SettingsColorPicker`` -- ``SettingsToolbar`` - -### Theme Settings Model - -- ``Theme`` -- ``ThemeModel`` diff --git a/Documentation.docc/AppPreferences/Create a View.md b/Documentation.docc/AppPreferences/Create a View.md deleted file mode 100644 index 99f09dd207..0000000000 --- a/Documentation.docc/AppPreferences/Create a View.md +++ /dev/null @@ -1,169 +0,0 @@ -# Create a View - -Now that you followed the guide it's time to create a view. - -## Add setting to existing Section - -In our example we added `ourNewOption` in ``Settings/GeneralSettings``. - -Now let's take a look at the ``GeneralSettingsView``. - -```swift -import SwiftUI - -struct GeneralSettingsView: View { - @AppSettings(\.general) - var settings - - var body: some View { - SettingsForm { - Section { - appearance - fileIconStyle - navigatorTabBarPosition - inspectorTabBarPosition - ... - } - } - } -} -``` - -As you can see ``SettingsModel`` is already setup and ready to use. - -To add your option toggle below the other options just add something like this: - -```swift -private extension GeneralSettingsView { - // MARK: - Settings View - - private var yourOption: some View { - Toggle("Your text", isOn: $general.yourNewOption) - } -} -``` - -Then add it to `var body: some View` - -```swift -struct GeneralSettingsView: View { - var body: some View { - SettingsForm { - Section { - appearanceSection - showIssuesSection - fileExtensionsSection - // REMOVEME: et cetera - yourOptionSection - } - } - } -} -``` - -And now you're done! - -## Implement a new section - -> Tip: Rename YourSection to the section name that you want - -To implement a new section first create a new folder inside the `Pages` folder and name it accordingly. - -Inside the folder create a new SwiftUI view and name it "YourSectionSettingsView.swift". - -Then create a new folder inside called `Models` and inside of it create a file named "YourSectionSettings.swift" - - -> Tip: The order that pages are arranged in the array is the same as in the settings window, the first array member will be the top item -``` - -Then find the file `SettingsPage.swift` and add `YourSection` to the `enum Name` like this: - -```swift -enum Name: String { - case general = "General" - case advanced = "Advanced" - // et cetera - case yourSection = "YourSection" -} -``` - -Back in `YourSectionView.swift` implement your option like this: - -```swift -import SwiftUI - -struct YourSectionSettingsView: View { - @AppSettings(\.yourSection) - var yourSection - - var body: some View { - SettingsForm { - Section { - yourToggleSection - } - } - } -} - -private extension YourSectionSettingsView { - // MARK: - Settings Views - - private var yourToggle: some View { - Toggle("Your option", isOn: $yourSection.yourNewOption) - } -} -``` - -There are 3 more steps, almost done. - -Open `ModelNameToSettingName.swift` and add your translated search result: - -```swift -let translator: [String: String] = [ - // MARK: - General Settings - "appAppearance": NSLocalizedString("Appearance", comment: ""), - "fileIconStyle": NSLocalizedString("File Icon Style", comment: ""), - // etc - // MARK: - Your Section - "yourOption": NSLocalizedString("Your Option", comment: "Your translation comment") -] -``` - -Now, open `SettingsView.swift` and add your section to the `populatePages()` method: - -```swift -/// Creates all the neccessary pages -private func populatePages() -> [SettingsPage] { - var pages = [SettingsPage]() - let settingsData = SettingsData() - - let generalSettings = SettingsPage(.general, baseColor: .gray, icon: .system("gear")) - pages = createPageAndSettings(settingsData.general, parent: generalSettings, prePages: pages) - - let accountsSettings = SettingsPage(.accounts, baseColor: .blue, icon: .system("at")) - pages = createPageAndSettings(settingsData.accounts, parent: accountsSettings, prePages: pages) - - // etc - let yourSectionSettings = SettingsPage(.yourSection, baseColor: /* add color here */, icon: /* add icon */) - pages = createPageAndSettings(settingsData.yourSection, parent: yourSectionSettings, prePages: pages) - - return pages -} -``` - - -When you are done, add `YourSectionSettingsView` to `SettingsView.swift`: - -```swift -Group { - switch selectedPage { - case .general: - GeneralSettingsView().environmentObject(updater) - case .yourSection: - YourSectionSettingsView() - default: - Text("Implementation Needed").frame(alignment: .center) - } -} -``` diff --git a/Documentation.docc/AppPreferences/Getting Started.md b/Documentation.docc/AppPreferences/Getting Started.md deleted file mode 100644 index c6f4f02271..0000000000 --- a/Documentation.docc/AppPreferences/Getting Started.md +++ /dev/null @@ -1,98 +0,0 @@ -# Getting Started - -There are a few things to consider when using the ``Settings``. - -## Reading/Writing Values - -The Settings can be accessed from everywhere in the app like this: - -```swift -@AppSettings(\.settingName) -var setting -``` - -```swift -Toggle("Enable some Feature", value: $setting) -``` - -## Creating a New Preference - -When implementing a new feature, we might have some options in regards to this new feature we want to show the user in the apps Settings Window. - -### Find a Section - -The settings window is structured in different sections. Figure out in which section your new option should appear in. - -If the section is already populated with other options (e.g. ``Settings/GeneralSettings``), just add your new option like this: - -```swift -struct GeneralSettings: Codable, Hashable { - - // ... - - // This will be your new option. Be sure to provide a default value - public var yourNewOption: Bool = true - - public init() {} - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - // ... - - // Try to decode the value from json. - self.yourNewOption = try container.decodeIfPresent( - Bool.self, - forKey: .yourNewOption - ) ?? true // If the key is not present in the json, set the default value - } -} -``` - -### Create a Section - -In some cases in early development the section you decided on where to put your option in might not yet have been implemented. In this -case you can create a new `struct` inside ``Settings`` like this: - -```swift -public extension YourNewSection: Codable { - - // This will be your new option. Be sure to provide a default value - public var yourNewOption: Bool = true - - public init() {} - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - // Try to decode the value from json. - self.yourNewOption = try container.decodeIfPresent( - Bool.self, - forKey: .yourNewOption - ) ?? true // If the key is not present in the json, set the default value - } -} -``` - -Now let's add the new section to ``Settings`` like this: - -```swift -public struct Settings: Codable { - // ... - - // Add your new section above the `public init() {}` - public var yourNewSection: YourNewSection = .init() - - // ... -} -``` - -## Topics - -### Up Next - -- - -### Main Components - -- ``Settings`` -- ``SettingsModel`` diff --git a/Documentation.docc/AppPreferences/Sections/AccountPreferencesView.md b/Documentation.docc/AppPreferences/Sections/AccountPreferencesView.md deleted file mode 100644 index adf62c7a6c..0000000000 --- a/Documentation.docc/AppPreferences/Sections/AccountPreferencesView.md +++ /dev/null @@ -1,23 +0,0 @@ -# ``CodeEdit/AccountSettingsView`` - -## Topics - -### Model - -- ``Settings/AccountsSettings`` -- ``SourceControlAccounts`` -- ``SourceControlProvider`` - -### Views - -- ``AccountListItemView`` -- ``AccountSelectionDialog`` -- ``GitAccountItem`` -- ``GitAccountItemView`` - -### Login Views - -- ``GitLabHostedLoginView`` -- ``GitLabLoginView`` -- ``GitHubLoginView`` -- ``GitHubEnterpriseLoginView`` diff --git a/Documentation.docc/AppPreferences/Sections/GeneralPreferencesView.md b/Documentation.docc/AppPreferences/Sections/GeneralPreferencesView.md deleted file mode 100644 index a59d4502b6..0000000000 --- a/Documentation.docc/AppPreferences/Sections/GeneralPreferencesView.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``CodeEdit/GeneralSettingsView`` - -## Topics - -### Model - -- ``Settings/GeneralSettings`` diff --git a/Documentation.docc/AppPreferences/Sections/KeybindingsPreferencesView.md b/Documentation.docc/AppPreferences/Sections/KeybindingsPreferencesView.md deleted file mode 100644 index 376c8416a8..0000000000 --- a/Documentation.docc/AppPreferences/Sections/KeybindingsPreferencesView.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``CodeEdit/KeybindingsSettingsView`` - -## Topics - -### Model - -- ``Settings/KeybindingsSettings`` diff --git a/Documentation.docc/AppPreferences/Sections/SourceControlPreferencesView.md b/Documentation.docc/AppPreferences/Sections/SourceControlPreferencesView.md deleted file mode 100644 index 2ab028ebed..0000000000 --- a/Documentation.docc/AppPreferences/Sections/SourceControlPreferencesView.md +++ /dev/null @@ -1,14 +0,0 @@ -# ``CodeEdit/SourceControlSettingsView`` - -## Topics - -### Model - -- ``Settings/SourceControlSettings`` -- ``IgnoredFiles`` - -### Views - -- ``SourceControlGeneralView`` -- ``SourceControlGitView`` -- ``IgnoredFileView`` diff --git a/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md b/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md deleted file mode 100644 index 63e727bea8..0000000000 --- a/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``CodeEdit/TerminalSettingsView`` - -## Topics - -### Model - -- ``Settings/TerminalSettings`` diff --git a/Documentation.docc/AppPreferences/Sections/TextEditingPreferencesView.md b/Documentation.docc/AppPreferences/Sections/TextEditingPreferencesView.md deleted file mode 100644 index 3af61fbc42..0000000000 --- a/Documentation.docc/AppPreferences/Sections/TextEditingPreferencesView.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``CodeEdit/TextEditingSettingsView`` - -## Topics - -### Model - -- ``Settings/TextEditingSettings`` diff --git a/Documentation.docc/AppPreferences/Sections/ThemePreferencesView.md b/Documentation.docc/AppPreferences/Sections/ThemePreferencesView.md deleted file mode 100644 index 928164a8b7..0000000000 --- a/Documentation.docc/AppPreferences/Sections/ThemePreferencesView.md +++ /dev/null @@ -1,15 +0,0 @@ -# ``CodeEdit/ThemeSettingsView`` - -## Topics - -### Model - -- ``Settings/ThemeSettings`` -- ``ThemeModel`` - -### Views - -- ``PreviewThemeView`` -- ``TerminalThemeView`` -- ``EditorThemeView`` -- ``ThemePreviewIcon`` diff --git a/Documentation.docc/AppPreferences/Themes.md b/Documentation.docc/AppPreferences/Themes.md deleted file mode 100644 index deff4204bf..0000000000 --- a/Documentation.docc/AppPreferences/Themes.md +++ /dev/null @@ -1,191 +0,0 @@ -# ``CodeEdit/Theme`` - -## Overview - -A ``Theme`` is stored in a `theme_name.json` file in the `~/Library/Application Support/CodeEdit/themes/` directory. There are a -couple of bundled themes that will automatically be put there once the app starts. - -Once a `JSON` file is loaded, the ``Theme`` gets added to ``ThemeModel/themes``. - -They can either be ``Theme/ThemeType/dark`` or ``Theme/ThemeType/light``. - -## JSON Structure - -```json -{ - "author" : "CodeEdit", - "name" : "codeedit-xcode-dark", - "displayName" : "Xcode Dark", - "description" : "Xcode dark theme.", - "version" : "0.0.1", - "license" : "MIT", - "type" : "dark", - "distributionURL" : "https:\/\/github.com\/CodeEditApp\/CodeEdit", - "editor" : { ... }, - "terminal" : { ... } -} -``` - -| Key | Description | -| --- | ----------- | -| ``author`` | Your Name | -| ``name`` | A unique string representing the theme. _It's good practice to start it with your name or domain to make sure it is unique._ | -| ``displayName`` | The name that will appear in the UI | -| description / ``metadataDescription`` | A short description that will appear when hovering over the theme thumbnail | -| ``version`` | A version number | -| ``license`` | Which license your theme is published under | -| type / ``appearance`` | The type of the theme [**dark**, **light**] | -| ``distributionURL`` | A URL to your web presentation | -| ``editor`` | A collection of colors for the editor | -| ``terminal`` | A collection of colors for the terminal | - -### Editor - -```json -{ - "invisibles" : { - "color" : "#424D5B" - }, - "comments" : { - "color" : "#73A74E" - }, - "numbers" : { - "color" : "#D0BF69" - }, - "commands" : { - "color" : "#67B7A4" - }, - "lineHighlight" : { - "color" : "#23252B" - }, - "values" : { - "color" : "#A167E6" - }, - "background" : { - "color" : "#1F1F24" - }, - "keywords" : { - "color" : "#FF7AB3" - }, - "text" : { - "color" : "#D9D9D9" - }, - "insertionPoint" : { - "color" : "#D9D9D9" - }, - "strings" : { - "color" : "#FC6A5D" - }, - "selection" : { - "color" : "#515B70" - }, - "types" : { - "color" : "#5DD8FF" - }, - "variables" : { - "color" : "#41A1C0" - }, - "attributes" : { - "color" : "#D0A8FF" - }, - "characters" : { - "color" : "#D0BF69" - } -} -``` - -### Terminal - -```json -{ - "white" : { - "color" : "#d9d9d9" - }, - "brightMagenta" : { - "color" : "#af52de" - }, - "brightRed" : { - "color" : "#ff3b30" - }, - "blue" : { - "color" : "#007aff" - }, - "red" : { - "color" : "#ff3b30" - }, - "green" : { - "color" : "#28cd41" - }, - "boldText" : { - "color" : "#d9d9d9" - }, - "brightGreen" : { - "color" : "#28cd41" - }, - "background" : { - "color" : "#1f2024" - }, - "cursor" : { - "color" : "#d9d9d9" - }, - "selection" : { - "color" : "#515b70" - }, - "magenta" : { - "color" : "#af52de" - }, - "black" : { - "color" : "#1f2024" - }, - "text" : { - "color" : "#d9d9d9" - }, - "brightWhite" : { - "color" : "#ffffff" - }, - "brightBlue" : { - "color" : "#007aff" - }, - "brightYellow" : { - "color" : "#ffff00" - }, - "cyan" : { - "color" : "#59adc4" - }, - "yellow" : { - "color" : "#ffcc00" - }, - "brightCyan" : { - "color" : "#55bef0" - }, - "brightBlack" : { - "color" : "#8e8e93" - } -} -``` - -## Topics - -### General Info - -- ``author`` -- ``name`` -- ``displayName`` -- ``metadataDescription`` -- ``version`` -- ``license`` -- ``appearance`` -- ``distributionURL`` -- ``ThemeType`` - -### Editor - -- ``Theme/EditorColors`` -- ``editor`` -- ``Attributes`` - -### Terminal - -- ``Theme/TerminalColors`` -- ``terminal`` -- ``Attributes`` diff --git a/Documentation.docc/CodeEditUI/CodeEditUI.md b/Documentation.docc/CodeEditUI/CodeEditUI.md deleted file mode 100644 index 71f8b89de6..0000000000 --- a/Documentation.docc/CodeEditUI/CodeEditUI.md +++ /dev/null @@ -1,23 +0,0 @@ -# CodeEditUI - -A collection of reusable UI elements for `CodeEdit`. - -## Overview - -This module contains UI elements that can be reused throughout the `CodeEdit` app. - -## Topics - -### Controls - -- ``ToolbarBranchPicker`` -- ``HelpButton`` -- ``SegmentedControl`` -- ``SettingsTextEditor`` - -### Other - -- ``EffectView`` -- ``OverlayPanel`` -- ``PressActions`` -- ``PanelDivider`` diff --git a/Documentation.docc/CodeEditUI/HelpButton.md b/Documentation.docc/CodeEditUI/HelpButton.md deleted file mode 100644 index 062d5db448..0000000000 --- a/Documentation.docc/CodeEditUI/HelpButton.md +++ /dev/null @@ -1,13 +0,0 @@ -# ``CodeEdit/HelpButton`` - -## Usage - -```swift -HelpButton { - // an action to perform on click -} -``` - -## Preview - -![Help Button](HelpButton_View.png) diff --git a/Documentation.docc/CodeEditUI/Resources/BranchPicker_View.png b/Documentation.docc/CodeEditUI/Resources/BranchPicker_View.png deleted file mode 100644 index cd5d89113859994f303fedb1bdf26b1170b68032..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 901778 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�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ɸ)I#VQg4Pe&Bp7UPViNTmBXY&E( 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

ej9w-NlTwHoe65Teg`TE~ux&Fxz!HQc!GiWE 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 Le}lLMgjOF1Bq-;uaEWClK;QNIv_m-()=tP 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�L~LJy zpC10M7g;0xfrH|m$51;(aQY)_>qNca<}hlBUgoDCD1v2dAg4MFBfM9BSnJ5A)%+J1 zPS6^Tu(P$+i2}5bJrYMVEM*hMUKhzXdxw;R-78Uf_vdA51MaV8Wh(4$pj?f6fx>=p zV9@I=+(`qDHP7Q4X-j~NjVjp&RKHycJ*##nJfTxKGYI7aBBa4$fKc*<& z8(`cc(ggpBtvKXcS$jK-fCXvTP#<}1|guG3xhdE+ZnFTj(@a!oe+{A+*=>I0SU zX^z(Ze(l`Uki_=s+cOzr`qPWtVN}zKyo{T}w_&B~{gujLKqGY_ZkeWZ1?B!?Q!or` zi8~WrEm-jW)3|Q(ZW0k>GQIW~W-_xc<*@KJY{Y9eR|_9}yn2)PCE9*~t;C85 zDGYI0h95>Hrx>OEqaQoc!%6GV5WUTG}wbRpL{>cb0z7z9c*Yq__Tj)clHM2ayd0{ zyEA|1cHXyZTFpQ_d?1-gNG;y$^%5*s!HFqH9`yJ0&Tw=3+=>_ z-nfCot!m7t|LthJmr2r;jF{?9P5S7;)1!C$ixT- zm3t0_-r4WhGU)!Th&)z0UU#unm)#taBiyuXrs&+8!($4CJ9yXuvU2MYQEAVSD z7VR=zBT5<%8no5x`yQG?!pYUVui2p&?X6bvCr%hmniy}lrP$h;pGC#Y5Z>n9%RhbR z*v(3Wv+FSKI$owMofJi}$YM)+&*y1g!KvY{&wIx;X9DAY!!ByTNBl1wf(wxw zEH1Web6HN+Z>ORzqF}-TfdNyvtHaj$zJ1MM3``fiiuFHqNOx%}EYWeGOPBO6)h{)f z6}4}0#msUx^^0Ivj;4oUh2!~igGVQ{fh&tAOUX`o? z=4$6D&NH`H?;s+1k|Y`(h>ZfmqTnzKG5)n{ZfuuIYnP8hiIL<6r&JoJA#kyGfEjwg z;qJ5oY-JQSH1l0&rYDnjH4MT?x-7SAtce*coM@=aaMn}W? z8LF{(ak4WAYh#J3$35}=Y&w2*S=TM?HM=mEIVIZoM}Flb;6o+|7J$sv#jW7u;lIM- zUtgkM1mhS0FiBR9r0xh&k*6TcNKos}-3<~m30oe5=0Va27G>sxtkM@tMCt)~exFIb zQbtq)zjc}je=4xrzUg)U*T-D&%nvg{_IhQFcq%IS=P*|3fU=<2ydu`HM-Bsw(#jWO z3Q-SynIRHPzwQPluipJir=1G;tZNzbY&z^a1Lqc3WC21IvZTiI!}|K|t9Vci`u&;` zfQTJL8zzk4khDjX;ni72*6)E*@VpS*2bb0wwbI6W`Q`Yu`ntf2ZKYAC_XM|- ze#Z-0ovJjSS+meCk>#s_YBu?O<$V!rIL`nH?2GqrL{>PI1T~)xI92uT8HVg-&;`~l;q&5 zZky}tVbj)Qp|$JHYn{Y}Z$a1)-TV=|2lMXJt>kjO7Dm2UW2FY{Eg76Oo6ulWt6z(; zoyt^v>69ufk5b$Jy{Z5H@<^NzgY!h=j(-!T$b|CjH&=$p)7UFSWT~I)b(`Q4v*wm5 zV7*VNI~~=7_l{I@L2~{WwD#cR!Mjojtyh>OnD1Ls0MmQOdMKc8D{Xv45g-%Q33k=N zb+yxviA`$=FRdD$F+|0i7OJQu&&fT$FZa1E^|Lxd9*I4HpShg$XZj&x$!&ZiFC9_P z`p9YaXk=u`$}U0g!w>fggNa#~?{gwLBiUImV~}_=IuIR)xYT+SyA}E#A}11l78Rf6 z7TLonuNg!OslU^z`i3Qr^_epxN0zww|9xTw7Rd%-I6JhGKB7)M&Dl1usNd1CjEOc# zIGm6%h@^cyKTcHbNjhH|)k~V}tdVwE+QMXBsfP$y&!zspB zhH?B9`{qPnS~TrkeJZWqhxVhgl52DeG|k&ccy#g$8t=kgU2fI$k^|p17yZon_sju) zojJJ84ZrHIrN6oAB?!J$lw0+I)Hxoq#+2xeo0W($x^<3~zx&!V1}<<_lr z|A<>i%Ta;(GJgN(2e^FviBMTd4O)qH_j)-~=-}eK)hfFgdGK1h)vmy~=Hg*G!rR&P za8Z*6TI1g6870$^F6wBR>fI`KrLp0Ui;jk$r}`wc;5eWtUKZ>~;3$rXr@AH(H6wWy z*w&C0v~1LPg|*O*6C`=+Ib{mGR9-!i7V*M}u}E5E;L8}2S)ApF{B*l%Myd`51WepL z|JN-Z_+$Lj0xuq3!n&F&u}*ZL<{e<*;0P*ly<#wLMz;=sL+>?qm6*~J`@~+SE$G2( z?|o0lW>0Lt=57+)m8M$>hF(2oUD>@TkFXyvm~|1ceLr6Gv&-|I8gH!3(a`)85&=1j zwzEL|Oxkqs6$ba^9KT+ro#O(0x_I3mZTX5kO~c98?GyT$!^DSjUB}$zP@p|VFVb_Z z1qzLTYQFmNm|&z*=snbRw}(e8xEZj&Uv(i|urJ zH|{~ky(ipKkUxmBI>&&l;Y-iy_?A->9gN*@8rz>tabZw-+pTZly`)vmD65iI<410I zZO%3&683TVKJ6b~kM|@<{`XLx$s%3g&fDvYl}cx{OdN+p;zP|ERf6d1x6{;za+!n1 zp`?f&BM^p1JN>WmAWET7K8%n`(`6hW;{ z@)&&wQiJYfAB$8S>w6~x`NRvdLKGMVto#s?1+Br@?{aWFJuUBvCu7_0REQ7?C;|9J zg<(b%j&smQNG_J{+X3seOv};MUsHc*GU(jvd|?!Ac<od!uq+` z$wf%PIb`+sv(3uadI5X7V^S~H`^9S2X;cPsBjn7WtI+&cX2>${}ct~ftrH~(Af zaeMOpK=?M@B20c|m<(~l&db@wy))u$&Y{$%*2LRA;E=)>ci?|Yu$n~s!?fVG)y4Mjt;!&94K2@Y;QjV3S7uYFr19u3+!_v4Ie*A* zsoOy(spXTW5Vs7~c{@>+qQvIlf7r4qzu5DfYRk$b_I)Y#oRX!4f0f7mSZ(sH{(fmt zErZc>Kh5T&S6vb6KFY(b7GYGKD#A_-d?`yFl%=kr9)eUsV~4HLrS*d?*n@PmVDK0_wg`_;f~0VNbov z-;lLT&7X}?IGxT28%=&W2}OrF@3<7fYm7y0nQ=%vSd~T27u6u0#^Oqnu*{nIQ3;Za zUc-s~<1Epgp3&KJ%>WGL!86Y8SP;>Ou{$&5aNI>CC^|n%Ge@IweCWer;@)HdvM-%K zcn3#ozB2sdq23?0x(jTcYhgv|2YAu0e_(bl%Q5vI zP5$b4_`hRL0XpIi9DJ8Jg^7zGFyXe$Cvx{F>6wB}=o^GC2l!KgcumaqMN*`+KWZmU zErK=8<35chCi^b0puwk-6^mTq{u%i(t5WpQgVapx!mRGC+#Y5ye(}W-r=>X@yi%z2 z@HIlev_dxkSuQBdn{$_CgE4{f#gnHGQbWU8`;*mD&7xD-qcx*tbzaZr6n(pmuoHry zv&l$|zQ%8Ceaj^cFC}A}y&GSD`b>!-t1O8MU|K&=;4xwtVBN0`EIu7+CzVEWZtJ$` zxD3Y2EDS(6CH|SG+M=Rwq`$Qvr<32lB`|PT0s_6Rer7**-NY%AB%gM|HRw|Xb<<5X+?KLO`Xjq50aS>KlS zzMkr+X_~x>^qZ&DBmx}y#g;w%hDNhk&)tN3ZW^(^0<==JO0?ysLG6gCI*Z(5@&ctF z!FhGDfu+7HKYx(tFfO;uANI{{u)O=J(Io$fOgtnH^|kt>aF$zB+L`tqojh&XyJz~o zBlyld$LaIkI`vOjOAbR6)!7R^ycdBYkxg2R6eG7m`0cq=UP)om{K7HlcxZGeNI7Q#{)CF zudks)z_0O&FK&wb@Yplege_|1t+l?G>3RiAY5&kOvF$H3dk3Tu_Fwm#gC9_Y1Mu+w z-;v@w;*@)YIl+1QWHNkJ;yLA?hW@l;#D*&(MDazbnu+0$ax zxAq(ZP*-#S|LyVB^n5?Uiynq*;;495=?`WB2!{BK(eO33nyrq3Gb$jAO1waHOYT_i zRbOuQQ^-3l1D(64gfE)|9E2Vde{oFX9>0f(EQsV0ub2S6s&ro=oSRibW8i zS{I`Azt2W~0YZBu`Kk^6lb_2a=LN6h1FI50Z)ddLpIm|;8%ENmn&3d*#H!IZ3RK@U z5EbPA_ET7V_qB6}q>-=0$xohWdMD%0&P1pc{ZFtm!NBi#Hm{0AM9S+Q+Tj`ai}MVD z@f@X1p9RXgDyiccpKE}XhF9Hg3v{+g>%9U}9jCRVoqGvMUzUm7HLEKny+lPGKAENs zcdg9y7SVty7rN#3ciYx|80VhzkmA4Gn21h{lWwp1W67WX#4}3zWXv5kY6p8UNl(h^ zSc09^u|-}c=4rP*ak})lx=4jtu@|R}+i{XB!~u9FzQ1tok#H!9HQ3${a3mPory@k7HCC zKv{drI)5XXx$`@xrsfne9>{5PH{<^B*Yf*$|D$xl* z$Qh|na0?462PM~S2+*^;GmAc@d%MK{V0%28QH_HmYFGhL>zKgXf(}WJ1!$VD#0yL9 zWv}>IN5!|SSi#GuKJ8eLa&>ATmvd4#)EyB@fV=VRfmq7Q_FiqPNcZ)y#D54=|M--} zb!5f8BmyQ%Pw{i^^G>x3#l9yS{0C}F{6KHw7LJQB&(`B_Ti#bCwberVl60)O0yWd9 zdM=ZN6QdZ!Gd7mu#q6cJa6WpSwov#K+g!Ii9r?=$swNS$@w1`GL!-lxG|QCh^SNo` zT8TX68Gm+GXHBVdk2Tj=m6mbS+xa|_tnM?~ULMih(dfR+S6`pZ)}@fXt&;1!7gT_+ z@a{Kc&_wg&k*Py-X&uO+rAouR@8Rv6Yu+0d``iwxY+e;{PQxk)HIIyLAr*oF(bvvS z3li3)W;&tU+ae~3o%Y;_(dQm?oR#MI>r#bW)CiPbefZL)_{jQQcgzwHKK>$E9{g#{sfB zr2{~>e*Pe?w|oR@4tQ)3rere7+opM|HNh{oP(`aH=97q_dYDgPR=^|CW;gQAF>U9L zJY*=~Gq=%)dU_tvN5Te=>Y27EslGG^?J)%K#!1KH2v~P;>YPX_?l-!3iHnCr8O3)` zPDpaCYsG`MnRAODv^v4wvJo>bj{PKO-?G-8!5GCBVPxt^%VE0ANP+JhWrqTLVkH0N z5N?YyZhL~?P1T6QUX`dO^C3^^N;%!!i2Er*+PxPnfXzr%kLeNxhI!k85^^t70|S7|WMsWD@ei zFI;PQXu)Yf{rT`p?^3MyVntBxBC~IFqfXI?{O6~##)IYGDZpcO7u|?v%?poITLOyV zuuVd?s$l}Jek6}j(mg|J*v!GxbQx%iQ+eNp*XYFp!&rUIaBR-IgYn*0yCtn6o%()x z344Npx$5bfKZXmpqvz;#JR<)2|? zVTQtByoc-QOo1KcGSOWQ(*W6{gn|~m_~4jw%wr`@q`u5;uwuX?>VVUANI-yP^ebT< z$k7KmO>R&_KX)yb5Cj_iq*)R#{)9|=WeK0lgp~j~F@wc;o#p#`Tg8cSVin3zB}>&f zMEN5>ALFOe4L5{tEEj%xK>;YPd*g>tfLsjJu5gFZazMKhAbUxq4WE}uaa(d@pFmRU zD)B<^GXq4X86}w;#%^`K;aS|e3%f^$u%paTphOfX!91tPWBK17;3+NP-M<-CTpM{z zTNJph<0~mO>l+##jj9Fj#RU|8T#yb;b(EL-oX|Z8W@YCgF~2dGMx)8M9JcOfP1#|8J@5k|YV1Ub~)yh|67uiD_F zAxYk?cd;?0x8?#1O!qTH_jO#oOy&qvhC#Uaz~e*By~t7S)$PZXog!cHKDmQ5n`UjF z!^Nn_z9rxfF-kH++q?pgrM*Rht0LzXp-X8|6A^d4I_G>bV>5EF>LqCX_2C$wxi2bj z(`l>OyC+HXmZ7d`05;s9{2qriZ64&|wWS?oM0mgiFIiMrQ3ZYdQhyNTyXfF&1j?)m z&YK+Vc^ypz_*8%XuWIn$@7kV(%RVxW|s$Q8;28uOuD*VrI2J zKVD=Tk`+Py-UW5MELRAitIH;`kB3M^#lI3pA{e1j!;GfZr%?z$TB5AIE&DFT$gKJu z25Szm5d@ur4-oY)!kRr6UrV4`W;48iQ#ExlK7dY#YRn6+&$^V|Pp$969Joh9-*5%z z#ltyevDD@)EDkiFNuFTj(cVUWKx&z08LpMZa;apE@+A{JivTRT%j?< zmf6LC0yebHMy5EVZ86O!8%XTG!1SoU!r&NRhnVjK$m|usi+=#G@0y+SyR^K;&n-&7 zys)dA+kh=crOOvq)nvqtw9Na8+8=)hzZXX5W6Ob0omEcBdCk+r9r82%hL}d3GCij$ z3Iu;M_G~|U5*=N?OD*|2vS$C}XL40ANq{r^%=8}|y0Zz}bxKIj^=Vt}b=&8|IT$Vg z3H7ppJLImeHwWic9qMX(h*aWH&++7Q?!99cxTuDeX<+r_UPCmzV$#4=5aPFtzq1{5 za!|UmHGS1D0q_%Pn3_F-S}JOD7WLXCwP?lte?h!|so-g{oU~`#^_@LsQz>~LO6ZKq0~%Dw5YT~wR{GD zXV=3GArNt#?Wb z-sU}E8?YmV-yc>2;k(Z)8BG3k=Ho6Mq+Hj>N~N80yVW0<{ZPUUo(zA)l!z{l)nN<4 z&hib;%?vM@S ze>}K7PM@zBB|K7~5{chH+h@pVYZIe?+QBHjEn-)U9;AZ_V->eJbtw_~1-7!n-x~tnkNF1tp{ng{G(l<36o>JDvP$C!Ks9{;>a>{)N!dh0v7*uj8vQl|3BL-+%Rw z?9P<0-j&R=R1Aa}oO;GHNp=sle)oTGkuF$U;mp=ROQ~Yy=)ycA>EF=%Od4MH62I+T z%vptw!LUvK)64~fcQgqX8ieh&+WQa&h_)D0-^gpG-R0KdBlB7N zdA4HRMruVj@aM$Ow92PU6T^S7OAB9y#)Qmy1vSFWO!el@c^4}$CGnU7y$WbtYvA8& z{IuAkr{UmR!8Q`m8enhZ8z2LFx5107q9->k-3n4MF!(HY*$mxB;HhtW!~OBx-%zzG70W>|P&(+lqNuydFnp zQh@|PfLs*t^;+*2Ze3l_;DbbXTYqUZ__lP=$Q^*>v9^;u<4bv-68km{iLS7!n|KZ^ z^(-Gq!#i3b_6hKyxSl=_kxwx$KcYy@Rq#cDA|D_t~N#5n`<=wbmaiZgt1+w(CeIsoQLkQa%kh!k<7* zw)4NLfNgicA$P!^$iZlDO?i$b5YvpY7VWq6A83hz!=?C=%Y zt^;H)ek|Z-6kJ=rlY{X{#fm3$L8AKy`#V-2tJc<%BWGVeb6!9Oxu7}k{#8<->*w}G zqqfgk!4r;~3&q#lb`R2MWu)MC3@pzr0`%&RJJd?-a~~Snv?$%AWV5FA*9RAdntI@& zMEw&E8`=jzbKa=P;lCfIf00yD)QQmwarkww>>L#=G;=W{dXTmjsTatI9jb`}l81Z0 zDwTj^j4h|z`gS)s?gBvAHJ%cnEPx3#d)RZcekIbPDpB*7zHWj;WV7-U#9v$G(A?}# z9BB>RF0?bU@2a@_DSPddMMqCdi_>Rds9bd(6({Y%tP!O#bh+`9WbDl(wWgv6Vy(3(p`~I`ME_+P; zE$Gv&yO8?qqBBl>1}TbkUBEX`{K4aDdE&VL$y48v3Am65bVg8B;!WXgU^I%n?wf9G zg*MOo9!PC`v2qNf5*?WUU2bvvKJ{^ta^{ zcZSz-{JR|$5Fh?m>JH0TdCNQ&X!u!q;OEu$=W2G&p~Pu1GVxKC&w>3_;K8AGia#O6 zpoVm#t!AQ;OQvTDf*=x{+^=sb7o)){2ZRs|q(Y!{<#N<_c8gqIb|LYlx41)0Nr16H^v-f^@6=YddzjY0M6VXpo zFxD+NaFrB|oY2w|M;}P8te}G=PJ_d$^#%}P(>3>pH}j?;ecfDbMg)d`R!xj~0w7PF zXMT`fT^O2GNk%L%`9AwO{h@&GnbX8f5Vaz{vk$-5=XAD5`{rRk1u)twkm+po&6WEf z*k4_)^ZYK&^s`dqXS<~wJ^cFMK257T;6IZ8TmOWBCo#IXNj{vM=lzTB_oDnyQET>& zKXhNWfu#HoV(f2AZMWm?Z^upC(1k$si~{YqvefvDn_Sb3kgac>Uv9Q1Qb|n`J$iUD z5${1HIcK4nShB*^qtW%HdUTB?Z6jux9~AU-8eIR4Unk(A0QhXVxamXqErAPlhHO%< ztYGt@dt1vYrR=boF$F?ik%53;lkEmnOsh|`PIYh7rL&7mzmJR3`7)3q9K_{Q80VP$ z+%JX>NLlHQ+)n0kyHF0~XK$b_o4`Zlj(1qg{{K#v?}!adh*N3_Q}FfO&h9w2*CRv@ zlRuN6y`6ZpQ!#|MMV*IRD0sn}Q5CkozS6CACbNdf-|;zFsKt+JlfF3P28jQTlU#v? ze>Jvu@`X#9%*+OTMY{_SF7Kr5Ori_0pL6jPr0$V^$1Tc|IC~}vMK{lX_<@RNbR73c zWwv}RvgqEvZC48#@Ayz^Lw4yP1>~YGvH_OSgEyb}v2q z3jm;AbSLdZ?u!R9E$H#X*J~B(p2*~#$Ml+*wy5vo*pEai5YRRqjJ@8^@yGK7nBMa; zZjLk$o!1w#@t7(Nk1zZ|j45$R9^1tt2KP%9d3_&5KoMH;ab(hhtm0Pmw_u^S*~J2P z7^LYnVT7Mts5J#CD@B?4<1l1;=#1iTk&!KUE(aT!HgT|)j)JqZ#nO=mdeDW{uM|DL zY4no;ew&=9jPisAO!(wqxc0F@TLqyEALQ~$TwC6>sRblr{Mn<1y7jqqr~Nqxx(ru) zr*Zi>X>TXneGP}#?*_;C@(VP-gQmbvo<)YWS(5;#!-aE7p7#@K2*vJ<8n?wH_JGO9?#{dyz5o+0toQ}-sTeTS^>uk!(J1 zSWj&1m-SNW@&aM~p|1D^X{~jy#kjV61U3*4l?Pa~>Z;K}x3Egxi+vh%|R(p{VhxI!-9|r%&6a7CAa4`LCG90}B zmOw!83xWaF9Hb$5?%b|e`r>g+fZyp%wBb0Z*-p9A%^ZG^RCi$}CRZtUgMe*^;DaX6 z|Lp%T_8!1=wQb+G5FtdnqW2QL>=2#31rbCi+SYp~dfQqMiQXk@^oZVD^xivLi{5+9 z#>Ttc&wV}b{meYq`+c)!7={_vI@fs~<^TU3+v1^g5NFd)5~BPtN|7m|@60-gpDi*Q z#~T1)xKnA1Tu$tV^heOUp4EgOBd+@NYv}#Z?|Y6bj~YGLWO z*L@o4YCm?ESeuEX5C^9aa~6{KB)_grp$#wp8;4agpN)nnky*r)=7|1mc1B=lUKt}z z`BO3O)b-Q(L9!b0I{pGD7Ai6_cmuqe+0C}OadU-`*C%Q^cGuYqG@bq#)AGxBLSFbL zpX+VQRgk*gE-&IZo4TPSZlR^CBpU>8fZ&;~tH-_IDYF?Pqe2F1R&cp;H@?ZvsLj~8 z3Ir8{VkaB>Wp56MnieH-f8|wDNq0cPMgA|w>=42z{k!G0Fhulfm^93EN0@9N35KDe zA$?NKbfqERHIu3ZYiG;g>!|zPtFm^eGie}+PW2yBEd-79Yl_-kK-~BsAcDT}Xl2uHEND5V~(VA`Mi4anPTC&%fW)`J&Tph0(`WQXSLbv52#no^=5 zyc?*!^Kk2~XlGr*4FdYVN?8qm%nb|YR%mUN6DRqA`lgE-70aiqC(4*L9frNm8)=*^ zmm3$Vuzm6cbZbTJ42Z*Cb==zctY$OcBiiz(rSMp#fwoD+>Y5y|pszbXTW8Z0d|t>8 zkr%E`kBNg@YnOaYvZbRc#O{{g|9qzYQ>$-xve+iYzCjzgr*zWf{$Hy}>keOeCDaR8 z^mpl*7W@DAWcknA^WVn}@J(g6$HatXNK#S9;Anp7YQK?g;fN-W%cCi$EpbA#bGWr6 zWbr1XhtfF1AP5o^ju_AEdFzUHzHMDnzP~b{QT{bdT7HUBhFVK%m+3Qz$_thy*YwD7 zGAB?sNWQ#NT1gaXnNDEqtDz5-+z*j_L6oe8K5FKgwaEV8lb%V zytC}MXS}{8loUv;*L^DlX!Woa3{{TJer=dXML<5Hzz-3PWJ`33`N9nL>avi3wh14l zMY8%vywn#XEDK@}!aP{VT$f&sixR64Sn^_MdHI22(!mge(xXfr$iuX#NH|h3m7{r$ zJ2m;u&n;7KdUWTVN1ALere?JG<1UJT9ntU${r7gF3;ouLH!E&i|8&d#W6=%JqQc%% zmn10UKGjJ z>?GsE{V6_bHJkmu*DO%97J`<(I`ApAipg}4ZhaM-;bi;lDhYB4E$r?kG_VoZFsAp= zX4S*|5>tfaVHW3%cm7JCmcjF)*C;ES! zpeG;|KF7VenAr)NENF02*Ku#UKm;_)fSA?MY0nSw!kpwn|Umm&EYhg zTl*ao1D}iB@v^fjf1MOmH?%rlmLWHkaQGPOG zU{odsA!ME>f=<5K5Uf7JG?A(Cf07$!J}egq4oggXZCt|b#`~FCqzm7mp+b@*{X93{ z1HNZ8KA`BU+C)42m)mxZ&0l!U`!A#Y^l&0m_vI>?$Jdul zv+z;fzupiGl=n~@lxX5BqzaCC>3WFsFX~|YHYXh9u)Ds>&nU7OISi8zRI?RMpP41+ zv;HO1bc*kL+;-zOzZ{SmotJe`t$C(ZH(q6>R;ZswgYyuRm1WWWd#PtNJ!;nf-|IjB zqdJJM^&xn4`y~?0dpR?^mM=aRVljLN?xMgqF+JY-W>UzcZzvV3=jhxO0|r0Nxb^MQ z)b9)&7vkTZAT?o*<*1$@rRs{IZd$i$`gq#i$> zqqHg=N<2pKC$1HoEtA7xVVlE`hI-(eZEP3xqBXgM=s0c@k?0ZJ@oL(dZs#?xW9aS3 zDZ8ZRM7A;J(YdpA|y&5M6@{F{RBWqyM<0>TsR#5=G z{#zfmB9y{o_U>0`fzBUljavIA#^hZ#wKY$e)ieI|TDcgPV}&F9 zvByXdr4Iq#6{^0DmyzhHuy&H~=h0rsml_;>%N>F129S6R9+&kneV*(01}0bUO9cn3 z`CV4dVp;fmIDk#Ld+}^5<8x}F>*KnQ<^W6pq>`Ll-iK2LbTuy*-TmfWCWf3k60L{3 z_ktH2=W=A@Y~vL8BWBi+Ycx9AH_pO@`$r?nqeQ;kL`9&ku34aZeT(s|pSY~o_p(Ua ztuGgBw;NyJW*OE|Bo)@d2>roK{9ZRbYye$HuhM0-Ed)*=O^IHp#nx$1;^?{lbuZ3S z)>(V{{H(ao6L>oQe?Oh$_o5j8Kb}qtMF2W;S@=^&i;!+#kzm$26HP~HLn2lQqmm|1 zEZI@%qzI30{L}Z$JRM1s%uX_1=WOyZBR|UgNAk=YW}uG<9ivspkp6VGY%T;k;pC9$%8NS20VBPwLgpdjJX;=O>* z?2xh^<`ea{7a7nH+mG)wX!NT``7t#yaVFlr!crVyLUmtsO?p5))GWHq&vN%rswu}R ziJk;CT}mAO_O|Dv?J4U6+u;hn(OtM?h*qfA=vI%o0bmaM*Lx-9&}!@1U@%w3 z;bH?c7>j*2Lg3=duLasB`-V0EtzeyBD>T6*PwFrNzNw19^C^ZF!V`6Dtwi?g;_QB% zFx8uDkT-S_mEo^{vq-OyrtyR)+gHW7?u>JG-=4=s9gIoS)I1EF9d6vW25787K7S5s zPj)k!eK1U&DTGcjyaA(6yZ33mLMoz?&lb)?{N$iUr;Cbv#IT^Uy558i+rmdhYR&9x1Jr%2&D7P^&WvnHMClpuMH8V?I^ z&-nF%#nSEmNe@lLUOM-hfQ}8I`yK&bRy3s?Jx_PIHW7`R#uI}%(D)((A`rWB6aw}| z#nsbuHEmBQ=6r8x3~}0T66F~CuyxM+whGbjX1BLGDV^pV^V6IcZx#r&xc)xvxTsT@ zcQ85BFkz=dEZ%>@j%=IwG%GvY7MTd=xR4@V+V4E(!nH3~r8DMpc3#lY&=oprsSoAjvUt>z0ysK|O8JTj zN*V`;Qe219Ux{pLH9;?&A`cJ$GbjAd)74N)!heLW{rALEqag;#l;`B$hxpZ~`+cu8 z)o%S=qHT^K&lui=PB|Zmgo?g)m}XC3m@d?kS1gn!$-W85bi5wo$tR) zl}Zs%Y|$>DC_zK^7&H}}kC#z4AvhH$<|1`+MsT7g(gM_)yv}3={3D z_+?Y=P5~3T!BiDJpcnV2nISnLA0eU_{R$^1nG0m>(pMI^V{%dYLFuNQ$}!>ls_Cb+ zHMYlb*KxSMD+742t{#1UY%HyAoIgNKf^9(1^IRkp9@7qYtQV&|CZ}MEfEO`$_f*(1BzTViOi{%0O>oz=VnO2ZQqg8 zF2mto0(8$x!1h%=1Gaj*0_MnKQ;j~8RL>>iEIcJ0(9Xf|Y z%dNQ}nc3&!=I&j(KZ8>Wz)-VKi#{{TI^GA>b{c}YW&lW%JIQ*g@EevE3%qXSla1Z) zj|xvt(_5BJ8L_@LxE@(eNj$=&5Z3MgS=#E1vl@3L_|uApSHaLh^q$q{ ze$m}+LSZHZha}WUd){EA(g_Z;^6^Tc*L;`23sUS~X(AVKvxSS6E5oWaSUx5vMz{lr zmsu5Qk|EZikse@U4MkbPwmL&iG`o=X)Q^0v@)*%fCFTi5-W97i{VXQOQj?R2^Gh?T z^vh_Beu|FjYD40Cx)c=M@vZ4_@LI^RMekr6oOnkRIU98P%nN*++H`kBprUHG!FE}S zJrCVDKKjHcEBBl^RJ1{NeCRFg%oSPtCtBypJUKR2(pmA!GR1kZ`QI+s>L%7l3ywgx<{~A3C?cYcC_XuKwr;UhK`w_Ec9K z)UZKa^kn2`9RK%3oic!kB48aWll#$AEG;;1&eu2I$NMXlTf?H2nO_T-M=8mKc_Fs# ziMO6W;g}WioWqp|zab zjINL0YFe&W6!f8frCEgjCkuWigtbEjDd_k!OCZw zH*=dQ4l!>y7Nga;h23}Ym56qxi$nOV7vTDSx9W{s2JSkyPjL3L!npK$%du4OZJWD@ z-PU97pD|laR&$%YoxlCFM4^QI0f1oiV0{}>SpM%!Gy#dJ&mWxHBm*yQ;>lE3jhnBf zp^{&Vh)be{=?FJ@0nPM#KaFrH);MORio%j#3%)l8wU#~Pf@9{By)pbA$Hn|+{qc1* zfKfrp?>dV;2p6)hEP^~{=!(2$z!S3^f!xS1EC}A7zp|}7hMA9~+ok?KU2q;D4Stb| z-%;>#bk*riJ_m&Lx7B=2+LVr;&&}P<(f+JOdd>Y!%YEl+ynfTQ*2QsB-6#OZI`~=0 zRjK{TVm2?u&Rm+xHGHa2a|9q3S}B3A-c%K+RK8`ote=H1_`ge@5jpy0CK<9|R9I32 zWwGBi-YeGZ*$i$m_1(!O*Ne3>|u zMh2Z-AHQDCXPqcMP>^IG6>*q`H&lz}ylG9Hark=sawX{(f33TfolUU@{OA>yI145L zRapnI+qBUTpB|GpmOTp#!QyOjM6o{mnFH2If1^RN;G)kG*lmg7+kM9Oy0tn662G5o z7d{RZGrzsS?V}e9!!=v>-thu2x~^}GJCmk2R_(F-y!AYr>=sybZ&O7u+>|dDRDK&Sz)S+xA&Dic05M3=)eC=r*g$vsOuz-(M;#naV=miev zd`ix7N?r5@U79pNCpZ)vuRqPAjuH8DnUR-0t0eeGnS?)U+bVG>Jk}->-27PZ4ydyI z>}4pUmmNl*PLk^9&@@Z$ z!+Q7WDqgoRcEc-vUBAvgy3{)-*uZmJz#;wRWPCNcu%wwQy)#zf#WNO`%nw;WY(W|< zl0clA_i-3rF4oN#)MUBm>byih!$Jhe62#~0x2mx7(~RzUK;eV?#K>kRjarssB4rL*Ua_1E&2SR7n3I0 z=?#u-cu56!PmH9#Oy+0H6-OXI_uURCKD_oU{?EhUL%#&IQd z@Re{n(ZSwyfeo7dK80e+*u5|Dk+pT5b)`bt8sY2Eh9l~ioj-vF{x5N%cTW~QZ7OYp z|7HO+g6{^OkFA{m9;2K6*@d5$QU=^VGz~>zm!oV&&hITNB~57bgK76CURm7h|9TlN#WYuGG3+ba zu*$75gu*jBA<3rNH^^h4Msby;bTE6{#rh0Z$s3sxr~ga0IQf+wOi3+p{GBl|#BViIcN?=xcOFX-dy{Dwomr6b-PK zOXU}_ds`}DH#Vnn1xIua!0uKl$8|_{w&y+I@4*vbJk|o>$=q)20&P2RDCU4PCN!&S zLyI_r@P(fDVuTfzW07l`r$*y`!fUiA3&t6~r(dY%R$tnTtsV#4I+5k}?d5)u7`|P} zp;XKiLM%(#&FHyfOQ}^m>pg_`hxl6h*jm1bl@O0zQcX{BLi)U!!wtyizBuNPiGn8- z`EKH=zuH;f713BzDjo@6!cCe!GiI$ABw{@72XxDxUVsv-?GM@>)%AYL<=70v+PK4! zs>dVjC(a$}pHeX^hj$C%{h6Q5Jilk;E7oGMtFs_-zMX2RS$$ajnLkm|2>#^IJe22l zsB)9QlA!bCPc1OL7&7N#JwrzvNVvNk>HTT0LrIi;YCe6u8&_m|R6=1O$KADbZ@>7@ z??|Z~rDuMonSOD^Td?~!Z{7>`8gTwGuQ*-G2IMtn$5&BsK)9&#-NFPM$L0hx~2wcfBDzL zI^ueR6R}*%nqP$j^0J(-P^Dcoy(t5kBvIN`hLMQUdeiMyHP2Td zZq~lPD43ogl zD1;TLWt$;R^jW2EIU}wx`I&KF6tMH3y(a;m9G7YxZ;(*uAjU&_uHdecK#-!}ib^?v z*MwLNd;8omO&KU|VmBMhxCLJhNIt@*NYWb?195#}cif%Q5o`SQXUTg87)3HsS1{YC z)I#r9ULX^nz?eINlLY~cc>$jC_ibzXD|9My%c`OagZDI zhQGVdb+4!?66e5O=Ze#oAf2oJ(&^?nE~@Bl9ouP}$dO)Q z5+6eK=2@3WX;HlEmV*X>==cP$mgX7 ztb06H6Vj=^8Mf^}0b!6rioL0>QH)%F zCSbu++)A?no~91Vpmra*fK{@M{SplDG%i{)%scj^6>C?5D^7t`q3?Y_MBz5ll11%S z-7cmwcSVH2Z^86-)i>MErN5&* z&Kvq_HeoT0b&qM#H*Me@f$+T(cCPUe^UWGcK6{w{99i7h{nHcdSUq_Do#Ji_zFXR~ zDIx3^WOLT7lumwSQ=&Rytudgx7F+7SF`{gFEcy9rt&_C|I<#9O)p60`%8dlIt`-0m zpBDL>sk%9)q);8n5^85f2C|z0!W(Yhm(V7Sh_4P2KiRj`K+mJJp68h=+Vs0VRxF%G z?CrL_A0Ck|LpmTFBA|T58W%sA!u`G;hfU#*WE-Vbn;aITb?0_{f%&C?nHyHD^UfTG z2p#t;v`s{Aqrn@H%PEZ#<0;7jX@_9HQr*S9ITm5$GO=5CU;dVw&WqA5^&~j+{V?G( z-2JJgUFNRUFkX&8Wx$4|citMkg6_RKm;HQLP-quWPqvlS|6||v1t##{RidW&cwDaU zOq5@)s+-_oW>C$n=2pSB5JbWo~t*cRZ>LOQj4O?xQc zd3WV1U8y*>kzBPgdqG3a&BAoyFF(bTn4IUPTtWqebLmqPNKOiV63dTB$V8L&YKg*# zY-uMb$%1e=F9IQWAIp&7fVo7U$!1cAJ4rFMgST(F1tdey!i>(K)nxJf8qD{@9dkPpxPhU^ZE)-f|?<26k`c zamOFGp(#yhL^5BI`;l+gH4){S zvXNo8L4A>KNBc(uktw`YZAwk%m2vHVg^DW*8M@Cq+zf>w1_fAUAIbIjcG#M-EcZ&g zyE;6d?hT0QdcN1hx&CfN5fD{qR0Xh&@eybE;Xy-ZR@ZXO#h+;noNde|7N|Ek7lk#-$P{2LM2d7 zz`juxGf_6!;o8f5s#L@)7=E- z%AN$0`TYBsn*)zYv!r^r1rjk+mP)&bM802xb^ji=hcxRmvyAB2Ub-u?XE6j3zj$owOS(8NP z4>v1BY=0?>^YhxQzaW8&pRZ&0mD;YzMTTEUj>knktBp4x`W3%9CjXS1&Qk_kGqy7m z0E#tiWAg6&=n-(*Cq?6Jz!z4whaup{#yK@c$&4=R()H%SMgUK2v}5Y42fi?GR}xOs zDNe_}V8U~qlQAQX`pt8wr0^$0f>j>MM<(@K1Ef~Cx{ht2_vD=uf} zYIy;vE-bBw=wrv+VAf0g@M~!b2}))xQ>%?$0)K}x2QR1dOGh{I3Pe^gCK2;%< z?`-DCuys>xMo1xDboS13u)Jo;7o!!>7$s8tOA=YyayOUEYZcoBl&VmPsk@ik0`1y^ zp1SqYg}`g(iJOEEfdj1Cp}pXp$U#-y0gz`53{{@JNumO3Jy93axK)?Go{#%8 zcK8v%sIl&;-h9PWSzNo&U$X1z*;Z4s{g1K~l#B$;AId8T|4#Tr@AGZL<;~S{gpVgt zx@FO=kBd~e)lUKCG*q9T%=P=*WB=lgo&3!mOF)yi{UN6eb4p63rXUYega~;d3~$Nh zG=-k1Oeb=CMW5z|5XznMHoNd7Ga}h~9_mC~G%xZyNo{Zip>-(YctSj!8rR!d%xNzN zucy9J``7|*{=dOv89ZmNNhQZ~N&0|!0&30|$5x@Gj%?E1y7dGGT0utf1)7S$Z;Hjf zl9oRC8SL1klHIe^^AuHa0EDW`$m@M+JMZ&nF{zh6@dA}NEq45eO1AQcu~(ocgdmsQ z$u{#~g|s*&p&_?<&OiW~@N#uGDLV?nM9est&Npyazh!^39QdbLb>p4eNmo+WM|2@j z#9}O)AsNv5YP_5)G;Zqy+D!OLGzF@;?>K&ZFIzWoUm}}mM#KV8FJ(9#TZuoqP#1*o z_CxNNXWB5{jIHT@p8Pl4z$dt6K+~$NX3on-~AetpdDyw^DY*eGnS!ndJoaNZ!`s+ z&6~UI#wzl_#4%{crwDrfyDkDPeSEFoSXxewoK^n8?MWCku$RdWge9`sCnD!S3!@gm zOm(-Mi>-yf3ME8tbf4BUC{qFy0}Nq89?IrNQk+U6Qp-3S2 z4v5Tba+&1Qsr9&<-8?o46NdPI*f1Z~+K|%|V-vZ9r&ujA?4SF6vjkX#6*nP-`k1j# zzViDdSKB(Pn%dP~)VO*6kyCZ7k7RvazZI}@Ie9knrbWlLpy&4*e*C2ETAn}7{@k!_(UNdiW) z;hJ=1Ci!{U(nUBQ5TZeR)%`LN@Py>}_XSjsCI5Qy6vjI1q5_R3{*FOB$zZi2Ul!Nw zPiVlstck0DNABYRX9J2%wjWtyv1H0J#Zgs7VcTt)Bcy)kKpW#ErbDe{oBI<;mC_3V zEk$gzx_bb}<+}7q0>w#TP|XUUFZ^^uVy_ta+7Xju2Q|?r`G`UhZ#Jv$IBZ=FHno8~ z+Xqy0m0fz7&h9Mh9%m6V#a8g5{qbC#^FoREYhcvH*Rkwk`H`Z8btNq0HkFdWq{X3~Pi7YZ|a-9kLDxZ-)d%g_KU5gaU>?J2wZ*4a}1n_+( zCXy;os^)eJbuO%@_P4%>&1bCib4NNI0o{?}N&sr9FY`#w^JWkz&hliykTE!va4~zB z`VXzP-Jm#Ccq^^L_O>7Ob43lomyuVh?zCpo*V}-l%>*^Wf7Jg3j$0w5rFV<)7~flEd$%@%>$JiAj(^f?h+B z;xTPgc6ZvHcLAD9wSeiJ!c&K@#33oo%Tz;OA8-ZMg87L{dryi!B#dfjYMIW34_f65 zq4}})=m=Y70LKnGM*-E^T~xCv2zKsi3i|24Q9be&K79NphjtNixNw6Qy@89mqyEV()_tREmJZH`Pb_<-PaU407Y|37POLxZ2Y{OX49jl zf0B+C2|g4ABC!jpD4?5?)h(BI1t7zgv?15ajSIS0u*;45=@1{!_1@9U0+df)k50_O z#P;ddqg{9^2Q^JYOB>L3SRsz@NocuUr=HYK0Fr+G7rTu)iJz-LVI+hJi|4+T-z0Lnk( zv1n(v^Xu%lQ51I;+&$qK^>GY_ff$R{m0N@Q#xj&nGGW0b1+2XABm-X~fjq zZq?vRxD8zr;4u5LII_Z)l@4x@GC;GZvQ1z=E3WtO+A9_~0Gj7;rWAW}#h4w{qDN93 zN6lx7@UOT0>8Ky+Q_3)?T|VeaBFuC;FAp0(br<^K+5y9DEFC=v$l|Of@@Ai}2&WS; zG*=k-UNQE!d8XLaRCYuO_D=)jLW&eJg%b0X(t@0TujcvZ?UROr+a4yG0PXcWABqyx zu`^v}+qtq~K%bcTnJX@KJs9$J!EfmUg_y5y&8XGXlm_-pd>&CBTgs zz(9Ry!Q~e{16-c=(N54QpbA3dXResc%L)@4!OOQRwg3v!#+N_qH`aDM69=$>LrS{# zL)yUb3N$HRuy`Fla4k)oZ}w~#X04%1jm{CVR{|(CY`$F-w55$SSw%HpIi|WnE??W! zY`p!8b28sXOBFDb>3j81dt3yPHr_l9fh(pEcYS#dy}P~r%CdEy;AO-4Evur6C1(@> zx^?vN&jizF>RNkGDBoE1yA*0=egr#7Z3Z>yQVrc7q9#g4iP019^F5QIy)8K^=6$!I z)+#|aaJ^#MEZ)R(zciV4&0pp(zlx56yN!tAafF2fkre2JCbBy0by+5JGL7#pNa6N{~&5CY)FZX8) zEx5>>_)l^ezIm)-B*<8mhuRY1QADV86US0JcZGoJ_W?j2?^_!1bnA5=+DE`T?%nm5 z5}0R}t;8uIZ8a>)?u+cx1o+Dye^c7nOxAirj&o61l_sGA|8Uv*>)-G$yrSOci4`lE?Cw|bxDY~`Z%&ig>|mE;aV1fiYrynG zaki%P=k5e!$pAP`d>`xkUUhw_Zol6kNsJlTT(Qcp{xZ`LKc-$VsQ!8U;^X0Tj_d?t z|lXfez!z40N&uJT@42kd3S|& zLe@0KfSlX`xzvWVO|V`8Qwv^o5Eqz|QU{7xKLLZq9})-g0lxp;mUnLJKG+ z%^!yi_xBHE`khx5Co8oOOF8veoND@~(2P_2wb_58BJpsE{t*FN+p6LoT9o+>35_ zqyXH5;>Js1{+uN{D>n2fR!qpRsWBD5IAs(kHI~P$4i7fgxhA|t2eA;p9>wI;`q%>K zl=XDBv%vf&jZPBF0ss&*uU!Ja03s|xp3-u_%ws=)30MXfIhOw4D-TGOu#hU5Lr)_j zT&6I~m`lk~y)dayUAU+&x6L~A!Oh!dfx5z{;-!w(&*9nz`+wM}FURwH1QaeFpGJYK zpAs7IVFwUhbZNqXy}2P8)<>-8QQy3>QAxKo`2;KjQPt&%RfRFW)|QnW$0ofvEv|3H zH>^;gLFHe~E)D7V+QlGu(7`Lh9EtLmVef`?7fomg=ySd4ac4+RSZgZKgn$5&3$@7e zaq*IR*fK`Cab}@0iN|SJv;O1mXjr+(5^ZWL&!CdwmpVj}G5+_~ogo2(C{3=xrH0kGnJ_|5#Q#=U&>)=-MY&=ZhF zbja#}v;#lbEwJXNR^WF6fZ~c2hi0Mlmme-?Ehsdx0LT=hI4$QLDW#&OHgjr4teo!F zD~PU8FxSKFxjE}ns7+?-J?>jzWrT_p zYNvCk2a=v(X9sldiGGJW+%{;{C)?B5PcN@r2F zswGL2QXr0%x%oqwm2-2~`~F`ftg58aTd(V?Cjmk?*XQGHp9BYIXYzaA!p;lC08#uv zbq@yrwv9Z<=St<7=`mqQy{QrD6};W#7T?4J9v!#8(wDJ15tl9F3}XQyS(?w^Ev$Vn z$_b7ky{nQzvN?8%KZz2;p-9H!J5Go_-;oN>jl%1~cnKSL@@+NjL5pzwnCXtRqXU<% zL>gd!vO_M(ZKw*U-SBz#32CUktbTWDz|Lt;w#T*vKR#iQj-Z10`C=(813VITOw?S? zH>|~rSg3`d3E&PSNw>=8E(Id8A;^$xSh`KV+^g4O)G znqxYHGJt%0B$wd~oqe*$rTf@*x4L#Ve#sm-TWmUYA;m+AH_GLfwHyb^# zVD>2T;KHcHC&eu29D1M~`wYJesUXB*fMl_o;J1>T^Bq4(CAFqLm*LF=I{m3dD&>Xyq7}*B7^`!zMH{Z}*grmVj|X*s4?2E# z$fb_uesLj_zk+MGGFKEBmNxy7QZH1vOw;kH=n&ZejVOWdSdW2qi0G6i3=ux2DGF$z zv;JBS%|x7^wrSx|JF4fSh%q81cKPy{#IJbX4(syatmb;10-WuB)@O3k^-q(!u^}>% zrgWRoRKyFc?eSZ`%V9L#(B!?fEvc^G1lUsejd0+BxJ&Kh5X8`9e*Wh#jywXS%~vP+)oo43Bl z#G%Kvdep#HYsGkl+-w9@HUcy&@sj zBKg*^g+Z;+NYi2chzo&y%Gk~v#-py7vqS`=8A$(YJ(I;dKV^t2jKA6r{JI{*e~ zDQvVljY2TsvOTHFB}`I6W2papRd=Ee2W+s+OpP7VqGInI7oEkSzSDNjIK-VkLw7PX z8lo1S-swZ@MF|QaCPNY2mDE0!{vP>jUHOyZ^%xzdnGBxmXrZEqX}esSOHf5?W?($p z?dVS`*?a8c-k`TUQ)2y-k47^wzK!EW^0@fBypIn>CPdTER`hl}8GngFiKDi*O_@fI zp05}u5!Pn*`hydbN)J`Pd-D71@0vG^I@bkgIW(Tz(YlC9Tub#3%5_PM&u`C{ClysK zA0b|(RGc0=zGf#+x`4&va9%sA#`vD1@NP%@s&4Nvmp9BxSbkWL=+&l|sCxbP#rdy= zafLh^IVfp7Jhkuyghw?%rwqP8Ltu~&@o4}9yUit_2PD-|blDzlgWEFzzs*gInI@*8 z?I_fUtYWbB6-a*!Of(sz_PPKY$P3dwa~x*vNQ=WNyPu|*ed=wub%vC`yT!9{zM#6C zKbjEb%0RGLXg2nA4r=2L>aV;Y28S7*eE}O$EqU_XGkpx@*g}s8YiNjOk|Dg19|VYNB~w`?71sz-!?^0Jq9(V3=BuB`ot<@EO;E z6)-!+jSV9)@Q=u-qOB*44EkK^4fa1@lT6rkf4VvK{8}6_a5;)PDmP2`?m|iIhy+h3 zPqMy=7M#Xw=QEqxrHis+@S;=%=Tr7a2V(R3Pa~uP^-uj>`avjEld}9(>#_xC+fgn* z6yC*z=v&IL4)}>9aT8*KZO0@FYx>ONJAScWSIo(PVdF~mrmvNSUgKzVY`^&4&8DRN zN%6GTa`YW;!Rcrmo(RX4tTvq;OGmlIxlQyBuwX>*kMZZ1XkN#%C?U;d4x^~!oSw^X z*l(>x9A4(qG{rLOZ?~yE(Mqu5HQ(kV_^!MU;_*`7s`fIb5$Fvte<)x9t zrYL~|E;FGaFwkIlxsTEfZUn@{&$FZcrKkq9F3rvQO}l0?fzekUk2Dm?!l9(AnraPe-7+*@}8}m{PJ;3u-^vXw&>%y>!3sS3{=A=d+Lq zcHP}R@}KR=?xq+f_;_RB+XErfbF{057+UCn&DzU$=)UQ% z!8ov%;}G71KJ&c4+oGvG7^A)EyTGBH8*J9GGrS)}%=|{!<^a<^W~dE%T=VX=nSsH* zEAAQ8zTc3I5vd5Dx1%`)6Z;J}87CVTDSS?LUtJeLm7T_SNEM-#e1qbPgh0_n;19Ne zGd{Iga!K-mTU+ImF|)J=_Hi*}h1C60^%2 z?ysrLcJevEIlJuVMr6XfD1BMf;P@%X2NYqHzV8w3SD~uDFMbi~pU6DHVJdy{(|NQY zP`F%{I*r!#9jgn^6J8Wo|MMRlxABsT>z-;9BPdvot0Z{HNlA=j{st9bkDI5z`@e7Pq&4=S4K4@TlV@coYds_`!_XRA ztD$7Rv#Rqmfc{on``c^a4%h)Xd?}%BzdbElx|B7fINo3rS_X^;pkoA`D53Wv*oV&Q$0%}OGA zvKivG7C1um!oFilqvkA1WOO3W)lk+XzpOl}`7YqX4{?Q`(05d0o6s2E{DT*=xM!8Hx<+=_?F}{S6C>b z&h{;{E1gp8C*f*5=xOh#$Y1ASc;q`IAi$EA`-dCkpG7Q580})oW3L{tp^*`tu;np}w ze_U2ZzBFN&(q{%Cbgv-AQ7$oJ=6y09VMtJqk3}u`q{4^aE$enK>ocZ?mbt8mbrPpt z|488}HFLf6c*$S=BJmgx`_nNkLrw@& zXP**^w*PrplGiSa8Zgf-vq-ql>4$vFD0(zK2KmZzHu%>IiAWUk532!6yjj^?wqfk% za|NPSY|4@om@&`zw9H)JrQwJ$1xm_+UxSA!b4V3kMmh` znWc-N-J=r!0mkh}cb^MlFQA3@aGOssb zcM~QfVlO%*Wwx$cLi zC(R`nCe0N5sCW{dA=n0W6v9`4w=F0y2&Ngv(Sk6awOn#{hY$u3K>~ znfXu&;;zdoZW}C*HseBe^7XF^LZT#JQ-6ykIUU0dFnuTH@u4?_1hTulqTDFMDD6er zAK0JckBFzI`3{)`c{W$TX;|44K{@%AC{&tT37YvoG$FjWPx>%+Uno+no1yFkMh)9A zxVuO&Fykc+ZNE={f1N4SV*CBFAY61;Xp$!B!b}x~Y+Q*<_#ADO8GX`zJtoO_MHcv( zIn?57Nh;oD6o`(^OY3t)V>^i2M*;bT`15~XKh%jo<8oB*y7UZd#GpF~mA#Mo$-)o) z{sLRq{S4=FET5*)FYLZDtnp8kS&}w?X{r+BezK%~N7$fcKC*GH?MB_UAU-J*i>(=P zE!TLof&mN{XsPT|%O@TANu4%auAa<0G|s zium&P;`-k|D~F?d+%H2mh5ApzT`>S@0@FwvRHPaGA@)_&k0f4xgAwV4lG^_hgQg!9 zPt2d;-FU6P*Y+b*acmqu$Q)$pU%m zU+3A#HxA(~7Hwaw$WiI1pTDhk;_y2PfteU|21B4qSUrinzYB30w z#-QR5E%2)R1-tm|%Q9NhF(HMh9qCn#$>_5 zxJH9r;Dn5n0j4tFT9(+!8UJTYOa%e4^rfRq(e(AeHRW4^xbmOe4@3XwdHvr{?*I8& zE*GHy0f?#e+rJj0zWT<*MiQLugO#cm`{h&wq#K%uiB>_;R0?|hEi8**0%_5eXrB!M zjHmy*OWBSR3PEwfpd1C-OS}t8zz( zz#nuu20v)e5q4ct(ZB4FW?LuEB3`{>)K{&NK9syIz3GmJuY9pLjJFr|>JR#hvk3k_ zW&?2-gS;Yh(bM`w(9^Sa)MnW5Fb1VPhj|sAb5$Or27NDNK;Ajkd4)7UWkT6o4eiHfzA?5kV!wqf-5on ziCU4G`Ymln#tQafI-`1yRK{ty0Okvw_9Mxpjo|kCPvx%{oS|_7F7J}3IRsV3FJ)Lr zgauDPI+wwcVy=c_J@RoTLTaDiU5TusPqJW%d8o3&O3~5JZP^WW&Y1vKC1us zk(u`1Ge%|LrT_o>Fw-(v14$UutSP&fb`BZ|5_rRisRvMn$0ND zp14R!kzpc^WF;qlMyVI&4m8*=afcyEPbRf)$ms-oq=~u5AgbcFnM#)cG-o2)+~?)l zt@oRjNILwdfJ^72y8n2W7_?K7+JE1saUS|B+W%?e|MyD!KYy!jkd50X5MoTohBW{X2Wt=>E>4RH^kQva<7{AW4MC_?Jyu?eKP>9)MbI~RWFg8O zHeyf#TeNH=8XJ|Sk3B9}#SzWeUM@1kf_S5{@t8wfe|#t#_~ZlZv5cW?KhMvI)(0Kp zz)wRhiyPl{r8OiFOE`&`NQz3^m70$DK%Qjvy2K0yfa77HQN-$mtPLNEcC z&S99omw-g3*yU@S;Nn|pwwskdZ>Z>qu{?*HbNgO95wqJ7GO9`cXQjw3`d7a@>F{R|FW>5IxHB}^+#?Yf);*Nk z&*$+~PZr$NfaSiTexD3#3mf`J@MUd`WvZZTvgG$1I%#4o+`ha(udV0d(Xz=h&xi8p zYGrRmgFTM_ydXA_X-1bm@IMw0hA4IZor0_{uI8&^F#@nTH* z0}qcZdhIGd!+03%$9%%6_U9-;6INM6IDhxPXNS(l3mlD zdJYJMK!5WypJXEBusMk@fRzr){Fukt_q$GOJQz&q^qldo7MY@iiaAnQTgll|)ssry zNVC5uH$JHDX`ZTpG#WHtUsn9ZPmxw5W_M)Pop${QQ0`Z21O3VmHqQUu>Mqzx*Ckdf zOk*fGfY!g%Q1aCJrrJX9UR+$UwDIggf{@2SAie+iOw}H7nDF*ts45cyIi_$GcSY-n zphe}Q2>0URdphb1JcfATlguTl7Up6iu!*>dkAfh<-Mb-lHHbc@we-AEdQnz2+f%tGi z4pFMFe}PY|OpI&@CjAM0<-%Kt;AS8U9IfUPMLt}+^{9nFaT2`Pf*`@~F?Wy=%X1kb z=^bj7mZK%pr2c)MeefXwqW|}YRV`jRt~>`lR@v2`I)~_BrUVJ?H?TwBp~w&u-ku zkvd(E>N;9O(AeI)@vIu^+1bZ&6+rZEvYi;<@FTzN^DW9N^8j%A zN#LS=iJ-r2kkP(n?8fHy!gH2uzvZu(RKw>;DbQHnSvxhmI3V$R9`*{V`P#&^dA%AK;tW#Fy35ssdE&dsXb<%0ExMX=JeuM+3lF zk4lIaf!X=|Fg8p2Iupxd{3;8!D?v{hw-}HdDC~nX7VOh2$t;r2tnRx}lIqqX+1R_1 zl|X?7rs~EG`cR9Lu%z)&^$X^E6gEJ=eR<^n=Ezk^xS_G-rPFi-aW zj1r&2DU<#mUnTFp>fDEZc>BL=PHrP8w}_a1BuBX#52V>LLmxHmeQ1m-=)igV-g4}~ z#dxk!;%0Ss3vopuyf=)QgHITEujF<4(xU|Os(@6jano%`q+Zk2Vm-9Cpk7>0r}@Im zIkUj(Q}w)emltf}bHO!FfkT$y~oftj#St(Xc(`4ue2h`=lB8JM=k9SUVT%H zG&K&LIQe}CIf=WFcjJDwC477!wA=2&9&{d1+1)iwpjG8LV_8w(sS@wg`&=7dT^B%h zCS;qZUDjm?MqQo5<3g%RMck%s?QGm^MrhdIf0$^Y`K4|c#=nqWcBZ1%uC29h2Suxari4hdFlb?-?YCe0$qQEw;Uj zb10Be8b?BVTsYxukpwf%T395reThnI6vM4tNDGKN@pafx{&Xz=!e^+rSseK1y!d@A zk3$;&%lmWy&Uc5Rplz_+K3|qXKjfhjQmkef2*_D`2=^vZhi%SF+??7h18SAXHWau0t}Q zye%0?uT_>0dB^WR&Zpv`7$WVMN^3ZRUOYBtn_s4oGw0r z#Doj3+(-MNdh+@Qs;7u%CFw`%dj}&1~(?2_{dA zutZHXlFZw?i#UW+R;<2A(776!20bs&F2-&m-uHR)I^nd79*Ukid|$nRl-|{H>9Y)? z=oU-Vv!`}J+-#p_&!=A0AxDrxAa)KSh@90H^~;(-b%n{j#0r}w_xb5-yQ_w@8~c*S zwnuFzAHqDPrpq*0Iw2K9PLp~4ikGOb+rTBOopoQePqS4pE+Sz<-$&hke%|O#V_+NH zJX$-{Ft=|s4f}A^WHkNM5>YL#C=*$q{NR@s zX>(kl*Vc|J#{CCE@}j5k8y(eeYr7>RDK$4U(E79gv-Nv({3Sen02_`|Ogo9UG{qnt zTE%5gw@gp2xZgP2q;#}(2@7Q(GdMdOE5;7)E&}cfiT`^8dY;>jg_4+hweZwV08hZcO_d9Rh zn$Q^(YJFl<;xrVK9*;K%+zihZG^d@7eYBtQTD6dq(ykJUatK- z^(kkLyAQ-(474KjKJ^llfyf%V)~US{c{7flW_$Je#FVM5aHce}?Bq?*FgpflYT5-A z(;jO^`-_$yW7EIt@H3lVjBGg={G=>Saq557%LTG_Vy8&2r*UI@t8AP7m6$!)?1CuDp_#5osRf0ry>JNM10D`n4MfVM@$ zpU<;ba7B!PdaKH9znm@?7ZO3U(Jc>RHNBwjw!{;*zKW83_=OJI)(WW|y5EA^%?O#t zsgl3W94=|{k0$PUu$hP35?r}3M9{*VXm$DDUH~~hGGNbSi#6rq(~uMAe)&H%4_+~f z;}dl}O=BE=L#>n~2%^T;cV)K}h+UmzgsujINB5&i94B5^vv?tlRt1Eya26is^w@IClle^-R!U{J1`qhJNb!MAk&UM3PtQLqeKttE{_}f zsrf84OXno+O9ET{0}Dg`o<4p(>%uy($9l`A`nO1EeUiO&86t&m?wpDGpVy7QzZ0`d zCDzDj^AnDa@;Q9H5=`8XZpjkyT5Xh$E6S)+c>gQ-8*f8Ze?PQdhnATAXzAobZ$mnN zScmFPj5EVKlg__bj2pDnM9HUrrBYmR;L3^r1yi3{!1Q?od%{6_Y7_^l4I}UPgVq2% zdhA5mN_-TxsP$e9@cYVBpfx2P-(EEJP3HD=SZVzVv%X-Y^X&fgA~RZdGuoziG|x6Y zP{Wo9a$|^&z-C~!i9cp0jIdF~@|kq2(sDg#mGPTwDz(MP>#K@6h2pzrTXg!rA$lnE zlE!4Ay9n;i=KjSG>6>!FLgSd$Mor4uz0&;T3fK$n8~SW-amCoYyV;* z&YWbZFQAe1N0QFh-=yH1Fzv81?d4Yj&YNLI?bx(ce|Ii-UK&2Hw)dRBfi~nEh}U!p zc!}|tRl*e1yw_6f;W|CGyQQbo+dl}WSX>nTdyDS12=E*TFkNT~;J2F*xYM0<>f^8l zoc8_(`WC4Y#6g5x3JA&+_KeXT_r}Il%N;2E=_{ z*b08M93G8>6~o``zBI#L1ydHWOz<)v%a}t4+4ALC$Mobgvo(}5Ff%WMzg4HqdTP6E zn$FbP{?qk^&+@AGBh03Q%dJhI9U6U5kKBg*`Q_A#D*oQAoDip81rV0C17uO%e zI^ij&^&C|zs>sdH36qZzkYp*CT-s=vOv2Nn%)S~6W<)LW@OJT;`#>0-P-oJFX`$uv zi>9I;w}Y%@_j$jRw^xV2khggmdqYF=zX~Y$x>V#8b)H8rN%)f?L{b7tadICg%%}*`frzb}W{sc!1GG zhuIzWR_v@68ZZs(=4<)!LojxCeW28JLVx{IcrdN!r7>YW$DD%=D05NjUETzi;W|~g z?g-IG-JaJWhhQLuIyrdch9^^YIrgSjRVSKt;t^_)Yq?9C)_r+xsw^t{i9T~*?S{6Y zUo^KItMZlcuVy7rea=;;EUq5V{pzHX-yyEfEeWXDCe&V=>wqaJo z>DWcjnefE>BQelAkA*vizrPJI# zDcdS}!$WBih6!DD+jhW+QG2m&4;SWhnt%zsCbE?8arQk*ycef$#j4{|7HZD#jDsw= z_vO5+@ySB6=*d~0)>(`$F?&C*=+%<5U&;c!-zU~w!Zh_jzx(DXR8Y)v&px&a44b`j zo|O2))vt-qmH?p;reqPW>(a5%cI6XQQr^U3S>Qz@5BuerViIxa z_;{k@+#YQC5B zHajQ;pR2Cf1&1xaC%M@ARi9Qc>Mf-x0E{S~R}WHj4?&Wo2Q)Y= zrp)*I`85@G$aB<0=Ob5wPIUIOXdBBu|gteEAp6~3L{!a zTYQq@?ZyfoiItm(uNc!%eN>_&UVU?th~?7j|D>t{pZqW_m|klId$@D}Qvku_`gVhr zb-Hn*9w^y!O2ADdGx$|N&Bp~L!w-rsm&&Ph`~#&Ar>+|gHn_NO5EYU^wSa)5=?K{< zyp+|zp*7VwI%SM%yh#aecw5HwZ{!(ZJTU7*!X9bRfyQHP>j8E7D2*|No6D0Uw!eZB zdMVST-(TH;hNiLA^2zz365DoBbuz8TLO=!If%{q1cIc~O%sJ4AsR-AB<$hS(;Fpet zXqKe)+2OhUUHQ?pd=HMal@ThAvCi+95fAugYohfC%v)D1s;H4Ox*Ak7pt zdKO|>)$I`vM&*{}1_AM_JlmM1m8&Pm%6>J0s6o3Te9KCTz~D*zS=|F^Jc7t^Mw#bdS-+(mT$! z^7b=hex^^JG1)o|s6GJqbQ%Ta9&=y!$aTJ-uo~2kxM3z17(GQhrxYR^Pv+2W7p@v+ zi)xduAxXX|>Pp#sjzj<>I479s?N-j}+1Ewm7PDJ%rLT38^{s}JyIr1rTyaq!$a z%qB$;{FU+SQAP9JqMkY40SM_HiPXv7stf%JbXiJU%1JBZfqwzhC(|Q6bUUGt&ln)d{VCb5KQpoLDDm{Vf2rUr*hT}>u4ccg_FKb+2=va^ z_2g5UE?URR-KuT63da!M+D1INNF;4OZM->5C=ska8iI*V>w8O@z#IPVGjn(?9lxyra- zKbOU%!PPP_S_RpE7 z|8@t|@376PQmCfp0Cz!3p4Poo=`U=xs5>t7(_$c*o5a{6n3;s6Vf1SX#W|X`n}l&C zl6^RWp3bBN2sw&BVW8U>Gdo-&APBm)BJntgaLR>@Vn>lETM%xwsD%4{IvyE+pR*$Q zY)y|#Qo70FzK{yLrr}^Wm;k!txMr7;Tr-0xMs(%7^utO$V?isckJv5wp6MvJD`BU}BKHwSU*E9f3E?eyOW)9HJgjwG&%Loh ze>!#(;rd^sQ&k6!LT|gVox1_!uU@NyqBu`FilvrAK+kej^l6gri|wbV7N1;=5`8+~ zPAc=|+7frQbLLp?9UpC&v zgzNf$=w4liscv4E{J@JDqy(r!N)9GU%;N;g{EG!hbuRjLX7Kh^u@5g{f3rUjP0zCT z>flL*jNF*_3Tu}MhoIR5MT*|q#!EAKy9!Yso1Clo$=Ek0{Th>$ahdlx~}mPw-wS!XobutxHTm$&6r8XJMRI7Ijf&aA3jjoY=iJe4+*9um$*R>lZhf`O)n}PA zTK07AwIr=Tx5WX^@A#2`)wz)UGlW7^xMX{?ol*}@OsZZ1k4=GM^?R=9I47c+v+h9t zo!dn!s#wz~oztvEDdoJgWrsgzwv>77o;UdhA37&msw&4#|A|kNPGKYH2j(8sl>s;hzPiiwF`&6h#+i-=iwLC-Vz<`D2}fAs)DlN`xwffDOaEW32XvyK`ZsA^}`1bGPjcTEJaFdrFAy6CthUm*aDwq6Q=rIT&q@?S7iV zWX*(=Jx)$B+PAOZOTYhClmK*VoH$>v@#0dh*i$WUnMt=_wAtC6UW?hr$)cThHUu}P zmgIgfw&j|{7&L5bf)}VkLG-U=%(w?!A;n)kVIGj>W}LG@hg))_(8U|s=~|t<J`;wW0|yP%OMhz&sPp?pV(AZ_d8Ye%g4lfM{&0v2@ogU z+j`k&kuHtz!Jv@V&alKtLUO#cni)tCBPZ%JlqROYLg}@o25{}Qm~;Z_RVvLW=BZ(2 zXlAgIz=}MjM-Lu>O!G$T!FWl4vjG)|R`Va}pkK%qz{m3EiK{pi9Z24{7(%awp+v2Y z(wa*Bi<#>u1f;tf-Kbq+cG(0FP(pzJF%3YX_CzyC7jTk`s%p@8TA^{f#;T=P$YQ;E zvqWPv=5N3;0TX*_d{WR()4`#2=yY+G+8jn+q^Wa_a5~?AC?Km#Yu099Y`i<~^FPx) zwhZm@DmL^^U!9E>#bk4i>MwWINGag9Y^`aygf&DnMrWe+yQ?J{O*Fp8n_WCdJ-RSr zd1A}+VMoBaqXI}hrv@$zv=~O?6V)EkiH_>~tt%??fC=IZ?HDmlKM4Z_>Pj~TC2rU4 zwiTL8*5p#sN%gXm{_S>Os z1T%uiZr;~Z;KtlwO6^HM$4Q@oD%&WX`)$OALD8AGeIg{Y>&>SjKe-&IALN`DDK)vm zF377^`yyeUwLYGnKXl$cV*LKyTX&jggcB5@wbKF(efb9W^Guv$)U@R@N}`SPXdbJP zW7m9Ve&|tR*xY82w3noZ>OYU4ATs07QP)NL zC=QHs=$bz(X^!M`WE}U5BX_%r@Fh89oZumXaDCX)d_>9pzG#CRKy2!6wi0*t9LU@! zL3o<;9Cb;{NG)=VlRg^kpRfXYTn}?fot&HagymzDLlH_P7&$iH3y#63b zR>S2^rQJ3Hc){;K|1Idy?2L(64(P-%Jtln|5dzME3-I{#;r&CKFtul%ogLI87 z;YVALCmf(2d0%#z+hS(jy`IeAdbtRK<@JEQGcvB}uB^?G3_KwET6cG~+`%hTXMrf- z%J!)x0};eYz7fAo>e-4z3HXqh%xcc(jAMB&WQ1g>0DaNvE0Zci7~_Z~JPl=WM6DI< zGoBImITf~_KlQzn!>;Q4(4EW4U&XoR9CXQksG zoJpleLR@}^eT1zO^WQ4%C{Hj_gWlDrN$RV#ALOeX-*xvZa2=KKt^;*mh9|}I0J=;^ zTv|krK%LB1fWYWi#m(G28XRVXiV$`nY@JeX*O^l9yKQIjj%#hs*5vX|KhHg|cp`SK zl6JCN1MK;{DR7YKx$`xd-^BpC?2p$oL4rvU8BuP5~bI1w& zTQcVQJ?GjjzHeScU%;wR^#CO{86nMkEVoFhq7{q)MO-tjK;>g7sg4&BiU(GP<8(a-$_H} z03X-BqqZaeGg~K_IZG#}8B^ zIkDqU|AY&R3!INq!c=y`1>dv7WWpa&zstey0YExdh$L$kGe#Bvw~op$Q%#Fpd20RR zQH_Sh0)ekf1Q1e=yy-yrx8hUrXTvJ^sc+(x3N4<)To+s)M?$yrKDO?^*AG)YmdrTy zm;Ou~3-F#Rp9KWr@?sxn4aVU`2@MTOX-RKC0D(R z!Ltg)?TcakP-s6NYCe$cy=MvBxNEJ^+ya3E{1!tDf4!)W1Zzyb7{Aa?6(__n6H9K{!4E(;}LWlzSh-1P6NpSbDsp(ROQ7@yTZ! zP-rRe`YfF&^d)g!;aZ(e-eZ$yMT$-HlsntY1%k$dw2=msrzzHJ1w_4s!#Eea+;JH^zyaf zH2C9zkxz}VQ|=21?5yB|#&wd$IyF$h_Z>rqtjaG5cyIrd!yF()4aaOQc#XLz+`_H?Is9^f!3DiKrrnVDt%(E_wbg71vJIH}L;xctTUgmXdo zUnJjpn~7_o8#kse!1VXCl4-u1k~3~cWWz>cvVOC4O$udVbQ1DY-)c+Lnm#9OW{0kr z*v(-DEHxy2_fwnqi}gC!9G+_d+nOlEk45AFA)~O^*lNq;El8KU`sRE@T+abn?tJa>~d$WgYzeJCB5!S?Eu{EU1`eCzDQWnJZp|4{v4VG zZ9Z6ap0f;LiY1*^8*0~`$PIB6epTDK0I&AU*~_SHuDinWZ(km>3zJaGcC%}`S1Gvn z{Pno|@x@SpX~qe^!`aV=)UZu>8&v3M+d(Lr_iPz+P2ueKpzZ(oLwWAQ`}c0^IqoiNR{yLVZ5JP(`>U=-CYq~`!7Gz(UQ2=Ee51MeY}?3c zD-KeDQU&K%X?OlJLN8OSH@XtaXFBhklgO@{+2Ic9_VJE z?@fCoL&=>*FYMVvMsLcXkQLAS+q>E^i2|QFNkpna!NIKU|%5KFky%v;Ep;D3`r?JW|qW{gInaT z6U8fio0dz$f?`6$%CQ8i2x=MchbwH2N?&5jh}koZLN?Ft@%W9p9gpa5wgzM^{wO04 zxmj+C0F=HAgx6cQ3y{X+VKz`qTw64S?ievU;1XD0)ad3ZykIE=6Z?p4iP)hxy*m`b zAb~;4xEH}1_HVv?%r; zjkoRIB;9K=61n>1d_B(L^S#se%f%DKq=v3T9>-3)@^ePr4WT}caeL&SY^~>i83>*C zB7c^@q_<9ZUNHDHnW*PDOh^F+q5-=6+Sd$aJUN^L1)@Sv*6tqd*Ykm$6(M_x zujhyGz1{@>EIKK6&{5`hbCw6LZe%&rKip00*iXjZ6f+8;eZT`Z0>WY)GxI)@s@K3z z`Rzxh-Y6zLHIOg&Zq3Hn*>L}<0;T)@Pe*;GUm!9~EGEgvc=`DZKJWocPPN(PAEG%w z&s-ZZx_6GaN3BA30K9f>dj0!gkm}aPe!4gLYVoL+ZQXaVv9RpQAnvY2mVtT}=tE0? z%o9hak+V*5#;F#Fr1~tNlmHmjze-7LlHXL-%zu>HaWF67#Qgyt)~Bun_H|+6$Z*pJS;`6Ur2Ov=$@_UXpxK;$|ylxs~($x|6I( zlrFxE?W+6zElJ3h`ezU|l_IZPq@sw=3KwrB7O%Lfn1{#Hr;H`9%&@3?RCIV^Ihm)r zhkDfMe6e_Gh9PvSRTHK>7o~Kw!z4tWV+9KNqCuVxE$Mp}!oV3r+THD|3NN4QybjR9 z_7Py;`ZaP5~l-}PPHjq*k6l9}It197|@ zU2-pps7JH!=~NDXYjEJ2JQ9BoZO^ zVzzVn2HplkfyI3NA!1t1(@9GW%F~87N=KxQCb2IRoyUWTiI@aXwtQt_XeXw>@3$`m z;LM9h9r(GM!mg)w-wYyORqLbukY0>!@nPz#EfPUbKXYNX*nXd{9*a_T2LyL?qKoUZ z7Q7WWwK|smj!5z|4vfZKw#e#{8U9DI8}I{9J49|tQJC`zJ@BR@{w*m|92hAP;tZ$^W>}uX)S6dkmS59d1Z~%6>pYm(>qr!| zoHqHF@82>PC}ykwyU{iEl4Qs;ZU=^oRU(QT`VmCc4d;j3C1$oipV&NMy2*qz97Z*# zL_mOD?(A(Rvwu5Pn7J%)ZKTnBqA?u6GSDcTm+@m;x?x{Pr|GJ-{&e7n3LJqH(~Y4{ zXa9RJt1OXOF}>IFiP7O(i%OK#l1_2mA5?s27`?f9l15oD+D*sK*j6duBe@SLCtp_P z5ewqEdsvcUMDuMpOe*=vR&OQ9(yw2;z^LK1<<8s$G=$sc`8wIjE5*ye1PJmc6!!en zY-Ue%;k<42Q{7J)BGW+^a9>C=LW4d#VWIlL62z;s_A+Lh%l3w|7~LO z`@g3jdE-|ek_2&??1}dsUUcJGc1dkG)HFK}iCJX_iYk2>1%T+9?q79hD4-)ASt~iG zZP9fdLkAkd@5m~($U>s{e_PU;d(P>thW=)~{O=J58SLz^?Vzp0#~3Yoc1XCV-82;+ zE_7&twvHF!cI~AQa+enhvo_lM80IY{aEeGe>lwEkcFJz|r-F$#)%J&|{9nX8AOh}uMf(xsX(G9W z{Qf}%&G$BQIP$^4A-Mi8G(=pwQ}22aA8lRKsq_zru&p4h40`z_Cy4A%<=wCcoKYMI zU2cm+>Ua94NiwQb_XM|E^D5Hg52Ti5xSe9b!F2j2I8hnV*u+%BZ|K%Zl4X-0Nf^c$d5Z5|M=Q|{)t_p5(~63 zQ|sJG8Z1!uh+cGN4(=VdEDIDPMA#ftaPz<;-vX!Fk>FXn5knb_(dyk|)6o9x)nmXd ztN{^aG)QFzZ6*;?df7tfy<AgI=tW9OQ2ly(NY%JDTFH9$#ri}Sl_3}TzZC!PUl5MIAOIWUIIx$vHH>3m3-G0b@?S51En)Np;<6O1y8 z%=Sy!_WdO<4RuJv?ZdNsl~OemC)$Cd!+>FVjSj(bb~*+4onq&!FB7ohTN}2js=k?p zmd8!MXDt%|_VNIX8lN*F4Q>)3H2o~Be)0IV%hsgF4biSmHCA+xLo1_;!2=i;*wZPt zq;BJ(j%Dx@Uv;479L#*+cj#t-UlM?j4~*+ew}OC&?esA~DqVoI>tH@ju*8A^V)m@Cjuj{gPs)a@8FVXqT#J0pd!#!x zZp4AZYZ3Rk*)V&@Z))bl|5!ndtJ82+MDS3hV9+CdB6OM0rsaU zIfSEu;jN_0*Vl8N@;lIxjTTiyQ1KLcuc4i?Mzng~N{5g(bqXH2!Ooaj=J+ziM?h(9 zMVv-otd@pH^md1Q+f%CJ2+{zTpq0I;?Okk;IO!I@)0p?+PY}1au4mmPMLpZR;@K z#p+4ycwn}179&O}bDdR}+Gee^UzkLtc!SVS)kSlyjG?{mhlFa zh?02x=Jgw+71^wzKJe89jo^m`-Za2CljO0MH+NAA2v6Jq9wd0SdnH`(M1a0A`Yvga zFM`qG=ilOS=i7D=gF1lB#FdZ|#7V5#rOlPjUmVu%*^i0*dyqK_#g%uRqdZEw`=V?f zAPhhW_*T0~RUX8x{jnm=d#H48P3fN}o(wCJAnv>mbEYChrU#4(fns{v%hF*kbvF1d zvLP-^1fDUHqMHsD5DnWmpSNUKof!yjx+Cchhsk@TNgDhQWkol!!gViTGGaa~fz;~d zSY0x>miNj>CrdwUMF^&nuXT&$E>kR!H$Gx*aPfz=&g zE9l_QvDx%;d$KILsAfs-mSvU)C2V>&=UhF|+;TnGnpMKlbiR}fY$6nHGmc=i-4$(N z)77sSvAXM4TDNu5>evpRkLFE+Zv9+cQ}3SH+1v@3mjlDlJU$7X3D51%MOq_=Z~s2Y zd_M-twIyaR2Ro0o3UX`Pcm=_6|XNjl%l19sO~DWdzZ@{qIH()Y1|pU*e8+aEPFe56+)xQuWc{6?X z#&ozcBKVg`(Au#R{>Q8wX4p-mWiR#QfL2&(EI`NgL1Jko;l!z?=%CA}WuC{O+U~J)w zG-;uNt55EU*}Rcd`(ko0j`!?Pya5&Gb>o0~?lgwKZD$}~do2On;l%5^*3w5iI~mlr z4oqy-+CGx>=#p}&-T5U-_e4-8h_^Qc5$f#JPpS;vj#vAzWqrjNaD?D5S_i zbC*&euBGD7a_1x<3o{i6-Jp8)b5SH;^O^kTVWS-ieX)+j6sNF`KgQB%=|Pn>L`hTS znb>VhHtPDz_uEQm;%ptL^oUani^d7^O!TtlM|-%lPL|KXN&GmPV=3Hy;xDRH4Up)6 zUexTq7IQ!Mb)QFvi55z|v!x~gzAFHiS*asga~|sBZTPJKkf=E{OsUr!iRS@6Xo)yu zU0pyRaS9A+9wKkOFUF+iK2j7pJQGlUk0-V$lJTSVz3ca7-lL{ybjli1J;~-|mLTIT zL=lIcrfl`)(2Ugf&SJXT+Nb}XRXUMK8~cPHbA6w|%X!h3jBv5Tr)cN++34pVcV>!c zmVdlcTL=C;3w79`6xvT1*kEC3bBx`FgCkcr3QRg&_DcQH5?^jgyXqwkrz%gw{jj%p z*^4VUQFWqN$*vchdj@*RUR$Y)5x9?>f+!(}3k10YZ$GHypJypu_Q;Fn0e8l5>?p>I zOn*%GfO)W z^)XUa*B|&JUrBQbE!%`$qNLFqJ2-novKrp9lF;HGruWBW#f~=~T&NM0Y@}h6_-E`s z`bb3!rya$W>-j(+!*4qwdr!G>1%aOk+>1G2m?SULmQ&$?6qH)TtZj&jC+dY8ADLAV z-a|(t)}U-n+(CeJt0t)%0~{(@&^AXLJ1f!316y;_^u88;^}E62y4D;zzk>@1R$9tC z;&i5gScfPn<$rxYfb-u=QGOdv)mXqP zAPjasJB{~^2^~=q8i;d%)B&fQnN=;52gZ6&PmA^Y-)-A2fZ~LnuyYgHpC4XE?NMr1 zWEW5rR#UW960-;Kh&8)I=mUxYWMVIP)&3UIiMIx8k&d=lSbHOTpH_2FIP1G@qvvUQ z>*%~+HTm?%{`&L7yU^q(uqyECdt#U%sBY*vG`Ok7Xiz%WI&n45{g2V)WoIK+=%L~8pS@9Rpsii~dVoR6@tm%iC|YTic$eQ`PA9C|Rv?zKLc{tnC4W4kVL z!O|8)QYj@`z0_7JEuyn?vCYJZOXjjF_3cHr=)@GTsqCW6^L(Y0&eMMt#Z5ZpQAFU% zLlZMX$6yV8NiloqLb5&bP=}f40L{-J-7NPe_KB!UN08pd~@7*K<$0A`&!t#{-gQi#{@e@F42xR zkG4b6nS@M*^CAt-7?^SWaE#GNH-{G)xVy;VPX%yA^P{+ zt%2tcAV=WcxWt}g&9I#y9xv=+`#iF@0_ZM$3y{dw>;&`C&v)3N-vL}2uKR(o3YbLc zg!awAl>*~`C9?GkhlmEnAAb6y0Cj&$7;CH>ME{7?^hkqJkp_&1l|z^a$$#9ouEFJ; zR;s8D4Fi~`x?>k~!gs3G6jvME`Y(t~I-JPnFIf0$r)BTXeTsYk^7VbIh4Mo7v|d0a z-%hm2?8kERIu6sAH{K-MiG>Ef|wrJ+PW_$2T28WJ-CGo{7DfDDY|)sr6~$*oTj_C!=5Axwbvn zmk~~-zr)=OQMzms{*A#&Bin3`BWBtofgk8!S1Q6L zN%&ofq7ujAH$t>GU4u&7p*+h&VAHDJBoyCkGe^mG)c*-BZ#RP!J%bmGCUqF?*=*7@ zPRB%NiUqcLKrf9NTYQ5lWwEi>E3E2lAYzBt*+P1Zs`aJ?e0p}FxG#e@j{FNVshLfqEHfC>ot`s+K1J|B`Ksj8aL zubn%)`jU>eP!8wv3?xs;y}h>`4_rP7Wzz56_VmJnn5hI zKuW(SS_m;0-PtaauB>cypXFhOM|ro-FU|~pjwS;f4d0Jb-mFlO0HXHqg#>LC^A6jJ zr-6iduysGzotcWdDI7w~>uMZ{#*Rq=8e9Z>Ln+JzHk?Gz`Z`1U;;5a6o9YjERLcg| zF2f5;Fh9eHp9z`|;GWHi$-nKQGe_4vSQg==BFd(jz#8Q0O4R4VuYq6b zIqJ2d*RmGX!Gw+U@pTsMzU!LR9m$FQVM0s5PCp@%IVZ-00+5CM_XOpDwz;I?;FsR) zonT3b?ah~eO1qgVKx83v-ti)sNy#SB%@!Q)fyNoaq>B&zC(fH=8k6<(A8iI^s`S9< zAt@MH!`0%I+vZsO)lwTLbzkbXy-KXIWLGbse(i(W)5PY1r$2VlCHcO^icnu{Q4K5m zVd+5j(?JKJp1^;kBhd=i3P+fs4+(v!pS9CVcr+)BT)!H)kmUiD05*MBn>i^qqLd=F z-_d5Q-!i{cP!NlVLzz<>&;DGweMNwtYmPvVPZKwXyd}^dX0N}xRy?ekgUYc}3dm(T z{@sK)Pj0i}F~5Kl1Zi5Qpxx&6CVdJW<1)U7{>;MYp;0}5tm6uXfg16LgSbVy_cCM$ z59o3*tb3wwnluS^v^dx}DLIz_vM=jJrz0@x{FjdRFzR(T_Ff{=VVyr{ZsN46dfu-q z!wISximY1$Fy2SAv`RuFH>iZw)KAMMLX>3+r~{ut-v6YE{z?z2HPl7+qmzFO`hpzx z&9#BL;zp2sArM-6I_M>2eR#;ofY+izA16hASo$%XUG?C9G4|DAQMX;U(p}PBN{57W zBPGZPNF$Aaj40hhNw;)MiG;wA(%l`x0D{y|g20eNo}bVA#COj1zTbQP;$q^0>;B#M zzV}{x?X{ls(+o1{%4|4oM?)wmiijYSi7c6lx9A_@czkgRbG-p}&}xyH*aAK;li^wj zg^9MvR}-mP@r^*jczoB=W&I>Xa6s)x<@ljTPahiFP%C1R^4sjrsI7MHY?S0Q#s44| zvN;)$`=Hdi@rY_AS;iVANf%4g^Bq3=&<3C%IfRRAwI18ZCRrDE*;3Uv}kdpO)_tqlRgU(Y$6OMbpb@q+-!hxv z{yXV--GzFbfi@h^tOy32bEtKWFl4jYjHjmJ&Ay!YO*rv^kIu>Ys7eSDuVq$VqJxDBguNeb|8Bm=@+ToL>H2f(<_sKO(cX=VEWIP$(ik=CIeGVkm53_p z87jr6iICuL1t8fJL-a!6Sl@RHfqss%Q@z<8)jQSH^tOD5r5K7SV;P8_QFE@*d*!-jp5sDx0}|it0UH5GB`7CW3L)sdZ?$=SY$YK z0FkVlWsU#WGT}S3r5Wic0B-Mi^%bKp)bXg=sk={z(-ss7=RKuSt+5N%*#gwYQ|zxp ze^$H|qpf;|X>9Q5jvQ#V!-+h`9hQ(9vT^bvoRD{UiZY>}=CFM;$-_}tkVH?k4( z7m<2=NdM4huQv0Wht{}qQ{^e6_nH>tQp}JE;Bw>hlc2fUEJO&Eu&`M&tg}wfY*Xr*4)Ad!^?4ecy8@!L>SdRwjjch6O61wZA&%2hzl|Dh8Xe9sD zfj?GM`nIy%W$7JEP74D%Y%!mu0A;iX{ZBO4$-}eOSjw_IUzOpzQ4wsdar<2DM}4-& zE6%KJ0SL)#F+hXv$|>W@SPv_bXT!yO)lY5o7sh&}!!NZ?NB-m7EsDQB?MKGD-%q3OFB=MMp% zuWYYxj+glO4i;+!1clFcN!U|r&xK}*z7o8 z^R`y<))_1D?d%GQ_U!7NJC=X=K$&Ku>+Ei?qNz<0xqqQa2-UkTVr`D2EW)PQ9KqFQFXi;L)M0xEaw}t&(ZjH z4^=Le^j#x?Fc~}f$W7Jx+q-0YF|i@K!*?rLd5PQA)Wp&y0WI4J%r}Ru#=xIwKO{_^ zB+~4A!r=Ap5Cy5dTgd3W6K3Y6%~V*{@nWKb{U#r8F~)HxjF32%tP!ut3Jw@O5dqZ4 z2`NHaWBb12oR{?ZG5Sx&f2R8+8(;bWAa*(mvpv#@Is#DmGQM2|foH32)E#Ea z4Jr1e&%ayc`XVBDfqwmLsV;ZBU78Xx-X!WnqZRm^Db(R@W2Xz_#+jAuT8AQD$_p<3 zxr6j2i3JZ?--CWZ<~NDVsd$c0NZ#K#wu;w0H=?GEN)5hRN@2!P^yP0p5ri0{QzYLz zvpsNLZG@l*|0@vU;X1nyV&|sYx6Mk-b}Q{*2Zk(V9#B!`BR1zeqN=uBw?CA~7Ww@g z!5hFA1bxzPm@f!B*>#EV+pX6a!g^23uFz@uD+G*ffAVxJ*Z;_w~Mm;3JvVuzJK@gz%7O!HV5HoofqrMCt2v;Z85p-sD=Wxc((q(8LO zWfRL}X&k|Owqeb2kD4zQ&N|-kS$`M!px0VsF?c0jo|!AL66}K#{#S%iM32vLRF8&| z_Ssw(Y0z(hpv z+W8ecyDS4MJQ>4*A$GV=-qXrDrC9k6wBP70z5-{t>U|#3%)>_vdMO#0?HS;EO>cCI zlRLi?kuk5lagNlmS{HVI>H?XH?uGKb7F;w5)dJ)$4V{qW*JtWt9?5tMSxd`i#shc_ z-88@Gj1Qa^@lYD`+ko6k{uRI@7d{s~Nx?UJ+DaiLg-@4Op%!P8Oy4=k4h-nHP6tpiqjq>GNnV$pX0BhcmQ%>p4weY)|cY1x` zwYbZWambc?(`8@Od}TIxry~y#t23NTNL?D7m$*xhy$nf@Wn$ve#$HmqyX%d6$J^!N z7Aa|9Y)hYu;YNs%yWbDYtDNLvee5j>7KC6IuDl?GNA&CZG$MSkk)IPG+rODLC+fMb zgC+01cbJk*2JaQ4(VZf6h;tRaJ`x0I!;jSBO%O9WDWKqY%-Pneq9qF()kX5vokgnh zm@x&=dp2&Go-~n@N@ZgbN&O``)e;YlHZ09C@x2c72;VUSRT*fvn2_$Y7K>O`;`1~L zgWMbd@!UfJ_}Yq)L#{-k;eld8>_v4+jsnG|mm-RIaVLu0lHOP;lU8iY)Q~z9tM`C_ zENWwx!O(t7P@7k0;KN$HxzRZ@e;bhOX`$btdT^TWAc?)>rhnq<;Oqb$OUOH*P;?#ZHk!}^3M8Imr5Id{qbsgopq4Xnb7O=;}y zz5|#-Np$&#RHTORydW8Tuaq`1fA4n76v9l}Dvv-EskJ_#eqNa5E7ir`Fm!KRUAcb2 zbEl3Z-412ND=#7uHOkV$(|b3d)|`ura#Huu@awgm}h16~@`Fvn4;h8QOQtnvMy5i}tTv?;qB> z9LI>%DZYG<)m66@r2QtEr>(Yqkr%)91OS6T#FOSwB}x~PAopV}mk;YWZNh#bcj}aH zLc$-5?N_O?cR=wLiC=PDYbRccx5C2M<|zADrsYbk65ix4xY{4+PUn&u(McK$89eC0 zRI_amo&#&+Y<+F2K-uYY+V2L*o}K6Ci4xpznyO8L4%|%@7}2Lb4*l@-Yp7%EC*9Vv z=D)N6{AcwZsby#n^HfORjwFg4UA?VYn1Ctyi+lb+do%n@bey{>$$^Qi@A~wcsM)!( zvc`qG0-H05Fc?s8(2(IsX_#?f@^xHt)|EB3Jlf;$qrNghllw{@x&SCyzmRn|gZKfh zM2*w&naBFZ-R>}NACK|t#frwpF8lf{LY{4gRhE`_qg7Nm?})5@+I3>ZCsj@_0jU_1 z2bCE6XSA4?mxt`5ttQTqt&p~xTS-PWn-iFTDf+o)J&n}aVG6{zf<_!k>OR>16 z?JQ@wIZhjZ;MI4FEH-|&$!yv`7&|K*Jl3i3c>%T3+Skm4Z2SUwv$6gRUDl@^|LOa@ zG-i6CWad~=b}CxwS3g=PzF?pC_CQeZX{;ldu68`IKKhC(KkFU9>ACcw5v+7sMAsFI zPLGqy`%>ln28)`HkjVAiyIvX}rI;|E4z=;3Gu2sE)5l-&Ehlm8(uPPZ5VLZI$PGD0 zzdrZr{_?;@oIHZ>O~%<}$*UjOe9#7x6!EJ4uh z|MiNgU`_nV`tN}fSyBC$MTUCD!vY@aLa_iXS+BxJ$qkSddA*$a__?J;MODBRqg#^0 zfSt!mUSF>JEK&`ni02mx#kD_^Z4jM-K}}e!n>XT~;JwwnZ>Y@wxB5R^cICODU?G;& z$p?NZ!PO#72?fKI3HhNwh~2l?!aEF4YX#QyT}GSTd)pJi8ar4j)PTq#*Hfk2r>Y5H z!wm~Gd#Zb}p(RePv6=}%I>l;vy(@xDV&aLP*9qa0&$#?1%5FaJ-prs(#YwrU^7+2T zsx4I{MC748VfB9Znw_)up6`cehF%w9AIW2ey=4YNDQ?6{RAs&H(YUJ0a-N(d`i}x~ z31W`S8$2X=+Ia6Zd30A>f)D^!Wb^&2(=EniBp(^H?f$1FjhrU;9c$U<=gVOX?qB!o z7a2Z&5yVQN7P^H_gWkfsrE-0raxa2w_ENn8l3&hZ+m4!XU&x)zQIAeb%f+8P?qrG; zQwjz!QegB=VD)T9nO@NXhosSptWDryRLxLZ?dZ21;g-3R_GRUkqf*MnGGq6XIFqJL z8VaMmVTZ$osxv-E^B)0_sa%>J%XM555loD4u^2N%=m^PF1^~Nt|jSD)v zU(It=Pt@xP&^|1tY)(BEUD>R28WB27RRcF5Wm&t_O77B2flclTROZT9{u!s=A3W9h z3VE(;^>M~Y!)G(J(U_)rWPf~E*e|NZB@W}ee^g4v;~!tquqb9%4PE8{fv2N;Vu-?) zcotA;%j~|IVP!}O?L@5EM3?STLC*pN0OhUP2rGK5M zy^%*rqdl}ugnTK$iZ`DhruQaslq1Yb|HP&)!TX{x*Lg7DqjLaFdFZ5?T#RmxpqsGF zAZvjjB- zU-T+qzu8WfI#M9Y5v&TAq=})HJR0-AwE+Jfn*5FmP%Y~HouI&$*3&ew-2eWo!}&;w za^bVq4_Jmnh0Xqg$H9BtIZn9TEA7liStXh3G^l8so}8X14isu+Rah6I;Of#t!gF1tGW~<91a60z8QwG z^PAn<( z6vEyBl9N>@a-k>US-b^xm~05W~_VYH6v<>SMkE71eulb_y0Qu5e4K$fazU z5=VGdt!e`*qU|^v?vdWg18olI5r=G!r6&rtU)e6Ty8Ma;EGx_Hu}{2K4SjZE^w(f>ezH2&4Ngn2E<;iAMwZInGZ zm9gJ=5DD5O(f2`b39za2kMa$Q?ZB01BCE=xKs|~&H^W2QCAQsm- zDPU5)RIuqn+xA?~NOD*-p{Wx8A#5;5pamhM2k~5ss*(r1OrC*81Dpr=!Ub=KBJf+C zxflfAtCT9n5eQj{zP(}}!uF8gHDKj4j)QZ?bBWM?LgE+kC~^4og^jAQ z`1Eos1z!VWqy*t(=TN+P4Fh4zwD z&1&UWi9X8q$Hnam+6)^c_8A);Q``}FByF`eh%El-|6|F^InZBx&0oed&?Bp6=m*TK z^d6v^c2u)Wo*4Z3NXmpzeW&m8*u7!4W}Bv@X+KOd9ic@pmoGn{zMM}oqe~S>{o|87 z^nrT%Cf13?8;~91Va0RVfidi2M~N?)*q$iEO~|&Z(m82#3W};|d~VzI53TMM3xO~? z>s%S$Yq?L--mX*Y{cTdx&)U|4IX+nCvB#Yub$r0#-08dM;TX(esr=qClO^^t(W^V; z+*Rpr33&PJgd!hS`+Z+z62nwsg^vj?21BLdXFpyK2!oDJlvtkCa>$b@lFk}LO<7Kd z*fYcO!zOBc$}!aY4MONnU_VC{i+xWJ1mF!DkcqRU;`H|(EG;%!sUv&Xq%i-eA~KjO zk&*fVJwyVjbi1Kg`B)&7(gG_1OVaXVYCACe7g!WWS%of~b?b({?0@%?_R+_?r|DQf z^dx?kyR7+5aC-cHRy9#jx^b<#EjY~d!&)?nn%Y3}N6&s)WGV>-!Xe2al z1R9aZZn$&$)c6a{WANgqMT6->I3>GE$8S%vz~s1&O9&PL_lZ&3*q5fc7}SH(}H z)sp9pc4{Ap<8!<>8P&s@D&4w^UpvQ7?YzdRX=Fy+qoty8c6B6Qc^w!7i7bJ>#;v|N zq*N=u9!Ma`_1sVli$grE6dNMdV6oRA8U#t-5?>0puPNEcnPUHF^gxOB5LFI&XmvyuA-B<^ti&*oq$Aaeuu_LuFd4^0gqgnOvf{`V5=dgC z7ug8g;Flex_4kAWChEor2R_(q>Fp}5+v>ndNh#Zt8VNO)=xn+`e}X`-ANUNiB2>s_40H#hsA^X$cc6KAlo;>y~@P`QqFMp z*uJ&L`e3D@%8Qc_wxBN@mKLb$PSY6MG4UF5k6w|S3>4~vG<XN&X2VcVQ1=KKr{ zFC_@O#gzGIku1SvM6DvbPz!#u`kt9(h>->lF`TZa_4A>xL<1W9pv~zEPzRch>)N>x|Sam=N zaF+?Z+*`qLHnl6qc-?gpP!T3)Zkc+&F`4n6op;Sf9B9a)^CCipD6V}I^&&ELQ@`zF zY<-4ehS_ndtUzx;BCsNyt+}Srf0`_vJ`oJlL%>i^Dg)Itd)rdC;{i?S={a{7c@6YC1e3+sM42)z>ER zvP?z}sblu$(yD-JXsbiUScyd57CTdf1`86Z@0%q-N1ZP)CVs|&)C z;!Ka?6BDoF5$ehi*Byyfa&>4hhgHF~QKBVxF~UKE57&~+<)&kfHb-ca`j#2+#Qf`k zbTmZ;;Ofu#pbVFKQGC+Kw$yiXwACpFMd9#EH7D=aE#{Q5A!(9&os5E)d2&ntSzO>^ z2>5?~s|sbUI>gN*v7q)3>0^-0+!6MC+*c|PFB6_me|5CIZC_0e!dHc^LIbyLRHOqC zMXh@;tUg|sFgDdL%UOXVij2@a!%c5bs?|*RbzCC^c^9|^LFBFtekk1zU&Cx()5NrV#8iXc^KOaehMoHJA!p+!?_ zy=*5v4=6%diLF$T$wK-~c77kMiZ6PYKg}u;*~e}tF*3s0QxI4peXNf26T9$eszpG% z*&z1c?xZI6)l}woqb*5Cz@uR}h11Hzz3p3R$h1LXeAyJ3gbX^WJh(M;e%O)(q42!RJUKsYmU<6a%;@yyFJRbf3 zcD#STXMl@}hi9OSL0Un)HpBqfx`NB6FPM^VeCqa`o-5Hyw=Kj2F3kx7p|CAV3!*&M zcJTI**3v!cdYY(IS!W~{rPbg-Ne0?ZFh`Bvo~zA$XK+LW?BLZfa-njWXdPhil%1)0t}iu|Zwa+pY2$(GN%Wd|9V zK%Z-uDL-pINa2gx>5ry$AUgH!fzV4>3(%uFn*kCydVm)a-m_2RX%<9MmmhD4E2M|w z&K4a2FR~d5y;AFv=*z}bv#Kq#9oqiUv+w;z*-o0u?62OXsfQU$^jl==Q7%?U{^I#& z=_l`G#1nw`zee3ZAB(RtgG{FM!?bAJH?Qn`pAWy1a6>uSYfvo3`WW^?lu6tN!39w} zt!9lC*;!3URQE#7R8cm8&(Td89L8@JT*J6zypbQrE5_wMT_Y?sjYuW@-o=4n{rQQAnf0dtuV2@jU8%u{~m&a{~w^O7z>4oD6T-q}UV8 z*`=LojtkV`8&;aamK^PCKV3=TDw_Bt(8Itg8@{2+x-~SSJW_tMK164V8&|tKPB?et zBb92wyPFYO@u(DZctJWjPh|efEeE6s;>6hORap!p%AtKr2YZt`SRD}QtGxve z70mIr7EMmAKpf{Y1hePaHM4be5j&fV@Dc1YU%=PH?H#zf6ukIW4wVmmjTsWE5AhXdKOvSm+_2^AvCOK_6+}e!D<3~0jMMNnk5bN%)5<4i&91Uk8#9QC z$Cq1SMxxRr@nr}Q5!~_^K~N*0p4WE7(vTtMMld{#S8#q9V1mzOs`1PAs_h3!C|+13 zR;USXAiCv`yu^}nO~nncto9BIRQhpv+-}fQ0)qYyQdP06AG^p;fx^gJmbjolw!Z!L z)(5I)>SNW1WA;|ykVK2VIo zgYEv^-KQDRHJq=>kHKr6kbC~u9{7Lnh(9Wd-!GQqqf?kN3$yfWDjJ;4&94{BFNE2S z+)DS-5=%*JoJ1e#Ch~-{48B#ff+xsIzt6gXz~@>9u!^jFtBO0<*+1}|lE%d#24YPw zYfa^kJFr^&4u_r*G3(&3$$zlV;^yrtyHCdaBrEA2ljfNM`n>=?dnZJm0owBywaiR) zhaZQwp*jgz?ib-nVWwVCauSdgTsiCwjk;4+a_h-Pe~!Tbhn(6`Pbw5KtvK|>b3og; z_bGfRj=#R#t#v|#O287Fi-d$c59k{FD|ImN__Ith6v_=rG|q9vQ(#tuVDNaj?pyx2 zK746nP+Y&i?k2y()qc^GyL9|~j^lMxZ_4ayqyvkr%Pf!NU1ZeWL1QC^kjYzs>hnxn z-{)tr#~!rUTgfm(vN-@Fn6{#EOvItqzV8Q~f#7qo^}i~ve{&W8UgkSNR6GhEY_m&l z$m_Je@QGI;eKJ1Y_i=o#OHr`F8=T4({Ib$GA<{nk$KAyQ;xawTIemED2IEZPBCzWA zxajNo!s|#iDX^~DN2vG4wytCVT@I#w%)sEd*q8Q!aZ~pb;v-%EKxqQ%_XJYX&K#2d z6}7~bMo*Ycg!nR|*~+2#R0WArb);gjr$hlfdUznXx9Woeef+g9D^@>qR{j+-(#Cs5 zVKbyyrj3P)vH04)y%SOs76k?;Q@$gzMRw(`xiPrOD@O^j+KcN z%GDm_X)xxUWwv9x03tK2EAgiy%2SciT-hIdc0Li$vPB-3GBh@4>XtvBL~xrA#;#)U z%-dbU{(8p$A;#Q3L#N2;5ELA-(6rlY&`Haeb|?2h*UDib>s!byjz{+a&xePFO%#i0 zQJGGqZ8OhLmOWAuDCkrWe~hy8svSJD4*C+H>U2Z`f?zXRv&2l2Dn7<4$$fqQLAQ#7 z9^o|^cU8h%mJPpG2BTqMM1a!6-shDV0V)R^3l-t2I_->!Yl42 zO&BaIHs5P7C+8YwS*FPL@5zy$|tH$GlTJ}zv zg&dn5W934umwaUIC$lrSWzjQMFbu{0+JUtD9gU`Hxvke_gQsIe2X#8SlAj#-UYO1_ z5X@ok6IYN1L2lkuxwV{L4wxcDA(5PAeVU*Sb$AU*Q*lX;v*>*l9x`D8DHmB%W*w)m3{F()j0g{)zb z)*Goq!bHx)VPf`D^Td>mYfSo(Yj{mnfQ<$tP{Ms&`#Z5I|GR`sC+eQUH2_d;G#{Wo z62dDpYsmSn>skM1`pXy7Y;S5Wkz#bRC>gffiQuw$y6~%J#<;6P_sQZSR^U{@W%81pT?+D~Fzt zRXt~w6NorlneVn8&lZpX(1LBMX;@`{>Wx<)FzJynXy$_~$@1*!Vyn|Jf$M<#wuMFK zSIfjZx&0+XE#-Ptw~DTNg2*x%6mrI1x}?NPV8B6Jsbd8Gy~AUmEYw5_9rOMkdD5qp z9MrD{rlBS_<8&(+_srEpd0YB=0x3Q1!GV-B0*vm&B0Xg0157ay5d;_TzT zKG3|fH^1wp(1$8uEJDimqaA`pmfsafUFLvfE#9`AyC=PLJA(g~;D=8-93nC+7j(vD zNyE14#TL(gG#loiru6^vfyvyik^Vd#Nu2!jje5(VM*94S)ByeCf4d&5jF^5`Qdy7E z8}oK9nKqTBfygBs4&6nmUDf3GGinLS_O&U4UJ5fvxnXVCRx)*sE&vmeB#g$_5g0_1#V&n%4xK8; ztTIwAfnj8%)EJMR?}ET!VfH?RKIDt8IP{wlYGx-YwM?X$$RUBV@&yhL3f7;fACQP; zEB}erjQY~0=yh~Oa#(tu?$23eiaY4FMQyxDammQFd8od5`?`q*&){igL(WU*eYl9l z)RQvp#>gd);g=#jwqMPdlMWS4mFaQaqqFM zCz3TdE%^E|;|3zH*q(QrsdvukMRNdvmVM({0dOk?B2exqmR28OTgQ`*{10rH2E?rt zOfMyzIxISgosR}|6tFlMtQa16%OuPNcci6k&e47p&CZcJ0l;>YFQWIvADEq7JemzI zzaAiGuS)kKR)5Ro?Hi#O$LFW`_^o9ZcMbvoo-Dhp)E5D)1NJT?o(cc{-%ARVcw5B( zLTL?!7z1s{tL$`-H%Ds%+X<^t+6ik>+VQI`SVPsxuK?5=HYQf&wc#VPB$?HL(A%iA zTk3OPa14{Gr#bQ4xAEER-CBFDPaqA9x1U{e`xQi!8qD(RkIC$ur@QSRnI~ntUk}zB z7CdVWP-5~v_-{!F*YCWI98%-j$~aR9j8*>& zszfgiSUC)z-uIRuE{`RwhRF_nl3bxK$?ohfpa89w7ZMFlIp zRj409W0AewyQWbO;}v-Pwhs^+t?r18Kobxq=^&|uWiVNt#8U)Ee-cHZw+dwQv1AIH zVvxL-KqMst8%`Ca7cVqDI_i$Swy*pOD@c^7O8?0R2w!SSuA$uW=2&HX%yK_Q5^d33?S^@#ZRYC4b%b@^|_>@%bP7`+ue+D?j9zOqgAP`;bLAyQ9lg(&uciZWJV?;&iJHe4OF?A{1}Z_#*M6sW-e|6S*^KrEcYz zVP<|@mk2WY5y1PtGTbxX`of-9OKXA>l`J{Dv&TTVFik^)l!E``Obs!ZN)a(i4<91W zm7(>VCk+-Q)}F*_L39tNm>x5?H%s z!o)yp9ATD2rbmqmpI5_4ta39BfIKAg-{c{i(zs*~a=n-RW6rDeVlq~}bNDcf(C6@8 z&gOm58bMuhyLqG6?kElTIYHbHx?dLT4FnU_OoPN8Bj))VmVHz>0+0fc2YbFzKx3f; z$n!c^exrk<=ieFLh%R}O(gK)F0+3W<2e*>+_8Q5sA?7Xdn^<7!kR=6emSoVY&*FHTE7nrn< zDM!=3Jw>f_MUn~r-dltX(c!aBjC>Znj zPF6{wP>hU&=Bkc@{H>EEp&h8grW9}~PL&X{%l2@6# z_zgT}GO3?^z`0Q@B0!_$teDja63Z^fAV)4P^voT2sX7c?-eDiJ_k z?O&oWerC~si7L?-Nefe*KLQH(Ue@Bx_@BuQjzM2XRL**y2NJaSCNoNsKq5ioV$HXx zvf(*Mg-C9*jj4j+8#%9pY|9m6J8gsl7Itigm_V9TVTi+rjXbqVfqEFW@cd_X5%232 z6dGe}S`JBBXJ5}p+CLtR5Q{_>G`>m-)B;;>kCKCAuxT?8!(pb!1mIB6>4qKzyizom zX6m5oWlgM){g^68sA)V1Bk1waP47S1*sQ=`{m*f%mHMk+wFi))*3GTG>9MNd4Ep%= z?e12Fx^o#MQ|$S?AAj>o`LUoxT2y5tPfmq)skK`3YKCULMK=LrTP2SEjlS8B<;}57 z$_hd*`&I@|VT#gs)x$;m6;(&1x6yzdl~;K-pt(+Mc0wezuq1P}8^y8sYWU0Ef`~p~ zgMqqpqNQcL%XwZQ0yMy=S7|qK6eXg z+93q#v9DN~jN^$6+Uxt}TV+w|^?h0xXTtFY6tvI670bk7M>%C4wX$fAvGgLE? z&IhFWQqpG^_er(sTB!%Z^XLxA08_pW#p)tT&QI1E#eKU8(@AE&r7HafF0AcU*x;Fx zxvZ*QO{YTJ=omyYj&Exg5Q{9^-Ge2f zL&_v;IrSMd!5}M;BveJM5E}a_tA|-;eTIlyBK&Ixo= z%pyoUo>c#oawyI$Pc4xP;35p5r=vrK=Uugo;we6g@}#Luby2Ud`=D?(Ix}t2Ftkp> zSiAL+r3kSFg;`qT^8G0jZldr5J+Z}791s)^xR;9=c!bjK8?-ZjKyc@ z(sW8Pb77U$$9*FvWjmsSM_~^1oy#S$ug?~VX$4sNH$&d0M?EFuw{Y8?t?P@)G!UcF zusrw!9yehBCz_q5*Sw-GuEJ-Ku#O=8?i|H7dlczFIStB`;iZVir$PL*YOFgyZ-L@8xx&I#aa+KRyU5(UhP4@^!dRQs++`Ji90R?GFnI^DiZk*)kw3Qd zgUs(*zqlpfVfqknID6bKDw*QZ4)b7yd^dytT6k50w4f`DW$tpKSd4>Ex{zupE{h5g z7#8oKsYPsBIQ>=5uPGi?oUPBOg-xLnL&$<6;eOK$8yu=5wb^H>%M+w|Tm)MGr~~ob zNn-RlDamy$kro+XwtVElpcfe1p0QaX^I7m%2fSG!s^#6A8ngM{6#1|_m1+pYM{@~O zQn^Bw8oBRRS`(?G6RHMb>n})hl-Lfx- z7Yy&-8O@GV>5#Wt&}P z%W2b%^!b65&dQV2oAcMN0K121-BJUAIL+XOW&eIt&wZs#5x=G<&`O5er3NtM)0;)t zOesWLWh;2R<-&vq-|zZ1>-L{Y{Xb<09ql1<9Ojec z3Ur2FVqCx7f4#IjAf#_<)ElVak#m$;BwM zCeTOuf!+$P!q<9TsJ`E06-V!2elvomfSG{i5C5PcErH~!+~kslr_%jEO=Npv-p_-@ zh9*Fr!Il~n!Z0)oLopa=s~)brc1viTgr5be_4v~861;%=FhF40duVHZ4YGK z^;56FH>XIg*2`%(`bBCbJIa^7CBVevY}ws=ztAY=KQSq)CuP~@x7SD!=M#=hAe+$r z{JeAbTSfEt-qe_n&G0#jFZ!=rJzqW# zL6E2HZ&3V~^9x|sDA?~&Q>MFxT@iLXuy*9MyvE*Uw%(qGf?)-zM47QY`Kd(S z^BkD17c=bPLQvR&zRa}^2n5d9#UaZzn7Pl<{+c3)V?n**y5~6r%-s8dzkZOe~n3p53IWvq~Qde3%*ChNzt57 z(2c_bLgMg{Q2HZNNmfqCGwf{0q2E1zEcNv*4KYO;XiDrPvAmQQzKxSD_q%dP5toic zACoGB)99>JjbMEFgqaoYd?5XD&1d1LX-rSbfAf|;>68d^3S`Kv~^Wrk)BX|YOsGaE#`>z=jEa0<7pjE9K?>Uv*HT`c{AQh_6pjL8P6dkXtA4e*;7Rl?3w?uPNz=|eUSVcd*IRk~U@-tgxT@7@4?MP<`(oJ^pIukqeT zGY{2ot&nRm{gKWa4tQS)&Np0L9>I1J6 zN*Wd>gI5Nc;qJJ>5zw1d49!8xA@oUbypehAl*I+xD4hTtpyR(m(_L(}RuDXe6eQ|v z;tKyB58g>;G+w2`GjLXp_PhL7L%+(Ksj524K+hdz7zK;YI)+x1Q#j2Yz(toBhR!9z zD6eGR0wQzXBhcxwW|;U^R>O?%H7&oLz!MAK6p4t39H}X`_Dy^5Frd%;QsE6!80_3a zrZkdpN}iQ|$c21+)||v5?KH1-QivhX#Gvy0vGJ_82_wV_M;C5? zj7s6p^>5SW|NF&RCG@VG9(#)=@jNF~JX*oM<1NP#p51wuTw0ndBnx>}m06th)yYCL zJLUuN`ksI!)|eu1bl1bkZYoK*{xdmZ8(f2xWGn_B#I(AmhVR2^v2Yd(6+&+spj|6f z{gXeQ6pglb=8~!)xQexMGcPR~z*Pq5$q#kqako?YrLvhwgX0ARp2hY;IlQemzOhtc zyh91s?0sP5Nv!1K4evHR)(68?usCIp2J*gM*fYSMs?e=jVGSyu0SrfPF!ME8=I{O{ zY>2Ij+IPZ29TNE>iIIUtRy@h1-(2SWZ{+-bmVuN%g9ze8a`+aIMbQ_aE z^pU1ACl4N_HHwKtu%c_#U*dMLSULk)Q~k#{yKYBs;p*}ecI9p#$YP4)Ay zG8bbde}QR5UnlkJ1zU7Oh-AzbD*@z7z)IQpboVhhm_Vc!jYtAU6Y5qpv(t2@Y|&|x z+EDj2nX8BkuekRY!232eA%*B!Hd3Nl$@-Tht?d~-TyCgQZ# z!=a$hL#DmgwD`+?fH`cCr-#bEvgJBIhUW1U8z={yT=t?qlt3ihVZl1_8K6^2`gSGY z_H3^PWPF23K*_CP*_!Knop5vclYdn>r&Hk9r%dpx%(rbJ`#`xEMJ`G1+L|807r1Jj z1}Mr7T;5&Rz&G$~aLKtb${T7}W2F{;wXDw`X2sM+@wLr-UFIqZ8l(LI5EMg982fD# zY}2|TF$rjm)_F5vondqtZ{_vmbAGr&#$PT zbje8h{90JIm1R)C1dTIDNws?R?M+YDh!TN7-Iy%(8`0BLT<;t2I|GKM8+?z)kC+*v0-a8(fyzogN$cL?+;r?h?K1pDy)Q&`zO3t%^ zfz%2DW7S4-qI7VwnF~L;>k!DaHzBT{O8C{Pb-P8-7{5 z@Rc_Au0)>w^ru&r75JYt(yOD$?~^?kM^IFaqzm=)wQUDcT^1&S>Tb?unG!b}-hF$X z%I5*Q_G-KXahv9Uy{S3;;T7@W;T}MwuC1*?Rlzm=)%5esK&(UTl+&=fFnb9>w+OkS z*HV%whNnZ@ktvH-C_@TPdmPokLkhmIsZlUo+a&I?W+Ow?tM^(lI-%GPoH6ZKZ6fn z?bv=&t``A~*rTj%5(rZ$B{CTYA(5<+i$Eu&{f4k= zJx*3&h-234V)Ykwrt5NfrEb}5{iw*I@*+Iq9N6abtjx>Gv~R5EOct3hkGdjQh}?g^ zmc2Q#N`jiIy8HlSb@s0D_(pVKS`(*z0qUN4A2-ex9dSaqy9+RoE_e()Ch5B4o($|E z4i(jS2Dn0RaSIrHtIBRJx{0FT3LVGYMqcetF#I6!ts9o#?0gc6v47Za|DjR?GQDQ( z_pKjwx_4a^*58whOu2|gIwK!NUmXtU;sxY9E3LI9Vx$cR9IAgVhX(S z&&`2Vp+l2E=rh1(801MG}q|lTqt6~;R~$TYHvb5G9*VT`mwBa{A?-J zh*q`T^E65f{^&|yvS6HUhae+5ccYKXFfe1Qyit<(9y+E7v4pCZupXq(-qABsN89}9 z*-eqU%>{}25r)WdU{y#VWhL}Fnb995-`x&-OCjEkm=!%GZ{9y^eit)y9&$Gi1TB)N5TkmjxeW9X&V0{~c!6Aj%*Yk{ z0Odlf(0Qn&xHs{gax6f*^Z7OJ$%S9MWa8&0si0(JB!!>yqGM{SWcdv6U+T%v6&e?l zbQu~Pm%X!&JNFf);Mi||DmT33FZU5}ORB$T_p3LeJZhE3}_l=uB)2A?BrFNkBkGHYOhyLgR6(X&uu z117gd-A$@eNQ32T6GoV$_ZRS4#a4_{WaYVcPacE%a9~0{BWhz{0tH+5}kfNK#GCond=*_|HIfiC8@8IbPQM5biETW)wteRsZNemI|Z_qlSlLz+$9J)Vks3`!i3iaw(#)Z+hX zHpY%wZb4ZWCDm^Iqs!`d3=4o$mWgjVt~%Qq=qU;j@ZJXen6>Z`+tkG9ltxj6Sbg`d z&cJZCZ+NQ9vRTD}dm7|H(DSy0pHzsKVQA&(cOVbC@aD}<`t>DTNwh2a15?IY|8qjd zK|6o^{PIP};}3FJZdV6C421+NU8P?NsCn0M7OcuzVi_)VYbI`k(kc>mT5p@o6ph{t zdce{21u~R#oe$vaZ+PS>+KnLvr%+1fmz7N4hnr6{I5|Z*9bPdC&9R zoF1sRyGN2&7lz8KE(s;-BY6JhEP*EX)Kda%2|Nyi^B38w6JBH;6D%5rJO^~7Qc9H8 zuR5UD1$-~dBg;Qpy*P=+b*C*6zmBv_Ti*+jO8TPW%M<|Ug-CL{@~|f)%HQK><|hF8 z8sQaKg1Z$n$$^U3yIa8d8c3_(>I|aXAKu+LorT=> z1?pQF<8ZX&0oO>U7zgBH*9k?AhQsjb7lD#EOuHMb{PLdwa3Qt9oS8K`uhw=Z#iWgR zOUz+CS6!&*@bMcmre>L4$#A3$m5mIg{iLyS%MIX!?;qgs&w?IFp7!{wtduO=|I@e; z%1}R7tQYj)O20#U|HiO^R`;>;$Yo*Ysi)C_Mg{HGybG0e2QGjAF5=n=Tojl2q4O{= z*|zEh@7GzNV?AsPKO$wBjhN8PEXbE(`tuU+REl|50A5^WQ@3;(A#iz?`QaD*g5S(( zfLWlAi~+}=VIohV%BswgBG+}%Il-Buo?EF6U6!}44 zhv1=&RlitkJLE~x#GMy=5k87Ai!O_9mefawkw2G^mQjS`TXFh8b=2HCxqSmizoY$mMdvM{^job^~ixVYR{uB6PAA&fPARl z-62kJY0mp;W_JV2F!db=E;|ffrJm8tNP8Ja;8TJ6Sz6YalsJQOxI|uwULYiQC+>nt zW6`};Dt&KqFKh{&qdtU~USaDaG0}io)946)rpy-fq9!^7=WBwk z`#5k+i?~il4;0LBavd20&aykCo1D+$)3?J!o6qDFp={T@q}DamPP0sqxwaXqG@hF| zbnF0{ZZ<-#R*GPis1mZsLTAKUOxKuN3B)H6x8uCz=mAk}jNH@ELJD$WERgzWE( zrMmw4yfzv8I6z!}esI+wa`^=+%y*6J5JF2mOzc&VK@YprjdogTan%vW)|$m!|C;`A z52;k4mzgiNGamA_V^fiR&t`ZHY*Q-P*?{`D$24Y&ZFRqO3OIJs^Q7Q0mv_S(#v18J zQ_)YjFh^k+v`F{<+Ty`3@@QtmUQ5nnzA5+}ICc7Ix_y&$I>12_3V?QS5UfewRoiC~ zFml>dxYGyz%8X1H7Y1!>l~sANuY`CyCL;f_n`KG#wq5V=j>*0 z*#|S#p5;QF>ijyt(WFG0D9P)5vbulh^<_nNwav}(PzmL{h*M7NZsl` zD6KUJ#0h`j23>kAS1;5xGh=s64IhFHxx*b`De75LPk|1uf5v>h69ihlRRS7eh|Hc0 zr4{^_w~d^n;X^abraMOz5c39(%L+g2jSU=TPtNFreQ2-uji1C(K zYAa7h3EwC?i@5vDpOM{{|l3x^xXD-lb@_7gFq5vZVO^4kFw8NTw}xuOxc^ zMei6k!>m?d=<3|S|EskAZrsfnbB4gCYN)pG`q?$4%4aJZ3M$V}JEYpF-7`;Sw97%XFi^)AlE7;B}waVM?mun%^~;J& z&u%V2XQhGMWs1z*KDDa<{baiJ-Z`X%6qzx{?vt!XmlDwJ8Q870RNpbveN1m^AH0o* zidHVSczy;3Zf;GqE&hBp4d7H?xPnN(SG%Eu6<7?7CPIhiqF!UyT6S+67vvcn@RQsN z8h8>SOwQg49hdfOpt#zM_m+Y#PEgot6=w3SXX~F{-<;QBz(@5c?_<(7kiZ z&vSWDxwWjM;R>6cqS4ySlK+xDkrs}$uCuYsAwQ73)oFvPBs z{QOMeeUq%a`k<>#pZyUo)nU&GdTdPXf`?91h2D3Zgipyun*PcI|F1o)E+jICLQ{%)m?BfEOPuz7T8OG#EO|EhiTx~|ay`)jn;J14cf*el9>)J64 z9ELy3C;{u8QNDk6phaZw8_YG_e5DQJp7lLd+A|7zRcz~SndUR5aIyEpc)`oUhq>9B z57rIc+Vlr1V8H69NqtzMj@R8^)=uVBNR>W3jn7yvyv10K_9{gK8%@LI6e~nm2TKd| zR1@O@oMqv)@=o8iWva|T^sLxc?D4_n(7tEkWgsPq+1>y!2z~Q5a9d8|Tk2>^P?4cB38b+%{n{#}4+xeuA68F*-gqv=+n~H#AxSHlPt(+xwIm~pma!a%;<9PU z0UoCD?cO2NC&!B6p)w(ND}Od8gR#7T?B~RlPR&=UQipkFa0t}h?#0vmC^4YklY_hx zmb@l#xm;#28aZ4RMJcXUWyBSlE~#t)OfA$MyJ~qi;o=NH%#T_X--9qrNWooBzIoxYsT=_2(S*qlMPaw^=k7-%5f+KMnR8vj>()xzA6y| zye7C3$ddr(XR_751tT1SB|KQhobFZ1iQRQk}_A(5WOK zN1=(u=EU|#iLrn1r4w}7>Owody8d|CWBjHCu^Kh0k*Q6!@O5Rw=HOmY_wefa^}CYa z!7HuqBg=p6^>lx;%cNTfQH~qeXb+hvT~zKEfL{*pg{tbz2FcLC#&@okT~WiVHZ!Js z4sEw&%Sf+Zw=BK@1Z7G1XDD&6GpmiaQVKlU$y11JHHysHn%3L-03vM2&Rf}%MW@y< zf3ajX(f#fJ=zjk8Y3gABdAWubNB9?%YZdM({<=c**Ag1$;Bhz`0!VCVK0!s_Ve0fn z{>}#nL#e`&FsutOXhmD@rc5y21a5|oMH*DU(m6*&8i3EyKNx?Sm?)0NO8=2_o?%L^$E|S6N z_+3Bgzk;?lSpRip@|m z__^rk89l>IKqK%KP2>G3mX$UImKIaW?q!?zsdg$Z|6(r_9bwB^zue_M3rCN_EX#s^ zbOQ)OB%f%}{+8_$7}^(rHs4H!+TF>?^Jq#F0Ei8$J&aK6^qRs?&k4FVa^ht?g^efj zlmNU`pr3EWE2LmNh~oNre?f*JAQ7-LZucjx4c5HS^;T#qpO#Mgc4#r2LS8ySPKC|h zCZm?%8-9)!w{W~`9Lu7qA>g@o{Wu3~o*=Afy-B(x7aRm)70jT*iz591AQ45yak20v zJM1vY%5g=LZ|gHh{*khi)TxRrbL$)GQjy*H8qe)}mO+n+`t?U(wW%`g1hchAqMqB> z>)MOyg%8g?QC}QSil0jmy8AY zbSPHF$O8+uFdGB#wONX2enAYgdOrg4w3F9zD4D+w0L{B9^bvxyb+!f+Z@(xL+iOQD zW^H%`0+`Nn?JV8ZPKsBvE1HG{TCjE-x21Ny1QW4XFf|0Odh2p>B5-KGbw*mg%IUi7 zwIP*|6w5N}k@?e1A>&jv?{mc?vCf}pbB)d7 z>M7du6!xaoXA6+@`?!I0U)aWofS^9ScceEPPozDGZ+VhP z(pQP4RsG^)+?_{M4Ya|77O1PG^Qy?05&&0M)ZjDDUsP*>p%awm3 zhU-w*7n+41p%*A|>W>`06Yko2E=KZR{q~HX;nA~I0N(rWoQ>8D!74oB#2vo(m$Vte z1nl)#A119})a1>Vd#K6zAN*Q8_i@TIEk^YJPfp=&6r=x_Qy2vgA<>^Mq#a}6(T6>0 zfx#95moNePVst5Nlgtx>X6xOWFLPf&C>S(zCfJ<2rX1w7NO54;R(Sn#Djx&&c6_K2tj z4M3L(FRkwvEto0plkt9yS@MEk@QGRrAGO|6yNvhcYbSD>;iUg1uec+%{7HO7&QzW$q%Yk3@!@1bZZK&~+Qi-pQbI%j+fGMdmu+g;&K#}J7 z7QRuGe(o1RM^A4jcFJKaO!{`dwQ|jst?XRRW{VVSof=O!0Jmb1#vlHnKzte! zCCAK&C$iXDaU}RU&s%fQS$kwkM=#=yPvh+$Asc0Zy)5g7N?`o9F#%&ygYPd$ zn~qzXHmW3tt9|_KjkXom@C9Pn_2vQ4%VD8jg^HjkEXQ+Bs!kgJ03C^3jsdCQW%5&38O0vA_1`^lp3yFg2+_mC5C?4Ot0hO8 zrn9U8jbW6T_5y%@CTT4k(E6m^*8YZt;#*7LP%6&=Ll~8LHspG`8|V5Zkx`W%UM=eI zVFYT3v6ZiVRZ=;A|8B;bqjU}kgZ4^XRsvrafrQT`z+yPkw<%eEReck zKgyHgKkuz6tndDsm|>XFH!N>2XXoVee;09Sgnd63Cq9~m%E%~1m$(^=1>-_-r8tCu z7z>UgGa0qu?%9VT4N_K@ z8_aM6?~B|Ew^vsMu8La)KiG;eExZ0VuV=FxMl|%u?3Ot?Cr;2Ot1yftO{j_4bdI=TQx5)(7FDt7T7HVY5|W zT|*c;*SPyJCMyRD&}r6MX7+}sf4;C%3mREI|Lk~pRRNhlggw-T9ZXkitCQRx%5FH+ z3|1RbJP6c2;r*5Ot9Jz3ztDtn>tV1=+sIo7CcnotQs%RjuABm5-K4zOF)gYFf3|8= zxwq59fxKW%#sZ|~M*B`1Uz4r1$6r()kvYET+kF~sqxQv6t--57V8u+YD8iuFtgn~% z+u5YoP+Orf)A%41ag7=GNcvFp3GB^cP+R?U!)Sv+tL6l3t(GsH;t786?+UsePUx_B zMN($oxc)nz9;Op5J$-iKO;;BG7d@%W+>%N6zV=^Q+?pGQoE9z!lqr z$x^D)BCYe=gGvT|$7y+r=Ze%RRvti8m3U0tmgzt+pBKL#+K;U zM`39%v-lyd-h$iRD2T8W-)JmXk1@!3*=kP0JMaDm8UdS!>5Uybe+PaBKpuiJW#E!X zs;TdOaSV2203AG>wpai%ji&AX_K};3cOqu7R=k6=IfE(Ws@ChfD>n0fV;YremPJi- zF16sT7lnP#t`k)mW;g9SvTcJ_b!vvCHL};X<*MV29~NQRD>3$hYpQw18igJrMH*$1 z&p8qwM@X(KYAqq*cyOv051<5FUjD^)<|iwF@nj9HyATpu{m}$m*8my9YjrO9legf< z@pRXO#|(=G;h)Z-+OzX;U-#j~gMPshr+h>GP|CaC-8c`+=JGckvVM>Q^#i`j8=b?5 zr0q2b)mf3=bgNCwQNsbXi$bz42&zJeai`A-Su5;;wwwlePeVS0x_K5w7i)K&CGOt+ zQO=cEKHz{0eK#{lJhr!`aGgG;UvwN}-wi7llhGh==LSuf&laCNPQ~;G!s7dWdrm`U zcTXN`Kk`LCUIi4nyIM9^s55CY@IvqW3VE`YI_->gYJv}XR&39j^dnf%Ed0`qy4tIT znx!C$iQnMuD`95sAA_`M^2MNO3Ac5r4=`rW3qgo{bp=sA8BE8%Qs+m~lt!PWB`|#! z@r+c^;|oftp8cnA-H=Ms&_I1^+~2yv#o{Dyje+Vew@STydc(zXXAdlXWjM8SvaF5s}V=9X&M3dNgG8 zP5kEUM`_BiDmaVucTaSVa+7s29Hb|-Zb@S59Gs)S>1UGZR`*mh$V`gfZ%9VN&Nvm} z76+#3!sS7|F`{8m%1^pmQEcz}Oo8~JlTE`=d|bM;GkCoH zz4BjsG#!wA0|c~*iK8%RlHV-Yo@$wvPG^uAT`gUfr@GEL zS0FH3ueYskfieuVeUe#9h{HF;K9k{^xRE@`z{IZVd0(SxZ9J>%!{lW-Z4D_R59Uwv zsB~r8oa0W*cnJeAZ0Zn#@pjC;zv>6AnDIh5u14KAksuSuvb#mkf$z80t;_Og6ANd5%m;Jvjn~q${vpe=E2kCQjw`e#sU`w7#@exg>&RF?{Id1`Qq2+Eo zgwM|K(amN8e0yZenC_tcHgmN=1{!E%ict3YjRHPQ`YRwkB~Esk(SP2f_osEFKi=Sc zZF7A6l=>v}?3ngen+{7#-=nY$7w;hV!;@bs*#R~rrOxjFfy5cHQwA_y399F5mi3fK zyA1<6yp$b^dEJ5Y8>_N;AD@ntxw~QWe`H8|LpoYl7&MqOd|`c8!P&mU$@Rp8C(C#@ zX3BX41@d#;(>bSc)6HEF9!6v%7Uo?zi|DZ<;p&gWYJk=D;1gOzhIMU+X~_M2xPm>- zovd|8a?NGdnjt<%Kg*|Ca-Pvs`MUyM%dOidooK1iIpj=^qs6HL!o$&g;)_ehN&7(g zWv$O$FNhszz%Dhb-quQSZiw_I9 zDGAy2G(T#LBi6A$wK-S<@TD8mYA@77&rwm!WI3$fT4?(ALjsGx(3@B${YAV5Gb=0m z(LYaMS-VBzu!SL|mL%KyAFf}&?$Fd2v^H`Xp&t1*dA?t=_dNbmBT4v7lP#Cx$6)l; z)@%#O3KJWv;t1p}>;k+4_u7eJTl5}KC_Z`{O9@T~8AziqN_7;>qpJtu41n0_x@5h<2UuN~2TL4Cs1& zLby?=6gHJ)y(~bVO|~(%3|+WQ_eyA3#(nLN2LPl$QymUJuDSX&`%BBDgT3xrYMci& zPG9+Fc7UO$0$kgGTkrG2L21QrNk{%rtH2d2+heCH^^(djj=a&$1cRX`kI5JwPCUVW ze$U}1rMe}Pr`y9^%#K2bZIYw|Vrx%lgm4jNhSXI4d~C_-W;(oksa z&K`BkR@B~`1ot<0OQSpf7Th!UnP_F^Yc22cyXM@%$5NV;J3(Xw$Et7*i_RTCE=RBP zhd_x_&t;3JdMaN4&rk5J_6!SK{v)aVtQ(ZRL&1?FYKu@bGL7SF8sZDm3_km%dZ zg*PX0!{AT>)1u(%ybRBVb-+lwq5wQbt4+7i`1Jw2muQaRJvGhD>4S1xrB`}`b4!kU z+By^SQ13zIatCc&Z1$#Iy4|lMfAeZ^*&kFLygw*!q7i{b%>6C*doxFHQ=n+q|p~mV`fodKgDJ*@_|4^adesn^V^^GQP@PA2xigRBvFHsFS`JKPMk9R=5My; zQn~mXB)2$`F0yL)okdNfhp(-#Vv4{^5-{<|j*j&;^7j$Hr;vbXwO{Nr+CbqX5j`)` zuKC&wriUf4=yW^3^t!FluKBe#pL{N}8ayC>2SMS>ZSMxxKXNJ=w%!Z9_DUGRNS9+_ zl}(K!;iwsa=DT71A`VW{czbFy^X#=O>*+$1*XRKJx8>W$PhZ`4lmXpu2R~0y0B+(| zrY{IKK;)=b{xL*zRTFTP=C&~EqIVYRTUP@?$LyE0_|(F)Y3|#J2>1zrAN2Yewj8(K zmR=mpaP-$pl-MlPlDF`Lx19l^O2}lXb{`XaA9P=%$ofj(Nn60#aO|-R7z-S}GQ|kI zNk<`3H2(jl>Np6#(O zj=fI4)e5ZG{|hK5*8`(t+y&8rNP$aGCUM+UIKR6xqUt>4hG8O^%8uMtbR427RK!>e>==V0wUXJsyZ{vAYaR7H5>b(?LGhQ%% zjON}LTXTwyZ`6$;51G41V3VePqc_@(S8wyR{1;bE)Z?{*&#cG0d?xAz>o<(?Gsvr_|fk^u()ICKJm8969~) z@Q6;`{)F5wlnYlM1Z1fwDtfH9XXnRroW`Wzpl)fhdsz~QRJ^yu8R89tiZsw}^~gAW z()C*H8`1sQC7@Six=9N7)PpFg6A^7BVQ;@@NxhVQXVevyQbF>P+^0?Q?0Ljq));(r zbtG$xF@h|l2l`}eBcC#AKm$BW7u|9ggAZv*JU>%SnPe~E@uLA~##fT;qLjqxfs)sI zu|J$SyYWOloVFy24*t8e&89q_Pe5&U0kONLz=i+DYKUytgX-X~fQ_N6Wh}8r=m=rDBgCR08H)TtY%bLKw@#WF#+GRiKibX5$ z(tfYGOYg9b*>nRPmKJ9B1)l{~WxUOFMyeCopH-l8@9BN} z(3+m>qM)vQmC>CBNb_lpz;(49y2791VORh4pI4HRXw?^SI&(mVuVqb#dIan*lzc@- zUb_2TQS@}ut^EU~AL;XQHO%~_RM+hWV86JUXZ=j}C!GNji!LI~Kez`Ot8NP=wL$MW z$$v<2dmDK6c?h3soj+NKV;in!^!Vf_gUXTt5;*2C5tH+TFUIRY2dJ_u6%Jtr1><4X&z| zM=pZia}l@My;Jmnw9= z?&8nxK#dA%x~oxrdw%O_@zT{O>ZQqJdR91*Bc8R3*rxlNet68&XJm@u4Qt`aJanZs z`%CxsMam#s!VldU5rb#M<%8*0b7cB+ncFHi#m@V3N|p3 z8SN=M|B=a4`i+ka5F82u8&}~&`GxD=IC9Yh4g>IPt9Q0$jQ!XC*8G~97i-nA_OM55 z(Pnsc7)_dI|A_R$p;=%WM6TO)l4B)+NP#_Q!+7mi*;0EUV_5A9N1E~Y<3{V{M$?GY z5+wL}-8QfV`1!A?VFdW>aOo0`G16n-H*RKAXTnkLvG$8z(f9gnOU8O}bn)u>k3*C= zn7Kk7qfnYP#Gj7L`1pn5mpO-H6A63CQ@5MJ{E;P!GJ0n9yp;Q^ZBpx&$}JxE-vl?8 zLyU(DGoI3|67?=$Zv413TEE&@)o-zDw91y)g^dcZd~QrR0xxD|Z9Q5XGq2{bw<3u4 zp6o@by^t!$d`c+wUiL)}x;O#hb2PL~+l-8HZ>Nx8tZ_41+N_M2cB-YN%*U(LttS4a zI#=B&IO5L5Tz|M(@6yG!6~-?;cWl&~u*Pk>=GAe3c#piT#UdI@hFfXqM^CCxpHC za>KyCz0y2K$%Ntqd(*neYMXmi<6h4D^9O*|N2%E`y$-KTPg9W7_(x-?wKRAU&L3(Fz&whZp?Os{ia9lZ1YbfCPET@z&1jO7(F48 zq4432Aok@;_`t6FUSIHhm4bWscR;7W7jn|ZSF?75vk~Gmm+wVKb3t1_@x8k-kkGxK zS#z0tRnvCoiJn;NAp$uFu0!#4PRggHJhpJ2&hhhBZpo{!L=O1;@28C_;kyueTc!0J z_vZ$O+(O!kQazz`6W=%v19ldTVon*nsZu>1#|}qI{Jet56l$gUFdh99ybENT6k+F2+D>0g#GXC$coDRH zx+r8GuTbzL!td##6mgMv*seDS@Loq=%`onpzwpqtEM4SAWRHaE%TwERqJ<|R&BdB> zcH2u(t$_7hzC4IMAjRP(gX8~rd%pPGlEl$3hntiDqRAUcd_$D4#Mj}vGH#Mi4fdl` zoyy(0Z5gUxBr@ANnrZYf+eK1CwZq-L(0uolC9d;T&%q-?!Q1|c=|OpT*rj}d+N z{I(M{7!%EKZ~nkWyo+2-fP&wxTEp@wdU%SEt74Mtmy({l6h6ZOaT*UF_FBk!_L;|7 zT^Iax??k-Pddi_hEJl8Rrj(JF7(ui^Opb(Y9KQEy2#Mc^?hC(DL+iAN=zE1REM|cy zjMz^vVElUGWaKTX!mj)E@?QDjg6oX#NjT$&9ULv(P7D_XER4_Fkbl81nfhXiBK9_r zy$}zYEH4;7#HeXQ-Wfo_7oF8bWicgJNyJ6Fx31Vdx7ONw?jo@srT)So6Cz_2x`dAMI^xva}fXE8GosPu(-{c+;c$vFu)> z(+CvHuz7_4@RVn%orMGz5-rtw&xE5WzlQq$NZvd%VL**i^pMy36$pc=+!y0^mj^+(LieU6WW~wHbi-5>5K*gNKZ72xNpD1k{0u0Z@r(ZbN}SSDI*p46GqI#+bJpbpEy;lY1P#I~G9oYzv`Xr| zt6Zu#VX?kTPczBqf%?fDFBkp8RC zRQX#EztQEsdY+E|sA)^d--VFd&VcxV-gpBKJ|}%Im17Aut|4obCv9mC%A*%^MG%{!dl<^LS00ftpwUaQIDg`fy#&2un`&FvKN`| zVo+l#oUte{T4Udh)JyX0sz5gEOXmH7tRo-`!0})Cn^tJ2GquMg!dOx+z!R$+gMhIok2Fq8yVbsLLH>IZ>cLWmC*Q# z_|1tPLK%U5Ouw(Mc+)+9%Y-8{7qDtI_5xYqiHAUI&^BIZgHibD)RO-AL0?IA3RPX< zT^(GQ#)J{0kJJ$s%8Mj-9~ZVcxi_J$!jO-1?-mhcB|6(PjX>3EO}h#Ur4*enz^CEV z`ZEHC*)ZSY9Y$+ViBROTCBKcIADKDtw^_jVxZs)k+AMLDu|M=q#27P1r)bG^Bx&f} zBx=L9^-(bZ3inC(U?SKHlA-1S9R+rC@K5xM^q^3`RI77G6?D8TjsYJ&b!3gQ2?Rfi zqncjhv%LDa{_3o≪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%0D+$N{Sn+NzsS|Q^&1nD4!F#?`R%x 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%0DeLBq~ zdb*8Re&6xeK67(p^V{|Z&fif3UWEBc^QuWzlfMI%A8Q+@n#R9?Dz1pNI8@((eL|t$ z7F_JHUbIZUm~EHpZrBK%;)ri9Oa($u%kVtn?g|{%328jDRS9%~NRsLDthGqp!!S;s z@=TbwH08Y0^kQQC7dBSkjo>8iBA3I?L=-c@SBS2l^NxW+G7m>cj{X&3u!39D2(&kS z)SXS{Tb{-)wro7^KFD3z!5n{x`FIHo7zN>bvlaV8LWgIwAA21~Neu3Xz^{bP~7UI{Rn zoBCYV9W2FRZN1K{Ex#TbE-Gm-3<`+rlODF7UR5YDrTU)bVV4fv+chg)a_Ulj^-RUF zteQOE6d zmFw382(($!O+gu4@u>$?dHe=|{F80X;ykG37!*DG_5l9JYJt2P3}f4`*U|S8>LIYb zeb_TIWxdU~dGc35V>m3leXOcY*JCARBWFi~jG*%Q8&M^4-d-VRoq*e(8657R<0eOv z?(K!@_CW2AtzR#t*gFW%6=m)SqsZq7xn4U(2=F zY&f*MdoY-JGE1I5ZxcpxNj^jCa`_#Xl$4g&wn@V<{B_KSnif5D(yR5yMjkUOg1`F`Dj!)HJQzGOE{Ax6tJL{&SC z`ZR6KcH7NCaNVmCIsa#w{H8fH#xJOC9r^Ffc=FXjtWGrc2;V8KVaUu}#DCAlC8-Yx z+ps+Jy7E&+qZz&ek;AEi zlxT{MD_hvDzl*$>D!g&@r-(Nc_BceZ426F*$f9X3=c_fbz&6j>!qdF=_^1cpg=R zs3Et4mBdr)%H(*xzNlbB?t0&AIfXyg^gDx!8V~uFN`gpZ%b56JzRrQRTo_i*Bybh) zUPR+UnCTdqHA9vipSo6!%7{#`AVy(g(V8wgKX);U0+)RQzKxapKn+B z0y~3BKi5KL0J7CZR+SdoFe%=@&VbgI4sd+S)BR`VF?VI7<_G;JiK<@PZ`8y(P?m95 z`Sfe-)H>&OyYuqalbJ13)?e_5JeNX7n3))5lA?U>&hxwg%7(Sl2#>xNWac9wQ?LWm zrYF|V6g#4CW|*uq;nc1jnR0~KCo;z|`{qKY50(VFYOnH50j0^s9=B4RZ;IPerv2yP zFj#iW`#Qp#=9`1Eyma@m>KS|J?#!#`qMzE2rB^WlYdcwMdk(I&8(^ceJX!BUVOHTw zkmZS7%yc-<8Yg~_P`&C7r!@TwC94ZB9yA;tBs6?U=Qw1>YOc0bFM z(S&Dz2>2x*o5Th|Kq68AFVxW`TC;8OC#Sx|#@y|JJc#ELtz?)kb;EqMwFZSmF~ndM zf$|ttO?*Zj7xOztJWfe8eI;cZ#LmBOgE}l?;HX;Y#&C`oP_9QVtA1dAhbqvfT@5ob5GQwRD*kN+R63 zhl@l@4vORS7d`1dIV&CAx(x@biCW6t6z4&Zv>+&C99?egroyV_1nEjC_~Q8c%n0WC z!bnXExu4<2#{}{h{ZEtA&YAV{Dwk`f^$huSpkV)U&V#XnPUAW<4qYQa3-g{ok4pks z&VS7>5tR;aHcn%y5_vs-^uab8E8G^Y%4zp9&vA3+$J(BKUzL~kOL>zUI_Hja-XlHi z5)vbl#$7)EQc>0QI>Ov%Ppgd&4Cm4 zRTARMTJf5jg%e3}cV5y+N3lApus?%U>#&wWz2&&Qm)qsG{INj+zG(juv2nEd=s=oA zeh&U9&diDPIIPYi3dBgFaf}`IVgnSq{b&`Iu*KgxV-hVqQ_W2YMos-JzL+9ZVbcxb z^u|OtD-TD%)g<}KT1Y`1I#4Y>->+Ve-==UK6V|TqY4a5E$cJlR5lSJP2S*rx{ zqMUEf(3t&b_{V;YlUM7E*R+L6Hv-Ws*?L6fBP(!n4z7)P7u8`YIk6O@fLQOoJUt|9 zP%xP?<@FIbD-p=?$;;7W7Ve~!Rg~Qwp;1aVlhp0=jSw(P8wqsC5@Q+3a4_IBFjz^B z!-90s=?Q+6Wb{=e%}W=I#ZVH*5rT2W;Zeut;S)svCQwW%c;@yok)|*DeVdwdiZjjG zPIes1v2iY$@if{ye&iRY$cPjhb!D!e->b7A$J^q3rFxF z_8QL0mw0`aJcag-8264kB0s8?#nhoQ&o;7G*IAuC2OO1Z`zum}RoM}OKYv6JzbA8} z7de;w{z~WPdIHa(kH2W_&`Ia^m5Lky_#{dgR4Ak*GA3<-OEF~ z&9@H{dB0jB(;X$AWb4>=kBLLuz8y%D=ZH^{tR(m`S`MyQSlVKctiUidsprQcZ1tvt z#k+mq8(LQYGF8YE*!v!@riB3k5TAoy&ZQkB*Dp3wtdCG=g;)=P2s(x^a~Cs3Gk@$= zA6xS;W^EGIDy*AjdzA!&^y88_)z1!0CyVV+i%?E!m6;+kR@ZVOCj8 zom30hF5f{Lb^lBl(hgs(-aJ}z(y5LxBoyj*UNkoiVUrc;Fjk-|67`k36t3ajPRt(@)yAs&r<@~*zrhA1#MeeV?c46n<%Vfqyeo3dUh<%sxHEk1O z|8V($x*>@UZ`68)QmT}`Ggkls7mhw|MPT3%PQt+FZ5VNTAvZIJT-<*1#&t;C-Krh& zPR@|JOv7;?R3m46eXd*dvR)qt9RKKhVBt=IT1~)$o$RM<+mTv|B9}z}kH?(%%42poO4U6K!q;G4n%A4oDOZ!^ ztbXj;jLC;(iPT`lo3_f}tRQn0QzHq&th$1(Qe3{V=<-VI(7dGsb#%iI8gJ~_0 zB9P^qPsNw;M;*qqbODK6ne5@O@nvBfRI3O;>{4NCBe<5M5>|F<*?RXKU*9F7wn$xU z8kiucEl73rC0vUbS5`2{l^cWMqEUp`bNtg@aDH8{)7L)BbgtMdPisfND(vs644r8- z@i(hE-1i+&I2Rle`8=>&ez0Mzh0y-hd)a7-hz&_YWC$HLu_e9#wq|WuGccFLX=V@e zpkY!`M3zx-N>pXTRfc`KP}p;2{*#KTK%N-QZo&+`q@=Vrq?&NUaNRi_>GPVX3m zXlHf)GMHbJU6SD3Ud@0xb51UH;4*0)7-(yH7Pt~0v?JV{9TG9WTQI;~PC#z}Kqv`Cp%Vk294G`nP=#DeD+}VBlu|$fHX~P#JZ?P<^>Y!HS#une~&BXg;ogw z>NF>EqK=lG!s1fGXh_zs5)wgi!N5EPS|ld(=onG@p9dMF5Z6D16wnzW-IM3JN>`;t z^;^_+*hl`QoZ6p^>yHY5gnfH0LGXhldzUWiRVE&2aV!!vQ(4hTq@+F_xT=!sy>n#2 zPN8=qc{hfr7b)r{-kHAYihMV@O&Yl~1`(CVO#kKhT#V@W1A`j#i4@BAwl~pRP2nW^ zLVvMO<>&JGw4UQXocL9Zx_+)0nR->;MOjP1RK;75C2Ka09|n_Yq6;WX`$7(+l8?*A z3Q6>Xf~LvQ7e00^GN=;G$6B^$h=VWhnd5(<6_sO@mcn`pN}B&93?%96dA=I`|_eEVhcPE)Xflqth9?Nu>3$j!S_i0wn42#+l7Gu*k zE+_LY_Fh;|Ezr1K(cb_X-^OnWyt5OUX|6Goc6tgeC}e@CL+`swaL9&rFP6iC9gBJs z@?6U4FWiSs&(b8T2yz)5myRlkR;4QOW zRrw56EZAULO~Yr}iPS7~8xIG}36dlTB`v#T50d^$AG zdwe=Jz)+iDlfTgJ*;yu<_3? zyT%-2K*q>{&KRmA&agdED5Z+qxdV~Wbu?xR=g|<^1qT$%2oEFdc9mi`b!=x8G@*M0oHnjjp?D@!V0PP0X!D`;!Cy`4ycL`BtAu`O@5ZC}#iH%AkBi zg^e9uMWi~}%XtrJfbhGXSzY!VUV9S0lCQVm0P%JRm%!aJVK!3cT1Q?*Z>$p(ciXokzmyu|@-1`$RsE1$ccC2+8zSZ;j*L#_reo3@$zYWVFf(`; zCn&DN6PH|R#zXT2n`Trh?DH0BlPv|vFoagvYBc{qS>q?yfyE zz1cQxf*D#){X&lME;-f+ns-(Z&bgE(_pGjnQXyIR&bo=mY5yNQ?4AVgBl27*(aqTA zW?7zXK1)QpZo_X=V={4mUeAfIV)=r`@Lr4KJZLum@Ur>^skPFO_bAQXek6g`v-zNW zb`IYlF=d_j_LCx1idM&#u-2=DRq)qy5 z;K&lqqAy&&LB8RryArv%7qD@`-9kh1i@j8BtewnYn$o;qBf{PZ+#z-Lx&8GUqWK~dei?t@TCxoTvBNv}-uJZ(fFaFDd;2=fa`U|{Fa-YK z`pCJVGS5dMq~dPOnmk_BI4A6X$t`>yg((jVDN$F?pJ`?ckgc?A9E~!H+&Npbs&PSI ze3MO5{flH1I%=34v7cfveEtuOjR^!MeKWz$WVO~>=Y@UjBrJVDEltS+^7cG$$S-NA zxowV&QK-ykM31uE89|H7Vx$pAwtsNkVv!a{N{J%dUD2yD-zFFk0O5>(@*XM75K*H= z5NL#|;%pd_&c(&#x`pt;Qp`RIRxg0)ul4_T+2VrX zXXz6?dEmY+6X7VAZUpbmq}l%Z>Ah4h5Y5*+W~x>R+D;t8aPGSe3|2ge4tO^hMlwRN z9qSEf#x=$c^L1(L3+jqSfkeaZKX1bSVYLn`TKgTAD)NV^%-QNxZZA0$YwnB6>x+=9 z$~BE6`PF76LfS9kF^}p$W0A2Ro382X)ofmpEj$POoNg;9kH6BGPDTjt0`-=mZxf7>8LpUhqR`8t>oWEhej&n_(I_2dm zX@hy^OY+VrSwzd?#k^L*r@Wb`)~x7AfIiy^c%>+Ghd=LiDf0PJ833j*FtjSI=7*lX zw%!AOX?A|{iKa$L^m+!t_y%Go8PZl z+;NJo3$$br7X`7qEQ?H~`5b7>dk0hREjSEco=7%K*y{CFesLTq+_p_F_LU9 zarR(2H~eAdbuasFvw>SOTS?;r8LEDSci8|>X!FNf(fe#^(W`H*EEk76lWHz)qI^s( z{r%3`B`v7@@MfntqU4>i+?3a5rHY~Z{S6PXN|Yl(>>F0c&&B?8)c%j(E6abAl22@A zW8wG0-cE3dntji-JKZxonpq!SJE;D{#�F{-JiE@dD2J1&*6a)QQ+Of53n6HTzQW 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

nwcMP8dpZH9=PZ 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 g_U(HeeQvd(} diff --git a/Documentation.docc/CodeEditUI/Resources/FontPicker_View.png b/Documentation.docc/CodeEditUI/Resources/FontPicker_View.png deleted file mode 100644 index b221f65ac11ede1f89a65f0588d8375a66dbb004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8917 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 diff --git a/Documentation.docc/CodeEditUI/Resources/HelpButton_View.png b/Documentation.docc/CodeEditUI/Resources/HelpButton_View.png deleted file mode 100644 index 4e94d2b208d797b937e469ec1f4c682c39326f99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4322 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 diff --git a/Documentation.docc/CodeEditUI/SegmentedControl.md b/Documentation.docc/CodeEditUI/SegmentedControl.md deleted file mode 100644 index 16276e63bb..0000000000 --- a/Documentation.docc/CodeEditUI/SegmentedControl.md +++ /dev/null @@ -1,20 +0,0 @@ -# ``CodeEdit/SegmentedControl`` - -## Usage - -```swift -@State var selected: Int = 0 -var items: [String] = ["Tab 1", "Tab 2"] - -SegmentedControl($selected, options: items) -``` - -## Preview - -![Segmented Control](SegmentedControl_View.png) - -## Topics - -### Item - -- ``SegmentedControlItem`` diff --git a/Documentation.docc/CodeEditUI/ToolbarBranchPicker.md b/Documentation.docc/CodeEditUI/ToolbarBranchPicker.md deleted file mode 100644 index c4cfb6663e..0000000000 --- a/Documentation.docc/CodeEditUI/ToolbarBranchPicker.md +++ /dev/null @@ -1,39 +0,0 @@ -# ``CodeEdit/ToolbarBranchPicker`` - -## Overview - -When the current project is a git repository, this will show the currently -checked-out branch as a subtitle. Once a tap is registered, a popup will -appear displaying the currently checked-out branch and all other local branches. - -This view should be set to the `view` property in a [`NSToolbarItem`](https://developer.apple.com/documentation/appkit/nstoolbaritem). - -## Usage - -First make sure a `WorkspaceDocument` is accessible in the context. - -```swift -var workspace: WorkspaceDocument? -``` - -Then in -[`toolbar(_:itemForItemIdentifier:willBeInsertedIntoToolbar:)`](https://developer.apple.com/documentation/appkit/nstoolbardelegate/1516985-toolbar), -create a new [`NSToolbarItem`](https://developer.apple.com/documentation/appkit/nstoolbaritem): - -```swift -let toolbarItem = NSToolbarItem(itemIdentifier: /* Identifier */) - -// create a NSHostingView -let view = BranchPickerToolbarItem(workspace?.workspaceClient) -let hostingView = NSHostingView(rootView: view) - -// set the view property of the toolbar item -toolbarItem.view = hostingView - -// return the toolbar item -return toolbarItem -``` - -## Preview - -![BranchPicker](BranchPicker_View.png) diff --git a/Documentation.docc/Documentation.md b/Documentation.docc/Documentation.md deleted file mode 100644 index bdc91525fc..0000000000 --- a/Documentation.docc/Documentation.md +++ /dev/null @@ -1,92 +0,0 @@ -# ``CodeEdit`` - -## Topics - -### About Window - -- - -### Settings - -- ``Settings`` - -### App Window - -- - -### CodeEditExtension - -- ``ExtensionManager`` -- ``FolderMonitor`` - -### CodeEditUI - -- - -### CodeFile - -- ``CodeFileDocument`` -- ``CodeFileError`` - -### CommandPalette - -- ``CommandPaletteView`` -- ``CommandPaletteViewModel`` - -### Documents - -- -- ``WorkspaceDocument`` -- ``CEWorkspaceFile`` -- ``CEWorkspaceFileManager`` -- ``CodeFileDocument`` -- ``CodeEditDocumentController`` - -### Feedback - -- ``FeedbackView`` -- ``FeedbackWindowController`` -- ``FeedbackToolbar`` -- ``FeedbackModel`` -- ``FeedbackType`` -- ``FeedbackIssueArea`` - -### Git - -- - -### Keybindings - -- ``KeybindingManager`` -- ``CommandManager`` -- ``Command`` -- ``CommandClosureWrapper`` -- ``KeyboardShortcutWrapper`` - -### LanguageServerProtocol - -- ``LSPClient`` - -### OpenQuickly - -- ``OpenQuicklyView`` -- ``OpenQuicklyListItemView`` -- ``OpenQuicklyViewModel`` -- ``OpenQuicklyPreviewView`` - -### Search - -- ``SearchModeModel`` -- ``SearchResultModel`` -- ``SearchResultMatchModel`` -- ``SearchResultLabel`` - -### Utils - -- ``CodeEditKeychain`` -- ``ShellClient`` -- ``FileIcon`` - -### Welcome - -- diff --git a/Documentation.docc/FileManagement/FileManagement.md b/Documentation.docc/FileManagement/FileManagement.md deleted file mode 100644 index 11852f09c2..0000000000 --- a/Documentation.docc/FileManagement/FileManagement.md +++ /dev/null @@ -1,10 +0,0 @@ -# File Management - -Working with files and directories in CodeEdit. - -## Overview - -CodeEdit manages files using three classes: -- ``CEWorkspaceFile`` for representing files and other file system objects. -- ``CEWorkspaceFileManager`` for loading, modifying, and listening to the file system. -- ``CodeFileDocument`` for loading contents of files for editing. diff --git a/Documentation.docc/Git/Git.md b/Documentation.docc/Git/Git.md deleted file mode 100644 index 01813c3f41..0000000000 --- a/Documentation.docc/Git/Git.md +++ /dev/null @@ -1,111 +0,0 @@ -# Git - -## Topics - -### Client - -- ``GitClient`` - -### Protocols - -- ``GitRouter`` -- ``GitJSONPostRouter`` -- ``GitRouterConfiguration`` -- ``GitURLSession`` -- ``GitURLSessionDataTaskProtocol`` - -### Structs - -- ``GitCommit`` -- ``GitAccountItem`` -- ``GitChangedFile`` -- ``GitHTTPHeader`` - -### Enums - -- ``GitHTTPEncoding`` -- ``GitHTTPMethod`` -- ``GitType`` -- ``GitTime`` -- ``GitURL`` -- ``GitSortDirection`` -- ``GitSortType`` - -### Views - -- ``GitCheckoutBranchView`` -- ``GitCloneView`` -- ``GitHubEnterpriseLoginView`` -- ``GitHubLoginView`` -- ``GitLabHostedLoginView`` -- ``GitLabLoginView`` - -### GitHub - -- ``GitHubFile`` -- ``GitHubGist`` -- ``GitHubIssue`` -- ``GitHubPullRequest`` -- ``GitHubRepositories`` -- ``GitHubUser`` -- ``GitHubAccount`` -- ``GitHubComment`` -- ``GitHubReview`` -- ``GitHubTokenConfiguration`` -- ``GitHubRouter`` -- ``GitHubUserRouter`` -- ``GitHubGistRouter`` -- ``GitHubIssueRouter`` -- ``GitHubOAuthRouter`` -- ``GitHubOAuthConfiguration`` -- ``GitHubOpenness`` -- ``GitHubPreviewHeader`` -- ``GitHubPublicKeyRouter`` -- ``GitHubPullRequestRouter`` -- ``GitHubRepositoryRouter`` -- ``GitHubReviewsRouter`` - -### GitLab - -- ``GitLabAvatarURL`` -- ``GitLabCommit`` -- ``GitLabCommitComment`` -- ``GitLabCommitDiff`` -- ``GitLabCommitStats`` -- ``GitLabCommitStatus`` -- ``GitLabEvent`` -- ``GitLabEventData`` -- ``GitLabGroupAccess`` -- ``GitLabPermissions`` -- ``GitLabProject`` -- ``GitLabProjectAccess`` -- ``GitLabProjectHook`` -- ``GitLabUser`` -- ``GitLabNamespace`` -- ``GitLabEventNote`` -- ``GitLabAccount`` -- ``GitLabTokenConfiguration`` -- ``GitLabOAuthConfiguration`` -- ``GitLabPrivateTokenConfiguration`` -- ``GitLabSort`` -- ``GitLabOrderBy`` -- ``GitLabVisibility`` -- ``GitLabVisibilityLevel`` -- ``GitLabUserRouter`` -- ``GitLabCommitRouter`` -- ``GitLabProjectRouter`` -- ``GitLabOAuthRouter`` - -### BitBucket - -- ``BitBucketEmail`` -- ``BitBucketRepositories`` -- ``BitBucketUser`` -- ``BitBucketAccount`` -- ``BitBucketOAuthConfiguration`` -- ``BitBucketTokenConfiguration`` -- ``BitBucketOAuthRouter`` -- ``BitBucketRepositoryRouter`` -- ``BitBucketTokenRouter`` -- ``BitBucketUserRouter`` -- ``BitbucketPaginatedResponse`` diff --git a/Documentation.docc/KeyChain/CodeEditKeychain.md b/Documentation.docc/KeyChain/CodeEditKeychain.md deleted file mode 100644 index 4b8cf0c012..0000000000 --- a/Documentation.docc/KeyChain/CodeEditKeychain.md +++ /dev/null @@ -1,12 +0,0 @@ -# ``CodeEdit/CodeEditKeychain`` - -## Topics - -### Articles - -- - -### Enumerations - -- ``CodeEditKeychainAccessOptions`` -- ``CodeEditKeychainConstants`` diff --git a/Documentation.docc/KeyChain/What is Keychain.md b/Documentation.docc/KeyChain/What is Keychain.md deleted file mode 100644 index 7c6b8765da..0000000000 --- a/Documentation.docc/KeyChain/What is Keychain.md +++ /dev/null @@ -1,66 +0,0 @@ -# What is Keychain? - -Keychain is the password management system in macOS, developed by Apple. It was introduced with Mac OS 8.6, and has been included in all subsequent versions of the operating system, now known as macOS. A Keychain can contain various types of data: passwords, private keys, certificates, and secure notes. - -## Notice: -This build of CodeEditKeychain could change at anytime if bugs or breaking changes are found in the module. - -## Usage - -### String - -```swift -let keychain = CodeEditKeychain() -keychain.set("hello world", forKey: "my key") -keychain.get("my key") -``` - -### Boolean - -```swift -let keychain = CodeEditKeychain() -keychain.set(true, forKey: "my key") -keychain.getBool("my key") -``` - -### Data - -```swift -let keychain = CodeEditKeychain() -keychain.set(dataObject, forKey: "my key") -keychain.getData("my key") -``` -### Removing Keys - -```swift -let keychain = CodeEditKeychain() -keychain.delete("my key") -``` - -### Return All Keys - -```swift -let keychain = CodeEditKeychain() -keychain.allKeys // Returns the names of all keys -``` - -### Check if operation was successful - -One can verify if `set`, `delete` and `clear` methods finished successfully by checking their return values. Those methods return `true` on success and `false` on error. - -```swift -if keychain.set("hello world", forKey: "my key") { - // Keychain item is saved successfully -} else { - // Report error -} -``` - -### Setting key prefix - -One can pass a `keyPrefix` argument when initializing a `CodeEditKeychain` object. The string passed in `keyPrefix` argument will be used as a prefix to **all the keys** used in `set`, `get`, `getData` and `delete` methods. Adding a prefix to the keychain keys can be useful in unit tests. This prevents the tests from changing the Keychain keys that are used when the app is launched manually. - -```swift -let keychain = CodeEditKeychain(keyPrefix: "myTestKey_") -keychain.set("hello world", forKey: "hello") // Value will be stored under "myTestKey_hello" key -``` diff --git a/Documentation.docc/Keybindings/KeybindingManager.md b/Documentation.docc/Keybindings/KeybindingManager.md deleted file mode 100644 index c90d412fbf..0000000000 --- a/Documentation.docc/Keybindings/KeybindingManager.md +++ /dev/null @@ -1,31 +0,0 @@ -# ``CodeEdit/KeybindingManager`` - -This module created in order to put all keybindings into single place in code, so it'd be easy to interact, reuse and change keybindings without going through every class and changing code to use other shortcut. It uses `default_keybindings.json` file to store initial set of keybindings. After app launched all keybindings loaded into memory and can be referenced via ``KeybindingManager/named(with:)`` function. - -## Initial setup - -In order to get it working you just need to add `Keybindings` as dependency to your module just like -``` -.target( - name: "WelcomeModule", - dependencies: [ - ...other dependencies - "Keybindings", - ]) -``` - -Keybinding module exists as singleton, so you always can reference Keybindings using `KeybindingManager.shared` - -## Topics - - -### Fetching shortcut - -In order to fetch keybinding you need to call following function with string name ``KeybindingManager/named(with:)`` returning you ``KeyboardShortcutWrapper`` which contains ``KeyboardShortcutWrapper/keyboardShortcut`` which can be passed directly to ``keyboardShortcut``. So the end code would look like `.keyboardShortcut(KeyboardShortcutWrapper.shared.named(with: "copy").keyboardShortcut` - -If shortcut wasn't found by name, it will return fallback shortcut which has following keybinding `Shift + ?` - -### Adding new shortcut - -To add new shortcut you need first to add new row to `default_keybindings.json` file. Make sure you follow other keybindings format. Also check that there's no other keybindings with same ID, -because we use it to identify keybindings later. Once added - you can refer to `Fetching Shortcut` section. It is possible to add new shortcut in runtime via ``KeybindingManager/addNewShortcut(shortcut:name:)`` diff --git a/Documentation.docc/Welcome/Welcome Window.md b/Documentation.docc/Welcome/Welcome Window.md deleted file mode 100644 index dc62a3fddd..0000000000 --- a/Documentation.docc/Welcome/Welcome Window.md +++ /dev/null @@ -1,12 +0,0 @@ -# Welcome Window - -## Topics - -### Views - -- ``WelcomeWindowView`` -- ``WelcomeView`` -- ``WelcomeActionView`` - -- ``RecentProjectsView`` -- ``RecentProjectItem`` From d78d5ad7bdda93f15c417662fedd1eb7ed338f29 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 15:24:23 +0200 Subject: [PATCH 320/335] Docs: Make every file header name its own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 67 headers named a file they were not, all fossils of renames the header comment never followed. Most record the old vocabulary directly: ThemePreferences, KeybindingsPreferences and AccountsPreferences for the retired Settings names; CommandPaletteView for QuickActionsView; TabManager for EditorManager; OutlineViewController for ProjectNavigatorViewController; SourceControlModel for SourceControlManager. Two were plain typos (AccoundSelectionView, OutlintViewController). Header-only: 67 files, 67 insertions, 67 deletions, and every changed line begins '// '. Found while researching the DocC catalog — ThemeSettingsView's header still named the deleted ThemePreferencesView, which suggested the pattern was wider. Note for anyone re-running the check: scope it to CodeEdit/, CodeEditModules/Sources/, CodeEditModules/Tests/, CodeEditTests/ and CodeEditUITests/. A bare find over CodeEditModules/ walks into .build/checkouts and reports dependency sources — that inflated the first count from 67 to 139. --- .../About/Acknowledgements/AcknowledgementRowView.swift | 2 +- .../About/Acknowledgements/AcknowledgementsViewModel.swift | 2 +- CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift | 2 +- .../Settings/Pages/AccountsSettings/AccountSelectionView.swift | 2 +- .../Pages/AccountsSettings/AccountsSettingsDetailsView.swift | 2 +- .../Pages/AccountsSettings/AccountsSettingsProviderRow.swift | 2 +- .../Settings/Pages/AccountsSettings/AccountsSettingsView.swift | 2 +- .../Settings/Pages/ExtensionsSettings/LanguageServersView.swift | 2 +- .../Pages/LocationsSettings/LocationsSettingsView.swift | 2 +- .../AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift | 2 +- CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift | 2 +- CodeEdit/Utils/KeyChain/KeychainSwiftAccessOptions.swift | 2 +- .../InspectorArea/NoSelectionInspectorView.swift | 2 +- .../ProjectNavigator/OutlineView/FileSystemTableViewCell.swift | 2 +- .../ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift | 2 +- .../OutlineView/ProjectNavigatorOutlineView.swift | 2 +- .../OutlineView/ProjectNavigatorTableViewCell.swift | 2 +- ...ctNavigatorViewController+OutlineTableViewCellDelegate.swift | 2 +- .../OutlineView/ProjectNavigatorViewController.swift | 2 +- CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift | 2 +- .../WorkspaceWindow/QuickActions/QuickActionsViewModel.swift | 2 +- CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift | 2 +- .../UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift | 2 +- .../WorkspacePanel/WorkspacePanelTabBar+IconButton.swift | 2 +- CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift | 2 +- CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift | 2 +- .../Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift | 2 +- .../CEEditor/Restoration/EditorLayout+StateRestoration.swift | 2 +- .../Sources/CEEditor/TabBar/EditorTabBarAccessory.swift | 2 +- .../Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift | 2 +- .../Sources/CELSP/Registry/PackageManagerProtocol.swift | 2 +- CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift | 2 +- .../Sources/CESearch/FindNavigator/FindNavigatorForm.swift | 2 +- .../FindNavigatorResultList/FindNavigatorMatchListCell.swift | 2 +- .../FindNavigatorResultList/FindNavigatorResultList.swift | 2 +- .../CESearch/FindNavigator/FindNavigatorToolbarBottom.swift | 2 +- .../Sources/CESearch/Model/SearchResultMatchModel.swift | 2 +- .../Accounts/GitHub/Model/GitHubRepositories.swift | 2 +- .../Accounts/GitLab/Model/GitLabAccountModel.swift | 2 +- .../Accounts/GitLab/Model/GitLabPermissions.swift | 2 +- .../CESourceControl/Accounts/Networking/GitURLSession.swift | 2 +- .../CESourceControl/Clone/GitCheckoutBranchViewModel.swift | 2 +- .../CESourceControl/Operations/SourceControlStashView.swift | 2 +- .../CESourceControl/Operations/SourceControlSwitchView.swift | 2 +- .../Sources/CESourceControl/Settings/AccountsSettings.swift | 2 +- .../CESourceControl/Settings/SourceControlSettings.swift | 2 +- .../Sources/CESourceControl/SourceControlManager.swift | 2 +- .../Repository/SourceControlNavigatorRepositoryItem.swift | 2 +- .../SourceControlNavigatorRepositoryView+contextMenu.swift | 2 +- .../SourceControlNavigatorRepositoryView+outlineGroupData.swift | 2 +- .../Sources/CETerminal/TerminalEmulator/Shell/Shell.swift | 2 +- .../Sources/CodeEditCore/Domain/Editor/EditorItemID.swift | 2 +- .../Sources/CodeEditCore/Domain/Git/GitChangedFile.swift | 2 +- CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift | 2 +- .../Sources/CodeEditSettings/Models/KeybindingsSettings.swift | 2 +- .../Sources/CodeEditSettings/Models/TextEditingSettings.swift | 2 +- .../Sources/CodeEditSettings/Models/ThemeSettings.swift | 2 +- CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift | 2 +- CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift | 2 +- .../WorkspaceDocument+SearchState+FindAndReplaceTests.swift | 2 +- .../Documents/WorkspaceDocument+SearchState+FindTests.swift | 2 +- .../Documents/WorkspaceDocument+SearchState+IndexTests.swift | 2 +- CodeEditTests/Features/Welcome/RecentProjectsTests.swift | 2 +- .../CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift | 2 +- CodeEditTests/Utils/UnitTests_Extensions.swift | 2 +- .../Features/ActivityViewer/Tasks/TasksMenuUITests.swift | 2 +- CodeEditUITests/Other Tests/HideInterfaceTests.swift | 2 +- 67 files changed, 67 insertions(+), 67 deletions(-) diff --git a/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementRowView.swift b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementRowView.swift index bbb9338260..a671d4d256 100644 --- a/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementRowView.swift +++ b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementRowView.swift @@ -1,5 +1,5 @@ // -// AcknowledgementsRowView.swift +// AcknowledgementRowView.swift // CodeEdit // // Created by Austin Condiff on 1/19/23. diff --git a/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsViewModel.swift b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsViewModel.swift index b4958a4167..9b3490c257 100644 --- a/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsViewModel.swift +++ b/CodeEdit/AuxiliaryWindows/About/Acknowledgements/AcknowledgementsViewModel.swift @@ -1,5 +1,5 @@ // -// AcknowledgementsModel.swift +// AcknowledgementsViewModel.swift // CodeEditModules/Acknowledgements // // Created by Lukas Pistrol on 01.05.22. diff --git a/CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift index d539e1ac6d..07db69aabc 100644 --- a/CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift +++ b/CodeEdit/AuxiliaryWindows/Extensions/ExtensionsManager.swift @@ -1,5 +1,5 @@ // -// ExtensionManager.swift +// ExtensionsManager.swift // CodeEdit // // Created by Wouter Hennen on 30/12/2022. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift index 32113b7b07..8a8294b35b 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountSelectionView.swift @@ -1,5 +1,5 @@ // -// AccoundSelectionView.swift +// AccountSelectionView.swift // CodeEdit // // Created by Austin Condiff on 4/5/23. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift index 07461a7a63..eaeb6f4396 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsDetailsView.swift @@ -1,5 +1,5 @@ // -// AccountsSettingsDetailView.swift +// AccountsSettingsDetailsView.swift // CodeEdit // // Created by Austin Condiff on 4/6/23. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift index eb6b13aa81..fde8b97c3f 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsProviderRow.swift @@ -1,5 +1,5 @@ // -// AccoundsSettingsAccountRow.swift +// AccountsSettingsProviderRow.swift // CodeEdit // // Created by Austin Condiff on 4/5/23. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift index bcbe837890..7d2490a3c2 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/AccountsSettings/AccountsSettingsView.swift @@ -1,5 +1,5 @@ // -// AccountSettingsView.swift +// AccountsSettingsView.swift // CodeEdit // // Created by Austin Condiff on 4/4/23. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift index 57dc778623..df41babfd8 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/ExtensionsSettings/LanguageServersView.swift @@ -1,5 +1,5 @@ // -// ExtensionsSettingsView.swift +// LanguageServersView.swift // CodeEdit // // Created by Abe Malla on 2/2/25. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift index bab7f6a2e0..3b3a5ecc5d 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Pages/LocationsSettings/LocationsSettingsView.swift @@ -1,5 +1,5 @@ // -// LocationSettingsView.swift +// LocationsSettingsView.swift // CodeEdit // // Created by Raymond Vleeshouwer on 02/04/23. diff --git a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift index 24502dee99..0d0b328d64 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/Search/SettingsSearchKeys.swift @@ -1,5 +1,5 @@ // -// SettingsData+Search.swift +// SettingsSearchKeys.swift // CodeEdit // // Created by Matthijs Eikelenboom. diff --git a/CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift b/CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift index 1713799470..c6ab6c9701 100644 --- a/CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift +++ b/CodeEdit/AuxiliaryWindows/Settings/SettingsPageView.swift @@ -1,5 +1,5 @@ // -// SettingPageView.swift +// SettingsPageView.swift // CodeEdit // // Created by Austin Condiff on 3/31/23. diff --git a/CodeEdit/Utils/KeyChain/KeychainSwiftAccessOptions.swift b/CodeEdit/Utils/KeyChain/KeychainSwiftAccessOptions.swift index d98b746364..99dc11d116 100644 --- a/CodeEdit/Utils/KeyChain/KeychainSwiftAccessOptions.swift +++ b/CodeEdit/Utils/KeyChain/KeychainSwiftAccessOptions.swift @@ -1,5 +1,5 @@ // -// CodeEditKeychainAccessOptions.swift +// KeychainSwiftAccessOptions.swift // CodeEditModules/CodeEditUtils // // Created by Nanashi Li on 2022/04/14. diff --git a/CodeEdit/WorkspaceWindow/InspectorArea/NoSelectionInspectorView.swift b/CodeEdit/WorkspaceWindow/InspectorArea/NoSelectionInspectorView.swift index 0115ac7349..1b9feba944 100644 --- a/CodeEdit/WorkspaceWindow/InspectorArea/NoSelectionInspectorView.swift +++ b/CodeEdit/WorkspaceWindow/InspectorArea/NoSelectionInspectorView.swift @@ -1,5 +1,5 @@ // -// NoSelectionView.swift +// NoSelectionInspectorView.swift // CodeEdit // // Created by Nanashi Li on 2022/04/18. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift index 92bc8e81c6..e827101d99 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/FileSystemTableViewCell.swift @@ -1,5 +1,5 @@ // -// FileSystemOutlineView.swift +// FileSystemTableViewCell.swift // CodeEdit // // Created by TAY KAI QUAN on 14/8/22. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 6cca6f2d45..6bfcb1dbfc 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -1,5 +1,5 @@ // -// OutlineMenu.swift +// ProjectNavigatorMenu.swift // CodeEdit // // Created by Lukas Pistrol on 07.04.22. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift index c6d4df5072..785f12db29 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorOutlineView.swift @@ -1,5 +1,5 @@ // -// OutlineView.swift +// ProjectNavigatorOutlineView.swift // CodeEdit // // Created by Lukas Pistrol on 05.04.22. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift index 9112c61784..2d169a5a9d 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorTableViewCell.swift @@ -1,5 +1,5 @@ // -// OutlineTableViewCell.swift +// ProjectNavigatorTableViewCell.swift // CodeEdit // // Created by Lukas Pistrol on 07.04.22. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift index 1f290d073d..3555335a62 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift @@ -1,5 +1,5 @@ // -// OutlintViewController+OutlineTableViewCellDelegate.swift +// ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift // CodeEdit // // Created by Ziyuan Zhao on 2023/2/5. diff --git a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift index 2e73005a1c..9c295ae80f 100644 --- a/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift +++ b/CodeEdit/WorkspaceWindow/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift @@ -1,5 +1,5 @@ // -// OutlineViewController.swift +// ProjectNavigatorViewController.swift // CodeEdit // // Created by Lukas Pistrol on 07.04.22. diff --git a/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift b/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift index f9ea7646c5..fae65f11bf 100644 --- a/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift +++ b/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsView.swift @@ -1,5 +1,5 @@ // -// CommandPaletteView.swift +// QuickActionsView.swift // CodeEdit // // Created by Alex Sinelnikov on 24.05.2022. diff --git a/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsViewModel.swift b/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsViewModel.swift index 6e010dc5fb..ada762e39e 100644 --- a/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsViewModel.swift +++ b/CodeEdit/WorkspaceWindow/QuickActions/QuickActionsViewModel.swift @@ -1,5 +1,5 @@ // -// CommandPaletteViewModel.swift +// QuickActionsViewModel.swift // CodeEdit // // Created by Alex on 25.05.2022. diff --git a/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift b/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift index d4b7d21005..48845e190e 100644 --- a/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift +++ b/CodeEdit/WorkspaceWindow/Settings/CEWorkspaceSettings.swift @@ -1,5 +1,5 @@ // -// CEWorkspaceSettingsManager.swift +// CEWorkspaceSettings.swift // CodeEdit // // Created by Axel Martinez on 27/3/24. diff --git a/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 0760c6ca43..209c22f2d0 100644 --- a/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/WorkspaceWindow/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -1,5 +1,5 @@ // -// UtilityAreaTerminal.swift +// UtilityAreaTerminalView.swift // CodeEdit // // Created by Austin Condiff on 5/25/23. diff --git a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar+IconButton.swift b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar+IconButton.swift index 4812b50bf0..ffbb559b89 100644 --- a/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar+IconButton.swift +++ b/CodeEdit/WorkspaceWindow/WorkspacePanel/WorkspacePanelTabBar+IconButton.swift @@ -1,5 +1,5 @@ // -// IconButton.swift +// WorkspacePanelTabBar+IconButton.swift // CodeEdit // // Created by Khan Winter on 9/3/25. diff --git a/CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift b/CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift index 6539308404..ec998aa7df 100644 --- a/CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift +++ b/CodeEditModules/Sources/CEEditor/Editor/Editor+TabSwitch.swift @@ -1,5 +1,5 @@ // -// EditorTabSwitchExtension.swift +// Editor+TabSwitch.swift // CodeEdit // // Created by Roscoe Rubin-Rottenberg on 4/22/24. diff --git a/CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift b/CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift index 522ccee199..d5f619612c 100644 --- a/CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift +++ b/CodeEditModules/Sources/CEEditor/Editor/EditorManager.swift @@ -1,5 +1,5 @@ // -// TabManager.swift +// EditorManager.swift // CodeEdit // // Created by Wouter Hennen on 03/03/2023. diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift index d550e24df5..766a5d94e1 100644 --- a/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift +++ b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift @@ -1,5 +1,5 @@ // -// EditorJumpBar.swift +// EditorJumpBarComponent.swift // CodeEdit // // Created by Lukas Pistrol on 18.03.22. diff --git a/CodeEditModules/Sources/CEEditor/Restoration/EditorLayout+StateRestoration.swift b/CodeEditModules/Sources/CEEditor/Restoration/EditorLayout+StateRestoration.swift index 68de7b710d..e6ce2f28dd 100644 --- a/CodeEditModules/Sources/CEEditor/Restoration/EditorLayout+StateRestoration.swift +++ b/CodeEditModules/Sources/CEEditor/Restoration/EditorLayout+StateRestoration.swift @@ -1,5 +1,5 @@ // -// Editor+StateRestoration.swift +// EditorLayout+StateRestoration.swift // CodeEdit // // Created by Khan Winter on 7/3/23. diff --git a/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarAccessory.swift b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarAccessory.swift index 6f98f2f0f3..ec7b214ea6 100644 --- a/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarAccessory.swift +++ b/CodeEditModules/Sources/CEEditor/TabBar/EditorTabBarAccessory.swift @@ -1,5 +1,5 @@ // -// TabBarAccessory.swift +// EditorTabBarAccessory.swift // CodeEdit // // Created by Lingxi Li on 4/28/22. diff --git a/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift index 10506c7df6..5b4904cef8 100644 --- a/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift +++ b/CodeEditModules/Sources/CEEditor/TabBar/Tab/EditorFileTabCloseButton.swift @@ -1,5 +1,5 @@ // -// FileEditorTabCloseButton.swift +// EditorFileTabCloseButton.swift // CodeEdit // // Created by Albert Vinizhanau on 10/13/23. diff --git a/CodeEditModules/Sources/CELSP/Registry/PackageManagerProtocol.swift b/CodeEditModules/Sources/CELSP/Registry/PackageManagerProtocol.swift index d987138b63..d532712eea 100644 --- a/CodeEditModules/Sources/CELSP/Registry/PackageManagerProtocol.swift +++ b/CodeEditModules/Sources/CELSP/Registry/PackageManagerProtocol.swift @@ -1,5 +1,5 @@ // -// PackageManager.swift +// PackageManagerProtocol.swift // CodeEdit // // Created by Abe Malla on 2/2/25. diff --git a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift index ec266970be..8ab776d67d 100644 --- a/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift +++ b/CodeEditModules/Sources/CELSP/Registry/RegistryManager.swift @@ -1,5 +1,5 @@ // -// Registry.swift +// RegistryManager.swift // CodeEdit // // Created by Abe Malla on 1/29/25. diff --git a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorForm.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorForm.swift index f6ce860729..77127b4797 100644 --- a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorForm.swift +++ b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorForm.swift @@ -1,5 +1,5 @@ // -// SearchModeSelector.swift +// FindNavigatorForm.swift // CodeEdit // // Created by Ziyuan Zhao on 2022/3/21. diff --git a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift index 1f60f23bb2..6a86d4aab2 100644 --- a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift +++ b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorMatchListCell.swift @@ -1,5 +1,5 @@ // -// FindNavigatorListCell.swift +// FindNavigatorMatchListCell.swift // CodeEdit // // Created by Khan Winter on 7/7/22. diff --git a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift index 2fafd03341..2ff14cb8e5 100644 --- a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift +++ b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorResultList/FindNavigatorResultList.swift @@ -1,5 +1,5 @@ // -// SearchResultList.swift +// FindNavigatorResultList.swift // CodeEdit // // Created by Ziyuan Zhao on 2022/3/22. diff --git a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift index a289503427..306627f0e0 100644 --- a/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift +++ b/CodeEditModules/Sources/CESearch/FindNavigator/FindNavigatorToolbarBottom.swift @@ -1,5 +1,5 @@ // -// SourceControlToolbarBottom.swift +// FindNavigatorToolbarBottom.swift // CodeEdit // // Created by Nanashi Li on 2022/05/20. diff --git a/CodeEditModules/Sources/CESearch/Model/SearchResultMatchModel.swift b/CodeEditModules/Sources/CESearch/Model/SearchResultMatchModel.swift index 5ee0b2dcb4..93598eaa4b 100644 --- a/CodeEditModules/Sources/CESearch/Model/SearchResultMatchModel.swift +++ b/CodeEditModules/Sources/CESearch/Model/SearchResultMatchModel.swift @@ -1,5 +1,5 @@ // -// SearchResultLineMatchModel.swift +// SearchResultMatchModel.swift // CodeEditModules/Search // // Created by Khan Winter on 7/6/22. diff --git a/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift index e771bab31f..ce5c459c5b 100644 --- a/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift +++ b/CodeEditModules/Sources/CESourceControl/Accounts/GitHub/Model/GitHubRepositories.swift @@ -1,5 +1,5 @@ // -// Repositories.swift +// GitHubRepositories.swift // CodeEditModules/GitAccounts // // Created by Nanashi Li on 2022/03/31. diff --git a/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift index aeae53cc21..ad54d3f50b 100644 --- a/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift +++ b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabAccountModel.swift @@ -1,5 +1,5 @@ // -// GitLabAccount.swift +// GitLabAccountModel.swift // CodeEditModules/GitAccounts // // Created by Wesley de Groot on 02/04/2022. diff --git a/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift index 9d16864b97..25f9147ceb 100644 --- a/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift +++ b/CodeEditModules/Sources/CESourceControl/Accounts/GitLab/Model/GitLabPermissions.swift @@ -1,5 +1,5 @@ // -// Permissions.swift +// GitLabPermissions.swift // CodeEditModules/GitAccounts // // Created by Nanashi Li on 2022/03/31. diff --git a/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift b/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift index 08bb5ebcb8..5da2dac97d 100644 --- a/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift +++ b/CodeEditModules/Sources/CESourceControl/Accounts/Networking/GitURLSession.swift @@ -1,5 +1,5 @@ // -// Session.swift +// GitURLSession.swift // CodeEditModules/GitAccounts // // Created by Nanashi Li on 2022/03/31. diff --git a/CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchViewModel.swift b/CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchViewModel.swift index 72a9f50940..84ddc1cd28 100644 --- a/CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchViewModel.swift +++ b/CodeEditModules/Sources/CESourceControl/Clone/GitCheckoutBranchViewModel.swift @@ -1,5 +1,5 @@ // -// GitCheckoutBranchView.swift +// GitCheckoutBranchViewModel.swift // CodeEdit // // Created by Albert Vinizhanau on 10/17/23. diff --git a/CodeEditModules/Sources/CESourceControl/Operations/SourceControlStashView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlStashView.swift index f5b7c31689..215edc2ca1 100644 --- a/CodeEditModules/Sources/CESourceControl/Operations/SourceControlStashView.swift +++ b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlStashView.swift @@ -1,5 +1,5 @@ // -// SourceControlAddRemoteView.swift +// SourceControlStashView.swift // CodeEdit // // Created by Austin Condiff on 11/17/23. diff --git a/CodeEditModules/Sources/CESourceControl/Operations/SourceControlSwitchView.swift b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlSwitchView.swift index 007592b1fb..53e9578a1d 100644 --- a/CodeEditModules/Sources/CESourceControl/Operations/SourceControlSwitchView.swift +++ b/CodeEditModules/Sources/CESourceControl/Operations/SourceControlSwitchView.swift @@ -1,5 +1,5 @@ // -// SourceControlFetchView.swift +// SourceControlSwitchView.swift // CodeEdit // // Created by Austin Condiff on 7/9/24. diff --git a/CodeEditModules/Sources/CESourceControl/Settings/AccountsSettings.swift b/CodeEditModules/Sources/CESourceControl/Settings/AccountsSettings.swift index 29059ace09..aa60b9074e 100644 --- a/CodeEditModules/Sources/CESourceControl/Settings/AccountsSettings.swift +++ b/CodeEditModules/Sources/CESourceControl/Settings/AccountsSettings.swift @@ -1,5 +1,5 @@ // -// AccountsPreferences.swift +// AccountsSettings.swift // CodeEditModules/Settings // // Created by Nanashi Li on 2022/04/08. diff --git a/CodeEditModules/Sources/CESourceControl/Settings/SourceControlSettings.swift b/CodeEditModules/Sources/CESourceControl/Settings/SourceControlSettings.swift index c5fa4834e4..bab323863e 100644 --- a/CodeEditModules/Sources/CESourceControl/Settings/SourceControlSettings.swift +++ b/CodeEditModules/Sources/CESourceControl/Settings/SourceControlSettings.swift @@ -1,5 +1,5 @@ // -// SourceControlPreferences.swift +// SourceControlSettings.swift // CodeEditModules/Settings // // Created by Nanashi Li on 2022/04/08. diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift b/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift index 47b1dab8ab..ffe8495edc 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlManager.swift @@ -1,5 +1,5 @@ // -// SourceControlModel.swift +// SourceControlManager.swift // CodeEdit // // Created by Nanashi Li on 2022/05/20. diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift index 5dbf33db05..faa8126d2c 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryItem.swift @@ -1,5 +1,5 @@ // -// SourceControlNavigatorRepositoriesItem.swift +// SourceControlNavigatorRepositoryItem.swift // CodeEdit // // Created by Austin Condiff on 11/29/23. diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift index c36ba1874d..380b2daeab 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+contextMenu.swift @@ -1,5 +1,5 @@ // -// SourceControlNavigatorRepositoriesView+contextMenu.swift +// SourceControlNavigatorRepositoryView+contextMenu.swift // CodeEdit // // Created by Austin Condiff on 11/29/23. diff --git a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift index cea00e0b9f..837ded3a7d 100644 --- a/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift +++ b/CodeEditModules/Sources/CESourceControl/SourceControlNavigator/Repository/SourceControlNavigatorRepositoryView+outlineGroupData.swift @@ -1,5 +1,5 @@ // -// SourceControlNavigatorRepositoriesView+outlineGroupData.swift +// SourceControlNavigatorRepositoryView+outlineGroupData.swift // CodeEdit // // Created by Austin Condiff on 11/29/23. diff --git a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/Shell.swift b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/Shell.swift index 79a5761539..bc67d3d0f8 100644 --- a/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/Shell.swift +++ b/CodeEditModules/Sources/CETerminal/TerminalEmulator/Shell/Shell.swift @@ -1,5 +1,5 @@ // -// ShellIntegration.swift +// Shell.swift // CodeEdit // // Created by Khan Winter on 6/1/24. diff --git a/CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift index 39b4f82d96..2433ffc44a 100644 --- a/CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift +++ b/CodeEditModules/Sources/CodeEditCore/Domain/Editor/EditorItemID.swift @@ -1,5 +1,5 @@ // -// EditorTabID.swift +// EditorItemID.swift // // // Created by Pavel Kasila on 30.04.22. diff --git a/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift index 23351eb6b9..5e06c07ed7 100644 --- a/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift +++ b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitChangedFile.swift @@ -1,5 +1,5 @@ // -// ChangedFile.swift +// GitChangedFile.swift // // // Created by Nanashi Li on 2022/05/20. diff --git a/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift index ce46dfc42f..ec2c346668 100644 --- a/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift +++ b/CodeEditModules/Sources/CodeEditCore/Domain/Git/GitStatus.swift @@ -1,5 +1,5 @@ // -// GitType.swift +// GitStatus.swift // // // Created by Nanashi Li on 2022/05/20. diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift index bbd84cfe71..b17cba532a 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/KeybindingsSettings.swift @@ -1,5 +1,5 @@ // -// KeybindingsPreferences.swift +// KeybindingsSettings.swift // CodeEditModules/Settings // // Created by Alex on 18.05.2022. diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift index bbeb92402d..0c7f7376b1 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/TextEditingSettings.swift @@ -1,5 +1,5 @@ // -// TextEditingPreferences.swift +// TextEditingSettings.swift // CodeEditModules/Settings // // Created by Nanashi Li on 2022/04/08. diff --git a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift index 2c446e0ea3..c7fdef9f05 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Models/ThemeSettings.swift @@ -1,5 +1,5 @@ // -// ThemePreferences.swift +// ThemeSettings.swift // CodeEditModules/Settings // // Created by Nanashi Li on 2022/04/08. diff --git a/CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift b/CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift index e7d97709f0..67939cea5d 100644 --- a/CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift +++ b/CodeEditModules/Tests/CESearchTests/MemoryIndexingTests.swift @@ -1,5 +1,5 @@ // -// MemoryIndexing.swift +// MemoryIndexingTests.swift // CodeEditTests // // Created by Tommy Ludwig on 08.12.23. diff --git a/CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift b/CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift index d02b420545..15e7d765bd 100644 --- a/CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift +++ b/CodeEditModules/Tests/CESearchTests/MemorySearchTests.swift @@ -1,5 +1,5 @@ // -// MemoryIndexSearch.swift +// MemorySearchTests.swift // CodeEditTests // // Created by Tommy Ludwig on 08.12.23. diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift index 7529eec5e2..27039ab0fe 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindAndReplaceTests.swift @@ -1,5 +1,5 @@ // -// Workspace+SearchState+FindAndReplaceTests.swift +// WorkspaceDocument+SearchState+FindAndReplaceTests.swift // CodeEditTests // // Created by Tommy Ludwig on 26.01.24. diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift index 38a2051dd4..3419976903 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+FindTests.swift @@ -1,5 +1,5 @@ // -// Workspace+SearchState+FindTests.swift +// WorkspaceDocument+SearchState+FindTests.swift // CodeEditTests // // Created by Tommy Ludwig on 26.01.24. diff --git a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift index b18ddd5469..f423892dba 100644 --- a/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift +++ b/CodeEditTests/Features/Documents/WorkspaceDocument+SearchState+IndexTests.swift @@ -1,5 +1,5 @@ // -// Workspace+SearchState+IndexTests.swift +// WorkspaceDocument+SearchState+IndexTests.swift // CodeEditTests // // Created by Tommy Ludwig on 26.01.24. diff --git a/CodeEditTests/Features/Welcome/RecentProjectsTests.swift b/CodeEditTests/Features/Welcome/RecentProjectsTests.swift index 19fc6ccb8c..6b654d1ef4 100644 --- a/CodeEditTests/Features/Welcome/RecentProjectsTests.swift +++ b/CodeEditTests/Features/Welcome/RecentProjectsTests.swift @@ -1,5 +1,5 @@ // -// RecentsStoreTests.swift +// RecentProjectsTests.swift // CodeEditTests // // Created by Khan Winter on 5/27/25. diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index 1990da4856..778b54f503 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -1,5 +1,5 @@ // -// UnitTests.swift +// CEWorkspaceFileManagerTests.swift // CodeEditModules/WorkspaceClient // // Created by Marco Carnevali on 16/03/22. diff --git a/CodeEditTests/Utils/UnitTests_Extensions.swift b/CodeEditTests/Utils/UnitTests_Extensions.swift index 1404c80454..e2fc62163d 100644 --- a/CodeEditTests/Utils/UnitTests_Extensions.swift +++ b/CodeEditTests/Utils/UnitTests_Extensions.swift @@ -1,5 +1,5 @@ // -// UnitTests.swift +// UnitTests_Extensions.swift // CodeEditModules/CodeEditUtilsTests // // Created by Lukas Pistrol on 01.05.22. diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift index dbb5655dcf..fdf7f1ea53 100644 --- a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -1,5 +1,5 @@ // -// ActivityViewerTasksMenuTests.swift +// TasksMenuUITests.swift // CodeEditUITests // // Created by Khan Winter on 1/3/25. diff --git a/CodeEditUITests/Other Tests/HideInterfaceTests.swift b/CodeEditUITests/Other Tests/HideInterfaceTests.swift index bf804747ca..f8adeca1e5 100644 --- a/CodeEditUITests/Other Tests/HideInterfaceTests.swift +++ b/CodeEditUITests/Other Tests/HideInterfaceTests.swift @@ -1,5 +1,5 @@ // -// HiderInterfaceTests.swift +// HideInterfaceTests.swift // CodeEditUITests // // Created by Simon Kudsk on 14/05/2025. From e713a66342b729cc25ff72125c5cb77b5b83dfcb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 19:26:14 +0200 Subject: [PATCH 321/335] Docs: Give the I/O norm the example that proves it, and settle CEWorkspaceFileManager The norm was justified in the abstract, so it read as decorative. It now carries what it actually prevents: merging CEWorkspaceFileManager into Core. That target holds 50 FileManager calls and a full FSEvents implementation with a C callback and its own dispatch queue. Without the norm the merge looks reasonable, since that target depends on nothing but Core and folding it in removes a target. With the norm it is obviously wrong, because it would put a live filesystem event stream in the sink all twelve targets rest on. The testability half is now empirical rather than asserted: CodeEditCoreTests is five files with zero FileManager, temporaryDirectory or Data(contentsOf:) use. Records CEWorkspaceFileManager alongside ShellClient. Core declares WorkspaceFileProviding, CEWorkspaceFileManager.swift:263 conforms to it, four CEEditor files depend on the protocol, and no package imports the implementation. Contract in Core, adapter in its own target, app composes. Also fixes a typo from the em-dash pass ('organized an,' to 'organized and,'). --- docs/ARCHITECTURE.md | 69 +++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bb0bb8f38a..3a743016ae 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,8 +1,7 @@ # CodeEdit Architecture Guide -This guide explains how the codebase is organized and — most importantly — **where new code -goes**. CI enforces the rules described here (see [Enforcement](#enforcement)), so reading this -before you add files will save you a failed check. +This guide explains how the codebase is organized and, most importantly, **where new code +goes**. The CI enforces the rules described here (see [Enforcement](#enforcement)), so reading this before you add files will save you a failed check. ## Package topology @@ -31,20 +30,18 @@ CodeEdit.xcworkspace Each library target publishes a like-named `.library` product, and the app target links the ones it needs. Inside the manifest, targets reference each other by bare name, so the whole graph is -legible in a single file — which is the point. See +legible in a single file, which is the point. See [History](#history-why-the-2022-module-split-failed) for what the previous arrangement cost. Naming: `CodeEdit*` marks substrate peer-named with the external CodeEdit libraries (`CodeEditSourceEditor`, `CodeEditSymbols`, …); `CE*` marks app-internal feature contexts, -peer-named with the `CE*` domain types. Apply `CE` only where the bare name would collide with a -stdlib/SwiftUI/AppKit/vendor type — it is a collision-avoider, not a namespace. +peer-named with the `CE*` domain types. Apply `CE` only where the bare name would collide with a stdlib/SwiftUI/AppKit/vendor type, it is a collision-avoider, not a namespace. Every target builds with Swift 6 strict concurrency **except `CEEditor`**, which declares -`.swiftLanguageMode(.v5)` and is the sole exception. The app target is still Swift 5 — write new -app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't +`.swiftLanguageMode(.v5)` and is the sole exception. The app target is still Swift 5, write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't isolated (it cascades). -## History: why the 2022 module split failed +## History: Why the 2022 module split failed The project already tried a single multi-target `CodeEditModules` package. It was deleted on 2022-12-03 (commit `4858de16`) after repeated cyclic-dependency problems, and everything moved into @@ -58,38 +55,25 @@ explain them: | **Shared UI depended on domain** | `CodeEditUI → WorkspaceClient, Git` | | **A god-module hub** | `AppPreferences → CodeEditUI, Git, Keybindings, CodeEditUtils, Sparkle, CodeEditTextView`, itself depended on by half the tree | -With the bottom of the graph pointing up into the top, cycles were the steady state rather than an -accident. The same manifest split across eleven separate packages fails identically — SwiftPM refuses -to resolve a cyclic graph either way. +With the bottom of the graph pointing up into the top, cycles were the steady state rather than an accident. The same manifest split across eleven separate packages fails identically, SwiftPM refuses to resolve a cyclic graph either way. -**`CodeEditCore` is the fix, and it now exists.** Zero dependencies, framework-free, holding domain -values, the typed `EventBus`, and the cross-feature command interfaces. Every "A and B both need X" -now resolves *downward*. Both 2022 killers are structurally impossible today: the domain lives in -`CodeEditCore`, which may import nothing, and `CodeEditUI → Git` is blocked by the `CodeEditUI` +**`CodeEditCore` is the fix, and it now exists.** Zero dependencies, framework-free, holding domain values, the typed `EventBus`, and the cross-feature command interfaces. Every "A and B both need X" now resolves *downward*. Both 2022 killers are structurally impossible today: the domain lives in `CodeEditCore`, which may import nothing, and `CodeEditUI → Git` is blocked by the `CodeEditUI` purity rule. ### Cycle-resolution playbook -Detection was never the problem — SwiftPM refuses a cyclic target graph as a hard error. The 2022 -failure was that detection had no accompanying *resolution* technique, so the exit taken was to -collapse everything into the app target. When you hit a cycle, the legal moves, in preference order: +Detection was never the problem, SwiftPM refuses a cyclic target graph as a hard error. The 2022 failure was that detection had no accompanying *resolution* technique, so the exit taken was to collapse everything into the app target. When you hit a cycle, the legal moves, in preference order: -1. **Push the shared thing down to `CodeEditCore`** — a protocol, an event, or a value type. This is - what `EventBus` and the command interfaces (`WorkspaceNavigator`, `TasksConfigurationProviding`, …) - are for. The default answer. -2. **Push the coordination up to the app target** — the app may depend on everything. Two leaf - features never need to know each other if a doer wires them (`WorkspaceOpener`, `DocumentOpener`). -3. **Merge the two targets** — if A and B genuinely will not separate, the boundary was drawn wrong. - Merging is a correct outcome, not a defeat; in one package it is a folder move plus a three-line - manifest edit. +1. **Push the shared thing down to `CodeEditCore`** — a protocol, an event, or a value type. This is what `EventBus` and the command interfaces (`WorkspaceNavigator` `TasksConfigurationProviding`, …) are for. The default answer. +2. **Push the coordination up to the app target** — the app may depend on everything. Two leaf features never need to know each other if a doer wires them (`WorkspaceOpener`, `DocumentOpener`). +3. **Merge the two targets** — if A and B genuinely will not separate, the boundary was drawn wrong. Merging is a correct outcome, not a defeat; in one package it is a folder move plus a three-line manifest edit. -Never resolve a cycle by moving code back into the app target. That is what happened in 2022 and it -cost four years of enforced boundaries. +Never resolve a cycle by moving code back into the app target. That is what happened in 2022 and it cost four years of enforced boundaries. ## Rules Three checks are enforced in CI. Each one blocks a specific failure documented in -[History](#history-why-the-2022-module-split-failed) — none is enforced on principle alone. +[History](#history-why-the-2022-module-split-failed), none is enforced on principle alone. 1. **`CodeEditCore` purity.** Zero dependencies, local or external. No `SwiftUI`, `AppKit`, or `Cocoa` import. Blocks 2022's `WorkspaceClient → TabBar`. The two constraints earn their keep @@ -141,9 +125,19 @@ previously implied it was enforced. It is not: the SwiftLint rule forbids `Swift and nothing more, and `Foundation` — which Core needs for `URL`, `Data` and `Codable`, and which 47 of its files import — *is* the I/O surface, so an import check cannot express this. -The reason to keep it out anyway: Core stays deterministic and testable with no filesystem, and I/O -already has a designated home — rule 4 of [Where does my code go?](#where-does-my-code-go) sends -services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. +The reason to keep it out anyway is concrete, not decorative. `CodeEditCoreTests` is five files with +zero use of `FileManager`, `temporaryDirectory` or `Data(contentsOf:)`: Core's tests need no +filesystem, no temp directories and no cleanup. And I/O already has a designated home, since rule 4 +of [Where does my code go?](#where-does-my-code-go) sends services to their own target, which is what +`CEWorkspaceFileManager` and `ShellClient` are. + +**What the norm actually prevents** (asked 2026-08-20): merging `CEWorkspaceFileManager` into Core. +That target holds 50 `FileManager` calls and a complete FSEvents implementation, `FSEventStreamCreate` +with a C callback, its own dispatch queue, and start/stop/invalidate/release. Without this norm the +merge looks reasonable, because that target depends on nothing but Core and folding it in removes a +target. With the norm it is obviously wrong: it would put a live filesystem event stream inside the +dependency sink all twelve targets rest on, and end Core's filesystem-free tests the same day. State +the norm with this example, not on principle alone. **`CodeEditDocument` and `CELSP` both stay their own targets** (asked and settled 2026-08-20). Neither is a leftover, and the two conclusions depend on each other. @@ -170,6 +164,15 @@ The abstraction is already sound where it counts: `LanguageServer`, `LSPContentC would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type — breaking its use as an existential for DI — to delete a five-file target. A bad trade. +**`CEWorkspaceFileManager` stays its own target, and must not merge into Core** (asked and settled +2026-08-20). The inversion is already in place: Core declares `WorkspaceFileProviding` and +`WorkspaceFileObserver`, and `CEWorkspaceFileManager.swift:263` is `extension CEWorkspaceFileManager: +WorkspaceFileProviding {}`. Four `CEEditor` files depend on the *protocol* (`EditorRestorer`, +`EditorJumpBarMenu`, `EditorLayout+StateRestoration`, the environment key) and **no package imports +the implementation**. Its 22 consumers are app-side, plus 6 test files. Same shape as `ShellClient` +below: contract in Core, adapter in its own target, app composes. See the I/O norm above for why the +merge is worse than it looks. + **`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). Size is the wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and `CELSP` — `GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers — and From c43ecde2a8dd2a5714aae9d4ccc90351c966ef7d Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 22:09:37 +0200 Subject: [PATCH 322/335] Docs: Remove em dashes from the architecture guide's prose 86 down to 14. The remainder are the two cases worth keeping: 10 columns in the topology diagram, where they align target names with their descriptions, and 4 separators between a bold label and its explanation in list items. Done in two passes, because a mechanical substitution produces bad prose. The first replaced the dashes; the second fixed what that broke, roughly 35 places. Comma splices became colons or full stops ('the acyclicity guarantee, because it makes Core a sink', 'not prevention: import honesty checks'). Paired dashes that had been holding a parenthetical became actual parentheses, which mattered most where a list would otherwise read wrong: 'across CESourceControl and CELSP (GitClient, SourceControlManager, ...)' had briefly read as though GitClient were a package. 73 lines changed, structure untouched: 19 headings, 4 code fences, 44 table rows before and after. --- docs/ARCHITECTURE.md | 146 +++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3a743016ae..14ca0baecd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -77,9 +77,9 @@ Three checks are enforced in CI. Each one blocks a specific failure documented i 1. **`CodeEditCore` purity.** Zero dependencies, local or external. No `SwiftUI`, `AppKit`, or `Cocoa` import. Blocks 2022's `WorkspaceClient → TabBar`. The two constraints earn their keep - separately: **zero local dependencies** is the acyclicity guarantee — it makes Core a sink, so + separately: **zero local dependencies** is the acyclicity guarantee, because it makes Core a sink, so every "A and B both need X" resolves downward, which is the direct fix for the "no kernel - existed" failure above. **No UI frameworks** keeps the placement question answerable — without + existed" failure above. **No UI frameworks** keeps the placement question answerable. Without it, Core becomes the place everything shared goes, which is what `AppPreferences` was and one of the two documented causes of the 2022 collapse. @@ -89,7 +89,7 @@ Three checks are enforced in CI. Each one blocks a specific failure documented i to need only SwiftUI, so it lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); fuzzy matching's concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` - (below); and `ActiveTheme` — the active light/dark theme, observed by the editor and terminal — + (below); and `ActiveTheme`, the active light/dark theme, observed by the editor and terminal, lives *in* Core, because `ObservableObject` comes from Combine rather than SwiftUI. That last one is worth remembering: the charter forbids `SwiftUI`/`AppKit`/`Cocoa` specifically, so observation is available in Core and an "it needs to be observable, therefore it needs SwiftUI" argument is @@ -97,17 +97,17 @@ Three checks are enforced in CI. Each one blocks a specific failure documented i The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry `NSFont.Weight`, which forces them out of Core. That is the rule flagging a presentation type - inside a settings model, not the rule obstructing a reasonable design — storing the weight as a + inside a settings model, not the rule obstructing a reasonable design. Storing the weight as a `Double` and converting at the presentation boundary would make both structs Core-eligible with no rule change. 2. **`CodeEditUI` purity.** No local target dependencies; external `CodeEditSymbols` only. Blocks - 2022's `CodeEditUI → Git`. This is why `FileIcon` is keyed on `URL` rather than on a domain type — + 2022's `CodeEditUI → Git`. This is why `FileIcon` is keyed on `URL` rather than on a domain type: a deliberate consequence, not an accident. 3. **Import honesty.** Every `import` in a target's sources must be declared in that target's manifest dependencies. Xcode workspace builds share one build directory, so an undeclared import of a sibling compiles fine and nothing else catches it. For the 7 targets that transitively need - `CodeEditSymbols`, a standalone `swift build` is not available as a backstop either — see - [Enforcement](#enforcement) — so this check is their only defence, not a redundant one. + `CodeEditSymbols`, a standalone `swift build` is not available as a backstop either (see + [Enforcement](#enforcement)), so this check is their only defence, not a redundant one. Plus one assertion: **only `CEEditor` may declare `.swiftLanguageMode(.v5)`.** Every other target inherits Swift 6 from the package's tools version. A target silently dropping to Swift 5 would lose @@ -118,12 +118,12 @@ strict-concurrency enforcement without anything failing. **Prefer features to be leaves.** Nothing should depend on a feature target. When an edge between two features is genuinely needed, try the three [cycle-resolution moves](#cycle-resolution-playbook) first, then declare the edge in the manifest -where it is visible to everyone. Acyclicity itself needs no rule — SwiftPM enforces it. +where it is visible to everyone. Acyclicity itself needs no rule, SwiftPM enforces it. -**Keep I/O out of Core.** A norm, not a gate — and worth being precise about, because the guide +**Keep I/O out of Core.** A norm, not a gate. It is worth being precise about, because the guide previously implied it was enforced. It is not: the SwiftLint rule forbids `SwiftUI`/`AppKit`/`Cocoa` -and nothing more, and `Foundation` — which Core needs for `URL`, `Data` and `Codable`, and which 47 -of its files import — *is* the I/O surface, so an import check cannot express this. +and nothing more, and `Foundation`, which Core needs for `URL`, `Data` and `Codable`, and which 47 +of its files import, *is* the I/O surface, so an import check cannot express this. The reason to keep it out anyway is concrete, not decorative. `CodeEditCoreTests` is five files with zero use of `FileManager`, `temporaryDirectory` or `Data(contentsOf:)`: Core's tests need no @@ -144,7 +144,7 @@ Neither is a leftover, and the two conclusions depend on each other. `CEEditor` and `CELSP` reference each other **zero times, in either direction**. They are siblings. What keeps them apart is `LanguageServicesProvider`, declared in `CodeEditDocument`, implemented by -`CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key — it +`CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key. It even ships a `NoOpLanguageServicesProvider`, so the editor works with no language service at all. So `CodeEditDocument` is not "the document type plus some bridging": **it is the contract that keeps two features independent.** `CodeFileDocument` imports AppKit, SwiftUI and the editor frameworks, so @@ -152,7 +152,7 @@ it cannot live in Core; two features need it, so it cannot live in either. Its o not chosen. `CELSP` is not part of the editor either. Its consumers are the settings UI (installing servers), the -utility area (reading logs) and app lifecycle — nothing in `CEEditor` imports it — and **28 of its 78 +utility area (reading logs) and app lifecycle (nothing in `CEEditor` imports it), and **28 of its 78 files are `Registry/`**: package managers, install steps and source parsers for Cargo, NPM, Pip, Go and GitHub. That is downloading and installing language servers, not editing text. Folding it into `CEEditor` would make a ~130-file target mixing the two. @@ -161,8 +161,8 @@ The abstraction is already sound where it counts: `LanguageServer`, `LSPContentC `SemanticTokenHighlightProvider` and `LanguageServerDocumentObjects` are all generic over `LanguageServerDocument`, a protocol requiring only `content`, `languageServerURI` and `getLanguage()`. Only `LSPService` itself pins the generic to `CodeFileDocument`. Decoupling that -would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type — -breaking its use as an existential for DI — to delete a five-file target. A bad trade. +would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type, +breaking its use as an existential for DI, all to delete a five-file target. A bad trade. **`CEWorkspaceFileManager` stays its own target, and must not merge into Core** (asked and settled 2026-08-20). The inversion is already in place: Core declares `WorkspaceFileProviding` and @@ -175,13 +175,13 @@ merge is worse than it looks. **`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). Size is the wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and -`CELSP` — `GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers — and +`CELSP` (`GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers), and **none of them imports the implementation**. Only the app target does, six files, composing it at the root. The abstraction is load-bearing, not ceremonial. What the separate target buys, stated precisely: reaching for the implementation from a feature needs a **manifest edit**, visible in review, rather than an import line inside a file. It is -reviewability, not prevention — import honesty checks that imports are *declared*, so a feature that +reviewability, not prevention: import honesty checks that imports are *declared*, so a feature that declared the dependency would pass the audit. Folding it into the app target is a coherent alternative (the app is the only consumer, and composing platform adapters is a composition-root job); it would cost the manifest-level visibility and nothing else demonstrable. @@ -190,16 +190,16 @@ job); it would cost the manifest-level visibility and nothing else demonstrable. `static let fileManager = FileManager.default` and uses it for `isEmptyFolder` and `doesExist`. Those are filesystem reads from a domain type. Moving them onto the file-manager service is the pure fix; it is not worth it today against 294 references. What was worth fixing, and has been, is code *outside* -Core borrowing that static to mutate the filesystem — a write routed through the domain layer. There +Core borrowing that static to mutate the filesystem, a write routed through the domain layer. There is now no such caller. **Hub heuristic.** Any target both depended on by three or more others *and* itself depending on three or more is a hub under review. 2022's `AppPreferences` was exactly this and would have been flagged years before it became fatal. `CodeEditSettings` is the current watch item: five dependents -as of the panel-contributions work (`CEEditor`, `CELSP`, `CESearch`, `CESourceControl`, `CETerminal` -— up from four; `CESearch` joined when `FindNavigatorContribution` started reading its own settings +as of the panel-contributions work (`CEEditor`, `CELSP`, `CESearch`, `CESourceControl`, `CETerminal`, +up from four; `CESearch` joined when `FindNavigatorContribution` started reading its own settings instead of taking them from an app-side wrapper) but only one dependency (`CodeEditCore`), so it -stays a well-formed shared substrate rather than a hub — and it imports `AppKit` in 1 file and +stays a well-formed shared substrate rather than a hub, and it imports `AppKit` in 1 file and `SwiftUI` in 4, down from 7 once the theme moved to Core and the colour conversion to `CodeEditUI` (re-measured 2026-08-16, not carried forward). @@ -210,13 +210,13 @@ Work through these in order; the first match wins. 1. **A new user-facing feature?** → A new target at `CodeEditModules/Sources/CE` (see the [recipe](#creating-a-new-feature-target)). Features start as targets; the app target is not the default. Exception: *shell chrome* that composes multiple features around the - concrete `Workspace` hub — navigator/inspector/utility areas, the status bar — stays + concrete `Workspace` hub (navigator/inspector/utility areas, the status bar) stays app-side, because its interface would effectively be "the whole app". **This exemption is about the panel, not any one tab inside it.** `NavigatorAreaView` hosts a tab bar over many features' tabs and has no single owner, so it stays app-side; a *tab* is one feature's own - UI and belongs in that feature's package — `CESearch` owns `FindNavigatorContribution` for + UI and belongs in that feature's package. `CESearch` owns `FindNavigatorContribution` for exactly this reason (see [Panel tab contributions](#panel-tab-contributions)). The only - app-side tabs that stay are the ones with no owning package to move to — + app-side tabs that stay are the ones with no owning package to move to: `ProjectNavigatorContribution`, `FileInspectorContribution`, `InternalDevelopmentInspectorContribution`, `DebugConsoleUtilityContribution` and `OutputUtilityContribution`. `TerminalUtilityContribution` is the one expected to move, to @@ -231,16 +231,16 @@ Work through these in order; the first match wins. `CodeEditModules/Sources/` (Core-only dependencies, its own library product, the app links it directly). 5. **Cross-service orchestration?** → A doer-style role-noun class in the feature that owns - the operation (`WorkspaceOpener`, `FileMover`, `RepositoryCloner` — the `NSFileCoordinator` + the operation (`WorkspaceOpener`, `FileMover`, `RepositoryCloner`, following the `NSFileCoordinator` naming idiom). One doer per operation that touches more than one service; dependencies arrive via the initializer. -6. **Everything else — composition, adapters, menu commands, settings pages, app lifecycle?** +6. **Everything else: composition, adapters, menu commands, settings pages, app lifecycle?** → The app target, inside the owning feature folder. ### The consumer-count litmus The most common placement mistake is moving something "up" because it *looks* generic. -Don't judge generic-ness — **count consumers**: +Don't judge generic-ness. **Count consumers**: - A helper with **one** consumer lives next to that consumer, even if it looks general-purpose. @@ -252,15 +252,15 @@ Don't judge generic-ness — **count consumers**: shared is coupling every context pays for and nobody uses. - The exception is **deliberate contracts**: events, command interfaces, and read-models stay in Core even with one publisher or one implementor today, because being a seam is their - entire purpose. `FileEditorOverrideValues` stays for the same reason — it is the payload of + entire purpose. `FileEditorOverrideValues` stays for the same reason: it is the payload of Core's `FileEditorOverrides` protocol. Applying this in July 2026 evicted five types: the registry install cluster (`InstallationMethod`, `PackageSource`, `PackageManagerType`, `RegistryManagerError`) to CELSP, and `GitBranchesGroup` to CESourceControl. -Worked example: fuzzy matching earned its place in CodeEditCore — three consumers across two -features (Open Quickly, Theme settings, Language Servers) — but its concurrency helper +Worked example: fuzzy matching earned its place in CodeEditCore, with three consumers across two +features (Open Quickly, Theme settings, Language Servers), but its concurrency helper depended on CollectionConcurrencyKit, which Core's zero-dependency charter forbids. The fix was to rewrite the helper over `withTaskGroup` (about ten lines) rather than admit the dependency, so `Domain/FuzzyMatching/` is now dependency-free. @@ -272,14 +272,14 @@ a move possible. Rewrite the helper, or mirror it locally, instead. Grouping is **purpose-first**: - Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never - by kind — the app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` + by kind. The app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` folders. - **`CEEditor` and `CESourceControl` are the worked examples** (2026-08-16). `CEEditor`'s - `Models/`, `Views/` and `UseCases/` became nine groups named for what their files are about — + `Models/`, `Views/` and `UseCases/` became nine groups named for what their files are about: `Editor/`, `Layout/`, `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, `Documents/`, `Restoration/`, `Theme/`, `Adapters/`. `CESourceControl`'s `Views/` grab-bag split into `Operations/` and `Branches/`, its cloner joined `Clone/`, and its settings types moved to - `Settings/`. Both were pure renames — 55 and 19 files, zero content changes — because Swift + `Settings/`. Both were pure renames (55 and 19 files, zero content changes), because Swift ignores directory layout and SwiftPM takes the whole target tree. - **`UseCases/` is now gone from every package.** The type-level rename to doers (`EditorRestorer`, `RepositoryCloner`) had stopped at the folder level; it no longer does. @@ -287,7 +287,7 @@ Grouping is **purpose-first**: held 1, 1, 4 and 3 files; a single `Panel/` group now holds the view model and every view that observes it, and the rest sits flat at the root. - **`CodeEditCore` followed**, with one deliberate exception. Its `Extensions/` became `Paths/` - (the four `URL` helpers, `String+ValidFileName`, and `String+Escaped` — whose escaping exists to + (the four `URL` helpers, `String+ValidFileName`, and `String+Escaped`, whose escaping exists to make paths safe as shell arguments), with the two genuinely unrelated helpers at the target root. `Event`/`EventBus` joined `Events/`, and `FindReplaceQuery` moved to `Domain/`, being a query model shared by `CEEditor` and `CESearch` rather than a seam. @@ -297,19 +297,19 @@ Grouping is **purpose-first**: a seam they talk through, and this guide already describes the target in exactly those terms. Do not "fix" this one. - **`CETerminal` followed**: `Shell/` (configuration) is the one subgroup that earned a folder, - while the three-level `CETerminalView` inheritance chain and its representable stay together — + while the three-level `CETerminalView` inheritance chain and its representable stay together, because splitting them would separate a base class from its subclasses. -- **`CELSP` followed**: `Utils/` split — two semantic-token helpers joined +- **`CELSP` followed**: `Utils/` split in two: the semantic-token helpers joined `Features/SemanticTokens/`, and the three that cross the protocol boundary became `Conversions/`. `Registry/`'s `Model/` and `Protocols/` dissolved into its root, where `PackageManagerProtocol` already sat. - **`CodeEditUI` is the second stated exception, and was deliberately left grouped by kind.** Grouping by kind is wrong *inside a feature*; this target is a component library with no feature semantics by charter, so there is no domain to group by and `Styles/`, `Views/` and - `EnvironmentKeys/` are the subject — the terms SwiftUI itself is documented in. Consumers browse + `EnvironmentKeys/` are the subject, being the terms SwiftUI itself is documented in. Consumers browse it asking "is there a button style for this?". Imposing subjects would yield several two-file folders; `SplitView/` remains the one genuine subsystem. Two files that were not styles moved out - of `Styles/`, and `MenuWithButtonStyle` — a `View`, not a `MenuStyle` — became `ButtonStyledMenu`. + of `Styles/`, and `MenuWithButtonStyle` (a `View`, not a `MenuStyle`) became `ButtonStyledMenu`. - **All 12 library targets have been reviewed** (2026-08-16/20). Six were regrouped; two are stated exceptions (`CodeEditCore`, `CodeEditUI`); `CodeEditDocument` (5 files) and `CEWorkspaceFileManager` (7) are correctly flat and need nothing; `ShellClient` is one file by @@ -327,7 +327,7 @@ Grouping is **purpose-first**: Note `Extensions/` counts too (`CodeEditCore`, `CESearch`, `CETerminal`): a folder of "things that are extensions" says nothing about what they extend. - Measure with this exact pattern, and widen it rather than trusting a smaller number — four + Measure with this exact pattern, and widen it rather than trusting a smaller number. Four successive counts here were wrong because the pattern matched `Models` but not `Model`, then not `Protocols`, then not `Extensions` or singular `Service`: @@ -340,7 +340,7 @@ Grouping is **purpose-first**: - **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and environment keys are distributed to their subject rather than gathered into an `Environment/` - group — which would be grouping by kind again. + group, which would be grouping by kind again. - A feature with roughly ten files or fewer stays flat. - Shell/entry views and the feature's primary models sit at the feature root. - Single-consumer helpers live next to their consumer. @@ -358,15 +358,15 @@ Grouping is **purpose-first**: notifications (NSWindow, NSApplication, NSMenu). - **No DI container.** `AppDependencies` is the app-scope composition root; objects receive dependencies through initializers, SwiftUI views through environment keys (`appServices(_:)`). - Only composition roots may hold the whole `AppDependencies`. A container — ask for a type, get - an instance — was removed deliberately: it hides who owns a thing and how long it lives, which + Only composition roots may hold the whole `AppDependencies`. A container (ask for a type, get + an instance) was removed deliberately: it hides who owns a thing and how long it lives, which is the question this section exists to answer. - **Scope determines owner; nothing scoped is reached ambiently.** See [Scopes and ownership](#scopes-and-ownership) below. A `static shared` is legitimate only where - the platform constructs the object and no initialiser parameter is available — today that is + the platform constructs the object and no initialiser parameter is available. Today that is `CodeFileDocument`, which is why `delegateProvider` is a static closure set at launch. Eight other singletons remain, listed there as known exceptions. -- No SwiftUI view observes a service directly — services expose a concrete view-state object +- No SwiftUI view observes a service directly. Services expose a concrete view-state object (the presentation-state split), and views issue commands through protocol-typed environment keys. @@ -386,7 +386,7 @@ utility areas, status bars and palettes must not be shared. That is why window-U `CodeEditWindowController` and not on `Workspace`. Document scope is where the platform pushes back. `NSDocument` subclasses are created by the -document architecture, not by us, so no initialiser parameter is available — hence +document architecture, not by us, so no initialiser parameter is available, hence `CodeFileDocument.delegateProvider`, a static closure set at launch. That is the shape of a legitimate exception: the platform owns construction. @@ -415,34 +415,34 @@ so is mechanical and changes no behaviour. `TerminalCache` is different, and is the one worth fixing rather than relocating. It is a process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. -Eviction is per-terminal only — nothing clears it when a workspace or window closes — so two open +Eviction is per-terminal only, and nothing clears it when a workspace or window closes, so two open projects share one bag of live terminal views. It works because UUIDs do not collide, but the lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. ## Panel tab contributions The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, -`InspectorTab`, `UtilityAreaTab`). Each panel is a list of `WorkspacePanelContribution` values — -a tab is a value, not a case — assembled by one function per panel in +`InspectorTab`, `UtilityAreaTab`). Each panel is a list of `WorkspacePanelContribution` values, so +a tab is a value, not a case, assembled by one function per panel in `CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`: first-party entries named directly, conditional ones (`InternalDevelopmentInspectorContribution`) as a plain `if`, then extension-provided ones appended through a single adapter call. The panel that renders the list -cannot tell a first-party tab from an extension's — that indistinguishability is the point; it is +cannot tell a first-party tab from an extension's. That indistinguishability is the point; it is what lets a new contribution source arrive without the panel code changing. `WorkspacePanelContribution` lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`), not `CodeEditCore`: a contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forbids UI imports -outright. `CodeEditUI`'s own charter (rule 2) is satisfied too — the protocol needs SwiftUI and +outright. `CodeEditUI`'s own charter (rule 2) is satisfied too: the protocol needs SwiftUI and nothing else. A contribution vends two views. `content` is the tab itself. `bottomView` is an optional bar pinned -below it — the navigator's filter field and sort controls are the existing examples — defaulted to +below it. The navigator's filter field and sort controls are the existing examples. It defaults to `nil` so a tab with no such bar says nothing about it. It is a *requirement* rather than something the panel is handed deliberately: it arrived from upstream as a `switch` over the retired tab enum, where every new tab had to remember to add its case, and a package could not contribute one at all because the switch lived app-side. Vended by the contribution, a tab cannot forget, and where the bar -is placed stays the panel's business — pre-Tahoe it insets the tab's own content, from macOS 26 it +is placed stays the panel's business: pre-Tahoe it insets the tab's own content, and from macOS 26 it spans the panel below the tab bar. **A panel's tab list changes at runtime**, so a stored selection can outlive the tab it names: the @@ -459,14 +459,14 @@ A contribution's owner follows the same placement rule as everything else in contribution from its package, reading whatever it needs (including settings, through the seam) directly rather than having the app assemble it. `CESearch`'s `FindNavigatorContribution` (`CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift`) replaced an app-side -`FindNavigatorTab` wrapper that existed only to shuttle settings values down — once the feature +`FindNavigatorTab` wrapper that existed only to shuttle settings values down. Once the feature could read its own settings, the wrapper had no reason to exist. The tab's id is now owned by the same package (`FindNavigatorContribution.tabID`); the app's `PanelTabID.search` references it rather than duplicating the literal, so there is exactly one source of truth even though the app still needs a compile-checked constant to select the tab by. `ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) -stays app-side — not because it is a tab (see the +stays app-side, not because it is a tab (see the [chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no owning package to move to. The same holds for the file inspector, the internal-development inspector, and the debug and output utility tabs. `TerminalUtilityContribution` is the one that @@ -479,8 +479,8 @@ vend `SourceControlNavigatorView` and `HistoryInspectorView` from the package; t `WorkspaceWindow/NavigatorArea/SourceControlNavigator/` and `WorkspaceWindow/InspectorArea/HistoryInspector/` groups no longer exist. The relocation is the shape a future one follows: whatever the view needs that the package cannot see becomes a -**required** initialiser parameter on the contribution — `WorkspaceNavigator` for the navigator -tab, `ActiveEditorState` for the inspector tab — rather than an environment key moved down with +**required** initialiser parameter on the contribution (`WorkspaceNavigator` for the navigator +tab, `ActiveEditorState` for the inspector tab) rather than an environment key moved down with it; both are non-optional so a missing injection at the call site (`PanelContributions.swift`) is a compile error, not a silently no-op tab. Each contribution still owns its own `tabID` constant, and the app's `PanelTabID.sourceControl` / `.gitHistory` reference @@ -498,7 +498,7 @@ arrive later without touching the panels. ## Reading and writing settings Feature packages reach settings through the **settings seam** in `CodeEditSettings` -(`Store/SettingsValue.swift`, `Store/SettingsAccessing.swift`) — never through a singleton and +(`Store/SettingsValue.swift`, `Store/SettingsAccessing.swift`), never through a singleton and never by naming the app-wide `SettingsData` aggregate. Three roles, pick by consumer kind: | Consumer | Use | Why | @@ -512,19 +512,19 @@ so a caller changing one field reads its section, mutates it and writes it back. - **`@AppSettings` is app-target only.** It observes the same store as `SettingsValue`, but addresses a section field through the app-wide `SettingsData` façade instead of naming one - section directly. There is no `Settings.shared` singleton any more — `PersistentSettingsStore` + section directly. There is no `Settings.shared` singleton any more, `PersistentSettingsStore` (owned by `AppDependencies`) is the concrete store, injected like everything else. `@AppSettings` is what 24 app-target files still use (40 declarations, re-counted 2026-08-16); feature packages must not use it, and new app-target code should prefer the seam. - **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies reaches menu-bar code. `CodeEditCommands`/`ViewCommands` are handed `PersistentSettingsStore` by - initializer and `@ObservedObject` it — reads, writes and menu invalidation all stop depending on + initializer and `@ObservedObject` it, reads, writes and menu invalidation all stop depending on undocumented behaviour. - **The environment does not cross an `NSHostingView`/`NSHostingController` boundary.** A new standalone hosting root must be wrapped in `SettingsInjector` (or `SettingsSceneInjector` for a scene), or every `@SettingsValue` under it **traps**. That is deliberate: this replaced a pair of - environment keys whose failure modes were silent — a subtree given neither read plausible + environment keys whose failure modes were silent, a subtree given neither read plausible defaults and discarded writes, and a subtree given the value but not the separate `Int` invalidation key read correctly and never re-rendered. `@EnvironmentObject` makes both unrepresentable, because SwiftUI subscribes to the store itself. Those two injectors are the only @@ -556,7 +556,7 @@ A section lives with **its owner**: | More than one module, or only the app | `CodeEditSettings` | `TextEditing`, `Theme`, `General`, `Navigation`, `Developer`, `Search`, `Keybindings` | **Never `CodeEditCore` or `CodeEditUI`.** A `Codable` config bag has no natural boundary and -accretes — that is precisely how Core became `AppPreferences` the first time, and Core's purity +accretes. That is precisely how Core became `AppPreferences` the first time, and Core's purity rationale is that the placement question stays answerable. `CodeEditUI` is excluded mechanically: it may not depend on a local target, so it cannot see settings types at all. @@ -572,7 +572,7 @@ Two field-level misplacements are **recorded but not fixed**, because both keys `settings.json` and moving a field is a data migration rather than a refactor: `GeneralSettings.findNavigatorDetail` is read by `CESearch` (a feature-specific field in a shared section), and `SearchSettings.ignoreGlobPatterns` is wired to its settings page and persisted but -never read by `CESearch` — the control works and has no effect, which is worse than dead code +never read by `CESearch`: the control works and has no effect, which is worse than dead code because nothing looks unused. ## Creating a new feature target @@ -596,23 +596,23 @@ because nothing looks unused. 2. Link the product to the app: CodeEdit target → *General* → *Frameworks, Libraries, and Embedded Content* → add `CE`. -3. Remember the target builds with **Swift 6 strict concurrency** — types crossing actor +3. Remember the target builds with **Swift 6 strict concurrency**, types crossing actor boundaries need `Sendable`, and UI-bound classes are usually `@MainActor`. (`CEEditor` is the - sole exception — see [Rules](#rules).) -4. Known quirk: targets that depend on `CodeEditSymbols` build via Xcode/xcodebuild only — + sole exception, see [Rules](#rules).) +4. Known quirk: targets that depend on `CodeEditSymbols` build via Xcode/xcodebuild only; standalone `swift build` fails on its `Bundle.module` resolution. 5. Declare **every** module you import in the manifest. The workspace's shared build directory - makes undeclared imports of sibling targets compile by accident — CI will catch it + makes undeclared imports of sibling targets compile by accident, CI will catch it (see below). ## Enforcement Two tools enforce the three [Rules](#rules) above; both run on every PR: -- **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`) — includes custom rules +- **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`), includes custom rules that reject UI imports in CodeEditCore and model/feature imports in CodeEditUI. These fire inside Xcode while you type. -- **The package audit** (`.github/scripts/audit_package_imports.py`) — verifies every `import` +- **The package audit** (`.github/scripts/audit_package_imports.py`), verifies every `import` in every target is declared in that target's manifest dependencies, and that the three [Rules](#rules) plus the language-mode assertion hold. It exists because Xcode workspace builds share one build directory, so an undeclared import of a sibling target compiles fine @@ -635,14 +635,14 @@ looks clean and CI fails on the same tree. Both checks constrain **local** targets only. `ui_package_purity` lists sibling module names in a regex, and the audit script intersects the target's declared dependencies with the package's library targets. So `CodeEditUI` is barred from importing -`CodeEditCore` — a zero-dependency, pure-types package — while nothing stops it taking an +`CodeEditCore`, a zero-dependency, pure-types package, while nothing stops it taking an arbitrary *external* dependency, up to and including a tree-sitter grammar bundle. The rule as written is narrower than its own stated intent ("presentation atoms must not know about models or features") in one direction and far wider in the other. This surfaced while deduplicating `FileIcon`, which is presentation keyed by a file's identity. -It was designed to take a `URL` rather than a domain type — so it needs neither `CodeEditCore` -nor the loophole — and its three custom colorsets moved into the package as resources, making +It was designed to take a `URL` rather than a domain type, so it needs neither `CodeEditCore` +nor the loophole, and its three custom colorsets moved into the package as resources, making `CodeEditUI` self-contained and letting its tests assert colours without an app host. Treat the asymmetry as a known weakness, not a licence: adding an external dependency to `CodeEditUI` to sidestep the local-package rule would satisfy the letter of the charter and defeat its purpose. @@ -654,14 +654,14 @@ qualified term whenever the bare one could be read two ways. | Term | Means | | --- | --- | -| **Editor** (`Editor`) | One tab group inside a workspace window — a split pane with its own tab bar and selection. | +| **Editor** (`Editor`) | One tab group inside a workspace window, a split pane with its own tab bar and selection. | | **Editor instance** (`EditorInstance`) | One open file within an editor, holding that file's editing state. | | **`EditorManager`** | The per-workspace owner of the editor layout (splits, the active editor). | | **CEEditor** | The package containing the editor feature. | | **CodeEditSourceEditor** | The external text-editing widget (a separate repository), not part of this codebase. | | **Search** | *Project* search: find/replace across files, the index, query modes, the Find navigator. Lives in `CESearch`, which owns its whole model. | | **Fuzzy matching** | Ranking candidates by match quality for typeahead (Open Quickly, theme and language-server pickers). A generic capability in CodeEditCore `Domain/FuzzyMatching/`. It does no searching; nothing here is named `*Search*`. | -| **Workspace** (`Workspace`) | The session aggregate for one open project: the project-scoped services and their lifetime. It owns lifecycle, *not* mutation routing — features mutate the sub-models they are handed. | +| **Workspace** (`Workspace`) | The session aggregate for one open project: the project-scoped services and their lifetime. It owns lifecycle, *not* mutation routing, features mutate the sub-models they are handed. | | **Workspace window** (`WorkspaceWindow/`) | The window and its chrome around a workspace: navigator, inspector, utility area, status bar. Window-UI state lives on `CodeEditWindowController`, not on `Workspace`. | | **Document** (`CodeFileDocument`) | An open, editable file backed by NSDocument. Distinct from `CEWorkspaceFile` (a node in the file tree) and from the file on disk. | | **Doer** | A role-noun class performing one operation that spans services (`WorkspaceOpener`, `FileMover`, `RepositoryCloner`), following the `NSFileCoordinator` naming idiom. Formerly called UseCases. | From 85256b81c45d96fa1efc0583b8438bca6f2020f0 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Thu, 20 Aug 2026 22:28:18 +0200 Subject: [PATCH 323/335] Docs: Reflow the architecture guide to one sentence per line It was hard-wrapped at 100 columns, which nothing in the repo asks for. README.md has a median line length of 142 and CONTRIBUTING.md 85, so this file was the odd one out, and every Markdown renderer soft-wraps regardless. The cost was real: hard wrapping reflows a whole paragraph when one sentence changes, so the em dash pass touched 73 lines to make mostly single-word edits. One sentence per line means a sentence edit changes one line. Verified content-preserving rather than assumed. Prose hashes identically with whitespace collapsed, code blocks are byte-identical, word count is 6484 before and after, and the sequence of block-start indents hashes identically so every list continuation keeps its nesting. The first attempt did not: it flattened 21 indented continuation paragraphs to column zero, which would have re-rendered them as siblings of their list items instead of part of them. --- docs/ARCHITECTURE.md | 761 +++++++++++++++++-------------------------- 1 file changed, 293 insertions(+), 468 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 14ca0baecd..8ef37b5659 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,12 +1,11 @@ # CodeEdit Architecture Guide -This guide explains how the codebase is organized and, most importantly, **where new code -goes**. The CI enforces the rules described here (see [Enforcement](#enforcement)), so reading this before you add files will save you a failed check. +This guide explains how the codebase is organized and, most importantly, **where new code goes**. +The CI enforces the rules described here (see [Enforcement](#enforcement)), so reading this before you add files will save you a failed check. ## Package topology -The workspace contains one app project and one local Swift package holding 12 library targets -and 6 test targets: +The workspace contains one app project and one local Swift package holding 12 library targets and 6 test targets: ``` CodeEdit.xcworkspace @@ -28,25 +27,21 @@ CodeEdit.xcworkspace CESourceControlTests ``` -Each library target publishes a like-named `.library` product, and the app target links the ones -it needs. Inside the manifest, targets reference each other by bare name, so the whole graph is -legible in a single file, which is the point. See -[History](#history-why-the-2022-module-split-failed) for what the previous arrangement cost. +Each library target publishes a like-named `.library` product, and the app target links the ones it needs. +Inside the manifest, targets reference each other by bare name, so the whole graph is legible in a single file, which is the point. +See [History](#history-why-the-2022-module-split-failed) for what the previous arrangement cost. -Naming: `CodeEdit*` marks substrate peer-named with the external CodeEdit libraries -(`CodeEditSourceEditor`, `CodeEditSymbols`, …); `CE*` marks app-internal feature contexts, -peer-named with the `CE*` domain types. Apply `CE` only where the bare name would collide with a stdlib/SwiftUI/AppKit/vendor type, it is a collision-avoider, not a namespace. +Naming: `CodeEdit*` marks substrate peer-named with the external CodeEdit libraries (`CodeEditSourceEditor`, `CodeEditSymbols`, …); `CE*` marks app-internal feature contexts, peer-named with the `CE*` domain types. +Apply `CE` only where the bare name would collide with a stdlib/SwiftUI/AppKit/vendor type, it is a collision-avoider, not a namespace. -Every target builds with Swift 6 strict concurrency **except `CEEditor`**, which declares -`.swiftLanguageMode(.v5)` and is the sole exception. The app target is still Swift 5, write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't -isolated (it cascades). +Every target builds with Swift 6 strict concurrency **except `CEEditor`**, which declares `.swiftLanguageMode(.v5)` and is the sole exception. +The app target is still Swift 5, write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't isolated (it cascades). ## History: Why the 2022 module split failed -The project already tried a single multi-target `CodeEditModules` package. It was deleted on -2022-12-03 (commit `4858de16`) after repeated cyclic-dependency problems, and everything moved into -the app target. **The cycles were not caused by the packaging shape.** The 2022 manifest's own edges -explain them: +The project already tried a single multi-target `CodeEditModules` package. +It was deleted on 2022-12-03 (commit `4858de16`) after repeated cyclic-dependency problems, and everything moved into the app target. +**The cycles were not caused by the packaging shape.** The 2022 manifest's own edges explain them: | Cause | Evidence in the 2022 manifest | |---|---| @@ -55,281 +50,201 @@ explain them: | **Shared UI depended on domain** | `CodeEditUI → WorkspaceClient, Git` | | **A god-module hub** | `AppPreferences → CodeEditUI, Git, Keybindings, CodeEditUtils, Sparkle, CodeEditTextView`, itself depended on by half the tree | -With the bottom of the graph pointing up into the top, cycles were the steady state rather than an accident. The same manifest split across eleven separate packages fails identically, SwiftPM refuses to resolve a cyclic graph either way. +With the bottom of the graph pointing up into the top, cycles were the steady state rather than an accident. +The same manifest split across eleven separate packages fails identically, SwiftPM refuses to resolve a cyclic graph either way. -**`CodeEditCore` is the fix, and it now exists.** Zero dependencies, framework-free, holding domain values, the typed `EventBus`, and the cross-feature command interfaces. Every "A and B both need X" now resolves *downward*. Both 2022 killers are structurally impossible today: the domain lives in `CodeEditCore`, which may import nothing, and `CodeEditUI → Git` is blocked by the `CodeEditUI` -purity rule. +**`CodeEditCore` is the fix, and it now exists.** Zero dependencies, framework-free, holding domain values, the typed `EventBus`, and the cross-feature command interfaces. +Every "A and B both need X" now resolves *downward*. +Both 2022 killers are structurally impossible today: the domain lives in `CodeEditCore`, which may import nothing, and `CodeEditUI → Git` is blocked by the `CodeEditUI` purity rule. ### Cycle-resolution playbook -Detection was never the problem, SwiftPM refuses a cyclic target graph as a hard error. The 2022 failure was that detection had no accompanying *resolution* technique, so the exit taken was to collapse everything into the app target. When you hit a cycle, the legal moves, in preference order: +Detection was never the problem, SwiftPM refuses a cyclic target graph as a hard error. +The 2022 failure was that detection had no accompanying *resolution* technique, so the exit taken was to collapse everything into the app target. +When you hit a cycle, the legal moves, in preference order: -1. **Push the shared thing down to `CodeEditCore`** — a protocol, an event, or a value type. This is what `EventBus` and the command interfaces (`WorkspaceNavigator` `TasksConfigurationProviding`, …) are for. The default answer. -2. **Push the coordination up to the app target** — the app may depend on everything. Two leaf features never need to know each other if a doer wires them (`WorkspaceOpener`, `DocumentOpener`). -3. **Merge the two targets** — if A and B genuinely will not separate, the boundary was drawn wrong. Merging is a correct outcome, not a defeat; in one package it is a folder move plus a three-line manifest edit. +1. **Push the shared thing down to `CodeEditCore`** — a protocol, an event, or a value type. + This is what `EventBus` and the command interfaces (`WorkspaceNavigator` `TasksConfigurationProviding`, …) are for. + The default answer. +2. **Push the coordination up to the app target** — the app may depend on everything. + Two leaf features never need to know each other if a doer wires them (`WorkspaceOpener`, `DocumentOpener`). +3. **Merge the two targets** — if A and B genuinely will not separate, the boundary was drawn wrong. + Merging is a correct outcome, not a defeat; in one package it is a folder move plus a three-line manifest edit. -Never resolve a cycle by moving code back into the app target. That is what happened in 2022 and it cost four years of enforced boundaries. +Never resolve a cycle by moving code back into the app target. +That is what happened in 2022 and it cost four years of enforced boundaries. ## Rules -Three checks are enforced in CI. Each one blocks a specific failure documented in -[History](#history-why-the-2022-module-split-failed), none is enforced on principle alone. - -1. **`CodeEditCore` purity.** Zero dependencies, local or external. No `SwiftUI`, `AppKit`, or - `Cocoa` import. Blocks 2022's `WorkspaceClient → TabBar`. The two constraints earn their keep - separately: **zero local dependencies** is the acyclicity guarantee, because it makes Core a sink, so - every "A and B both need X" resolves downward, which is the direct fix for the "no kernel - existed" failure above. **No UI frameworks** keeps the placement question answerable. Without - it, Core becomes the place everything shared goes, which is what `AppPreferences` was and one of - the two documented causes of the 2022 collapse. - - The friction this produces is usually the rule working. Four worked examples already in this - codebase: `FileIcon` is keyed on `URL` rather than a domain type, so it needs neither - `CodeEditCore` nor a charter exception (see rule 2 below); `WorkspacePanelContribution` was shaped - to need only SwiftUI, so it lives in `CodeEditUI` - (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); fuzzy matching's - concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` - (below); and `ActiveTheme`, the active light/dark theme, observed by the editor and terminal, - lives *in* Core, because `ObservableObject` comes from Combine rather than SwiftUI. That last one - is worth remembering: the charter forbids `SwiftUI`/`AppKit`/`Cocoa` specifically, so observation - is available in Core and an "it needs to be observable, therefore it needs SwiftUI" argument is - simply false. - - The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry - `NSFont.Weight`, which forces them out of Core. That is the rule flagging a presentation type - inside a settings model, not the rule obstructing a reasonable design. Storing the weight as a - `Double` and converting at the presentation boundary would make both structs Core-eligible with - no rule change. -2. **`CodeEditUI` purity.** No local target dependencies; external `CodeEditSymbols` only. Blocks - 2022's `CodeEditUI → Git`. This is why `FileIcon` is keyed on `URL` rather than on a domain type: - a deliberate consequence, not an accident. -3. **Import honesty.** Every `import` in a target's sources must be declared in that target's - manifest dependencies. Xcode workspace builds share one build directory, so an undeclared import - of a sibling compiles fine and nothing else catches it. For the 7 targets that transitively need - `CodeEditSymbols`, a standalone `swift build` is not available as a backstop either (see - [Enforcement](#enforcement)), so this check is their only defence, not a redundant one. - -Plus one assertion: **only `CEEditor` may declare `.swiftLanguageMode(.v5)`.** Every other target -inherits Swift 6 from the package's tools version. A target silently dropping to Swift 5 would lose -strict-concurrency enforcement without anything failing. +Three checks are enforced in CI. +Each one blocks a specific failure documented in [History](#history-why-the-2022-module-split-failed), none is enforced on principle alone. + +1. **`CodeEditCore` purity.** Zero dependencies, local or external. No `SwiftUI`, `AppKit`, or `Cocoa` import. + Blocks 2022's `WorkspaceClient → TabBar`. + The two constraints earn their keep separately: **zero local dependencies** is the acyclicity guarantee, because it makes Core a sink, so every "A and B both need X" resolves downward, which is the direct fix for the "no kernel existed" failure above. + **No UI frameworks** keeps the placement question answerable. + Without it, Core becomes the place everything shared goes, which is what `AppPreferences` was and one of the two documented causes of the 2022 collapse. + + The friction this produces is usually the rule working. + Four worked examples already in this codebase: `FileIcon` is keyed on `URL` rather than a domain type, so it needs neither `CodeEditCore` nor a charter exception (see rule 2 below); `WorkspacePanelContribution` was shaped to need only SwiftUI, so it lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); fuzzy matching's concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` (below); and `ActiveTheme`, the active light/dark theme, observed by the editor and terminal, lives *in* Core, because `ObservableObject` comes from Combine rather than SwiftUI. + That last one is worth remembering: the charter forbids `SwiftUI`/`AppKit`/`Cocoa` specifically, so observation is available in Core and an "it needs to be observable, therefore it needs SwiftUI" argument is simply false. + + The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry `NSFont.Weight`, which forces them out of Core. + That is the rule flagging a presentation type inside a settings model, not the rule obstructing a reasonable design. + Storing the weight as a `Double` and converting at the presentation boundary would make both structs Core-eligible with no rule change. +2. **`CodeEditUI` purity.** No local target dependencies; external `CodeEditSymbols` only. + Blocks 2022's `CodeEditUI → Git`. + This is why `FileIcon` is keyed on `URL` rather than on a domain type: a deliberate consequence, not an accident. +3. **Import honesty.** Every `import` in a target's sources must be declared in that target's manifest dependencies. + Xcode workspace builds share one build directory, so an undeclared import of a sibling compiles fine and nothing else catches it. + For the 7 targets that transitively need `CodeEditSymbols`, a standalone `swift build` is not available as a backstop either (see [Enforcement](#enforcement)), so this check is their only defence, not a redundant one. + +Plus one assertion: **only `CEEditor` may declare `.swiftLanguageMode(.v5)`.** Every other target inherits Swift 6 from the package's tools version. +A target silently dropping to Swift 5 would lose strict-concurrency enforcement without anything failing. ### Norms (review-time, not gates) -**Prefer features to be leaves.** Nothing should depend on a feature target. When an edge between two -features is genuinely needed, try the three -[cycle-resolution moves](#cycle-resolution-playbook) first, then declare the edge in the manifest -where it is visible to everyone. Acyclicity itself needs no rule, SwiftPM enforces it. +**Prefer features to be leaves.** Nothing should depend on a feature target. +When an edge between two features is genuinely needed, try the three [cycle-resolution moves](#cycle-resolution-playbook) first, then declare the edge in the manifest where it is visible to everyone. +Acyclicity itself needs no rule, SwiftPM enforces it. -**Keep I/O out of Core.** A norm, not a gate. It is worth being precise about, because the guide -previously implied it was enforced. It is not: the SwiftLint rule forbids `SwiftUI`/`AppKit`/`Cocoa` -and nothing more, and `Foundation`, which Core needs for `URL`, `Data` and `Codable`, and which 47 -of its files import, *is* the I/O surface, so an import check cannot express this. +**Keep I/O out of Core.** A norm, not a gate. +It is worth being precise about, because the guide previously implied it was enforced. +It is not: the SwiftLint rule forbids `SwiftUI`/`AppKit`/`Cocoa` and nothing more, and `Foundation`, which Core needs for `URL`, `Data` and `Codable`, and which 47 of its files import, *is* the I/O surface, so an import check cannot express this. -The reason to keep it out anyway is concrete, not decorative. `CodeEditCoreTests` is five files with -zero use of `FileManager`, `temporaryDirectory` or `Data(contentsOf:)`: Core's tests need no -filesystem, no temp directories and no cleanup. And I/O already has a designated home, since rule 4 -of [Where does my code go?](#where-does-my-code-go) sends services to their own target, which is what -`CEWorkspaceFileManager` and `ShellClient` are. +The reason to keep it out anyway is concrete, not decorative. +`CodeEditCoreTests` is five files with zero use of `FileManager`, `temporaryDirectory` or `Data(contentsOf:)`: Core's tests need no filesystem, no temp directories and no cleanup. +And I/O already has a designated home, since rule 4 of [Where does my code go?](#where-does-my-code-go) sends services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. **What the norm actually prevents** (asked 2026-08-20): merging `CEWorkspaceFileManager` into Core. -That target holds 50 `FileManager` calls and a complete FSEvents implementation, `FSEventStreamCreate` -with a C callback, its own dispatch queue, and start/stop/invalidate/release. Without this norm the -merge looks reasonable, because that target depends on nothing but Core and folding it in removes a -target. With the norm it is obviously wrong: it would put a live filesystem event stream inside the -dependency sink all twelve targets rest on, and end Core's filesystem-free tests the same day. State -the norm with this example, not on principle alone. +That target holds 50 `FileManager` calls and a complete FSEvents implementation, `FSEventStreamCreate` with a C callback, its own dispatch queue, and start/stop/invalidate/release. +Without this norm the merge looks reasonable, because that target depends on nothing but Core and folding it in removes a target. +With the norm it is obviously wrong: it would put a live filesystem event stream inside the dependency sink all twelve targets rest on, and end Core's filesystem-free tests the same day. +State the norm with this example, not on principle alone. **`CodeEditDocument` and `CELSP` both stay their own targets** (asked and settled 2026-08-20). Neither is a leftover, and the two conclusions depend on each other. -`CEEditor` and `CELSP` reference each other **zero times, in either direction**. They are siblings. -What keeps them apart is `LanguageServicesProvider`, declared in `CodeEditDocument`, implemented by -`CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key. It -even ships a `NoOpLanguageServicesProvider`, so the editor works with no language service at all. -So `CodeEditDocument` is not "the document type plus some bridging": **it is the contract that keeps -two features independent.** `CodeFileDocument` imports AppKit, SwiftUI and the editor frameworks, so -it cannot live in Core; two features need it, so it cannot live in either. Its own target is forced, -not chosen. - -`CELSP` is not part of the editor either. Its consumers are the settings UI (installing servers), the -utility area (reading logs) and app lifecycle (nothing in `CEEditor` imports it), and **28 of its 78 -files are `Registry/`**: package managers, install steps and source parsers for Cargo, NPM, Pip, Go -and GitHub. That is downloading and installing language servers, not editing text. Folding it into -`CEEditor` would make a ~130-file target mixing the two. - -The abstraction is already sound where it counts: `LanguageServer`, `LSPContentCoordinator`, -`SemanticTokenHighlightProvider` and `LanguageServerDocumentObjects` are all generic over -`LanguageServerDocument`, a protocol requiring only `content`, `languageServerURI` and -`getLanguage()`. Only `LSPService` itself pins the generic to `CodeFileDocument`. Decoupling that -would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type, -breaking its use as an existential for DI, all to delete a five-file target. A bad trade. - -**`CEWorkspaceFileManager` stays its own target, and must not merge into Core** (asked and settled -2026-08-20). The inversion is already in place: Core declares `WorkspaceFileProviding` and -`WorkspaceFileObserver`, and `CEWorkspaceFileManager.swift:263` is `extension CEWorkspaceFileManager: -WorkspaceFileProviding {}`. Four `CEEditor` files depend on the *protocol* (`EditorRestorer`, -`EditorJumpBarMenu`, `EditorLayout+StateRestoration`, the environment key) and **no package imports -the implementation**. Its 22 consumers are app-side, plus 6 test files. Same shape as `ShellClient` -below: contract in Core, adapter in its own target, app composes. See the I/O norm above for why the -merge is worse than it looks. - -**`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). Size is the -wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and -`CELSP` (`GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers), and -**none of them imports the implementation**. Only the app target does, six files, composing it at -the root. The abstraction is load-bearing, not ceremonial. - -What the separate target buys, stated precisely: reaching for the implementation from a feature -needs a **manifest edit**, visible in review, rather than an import line inside a file. It is -reviewability, not prevention: import honesty checks that imports are *declared*, so a feature that -declared the dependency would pass the audit. Folding it into the app target is a coherent -alternative (the app is the only consumer, and composing platform adapters is a composition-root -job); it would cost the manifest-level visibility and nothing else demonstrable. - -**Known exception, recorded rather than pretended away:** `CEWorkspaceFile` exposes -`static let fileManager = FileManager.default` and uses it for `isEmptyFolder` and `doesExist`. -Those are filesystem reads from a domain type. Moving them onto the file-manager service is the pure fix; it is -not worth it today against 294 references. What was worth fixing, and has been, is code *outside* -Core borrowing that static to mutate the filesystem, a write routed through the domain layer. There -is now no such caller. - -**Hub heuristic.** Any target both depended on by three or more others *and* itself depending on -three or more is a hub under review. 2022's `AppPreferences` was exactly this and would have been -flagged years before it became fatal. `CodeEditSettings` is the current watch item: five dependents -as of the panel-contributions work (`CEEditor`, `CELSP`, `CESearch`, `CESourceControl`, `CETerminal`, -up from four; `CESearch` joined when `FindNavigatorContribution` started reading its own settings -instead of taking them from an app-side wrapper) but only one dependency (`CodeEditCore`), so it -stays a well-formed shared substrate rather than a hub, and it imports `AppKit` in 1 file and -`SwiftUI` in 4, down from 7 once the theme moved to Core and the colour conversion to `CodeEditUI` -(re-measured 2026-08-16, not carried forward). +`CEEditor` and `CELSP` reference each other **zero times, in either direction**. +They are siblings. +What keeps them apart is `LanguageServicesProvider`, declared in `CodeEditDocument`, implemented by `CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key. +It even ships a `NoOpLanguageServicesProvider`, so the editor works with no language service at all. +So `CodeEditDocument` is not "the document type plus some bridging": **it is the contract that keeps two features independent.** `CodeFileDocument` imports AppKit, SwiftUI and the editor frameworks, so it cannot live in Core; two features need it, so it cannot live in either. +Its own target is forced, not chosen. + +`CELSP` is not part of the editor either. +Its consumers are the settings UI (installing servers), the utility area (reading logs) and app lifecycle (nothing in `CEEditor` imports it), and **28 of its 78 files are `Registry/`**: package managers, install steps and source parsers for Cargo, NPM, Pip, Go and GitHub. +That is downloading and installing language servers, not editing text. +Folding it into `CEEditor` would make a ~130-file target mixing the two. + +The abstraction is already sound where it counts: `LanguageServer`, `LSPContentCoordinator`, `SemanticTokenHighlightProvider` and `LanguageServerDocumentObjects` are all generic over `LanguageServerDocument`, a protocol requiring only `content`, `languageServerURI` and `getLanguage()`. +Only `LSPService` itself pins the generic to `CodeFileDocument`. +Decoupling that would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type, breaking its use as an existential for DI, all to delete a five-file target. +A bad trade. + +**`CEWorkspaceFileManager` stays its own target, and must not merge into Core** (asked and settled 2026-08-20). +The inversion is already in place: Core declares `WorkspaceFileProviding` and `WorkspaceFileObserver`, and `CEWorkspaceFileManager.swift:263` is `extension CEWorkspaceFileManager: WorkspaceFileProviding {}`. +Four `CEEditor` files depend on the *protocol* (`EditorRestorer`, `EditorJumpBarMenu`, `EditorLayout+StateRestoration`, the environment key) and **no package imports the implementation**. +Its 22 consumers are app-side, plus 6 test files. +Same shape as `ShellClient` below: contract in Core, adapter in its own target, app composes. +See the I/O norm above for why the merge is worse than it looks. + +**`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). +Size is the wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and `CELSP` (`GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers), and **none of them imports the implementation**. +Only the app target does, six files, composing it at the root. +The abstraction is load-bearing, not ceremonial. + +What the separate target buys, stated precisely: reaching for the implementation from a feature needs a **manifest edit**, visible in review, rather than an import line inside a file. +It is reviewability, not prevention: import honesty checks that imports are *declared*, so a feature that declared the dependency would pass the audit. +Folding it into the app target is a coherent alternative (the app is the only consumer, and composing platform adapters is a composition-root job); it would cost the manifest-level visibility and nothing else demonstrable. + +**Known exception, recorded rather than pretended away:** `CEWorkspaceFile` exposes `static let fileManager = FileManager.default` and uses it for `isEmptyFolder` and `doesExist`. +Those are filesystem reads from a domain type. +Moving them onto the file-manager service is the pure fix; it is not worth it today against 294 references. +What was worth fixing, and has been, is code *outside* Core borrowing that static to mutate the filesystem, a write routed through the domain layer. +There is now no such caller. + +**Hub heuristic.** Any target both depended on by three or more others *and* itself depending on three or more is a hub under review. +2022's `AppPreferences` was exactly this and would have been flagged years before it became fatal. `CodeEditSettings` is the current watch item: five dependents as of the panel-contributions work (`CEEditor`, `CELSP`, `CESearch`, `CESourceControl`, `CETerminal`, up from four; `CESearch` joined when `FindNavigatorContribution` started reading its own settings instead of taking them from an app-side wrapper) but only one dependency (`CodeEditCore`), so it stays a well-formed shared substrate rather than a hub, and it imports `AppKit` in 1 file and `SwiftUI` in 4, down from 7 once the theme moved to Core and the colour conversion to `CodeEditUI` (re-measured 2026-08-16, not carried forward). ## Where does my code go? Work through these in order; the first match wins. -1. **A new user-facing feature?** → A new target at `CodeEditModules/Sources/CE` (see the - [recipe](#creating-a-new-feature-target)). Features start as targets; the app target is - not the default. Exception: *shell chrome* that composes multiple features around the - concrete `Workspace` hub (navigator/inspector/utility areas, the status bar) stays - app-side, because its interface would effectively be "the whole app". **This exemption is - about the panel, not any one tab inside it.** `NavigatorAreaView` hosts a tab bar over many - features' tabs and has no single owner, so it stays app-side; a *tab* is one feature's own - UI and belongs in that feature's package. `CESearch` owns `FindNavigatorContribution` for - exactly this reason (see [Panel tab contributions](#panel-tab-contributions)). The only - app-side tabs that stay are the ones with no owning package to move to: - `ProjectNavigatorContribution`, `FileInspectorContribution`, - `InternalDevelopmentInspectorContribution`, `DebugConsoleUtilityContribution` and - `OutputUtilityContribution`. `TerminalUtilityContribution` is the one expected to move, to - `CETerminal`. None of them stays because it is a tab. -2. **A type, protocol, event, or command interface needed by two or more features?** → - `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI imports, no - external dependencies). Events (facts, e.g. `TaskNotificationEvent`) and command interfaces - (requests with exactly one handler, e.g. `WorkspaceNavigator`) always live here. -3. **A reusable view, style, or view modifier with no feature semantics?** → - `CodeEditModules/Sources/CodeEditUI`. -4. **A service that performs I/O and has no UI?** → A new target at - `CodeEditModules/Sources/` (Core-only dependencies, its own library product, the - app links it directly). -5. **Cross-service orchestration?** → A doer-style role-noun class in the feature that owns - the operation (`WorkspaceOpener`, `FileMover`, `RepositoryCloner`, following the `NSFileCoordinator` - naming idiom). One doer per operation that touches more than one service; dependencies - arrive via the initializer. -6. **Everything else: composition, adapters, menu commands, settings pages, app lifecycle?** - → The app target, inside the owning feature folder. +1. **A new user-facing feature?** → A new target at `CodeEditModules/Sources/CE` (see the [recipe](#creating-a-new-feature-target)). + Features start as targets; the app target is not the default. + Exception: *shell chrome* that composes multiple features around the concrete `Workspace` hub (navigator/inspector/utility areas, the status bar) stays app-side, because its interface would effectively be "the whole app". + **This exemption is about the panel, not any one tab inside it.** `NavigatorAreaView` hosts a tab bar over many features' tabs and has no single owner, so it stays app-side; a *tab* is one feature's own UI and belongs in that feature's package. + `CESearch` owns `FindNavigatorContribution` for exactly this reason (see [Panel tab contributions](#panel-tab-contributions)). + The only app-side tabs that stay are the ones with no owning package to move to: `ProjectNavigatorContribution`, `FileInspectorContribution`, `InternalDevelopmentInspectorContribution`, `DebugConsoleUtilityContribution` and `OutputUtilityContribution`. + `TerminalUtilityContribution` is the one expected to move, to `CETerminal`. + None of them stays because it is a tab. +2. **A type, protocol, event, or command interface needed by two or more features?** → `CodeEditModules/Sources/CodeEditCore`, *if* it passes the charter (no UI imports, no external dependencies). + Events (facts, e.g. `TaskNotificationEvent`) and command interfaces (requests with exactly one handler, e.g. `WorkspaceNavigator`) always live here. +3. **A reusable view, style, or view modifier with no feature semantics?** → `CodeEditModules/Sources/CodeEditUI`. +4. **A service that performs I/O and has no UI?** → A new target at `CodeEditModules/Sources/` (Core-only dependencies, its own library product, the app links it directly). +5. **Cross-service orchestration?** → A doer-style role-noun class in the feature that owns the operation (`WorkspaceOpener`, `FileMover`, `RepositoryCloner`, following the `NSFileCoordinator` naming idiom). + One doer per operation that touches more than one service; dependencies arrive via the initializer. +6. **Everything else: composition, adapters, menu commands, settings pages, app lifecycle?** → The app target, inside the owning feature folder. ### The consumer-count litmus The most common placement mistake is moving something "up" because it *looks* generic. -Don't judge generic-ness. **Count consumers**: - -- A helper with **one** consumer lives next to that consumer, even if it looks - general-purpose. -- It moves to `CodeEditCore` only when a **second consumer actually appears** *and* it passes - the zero-dependency charter. -- The rule runs **both ways**. A type already in `CodeEditCore` that turns out to have a - single consumer moves *out*, into that consumer's package. Framework-freedom is necessary - but not sufficient: Core is a shared kernel, and every type in it that isn't actually - shared is coupling every context pays for and nobody uses. -- The exception is **deliberate contracts**: events, command interfaces, and read-models stay - in Core even with one publisher or one implementor today, because being a seam is their - entire purpose. `FileEditorOverrideValues` stays for the same reason: it is the payload of - Core's `FileEditorOverrides` protocol. - -Applying this in July 2026 evicted five types: the registry install cluster -(`InstallationMethod`, `PackageSource`, `PackageManagerType`, `RegistryManagerError`) to -CELSP, and `GitBranchesGroup` to CESourceControl. - -Worked example: fuzzy matching earned its place in CodeEditCore, with three consumers across two -features (Open Quickly, Theme settings, Language Servers), but its concurrency helper -depended on CollectionConcurrencyKit, which Core's zero-dependency charter forbids. The fix -was to rewrite the helper over `withTaskGroup` (about ten lines) rather than admit the -dependency, so `Domain/FuzzyMatching/` is now dependency-free. -**Dependency honesty beats tidiness**: never add a dependency to a foundation package to make -a move possible. Rewrite the helper, or mirror it locally, instead. +Don't judge generic-ness. +**Count consumers**: + +- A helper with **one** consumer lives next to that consumer, even if it looks general-purpose. +- It moves to `CodeEditCore` only when a **second consumer actually appears** *and* it passes the zero-dependency charter. +- The rule runs **both ways**. + A type already in `CodeEditCore` that turns out to have a single consumer moves *out*, into that consumer's package. + Framework-freedom is necessary but not sufficient: Core is a shared kernel, and every type in it that isn't actually shared is coupling every context pays for and nobody uses. +- The exception is **deliberate contracts**: events, command interfaces, and read-models stay in Core even with one publisher or one implementor today, because being a seam is their entire purpose. + `FileEditorOverrideValues` stays for the same reason: it is the payload of Core's `FileEditorOverrides` protocol. + +Applying this in July 2026 evicted five types: the registry install cluster (`InstallationMethod`, `PackageSource`, `PackageManagerType`, `RegistryManagerError`) to CELSP, and `GitBranchesGroup` to CESourceControl. + +Worked example: fuzzy matching earned its place in CodeEditCore, with three consumers across two features (Open Quickly, Theme settings, Language Servers), but its concurrency helper depended on CollectionConcurrencyKit, which Core's zero-dependency charter forbids. +The fix was to rewrite the helper over `withTaskGroup` (about ten lines) rather than admit the dependency, so `Domain/FuzzyMatching/` is now dependency-free. +**Dependency honesty beats tidiness**: never add a dependency to a foundation package to make a move possible. +Rewrite the helper, or mirror it locally, instead. ## Folder conventions (app target) Grouping is **purpose-first**: -- Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never - by kind. The app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` - folders. -- **`CEEditor` and `CESourceControl` are the worked examples** (2026-08-16). `CEEditor`'s - `Models/`, `Views/` and `UseCases/` became nine groups named for what their files are about: - `Editor/`, `Layout/`, `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, - `Documents/`, `Restoration/`, `Theme/`, `Adapters/`. `CESourceControl`'s `Views/` grab-bag split - into `Operations/` and `Branches/`, its cloner joined `Clone/`, and its settings types moved to - `Settings/`. Both were pure renames (55 and 19 files, zero content changes), because Swift - ignores directory layout and SwiftPM takes the whole target tree. -- **`UseCases/` is now gone from every package.** The type-level rename to doers - (`EditorRestorer`, `RepositoryCloner`) had stopped at the folder level; it no longer does. -- **`CENotifications` followed** (13 files): `Models/`, `Protocols/`, `ViewModels/` and `Views/` - held 1, 1, 4 and 3 files; a single `Panel/` group now holds the view model and every view that - observes it, and the rest sits flat at the root. -- **`CodeEditCore` followed**, with one deliberate exception. Its `Extensions/` became `Paths/` - (the four `URL` helpers, `String+ValidFileName`, and `String+Escaped`, whose escaping exists to - make paths safe as shell arguments), with the two genuinely unrelated helpers at the target root. - `Event`/`EventBus` joined `Events/`, and `FindReplaceQuery` moved to `Domain/`, being a query - model shared by `CEEditor` and `CESearch` rather than a seam. - - **`Domain/` and `Infrastructure/` stay.** A layer split is normally kind-grouping, but in this - target the layer *is* the purpose: `GitBranch` is a fact features share, `WorkspaceNavigator` is - a seam they talk through, and this guide already describes the target in exactly those terms. +- Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never by kind. + The app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` folders. +- **`CEEditor` and `CESourceControl` are the worked examples** (2026-08-16). + `CEEditor`'s `Models/`, `Views/` and `UseCases/` became nine groups named for what their files are about: `Editor/`, `Layout/`, `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, `Documents/`, `Restoration/`, `Theme/`, `Adapters/`. + `CESourceControl`'s `Views/` grab-bag split into `Operations/` and `Branches/`, its cloner joined `Clone/`, and its settings types moved to `Settings/`. + Both were pure renames (55 and 19 files, zero content changes), because Swift ignores directory layout and SwiftPM takes the whole target tree. +- **`UseCases/` is now gone from every package.** The type-level rename to doers (`EditorRestorer`, `RepositoryCloner`) had stopped at the folder level; it no longer does. +- **`CENotifications` followed** (13 files): `Models/`, `Protocols/`, `ViewModels/` and `Views/` held 1, 1, 4 and 3 files; a single `Panel/` group now holds the view model and every view that observes it, and the rest sits flat at the root. +- **`CodeEditCore` followed**, with one deliberate exception. + Its `Extensions/` became `Paths/` (the four `URL` helpers, `String+ValidFileName`, and `String+Escaped`, whose escaping exists to make paths safe as shell arguments), with the two genuinely unrelated helpers at the target root. + `Event`/`EventBus` joined `Events/`, and `FindReplaceQuery` moved to `Domain/`, being a query model shared by `CEEditor` and `CESearch` rather than a seam. + + **`Domain/` and `Infrastructure/` stay.** A layer split is normally kind-grouping, but in this target the layer *is* the purpose: `GitBranch` is a fact features share, `WorkspaceNavigator` is a seam they talk through, and this guide already describes the target in exactly those terms. Do not "fix" this one. -- **`CETerminal` followed**: `Shell/` (configuration) is the one subgroup that earned a folder, - while the three-level `CETerminalView` inheritance chain and its representable stay together, because - splitting them would separate a base class from its subclasses. -- **`CELSP` followed**: `Utils/` split in two: the semantic-token helpers joined - `Features/SemanticTokens/`, and the three that cross the protocol boundary became - `Conversions/`. `Registry/`'s `Model/` and `Protocols/` dissolved into its root, where - `PackageManagerProtocol` already sat. -- **`CodeEditUI` is the second stated exception, and was deliberately left grouped by kind.** - Grouping by kind is wrong *inside a feature*; this target is a component library with no feature - semantics by charter, so there is no domain to group by and `Styles/`, `Views/` and - `EnvironmentKeys/` are the subject, being the terms SwiftUI itself is documented in. Consumers browse - it asking "is there a button style for this?". Imposing subjects would yield several two-file - folders; `SplitView/` remains the one genuine subsystem. Two files that were not styles moved out - of `Styles/`, and `MenuWithButtonStyle` (a `View`, not a `MenuStyle`) became `ButtonStyledMenu`. -- **All 12 library targets have been reviewed** (2026-08-16/20). Six were regrouped; two are stated - exceptions (`CodeEditCore`, `CodeEditUI`); `CodeEditDocument` (5 files) and - `CEWorkspaceFileManager` (7) are correctly flat and need nothing; `ShellClient` is one file by - design. Two are blocked on decisions rather than effort: `CodeEditSettings` on its naming - question, `CESearch` on its rebuild. -- **8 kind-grouped folders remain**, but only two are work: `CESearch`'s `Model/` and - `Extensions/`, pending its rebuild. Three are the excluded `CESourceControl/Accounts/`, - `CodeEditUI/Views` is the exception above, `CELSP/Service` is named after `LSPService` rather - than being a layer, and `CodeEditSettings/Models` waits on that target's naming question. Follow the convention - in new code; those are a pending cleanup, not a counter-precedent. **`CESourceControl/Accounts/` - is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 - files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own - subtree. - - Note `Extensions/` counts too (`CodeEditCore`, `CESearch`, `CETerminal`): a folder of "things - that are extensions" says nothing about what they extend. - - Measure with this exact pattern, and widen it rather than trusting a smaller number. Four - successive counts here were wrong because the pattern matched `Models` but not `Model`, then not - `Protocols`, then not `Extensions` or singular `Service`: +- **`CETerminal` followed**: `Shell/` (configuration) is the one subgroup that earned a folder, while the three-level `CETerminalView` inheritance chain and its representable stay together, because splitting them would separate a base class from its subclasses. +- **`CELSP` followed**: `Utils/` split in two: the semantic-token helpers joined `Features/SemanticTokens/`, and the three that cross the protocol boundary became `Conversions/`. + `Registry/`'s `Model/` and `Protocols/` dissolved into its root, where `PackageManagerProtocol` already sat. +- **`CodeEditUI` is the second stated exception, and was deliberately left grouped by kind.** Grouping by kind is wrong *inside a feature*; this target is a component library with no feature semantics by charter, so there is no domain to group by and `Styles/`, `Views/` and `EnvironmentKeys/` are the subject, being the terms SwiftUI itself is documented in. + Consumers browse it asking "is there a button style for this?". + Imposing subjects would yield several two-file folders; `SplitView/` remains the one genuine subsystem. + Two files that were not styles moved out of `Styles/`, and `MenuWithButtonStyle` (a `View`, not a `MenuStyle`) became `ButtonStyledMenu`. +- **All 12 library targets have been reviewed** (2026-08-16/20). + Six were regrouped; two are stated exceptions (`CodeEditCore`, `CodeEditUI`); `CodeEditDocument` (5 files) and `CEWorkspaceFileManager` (7) are correctly flat and need nothing; `ShellClient` is one file by design. + Two are blocked on decisions rather than effort: `CodeEditSettings` on its naming question, `CESearch` on its rebuild. +- **8 kind-grouped folders remain**, but only two are work: `CESearch`'s `Model/` and `Extensions/`, pending its rebuild. + Three are the excluded `CESourceControl/Accounts/`, `CodeEditUI/Views` is the exception above, `CELSP/Service` is named after `LSPService` rather than being a layer, and `CodeEditSettings/Models` waits on that target's naming question. + Follow the convention in new code; those are a pending cleanup, not a counter-precedent. + **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own subtree. + + Note `Extensions/` counts too (`CodeEditCore`, `CESearch`, `CETerminal`): a folder of "things that are extensions" says nothing about what they extend. + + Measure with this exact pattern, and widen it rather than trusting a smaller number. + Four successive counts here were wrong because the pattern matched `Models` but not `Model`, then not `Protocols`, then not `Extensions` or singular `Service`: ```bash find CodeEditModules/Sources -type d \ @@ -337,42 +252,35 @@ Grouping is **purpose-first**: -o -name ViewModels -o -name Service -o -name Services -o -name Protocol \ -o -name Protocols -o -name UseCase -o -name UseCases -o -name Extensions \) | wc -l ``` -- **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol - it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and - environment keys are distributed to their subject rather than gathered into an `Environment/` - group, which would be grouping by kind again. +- **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and environment keys are distributed to their subject rather than gathered into an `Environment/` group, which would be grouping by kind again. - A feature with roughly ten files or fewer stays flat. - Shell/entry views and the feature's primary models sit at the feature root. - Single-consumer helpers live next to their consumer. -- `Utils/` is closed. Every file in it carries a justification (an app-wide platform patch, a - helper genuinely shared by multiple features with no better home). "It's generic" is not a - justification. +- `Utils/` is closed. + Every file in it carries a justification (an app-wide platform patch, a helper genuinely shared by multiple features with no better home). + "It's generic" is not a justification. ## Communication rules - **Feature packages never import each other.** Cross-feature signaling is typed: - **Facts** (something happened) → an event on the `EventBus` in CodeEditCore. - - **Requests** (do something, exactly one rightful handler) → a command interface in - CodeEditCore, implemented by an app-side adapter. -- No custom `Notification.Name`s. `NotificationCenter` is only used to observe platform - notifications (NSWindow, NSApplication, NSMenu). -- **No DI container.** `AppDependencies` is the app-scope composition root; objects receive - dependencies through initializers, SwiftUI views through environment keys (`appServices(_:)`). - Only composition roots may hold the whole `AppDependencies`. A container (ask for a type, get - an instance) was removed deliberately: it hides who owns a thing and how long it lives, which - is the question this section exists to answer. -- **Scope determines owner; nothing scoped is reached ambiently.** See - [Scopes and ownership](#scopes-and-ownership) below. A `static shared` is legitimate only where - the platform constructs the object and no initialiser parameter is available. Today that is - `CodeFileDocument`, which is why `delegateProvider` is a static closure set at launch. Eight - other singletons remain, listed there as known exceptions. -- No SwiftUI view observes a service directly. Services expose a concrete view-state object - (the presentation-state split), and views issue commands through protocol-typed environment - keys. + - **Requests** (do something, exactly one rightful handler) → a command interface in CodeEditCore, implemented by an app-side adapter. +- No custom `Notification.Name`s. + `NotificationCenter` is only used to observe platform notifications (NSWindow, NSApplication, NSMenu). +- **No DI container.** `AppDependencies` is the app-scope composition root; objects receive dependencies through initializers, SwiftUI views through environment keys (`appServices(_:)`). + Only composition roots may hold the whole `AppDependencies`. + A container (ask for a type, get an instance) was removed deliberately: it hides who owns a thing and how long it lives, which is the question this section exists to answer. +- **Scope determines owner; nothing scoped is reached ambiently.** See [Scopes and ownership](#scopes-and-ownership) below. + A `static shared` is legitimate only where the platform constructs the object and no initialiser parameter is available. + Today that is `CodeFileDocument`, which is why `delegateProvider` is a static closure set at launch. + Eight other singletons remain, listed there as known exceptions. +- No SwiftUI view observes a service directly. + Services expose a concrete view-state object (the presentation-state split), and views issue commands through protocol-typed environment keys. ### Scopes and ownership -Four lifetimes exist. Each has an owner, and a type belongs to the narrowest one that fits. +Four lifetimes exist. +Each has an owner, and a type belongs to the narrowest one that fits. | Scope | Owner | Examples | | --- | --- | --- | @@ -381,23 +289,22 @@ Four lifetimes exist. Each has an owner, and a type belongs to the narrowest one | Window | `CodeEditWindowController` | `utilityAreaModel`, `statusBarViewModel`, `notificationPanel`, `openQuicklyViewModel` | | Document | `CodeFileDocument` | per-file editing state | -Window scope is not a subdivision of workspace scope: two windows may show one project, and their -utility areas, status bars and palettes must not be shared. That is why window-UI state lives on -`CodeEditWindowController` and not on `Workspace`. +Window scope is not a subdivision of workspace scope: two windows may show one project, and their utility areas, status bars and palettes must not be shared. +That is why window-UI state lives on `CodeEditWindowController` and not on `Workspace`. -Document scope is where the platform pushes back. `NSDocument` subclasses are created by the -document architecture, not by us, so no initialiser parameter is available, hence -`CodeFileDocument.delegateProvider`, a static closure set at launch. That is the shape of a -legitimate exception: the platform owns construction. +Document scope is where the platform pushes back. +`NSDocument` subclasses are created by the document architecture, not by us, so no initialiser parameter is available, hence `CodeFileDocument.delegateProvider`, a static closure set at launch. +That is the shape of a legitimate exception: the platform owns construction. -**Note on idiom:** "no singletons" is not the goal and never was. Apple's own frameworks are full of -them (`NSApplication.shared`, `FileManager.default`, `NSDocumentController.shared`). What was -removed was a *container*. A `shared` is a problem here only when it gives ambient access to -something whose lifetime is narrower than the process, or when it hides an owner that could hold it. +**Note on idiom:** "no singletons" is not the goal and never was. +Apple's own frameworks are full of them (`NSApplication.shared`, `FileManager.default`, `NSDocumentController.shared`). +What was removed was a *container*. +A `shared` is a problem here only when it gives ambient access to something whose lifetime is narrower than the process, or when it hides an owner that could hold it. #### Known exceptions (2026-08-16) -Eight singletons remain. None is load-bearing; each is listed with the scope it actually has. +Eight singletons remain. +None is load-bearing; each is listed with the scope it actually has. | Singleton | True scope | Note | | --- | --- | --- | @@ -410,96 +317,59 @@ Eight singletons remain. None is load-bearing; each is listed with the scope it | `EditorStateRestoration` | process | a GRDB database; `nonisolated(unsafe)` and optional | | `TerminalCache` | **workspace** | see below | -The first seven are process-scoped services that simply have not moved to `AppDependencies`; doing -so is mechanical and changes no behaviour. +The first seven are process-scoped services that simply have not moved to `AppDependencies`; doing so is mechanical and changes no behaviour. -`TerminalCache` is different, and is the one worth fixing rather than relocating. It is a -process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. -Eviction is per-terminal only, and nothing clears it when a workspace or window closes, so two open -projects share one bag of live terminal views. It works because UUIDs do not collide, but the -lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. +`TerminalCache` is different, and is the one worth fixing rather than relocating. +It is a process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. +Eviction is per-terminal only, and nothing clears it when a workspace or window closes, so two open projects share one bag of live terminal views. +It works because UUIDs do not collide, but the lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. ## Panel tab contributions -The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, -`InspectorTab`, `UtilityAreaTab`). Each panel is a list of `WorkspacePanelContribution` values, so -a tab is a value, not a case, assembled by one function per panel in -`CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`: first-party entries named -directly, conditional ones (`InternalDevelopmentInspectorContribution`) as a plain `if`, then -extension-provided ones appended through a single adapter call. The panel that renders the list -cannot tell a first-party tab from an extension's. That indistinguishability is the point; it is -what lets a new contribution source arrive without the panel code changing. - -`WorkspacePanelContribution` lives in `CodeEditUI` -(`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`), not `CodeEditCore`: a -contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forbids UI imports -outright. `CodeEditUI`'s own charter (rule 2) is satisfied too: the protocol needs SwiftUI and -nothing else. - -A contribution vends two views. `content` is the tab itself. `bottomView` is an optional bar pinned -below it. The navigator's filter field and sort controls are the existing examples. It defaults to -`nil` so a tab with no such bar says nothing about it. It is a *requirement* rather than something -the panel is handed deliberately: it arrived from upstream as a `switch` over the retired tab enum, -where every new tab had to remember to add its case, and a package could not contribute one at all -because the switch lived app-side. Vended by the contribution, a tab cannot forget, and where the bar -is placed stays the panel's business: pre-Tahoe it insets the tab's own content, and from macOS 26 it -spans the panel below the tab bar. - -**A panel's tab list changes at runtime**, so a stored selection can outlive the tab it names: the -inspector rebuilds its list when a setting changes, and any panel's list changes when an extension is -enabled or disabled. `Collection.reconcilingSelection(_:)` (alongside the protocol) keeps a selection -that still resolves and otherwise falls back to the first tab; `WorkspacePanelView` applies it on -appear and on every change to the list. Without it the panel reads "No Selection" until the user -clicks something, because the stored id is stale rather than absent and nothing recovers on its own. -The rule lives in the package rather than the view so it holds for every panel and can be tested -without one. - -A contribution's owner follows the same placement rule as everything else in -[Where does my code go?](#where-does-my-code-go): a feature that owns a tab vends its own -contribution from its package, reading whatever it needs (including settings, through the seam) -directly rather than having the app assemble it. `CESearch`'s `FindNavigatorContribution` -(`CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift`) replaced an app-side -`FindNavigatorTab` wrapper that existed only to shuttle settings values down. Once the feature -could read its own settings, the wrapper had no reason to exist. The tab's id is now owned by the -same package (`FindNavigatorContribution.tabID`); the app's `PanelTabID.search` references it -rather than duplicating the literal, so there is exactly one source of truth even though the app -still needs a compile-checked constant to select the tab by. - -`ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) -stays app-side, not because it is a tab (see the -[chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no -owning package to move to. The same holds for the file inspector, the internal-development -inspector, and the debug and output utility tabs. `TerminalUtilityContribution` is the one that -should move, to `CETerminal`; it has not because it is coupled to app-side utility-area chrome. +The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, `InspectorTab`, `UtilityAreaTab`). +Each panel is a list of `WorkspacePanelContribution` values, so a tab is a value, not a case, assembled by one function per panel in `CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`: first-party entries named directly, conditional ones (`InternalDevelopmentInspectorContribution`) as a plain `if`, then extension-provided ones appended through a single adapter call. +The panel that renders the list cannot tell a first-party tab from an extension's. +That indistinguishability is the point; it is what lets a new contribution source arrive without the panel code changing. + +`WorkspacePanelContribution` lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`), not `CodeEditCore`: a contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forbids UI imports outright. +`CodeEditUI`'s own charter (rule 2) is satisfied too: the protocol needs SwiftUI and nothing else. + +A contribution vends two views. +`content` is the tab itself. +`bottomView` is an optional bar pinned below it. +The navigator's filter field and sort controls are the existing examples. +It defaults to `nil` so a tab with no such bar says nothing about it. +It is a *requirement* rather than something the panel is handed deliberately: it arrived from upstream as a `switch` over the retired tab enum, where every new tab had to remember to add its case, and a package could not contribute one at all because the switch lived app-side. +Vended by the contribution, a tab cannot forget, and where the bar is placed stays the panel's business: pre-Tahoe it insets the tab's own content, and from macOS 26 it spans the panel below the tab bar. + +**A panel's tab list changes at runtime**, so a stored selection can outlive the tab it names: the inspector rebuilds its list when a setting changes, and any panel's list changes when an extension is enabled or disabled. +`Collection.reconcilingSelection(_:)` (alongside the protocol) keeps a selection that still resolves and otherwise falls back to the first tab; `WorkspacePanelView` applies it on appear and on every change to the list. +Without it the panel reads "No Selection" until the user clicks something, because the stored id is stale rather than absent and nothing recovers on its own. +The rule lives in the package rather than the view so it holds for every panel and can be tested without one. + +A contribution's owner follows the same placement rule as everything else in [Where does my code go?](#where-does-my-code-go): a feature that owns a tab vends its own contribution from its package, reading whatever it needs (including settings, through the seam) directly rather than having the app assemble it. +`CESearch`'s `FindNavigatorContribution` (`CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift`) replaced an app-side `FindNavigatorTab` wrapper that existed only to shuttle settings values down. +Once the feature could read its own settings, the wrapper had no reason to exist. +The tab's id is now owned by the same package (`FindNavigatorContribution.tabID`); the app's `PanelTabID.search` references it rather than duplicating the literal, so there is exactly one source of truth even though the app still needs a compile-checked constant to select the tab by. + +`ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) stays app-side, not because it is a tab (see the [chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no owning package to move to. +The same holds for the file inspector, the internal-development inspector, and the debug and output utility tabs. +`TerminalUtilityContribution` is the one that should move, to `CETerminal`; it has not because it is coupled to app-side utility-area chrome. `CESourceControl` is the second feature package to own a panel tab, after `CESearch`. -`SourceControlNavigatorContribution` and `GitHistoryInspectorContribution` -(`CodeEditModules/Sources/CESourceControl/SourceControlNavigator/` and `.../HistoryInspector/`) -vend `SourceControlNavigatorView` and `HistoryInspectorView` from the package; the app-side -`WorkspaceWindow/NavigatorArea/SourceControlNavigator/` and -`WorkspaceWindow/InspectorArea/HistoryInspector/` groups no longer exist. The relocation is the -shape a future one follows: whatever the view needs that the package cannot see becomes a -**required** initialiser parameter on the contribution (`WorkspaceNavigator` for the navigator -tab, `ActiveEditorState` for the inspector tab) rather than an environment key moved down with -it; both are non-optional so a missing injection at the call site -(`PanelContributions.swift`) is a compile error, not a silently no-op tab. Each contribution still -owns its own `tabID` constant, and the app's `PanelTabID.sourceControl` / `.gitHistory` reference -it rather than duplicating the literal, same as `PanelTabID.search`. A third relocation onto this -shape is expected. - -Extensions are the third contribution source. `ExtensionPanelContribution` -(`CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift`) is the **only** app -file, outside the pre-existing extension-management UI under `AuxiliaryWindows/Extensions/`, that -may name `AppExtensionIdentity` or `ResolvedSidebar`. Confining ExtensionKit's vocabulary to this -one adapter is what let the app-side panel-contribution list above be written without an -ExtensionKit import in sight, and is what would let a second, non-ExtensionKit contribution source -arrive later without touching the panels. +`SourceControlNavigatorContribution` and `GitHistoryInspectorContribution` (`CodeEditModules/Sources/CESourceControl/SourceControlNavigator/` and `.../HistoryInspector/`) vend `SourceControlNavigatorView` and `HistoryInspectorView` from the package; the app-side `WorkspaceWindow/NavigatorArea/SourceControlNavigator/` and `WorkspaceWindow/InspectorArea/HistoryInspector/` groups no longer exist. +The relocation is the shape a future one follows: whatever the view needs that the package cannot see becomes a **required** initialiser parameter on the contribution (`WorkspaceNavigator` for the navigator tab, `ActiveEditorState` for the inspector tab) rather than an environment key moved down with it; both are non-optional so a missing injection at the call site (`PanelContributions.swift`) is a compile error, not a silently no-op tab. +Each contribution still owns its own `tabID` constant, and the app's `PanelTabID.sourceControl` / `.gitHistory` reference it rather than duplicating the literal, same as `PanelTabID.search`. +A third relocation onto this shape is expected. + +Extensions are the third contribution source. +`ExtensionPanelContribution` (`CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift`) is the **only** app file, outside the pre-existing extension-management UI under `AuxiliaryWindows/Extensions/`, that may name `AppExtensionIdentity` or `ResolvedSidebar`. +Confining ExtensionKit's vocabulary to this one adapter is what let the app-side panel-contribution list above be written without an ExtensionKit import in sight, and is what would let a second, non-ExtensionKit contribution source arrive later without touching the panels. ## Reading and writing settings -Feature packages reach settings through the **settings seam** in `CodeEditSettings` -(`Store/SettingsValue.swift`, `Store/SettingsAccessing.swift`), never through a singleton and -never by naming the app-wide `SettingsData` aggregate. Three roles, pick by consumer kind: +Feature packages reach settings through the **settings seam** in `CodeEditSettings` (`Store/SettingsValue.swift`, `Store/SettingsAccessing.swift`), never through a singleton and never by naming the app-wide `SettingsData` aggregate. +Three roles, pick by consumer kind: | Consumer | Use | Why | | --- | --- | --- | @@ -507,44 +377,25 @@ never by naming the app-wide `SettingsData` aggregate. Three roles, pick by cons | Read-only object (managers, services) | `SettingsReading` by initializer | No environment outside a view; narrow protocol makes read-only visible at the call site. | | Object that also writes | `SettingsAccessing` by initializer | The read+write half; every `SettingsAccessing` satisfies `SettingsReading`. | -Access is **section-granular**: `value(_:)`/`setValue(_:)` deal in whole `SettingsSection` values, -so a caller changing one field reads its section, mutates it and writes it back. - -- **`@AppSettings` is app-target only.** It observes the same store as `SettingsValue`, but - addresses a section field through the app-wide `SettingsData` façade instead of naming one - section directly. There is no `Settings.shared` singleton any more, `PersistentSettingsStore` - (owned by `AppDependencies`) is the concrete store, injected like everything else. `@AppSettings` - is what 24 app-target files still use (40 declarations, re-counted 2026-08-16); feature packages must not use it, and - new app-target code should prefer the seam. -- **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's - content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies - reaches menu-bar code. `CodeEditCommands`/`ViewCommands` are handed `PersistentSettingsStore` by - initializer and `@ObservedObject` it, reads, writes and menu invalidation all stop depending on - undocumented behaviour. -- **The environment does not cross an `NSHostingView`/`NSHostingController` boundary.** A new - standalone hosting root must be wrapped in `SettingsInjector` (or `SettingsSceneInjector` for a - scene), or every `@SettingsValue` under it **traps**. That is deliberate: this replaced a pair of - environment keys whose failure modes were silent, a subtree given neither read plausible - defaults and discarded writes, and a subtree given the value but not the separate `Int` - invalidation key read correctly and never re-rendered. `@EnvironmentObject` makes both - unrepresentable, because SwiftUI subscribes to the store itself. Those two injectors are the only - places the store is injected, which is what makes the coverage question answerable by grep rather - than by reachability analysis. -- **A protocol double cannot be substituted into a view.** `@EnvironmentObject` cannot carry an - existential, so a view-level test injects a real `PersistentSettingsStore` on a temporary file. - The protocol seam still applies to every initializer-injected consumer, which is where - `RecordingSettingsStore` and `SnapshotSettingsReader` are used. -- **`DefaultSettingsReader` is not the environment's fallback any more** — there is no fallback. It - survives as the stand-in four singletons (`ThemeModel`, `FeedbackModel`, `SearchSettingsModel`, - `HistoryInspectorModel`) hold between construction and `configure(_:)`. It still - `assertionFailure`s outside previews, since reaching it means a real store never arrived. -- **`PersistentSettingsStore` is the concrete store.** Section-keyed storage, owned by - `AppDependencies` (no `shared`), driving the same throttled save pipeline `Settings.shared` used - to own. Sections nothing here decodes are held verbatim and re-emitted on save, so a disabled - extension's configuration survives. The same holds for a section that is present but - *undecodable*: it reads as defaults but is re-emitted unchanged, and the one write that would - replace it is announced through `SettingsStore.willReplaceUndecodableSection` so the file is - copied to `settings.json.corrupt-` first. +Access is **section-granular**: `value(_:)`/`setValue(_:)` deal in whole `SettingsSection` values, so a caller changing one field reads its section, mutates it and writes it back. + +- **`@AppSettings` is app-target only.** It observes the same store as `SettingsValue`, but addresses a section field through the app-wide `SettingsData` façade instead of naming one section directly. + There is no `Settings.shared` singleton any more, `PersistentSettingsStore` (owned by `AppDependencies`) is the concrete store, injected like everything else. + `@AppSettings` is what 24 app-target files still use (40 declarations, re-counted 2026-08-16); feature packages must not use it, and new app-target code should prefer the seam. +- **Neither wrapper works in a `Commands` conformer.** `.commands { }` attaches beside a scene's content, not inside it, so nothing guarantees the environment `SettingsSceneInjector` supplies reaches menu-bar code. + `CodeEditCommands`/`ViewCommands` are handed `PersistentSettingsStore` by initializer and `@ObservedObject` it, reads, writes and menu invalidation all stop depending on undocumented behaviour. +- **The environment does not cross an `NSHostingView`/`NSHostingController` boundary.** A new standalone hosting root must be wrapped in `SettingsInjector` (or `SettingsSceneInjector` for a scene), or every `@SettingsValue` under it **traps**. + That is deliberate: this replaced a pair of environment keys whose failure modes were silent, a subtree given neither read plausible defaults and discarded writes, and a subtree given the value but not the separate `Int` invalidation key read correctly and never re-rendered. + `@EnvironmentObject` makes both unrepresentable, because SwiftUI subscribes to the store itself. + Those two injectors are the only places the store is injected, which is what makes the coverage question answerable by grep rather than by reachability analysis. +- **A protocol double cannot be substituted into a view.** `@EnvironmentObject` cannot carry an existential, so a view-level test injects a real `PersistentSettingsStore` on a temporary file. + The protocol seam still applies to every initializer-injected consumer, which is where `RecordingSettingsStore` and `SnapshotSettingsReader` are used. +- **`DefaultSettingsReader` is not the environment's fallback any more** — there is no fallback. + It survives as the stand-in four singletons (`ThemeModel`, `FeedbackModel`, `SearchSettingsModel`, `HistoryInspectorModel`) hold between construction and `configure(_:)`. + It still `assertionFailure`s outside previews, since reaching it means a real store never arrived. +- **`PersistentSettingsStore` is the concrete store.** Section-keyed storage, owned by `AppDependencies` (no `shared`), driving the same throttled save pipeline `Settings.shared` used to own. + Sections nothing here decodes are held verbatim and re-emitted on save, so a disabled extension's configuration survives. + The same holds for a section that is present but *undecodable*: it reads as defaults but is re-emitted unchanged, and the one write that would replace it is announced through `SettingsStore.willReplaceUndecodableSection` so the file is copied to `settings.json.corrupt-` first. ### Where a settings section lives @@ -555,30 +406,20 @@ A section lives with **its owner**: | Exactly one feature package | that package | `TerminalSettings` → `CETerminal`, `LanguageServerSettings` → `CELSP`, `SourceControlSettings`/`AccountsSettings` → `CESourceControl` | | More than one module, or only the app | `CodeEditSettings` | `TextEditing`, `Theme`, `General`, `Navigation`, `Developer`, `Search`, `Keybindings` | -**Never `CodeEditCore` or `CodeEditUI`.** A `Codable` config bag has no natural boundary and -accretes. That is precisely how Core became `AppPreferences` the first time, and Core's purity -rationale is that the placement question stays answerable. `CodeEditUI` is excluded mechanically: -it may not depend on a local target, so it cannot see settings types at all. +**Never `CodeEditCore` or `CodeEditUI`.** A `Codable` config bag has no natural boundary and accretes. +That is precisely how Core became `AppPreferences` the first time, and Core's purity rationale is that the placement question stays answerable. +`CodeEditUI` is excluded mechanically: it may not depend on a local target, so it cannot see settings types at all. -**Migration trigger:** when a section's readers collapse to a single feature, it moves with that -feature. That is how the four package-owned sections got where they are. +**Migration trigger:** when a section's readers collapse to a single feature, it moves with that feature. +That is how the four package-owned sections got where they are. -App-only sections (`SearchSettings`, `KeybindingsSettings`) stay in `CodeEditSettings` rather than -moving app-side: `SettingsFormatTests` guards the on-disk format for every section in one place -using `Bundle.module` fixtures, and splitting two sections into the app target would split that -guard across two bundle mechanisms to satisfy a boundary nothing enforces. +App-only sections (`SearchSettings`, `KeybindingsSettings`) stay in `CodeEditSettings` rather than moving app-side: `SettingsFormatTests` guards the on-disk format for every section in one place using `Bundle.module` fixtures, and splitting two sections into the app target would split that guard across two bundle mechanisms to satisfy a boundary nothing enforces. -Two field-level misplacements are **recorded but not fixed**, because both keys live in users' -`settings.json` and moving a field is a data migration rather than a refactor: -`GeneralSettings.findNavigatorDetail` is read by `CESearch` (a feature-specific field in a shared -section), and `SearchSettings.ignoreGlobPatterns` is wired to its settings page and persisted but -never read by `CESearch`: the control works and has no effect, which is worse than dead code -because nothing looks unused. +Two field-level misplacements are **recorded but not fixed**, because both keys live in users' `settings.json` and moving a field is a data migration rather than a refactor: `GeneralSettings.findNavigatorDetail` is read by `CESearch` (a feature-specific field in a shared section), and `SearchSettings.ignoreGlobPatterns` is wired to its settings page and persisted but never read by `CESearch`: the control works and has no effect, which is worse than dead code because nothing looks unused. ## Creating a new feature target -1. Create the folder `CodeEditModules/Sources/CE/` and add a target and product for it in - `CodeEditModules/Package.swift`: +1. Create the folder `CodeEditModules/Sources/CE/` and add a target and product for it in `CodeEditModules/Package.swift`: ```swift .library(name: "CE", targets: ["CE"]), @@ -594,31 +435,22 @@ because nothing looks unused. ), ``` -2. Link the product to the app: CodeEdit target → *General* → *Frameworks, Libraries, and - Embedded Content* → add `CE`. -3. Remember the target builds with **Swift 6 strict concurrency**, types crossing actor - boundaries need `Sendable`, and UI-bound classes are usually `@MainActor`. (`CEEditor` is the - sole exception, see [Rules](#rules).) -4. Known quirk: targets that depend on `CodeEditSymbols` build via Xcode/xcodebuild only; - standalone `swift build` fails on its `Bundle.module` resolution. -5. Declare **every** module you import in the manifest. The workspace's shared build directory - makes undeclared imports of sibling targets compile by accident, CI will catch it - (see below). +2. Link the product to the app: CodeEdit target → *General* → *Frameworks, Libraries, and Embedded Content* → add `CE`. +3. Remember the target builds with **Swift 6 strict concurrency**, types crossing actor boundaries need `Sendable`, and UI-bound classes are usually `@MainActor`. + (`CEEditor` is the sole exception, see [Rules](#rules).) +4. Known quirk: targets that depend on `CodeEditSymbols` build via Xcode/xcodebuild only; standalone `swift build` fails on its `Bundle.module` resolution. +5. Declare **every** module you import in the manifest. + The workspace's shared build directory makes undeclared imports of sibling targets compile by accident, CI will catch it (see below). ## Enforcement Two tools enforce the three [Rules](#rules) above; both run on every PR: -- **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`), includes custom rules - that reject UI imports in CodeEditCore and model/feature imports in CodeEditUI. These fire - inside Xcode while you type. -- **The package audit** (`.github/scripts/audit_package_imports.py`), verifies every `import` - in every target is declared in that target's manifest dependencies, and that the three - [Rules](#rules) plus the language-mode assertion hold. It exists because Xcode workspace - builds share one build directory, so an undeclared import of a sibling target compiles fine - locally. For the 7 targets that transitively need `CodeEditSymbols` there is no standalone - `swift build` to fall back on either (see the quirk above), so for most of the graph this - audit is the only thing standing between a leaky import and `main`. +- **SwiftLint** (`swiftlint --strict`, config in `.swiftlint.yml`), includes custom rules that reject UI imports in CodeEditCore and model/feature imports in CodeEditUI. + These fire inside Xcode while you type. +- **The package audit** (`.github/scripts/audit_package_imports.py`), verifies every `import` in every target is declared in that target's manifest dependencies, and that the three [Rules](#rules) plus the language-mode assertion hold. + It exists because Xcode workspace builds share one build directory, so an undeclared import of a sibling target compiles fine locally. + For the 7 targets that transitively need `CodeEditSymbols` there is no standalone `swift build` to fall back on either (see the quirk above), so for most of the graph this audit is the only thing standing between a leaky import and `main`. Run both locally from the repo root: @@ -627,30 +459,23 @@ swiftlint lint --strict --quiet python3 .github/scripts/audit_package_imports.py ``` -`--strict` matters: without it SwiftLint reports violations as warnings and exits 0, so a local run -looks clean and CI fails on the same tree. +`--strict` matters: without it SwiftLint reports violations as warnings and exits 0, so a local run looks clean and CI fails on the same tree. ### Known weakness in the CodeEditUI charter (2026-08-05) -Both checks constrain **local** targets only. `ui_package_purity` lists sibling module names in -a regex, and the audit script intersects the target's declared dependencies with the package's -library targets. So `CodeEditUI` is barred from importing -`CodeEditCore`, a zero-dependency, pure-types package, while nothing stops it taking an -arbitrary *external* dependency, up to and including a tree-sitter grammar bundle. The rule as -written is narrower than its own stated intent ("presentation atoms must not know about models -or features") in one direction and far wider in the other. +Both checks constrain **local** targets only. +`ui_package_purity` lists sibling module names in a regex, and the audit script intersects the target's declared dependencies with the package's library targets. +So `CodeEditUI` is barred from importing `CodeEditCore`, a zero-dependency, pure-types package, while nothing stops it taking an arbitrary *external* dependency, up to and including a tree-sitter grammar bundle. +The rule as written is narrower than its own stated intent ("presentation atoms must not know about models or features") in one direction and far wider in the other. This surfaced while deduplicating `FileIcon`, which is presentation keyed by a file's identity. -It was designed to take a `URL` rather than a domain type, so it needs neither `CodeEditCore` -nor the loophole, and its three custom colorsets moved into the package as resources, making -`CodeEditUI` self-contained and letting its tests assert colours without an app host. Treat the -asymmetry as a known weakness, not a licence: adding an external dependency to `CodeEditUI` to -sidestep the local-package rule would satisfy the letter of the charter and defeat its purpose. +It was designed to take a `URL` rather than a domain type, so it needs neither `CodeEditCore` nor the loophole, and its three custom colorsets moved into the package as resources, making `CodeEditUI` self-contained and letting its tests assert colours without an app host. +Treat the asymmetry as a known weakness, not a licence: adding an external dependency to `CodeEditUI` to sidestep the local-package rule would satisfy the letter of the charter and defeat its purpose. ## Glossary -Several words are overloaded in this codebase. These are the intended meanings; prefer the -qualified term whenever the bare one could be read two ways. +Several words are overloaded in this codebase. +These are the intended meanings; prefer the qualified term whenever the bare one could be read two ways. | Term | Means | | --- | --- | From 716209f6a405dc04be8a49c8d486c98f5ead39dc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Fri, 21 Aug 2026 14:55:45 +0200 Subject: [PATCH 324/335] Docs: Move the architecture guide to the root and split out the decision log The repo had two files called ARCHITECTURE.md. The root one, an untracked Architecture Vision file excluded via .git/info/exclude, was last touched on 26 July and described the tiered Packages/ layout that the consolidation replaced. The tracked docs/ARCHITECTURE.md was the maintained one. Reading the stale file while I edited the other is what surfaced this. The guide now sits at the root, where a newcomer looks alongside README and CONTRIBUTING, and is tracked normally with the exclude entry removed. Three sections were merged from the vision file first, each verified against the code: the design principles (point 8 rewritten, since it described the retired Packages/ tiers), state ownership (reworded to stop overloading the word service, because git status, tasks and LSP sessions live in feature targets rather than service targets), and the rejected-options list. Two claims in that list were wrong and are corrected. The @Observable entry justified itself with a macOS 13 minimum, but the declared target is 14 in both the pbxproj and the manifest, so the blocker is team agreement rather than code. The Combine entry claimed the EventBus could swap to AsyncStream without touching call sites; subscribe(_:) returns AnyPublisher and all five subscribers use sink and AnyCancellable, so it would rewrite every one. The guide dropped from 557 to 435 lines by moving decision records to docs/architecture-decisions.md: the four target-separation rulings, what the I/O norm prevents, the panel seam detail, the Core charter evidence, and the singletons inventory. What stayed is the rule plus its reasoning. Panel contributions shrank from 41 lines to the reusable pattern, since it is the first of several seams and logging each in full would bloat the guide. Folder conventions lost its per-target history, which is the only content deleted rather than moved: 170 words. Also removes the em dash separator from CONTRIBUTING.md prose, per the writing style rule. --- docs/ARCHITECTURE.md => ARCHITECTURE.md | 257 +++++++++--------------- CONTRIBUTING.md | 4 +- CodeEditModules/Package.swift | 4 +- docs/architecture-decisions.md | 132 ++++++++++++ 4 files changed, 236 insertions(+), 161 deletions(-) rename docs/ARCHITECTURE.md => ARCHITECTURE.md (60%) create mode 100644 docs/architecture-decisions.md diff --git a/docs/ARCHITECTURE.md b/ARCHITECTURE.md similarity index 60% rename from docs/ARCHITECTURE.md rename to ARCHITECTURE.md index 8ef37b5659..1174262253 100644 --- a/docs/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -37,6 +37,70 @@ Apply `CE` only where the bare name would collide with a stdlib/SwiftUI/AppKit/v Every target builds with Swift 6 strict concurrency **except `CEEditor`**, which declares `.swiftLanguageMode(.v5)` and is the sole exception. The app target is still Swift 5, write new app-side code Swift-6-ready, and don't add `@MainActor` to app types whose callers aren't isolated (it cascades). +## Design principles + +Verified against the codebase on 2026-08-21. +Each is a claim about how the code is arranged today, not an aspiration. + +1. **Dependencies point one way.** + App shell, then features, then services, then foundation, with dependency-free `CodeEditCore` at the base. + Acyclic, every edge pointing at something more fundamental, and no peer edges between features: there are currently zero feature-to-feature imports. + Enforced by package boundaries, not convention. +2. **Every boundary is a protocol.** + Anything performing I/O or reached across a feature boundary is protocol-backed and substitutable in tests: `ShellClientProtocol`, `GitClientProtocol`, `LSPServiceProtocol`, `RegistryManaging`, `KeybindingManaging`, `NotificationManaging`, `WorkspaceWindowManaging`, `SettingsAccessing`. +3. **Features are islands.** + No feature imports another. + Cross-feature interaction happens only through events, command interfaces, and shared substrate. +4. **State has one owner.** + See [State ownership](#state-ownership) below. +5. **Facts are broadcast; commands have one handler.** + The mechanism follows the intent, see [Communication rules](#communication-rules). +6. **Native first, framework-light.** + SwiftUI and AppKit plus one local package. + No meta-frameworks: there is no Composable Architecture or equivalent anywhere in the tree. + The architecture is conventions plus compile-time boundaries. +7. **Testability is the acceptance test.** + If a feature cannot be tested without building the whole app graph, the architecture has failed at that spot. +8. **Idiomatic by default.** + Follow standard Swift and Xcode conventions rather than inventing project-specific layouts. + Local code lives in one multi-target package, `CodeEditModules`, whose whole dependency graph is legible in a single manifest. + The Swift API Design Guidelines and the repo's SwiftLint rules govern code. + When a choice is unclear, prefer the community-idiomatic option over a bespoke one. + +### State ownership + +**Domain state lives in the object that owns the domain**: `SourceControlManager` for git status, `TaskManager` for running tasks, `LSPService` for language servers, `CEWorkspaceFileManager` for the file tree. +Note that most of those live in the *feature* that owns them (`CESourceControl`, `CETerminal`, `CELSP`), not in a service target; only the file tree does. +Such an object never holds UI state: `SourceControlManager` does not know that sheets exist. + +**Presentation state lives in a view-state object**, which is the presentation-state split described under [Communication rules](#communication-rules). +Sheet and popover flags, selection, expansion, scroll targets. + +**Views own only ephemera**, via `@State`: hover, focus, in-progress text. + +Data flows down through observation, actions flow up through method calls and operation doers, and cross-feature effects travel only via events and command interfaces. + +### Deliberately not chosen (or not yet) + +- **The `@Observable` macro.** + Not adopted. + Every observable model is `ObservableObject` with `@Published` (48 conformances); the only two mentions of `@Observable` in the tree are TODO comments. + Note that the deployment target *is* macOS 14 in both `project.pbxproj` and `Package.swift`, so the blocker is team agreement on that minimum rather than the code, and adopting `@Observable` would make the 14+ floor irreversible. +- **Swapping the `EventBus` off Combine cheaply.** + It is not cheap, contrary to an earlier claim in this guide. + `subscribe(_:)` returns `AnyPublisher`, so Combine is in the bus's public signature rather than hidden behind it, and all five subscribers use `sink` and `AnyCancellable`. + An `AsyncStream` backend would change the return type and rewrite every call site. + The bus is still regarded as a stopgap, with the platform's typed notifications as its natural replacement, but treat that as a migration rather than a substitution. +- **DI frameworks.** + The explicit `AppDependencies` composition root is the whole mechanism, and there is no container anywhere in the tree. + Dependencies are visible in initialisers and resolution failures are compile errors. +- **Architecture frameworks such as TCA.** + The cost, meaning a learning curve for a community codebase, framework lock-in, and fighting AppKit interop, outweighs the benefit. + Conventions plus package boundaries achieve the same testability. +- **Per-feature interface micro-packages.** + Interface and implementation splits per feature are overkill at this scale. + `CodeEditCore` carries the interfaces, 12 files under `Infrastructure/`. + ## History: Why the 2022 module split failed The project already tried a single multi-target `CodeEditModules` package. @@ -85,13 +149,8 @@ Each one blocks a specific failure documented in [History](#history-why-the-2022 **No UI frameworks** keeps the placement question answerable. Without it, Core becomes the place everything shared goes, which is what `AppPreferences` was and one of the two documented causes of the 2022 collapse. - The friction this produces is usually the rule working. - Four worked examples already in this codebase: `FileIcon` is keyed on `URL` rather than a domain type, so it needs neither `CodeEditCore` nor a charter exception (see rule 2 below); `WorkspacePanelContribution` was shaped to need only SwiftUI, so it lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); fuzzy matching's concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` (below); and `ActiveTheme`, the active light/dark theme, observed by the editor and terminal, lives *in* Core, because `ObservableObject` comes from Combine rather than SwiftUI. - That last one is worth remembering: the charter forbids `SwiftUI`/`AppKit`/`Cocoa` specifically, so observation is available in Core and an "it needs to be observable, therefore it needs SwiftUI" argument is simply false. - - The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry `NSFont.Weight`, which forces them out of Core. - That is the rule flagging a presentation type inside a settings model, not the rule obstructing a reasonable design. - Storing the weight as a `Double` and converting at the presentation boundary would make both structs Core-eligible with no rule change. + The friction this produces is usually the rule working, not obstructing you. + Four worked examples, and the `NSFont.Weight` counter-example people cite, are in [docs/architecture-decisions.md](docs/architecture-decisions.md). 2. **`CodeEditUI` purity.** No local target dependencies; external `CodeEditSymbols` only. Blocks 2022's `CodeEditUI → Git`. This is why `FileIcon` is keyed on `URL` rather than on a domain type: a deliberate consequence, not an accident. @@ -116,47 +175,8 @@ The reason to keep it out anyway is concrete, not decorative. `CodeEditCoreTests` is five files with zero use of `FileManager`, `temporaryDirectory` or `Data(contentsOf:)`: Core's tests need no filesystem, no temp directories and no cleanup. And I/O already has a designated home, since rule 4 of [Where does my code go?](#where-does-my-code-go) sends services to their own target, which is what `CEWorkspaceFileManager` and `ShellClient` are. -**What the norm actually prevents** (asked 2026-08-20): merging `CEWorkspaceFileManager` into Core. -That target holds 50 `FileManager` calls and a complete FSEvents implementation, `FSEventStreamCreate` with a C callback, its own dispatch queue, and start/stop/invalidate/release. -Without this norm the merge looks reasonable, because that target depends on nothing but Core and folding it in removes a target. -With the norm it is obviously wrong: it would put a live filesystem event stream inside the dependency sink all twelve targets rest on, and end Core's filesystem-free tests the same day. -State the norm with this example, not on principle alone. - -**`CodeEditDocument` and `CELSP` both stay their own targets** (asked and settled 2026-08-20). -Neither is a leftover, and the two conclusions depend on each other. - -`CEEditor` and `CELSP` reference each other **zero times, in either direction**. -They are siblings. -What keeps them apart is `LanguageServicesProvider`, declared in `CodeEditDocument`, implemented by `CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key. -It even ships a `NoOpLanguageServicesProvider`, so the editor works with no language service at all. -So `CodeEditDocument` is not "the document type plus some bridging": **it is the contract that keeps two features independent.** `CodeFileDocument` imports AppKit, SwiftUI and the editor frameworks, so it cannot live in Core; two features need it, so it cannot live in either. -Its own target is forced, not chosen. - -`CELSP` is not part of the editor either. -Its consumers are the settings UI (installing servers), the utility area (reading logs) and app lifecycle (nothing in `CEEditor` imports it), and **28 of its 78 files are `Registry/`**: package managers, install steps and source parsers for Cargo, NPM, Pip, Go and GitHub. -That is downloading and installing language servers, not editing text. -Folding it into `CEEditor` would make a ~130-file target mixing the two. - -The abstraction is already sound where it counts: `LanguageServer`, `LSPContentCoordinator`, `SemanticTokenHighlightProvider` and `LanguageServerDocumentObjects` are all generic over `LanguageServerDocument`, a protocol requiring only `content`, `languageServerURI` and `getLanguage()`. -Only `LSPService` itself pins the generic to `CodeFileDocument`. -Decoupling that would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type, breaking its use as an existential for DI, all to delete a five-file target. -A bad trade. - -**`CEWorkspaceFileManager` stays its own target, and must not merge into Core** (asked and settled 2026-08-20). -The inversion is already in place: Core declares `WorkspaceFileProviding` and `WorkspaceFileObserver`, and `CEWorkspaceFileManager.swift:263` is `extension CEWorkspaceFileManager: WorkspaceFileProviding {}`. -Four `CEEditor` files depend on the *protocol* (`EditorRestorer`, `EditorJumpBarMenu`, `EditorLayout+StateRestoration`, the environment key) and **no package imports the implementation**. -Its 22 consumers are app-side, plus 6 test files. -Same shape as `ShellClient` below: contract in Core, adapter in its own target, app composes. -See the I/O norm above for why the merge is worse than it looks. - -**`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). -Size is the wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and `CELSP` (`GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers), and **none of them imports the implementation**. -Only the app target does, six files, composing it at the root. -The abstraction is load-bearing, not ceremonial. - -What the separate target buys, stated precisely: reaching for the implementation from a feature needs a **manifest edit**, visible in review, rather than an import line inside a file. -It is reviewability, not prevention: import honesty checks that imports are *declared*, so a feature that declared the dependency would pass the audit. -Folding it into the app target is a coherent alternative (the app is the only consumer, and composing platform adapters is a composition-root job); it would cost the manifest-level visibility and nothing else demonstrable. +See [docs/architecture-decisions.md](docs/architecture-decisions.md) for what this norm has actually prevented, and for why `ShellClient`, `CEWorkspaceFileManager`, `CodeEditDocument` and `CELSP` each stay separate targets. + **Known exception, recorded rather than pretended away:** `CEWorkspaceFile` exposes `static let fileManager = FileManager.default` and uses it for `isEmptyFolder` and `doesExist`. Those are filesystem reads from a domain type. @@ -208,57 +228,26 @@ The fix was to rewrite the helper over `withTaskGroup` (about ten lines) rather **Dependency honesty beats tidiness**: never add a dependency to a foundation package to make a move possible. Rewrite the helper, or mirror it locally, instead. -## Folder conventions (app target) - -Grouping is **purpose-first**: - -- Group by sub-feature (`ProjectNavigator/`, `History/`, `StatusBarItems/`, `Toolbar/`), never by kind. - The app target has no `Models/`, `Views/`, `ViewModels/`, `Services/` or `UseCases/` folders. -- **`CEEditor` and `CESourceControl` are the worked examples** (2026-08-16). - `CEEditor`'s `Models/`, `Views/` and `UseCases/` became nine groups named for what their files are about: `Editor/`, `Layout/`, `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, `Documents/`, `Restoration/`, `Theme/`, `Adapters/`. - `CESourceControl`'s `Views/` grab-bag split into `Operations/` and `Branches/`, its cloner joined `Clone/`, and its settings types moved to `Settings/`. - Both were pure renames (55 and 19 files, zero content changes), because Swift ignores directory layout and SwiftPM takes the whole target tree. -- **`UseCases/` is now gone from every package.** The type-level rename to doers (`EditorRestorer`, `RepositoryCloner`) had stopped at the folder level; it no longer does. -- **`CENotifications` followed** (13 files): `Models/`, `Protocols/`, `ViewModels/` and `Views/` held 1, 1, 4 and 3 files; a single `Panel/` group now holds the view model and every view that observes it, and the rest sits flat at the root. -- **`CodeEditCore` followed**, with one deliberate exception. - Its `Extensions/` became `Paths/` (the four `URL` helpers, `String+ValidFileName`, and `String+Escaped`, whose escaping exists to make paths safe as shell arguments), with the two genuinely unrelated helpers at the target root. - `Event`/`EventBus` joined `Events/`, and `FindReplaceQuery` moved to `Domain/`, being a query model shared by `CEEditor` and `CESearch` rather than a seam. - - **`Domain/` and `Infrastructure/` stay.** A layer split is normally kind-grouping, but in this target the layer *is* the purpose: `GitBranch` is a fact features share, `WorkspaceNavigator` is a seam they talk through, and this guide already describes the target in exactly those terms. - Do not "fix" this one. -- **`CETerminal` followed**: `Shell/` (configuration) is the one subgroup that earned a folder, while the three-level `CETerminalView` inheritance chain and its representable stay together, because splitting them would separate a base class from its subclasses. -- **`CELSP` followed**: `Utils/` split in two: the semantic-token helpers joined `Features/SemanticTokens/`, and the three that cross the protocol boundary became `Conversions/`. - `Registry/`'s `Model/` and `Protocols/` dissolved into its root, where `PackageManagerProtocol` already sat. -- **`CodeEditUI` is the second stated exception, and was deliberately left grouped by kind.** Grouping by kind is wrong *inside a feature*; this target is a component library with no feature semantics by charter, so there is no domain to group by and `Styles/`, `Views/` and `EnvironmentKeys/` are the subject, being the terms SwiftUI itself is documented in. - Consumers browse it asking "is there a button style for this?". - Imposing subjects would yield several two-file folders; `SplitView/` remains the one genuine subsystem. - Two files that were not styles moved out of `Styles/`, and `MenuWithButtonStyle` (a `View`, not a `MenuStyle`) became `ButtonStyledMenu`. -- **All 12 library targets have been reviewed** (2026-08-16/20). - Six were regrouped; two are stated exceptions (`CodeEditCore`, `CodeEditUI`); `CodeEditDocument` (5 files) and `CEWorkspaceFileManager` (7) are correctly flat and need nothing; `ShellClient` is one file by design. - Two are blocked on decisions rather than effort: `CodeEditSettings` on its naming question, `CESearch` on its rebuild. -- **8 kind-grouped folders remain**, but only two are work: `CESearch`'s `Model/` and `Extensions/`, pending its rebuild. - Three are the excluded `CESourceControl/Accounts/`, `CodeEditUI/Views` is the exception above, `CELSP/Service` is named after `LSPService` rather than being a layer, and `CodeEditSettings/Models` waits on that target's naming question. - Follow the convention in new code; those are a pending cleanup, not a counter-precedent. - **`CESourceControl/Accounts/` is deliberately excluded** until its dead surface is settled: it is 58 of that target's 133 files with three call sites in the whole codebase, and BitBucket is unreferenced outside its own subtree. - - Note `Extensions/` counts too (`CodeEditCore`, `CESearch`, `CETerminal`): a folder of "things that are extensions" says nothing about what they extend. - - Measure with this exact pattern, and widen it rather than trusting a smaller number. - Four successive counts here were wrong because the pattern matched `Models` but not `Model`, then not `Protocols`, then not `Extensions` or singular `Service`: - - ```bash - find CodeEditModules/Sources -type d \ - \( -name Model -o -name Models -o -name View -o -name Views -o -name ViewModel \ - -o -name ViewModels -o -name Service -o -name Services -o -name Protocol \ - -o -name Protocols -o -name UseCase -o -name UseCases -o -name Extensions \) | wc -l - ``` -- **Two placements from `CEEditor` worth reusing.** A conformance file belongs beside the protocol it satisfies (`CEWorkspaceFile+Editor` sits in `TabBar/Tab/` with `EditorTabRepresentable`), and environment keys are distributed to their subject rather than gathered into an `Environment/` group, which would be grouping by kind again. +## Folder conventions + +Grouping is **by purpose, never by kind**. +There are no `Models/`, `Views/`, `ViewModels/`, `Services/`, `Protocols/`, `UseCases/` or `Extensions/` folders. + +- Group by sub-feature or subject (`ProjectNavigator/`, `Restoration/`, `TabBar/`, `Shell/`), and if you cannot name the group without saying what kind of type it holds, it is not a group. + A conformance file belongs beside the protocol it satisfies, and environment keys belong with their subject rather than in an `Environment/` folder. - A feature with roughly ten files or fewer stays flat. -- Shell/entry views and the feature's primary models sit at the feature root. +- Shell and entry views, plus the feature's primary models, sit at the feature root. - Single-consumer helpers live next to their consumer. -- `Utils/` is closed. - Every file in it carries a justification (an app-wide platform patch, a helper genuinely shared by multiple features with no better home). - "It's generic" is not a justification. +- `Utils/` is closed. Place a new utility next to its consumer, and argue the case if you think it belongs in `Utils/`. + +**Worked example.** `CEEditor` had 55 files under `Models/`, `Views/` and `UseCases/`. +They became `Editor/`, `Layout/`, `FileViews/`, `TabBar/` (with `Tabs/` and `Tab/`), `JumpBar/`, `Documents/`, `Restoration/`, `Theme/` and `Adapters/`, as pure renames with no content change. +Two placements are worth copying: `CEWorkspaceFile+Editor` sits in `TabBar/Tab/` beside the `EditorTabRepresentable` protocol it conforms to, and `UndoManagerRegistry` sits in `Documents/` rather than `Restoration/`, because it performs no saving. + +**Two targets are stated exceptions.** +`CodeEditCore` keeps its `Domain/` and `Infrastructure/` split, because there the layer *is* the purpose. +`CodeEditUI` keeps `Styles/`, `Views/` and `EnvironmentKeys/`, because it is a component library with no feature semantics by charter, so kind is the subject a consumer browses by. +Do not "fix" either. ## Communication rules @@ -301,70 +290,23 @@ Apple's own frameworks are full of them (`NSApplication.shared`, `FileManager.de What was removed was a *container*. A `shared` is a problem here only when it gives ambient access to something whose lifetime is narrower than the process, or when it hides an owner that could hold it. -#### Known exceptions (2026-08-16) +Eight singletons remain, none load-bearing. +They are listed with the scope each actually has, plus the one that is a scope error rather than a leftover, in [docs/architecture-decisions.md](docs/architecture-decisions.md). -Eight singletons remain. -None is load-bearing; each is listed with the scope it actually has. +## Panel tab contributions -| Singleton | True scope | Note | -| --- | --- | --- | -| `ThemeModel` | process | already takes its settings store via `configure(_:)` at launch | -| `FeedbackModel` | process | ditto | -| `SearchSettingsModel` | process | ditto | -| `ExtensionManager` | process | | -| `ExtensionDiscovery` | process | | -| `InternalDevelopmentOutputSource` | process | debug-only | -| `EditorStateRestoration` | process | a GRDB database; `nonisolated(unsafe)` and optional | -| `TerminalCache` | **workspace** | see below | - -The first seven are process-scoped services that simply have not moved to `AppDependencies`; doing so is mechanical and changes no behaviour. - -`TerminalCache` is different, and is the one worth fixing rather than relocating. -It is a process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. -Eviction is per-terminal only, and nothing clears it when a workspace or window closes, so two open projects share one bag of live terminal views. -It works because UUIDs do not collide, but the lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. +The navigator, inspector and utility area do not switch on closed enums. +Each panel is a `[any WorkspacePanelContribution]` assembled by one function per panel in `CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`, so a first-party tab, app shell chrome and an extension's tab are the same kind of value. -## Panel tab contributions +**This is the pattern any future seam should follow**, and there will be others (settings pages, menu commands, the extension contract): + +- The feature vends its own contribution from its own package, reading whatever it needs directly. +- The app assembles the list at a composition root. +- Whatever the contribution needs and its package cannot see becomes a **required initialiser parameter**, never a defaulted one, so a missing injection is a compile error rather than a silently empty tab. +- Vendor vocabulary stays in one adapter: `ExtensionPanelContribution` is the only app file outside `AuxiliaryWindows/Extensions/` permitted to name `AppExtensionIdentity` or `ResolvedSidebar`. -The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, `InspectorTab`, `UtilityAreaTab`). -Each panel is a list of `WorkspacePanelContribution` values, so a tab is a value, not a case, assembled by one function per panel in `CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`: first-party entries named directly, conditional ones (`InternalDevelopmentInspectorContribution`) as a plain `if`, then extension-provided ones appended through a single adapter call. -The panel that renders the list cannot tell a first-party tab from an extension's. -That indistinguishability is the point; it is what lets a new contribution source arrive without the panel code changing. - -`WorkspacePanelContribution` lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`), not `CodeEditCore`: a contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forbids UI imports outright. -`CodeEditUI`'s own charter (rule 2) is satisfied too: the protocol needs SwiftUI and nothing else. - -A contribution vends two views. -`content` is the tab itself. -`bottomView` is an optional bar pinned below it. -The navigator's filter field and sort controls are the existing examples. -It defaults to `nil` so a tab with no such bar says nothing about it. -It is a *requirement* rather than something the panel is handed deliberately: it arrived from upstream as a `switch` over the retired tab enum, where every new tab had to remember to add its case, and a package could not contribute one at all because the switch lived app-side. -Vended by the contribution, a tab cannot forget, and where the bar is placed stays the panel's business: pre-Tahoe it insets the tab's own content, and from macOS 26 it spans the panel below the tab bar. - -**A panel's tab list changes at runtime**, so a stored selection can outlive the tab it names: the inspector rebuilds its list when a setting changes, and any panel's list changes when an extension is enabled or disabled. -`Collection.reconcilingSelection(_:)` (alongside the protocol) keeps a selection that still resolves and otherwise falls back to the first tab; `WorkspacePanelView` applies it on appear and on every change to the list. -Without it the panel reads "No Selection" until the user clicks something, because the stored id is stale rather than absent and nothing recovers on its own. -The rule lives in the package rather than the view so it holds for every panel and can be tested without one. - -A contribution's owner follows the same placement rule as everything else in [Where does my code go?](#where-does-my-code-go): a feature that owns a tab vends its own contribution from its package, reading whatever it needs (including settings, through the seam) directly rather than having the app assemble it. -`CESearch`'s `FindNavigatorContribution` (`CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift`) replaced an app-side `FindNavigatorTab` wrapper that existed only to shuttle settings values down. -Once the feature could read its own settings, the wrapper had no reason to exist. -The tab's id is now owned by the same package (`FindNavigatorContribution.tabID`); the app's `PanelTabID.search` references it rather than duplicating the literal, so there is exactly one source of truth even though the app still needs a compile-checked constant to select the tab by. - -`ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) stays app-side, not because it is a tab (see the [chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no owning package to move to. -The same holds for the file inspector, the internal-development inspector, and the debug and output utility tabs. -`TerminalUtilityContribution` is the one that should move, to `CETerminal`; it has not because it is coupled to app-side utility-area chrome. - -`CESourceControl` is the second feature package to own a panel tab, after `CESearch`. -`SourceControlNavigatorContribution` and `GitHistoryInspectorContribution` (`CodeEditModules/Sources/CESourceControl/SourceControlNavigator/` and `.../HistoryInspector/`) vend `SourceControlNavigatorView` and `HistoryInspectorView` from the package; the app-side `WorkspaceWindow/NavigatorArea/SourceControlNavigator/` and `WorkspaceWindow/InspectorArea/HistoryInspector/` groups no longer exist. -The relocation is the shape a future one follows: whatever the view needs that the package cannot see becomes a **required** initialiser parameter on the contribution (`WorkspaceNavigator` for the navigator tab, `ActiveEditorState` for the inspector tab) rather than an environment key moved down with it; both are non-optional so a missing injection at the call site (`PanelContributions.swift`) is a compile error, not a silently no-op tab. -Each contribution still owns its own `tabID` constant, and the app's `PanelTabID.sourceControl` / `.gitHistory` reference it rather than duplicating the literal, same as `PanelTabID.search`. -A third relocation onto this shape is expected. - -Extensions are the third contribution source. -`ExtensionPanelContribution` (`CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift`) is the **only** app file, outside the pre-existing extension-management UI under `AuxiliaryWindows/Extensions/`, that may name `AppExtensionIdentity` or `ResolvedSidebar`. -Confining ExtensionKit's vocabulary to this one adapter is what let the app-side panel-contribution list above be written without an ExtensionKit import in sight, and is what would let a second, non-ExtensionKit contribution source arrive later without touching the panels. +`WorkspacePanelContribution` lives in `CodeEditUI` because it needs SwiftUI and nothing else; it cannot live in `CodeEditCore`, which forbids UI imports. +Specifics of this seam, including `bottomView` and selection reconciliation, are documented on the protocol itself and in [docs/architecture-decisions.md](docs/architecture-decisions.md). ## Reading and writing settings @@ -489,4 +431,5 @@ These are the intended meanings; prefer the qualified term whenever the bare one | **Workspace** (`Workspace`) | The session aggregate for one open project: the project-scoped services and their lifetime. It owns lifecycle, *not* mutation routing, features mutate the sub-models they are handed. | | **Workspace window** (`WorkspaceWindow/`) | The window and its chrome around a workspace: navigator, inspector, utility area, status bar. Window-UI state lives on `CodeEditWindowController`, not on `Workspace`. | | **Document** (`CodeFileDocument`) | An open, editable file backed by NSDocument. Distinct from `CEWorkspaceFile` (a node in the file tree) and from the file on disk. | +| **Service** | A *target* holding an I/O adapter with no UI: `ShellClient`, `CEWorkspaceFileManager`. Do not use it loosely for "a long-lived object owning domain state", because most of those (`SourceControlManager`, `TaskManager`, `LSPService`) live in feature targets. | | **Doer** | A role-noun class performing one operation that spans services (`WorkspaceOpener`, `FileMover`, `RepositoryCloner`), following the `NSFileCoordinator` naming idiom. Formerly called UseCases. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 322b41cca7..6395a8d4d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,12 +32,12 @@ Please read our guide on [Code Style](https://github.com/CodeEditApp/CodeEdit/wi CodeEdit is a thin app target plus one local multi-target Swift package, `CodeEditModules`, whose single `Package.swift` holds the entire local dependency graph. Before adding files, -please consult the decision tree in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — it answers +please consult the decision tree in [ARCHITECTURE.md](ARCHITECTURE.md). It answers "where does my code go?" in a few steps. The short version: a new feature starts as a new target in `CodeEditModules/Package.swift`, `CodeEditCore` stays dependency-free and UI-free, and `CodeEditUI` depends on `CodeEditSymbols` alone. CI enforces those as hard rules (SwiftLint charter rules + a package import audit), so a misplaced file will fail checks. Features should -also prefer to be leaves that nothing else depends on — that one is a review preference rather +also prefer to be leaves that nothing else depends on, though that one is a review preference rather than a gate, so declare any target-to-target edge in the manifest where reviewers can see it. ## Pull Request diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index ef84c183bf..ee70991428 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -37,7 +37,7 @@ let package = Package( ], targets: [ // MARK: - Kernel - // Rule: zero dependencies, no UI imports, platform-free. See docs/ARCHITECTURE.md. + // Rule: zero dependencies, no UI imports, platform-free. See ARCHITECTURE.md. .target(name: "CodeEditCore"), // MARK: - Shared substrate @@ -84,7 +84,7 @@ let package = Package( .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "DequeModule", package: "swift-collections") ], - // The ONLY target permitted to opt out of Swift 6. See docs/ARCHITECTURE.md. + // The ONLY target permitted to opt out of Swift 6. See ARCHITECTURE.md. swiftSettings: [.swiftLanguageMode(.v5)] ), .target( diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md new file mode 100644 index 0000000000..41040bc21a --- /dev/null +++ b/docs/architecture-decisions.md @@ -0,0 +1,132 @@ +# CodeEdit Architecture Decisions + +Questions that were asked, investigated, and settled, with the measurements that settled them. + +This file exists so [ARCHITECTURE.md](../ARCHITECTURE.md) can stay short. +That guide states the rules; this one records why, and what was rejected. +Read it when you are about to reopen a decision, so you can see whether the reasoning still holds or the ground has moved. + +Each entry names what was measured, because a rule documented with only its conclusion is one argument away from deletion. + +## Targets that stay separate + +**`ShellClient` is one file, and stays its own target** (asked and settled 2026-08-16). +Size is the wrong measure: `ShellClientProtocol` in Core is used by **19 files** across `CESourceControl` and `CELSP` (`GitClient`, `SourceControlManager`, `RegistryManager`, all five package managers), and **none of them imports the implementation**. +Only the app target does, six files, composing it at the root. +The abstraction is load-bearing, not ceremonial. + +What the separate target buys, stated precisely: reaching for the implementation from a feature needs a **manifest edit**, visible in review, rather than an import line inside a file. +It is reviewability, not prevention: import honesty checks that imports are *declared*, so a feature that declared the dependency would pass the audit. +Folding it into the app target is a coherent alternative (the app is the only consumer, and composing platform adapters is a composition-root job); it would cost the manifest-level visibility and nothing else demonstrable. +**`CEWorkspaceFileManager` stays its own target, and must not merge into Core** (asked and settled 2026-08-20). +The inversion is already in place: Core declares `WorkspaceFileProviding` and `WorkspaceFileObserver`, and `CEWorkspaceFileManager.swift:263` is `extension CEWorkspaceFileManager: WorkspaceFileProviding {}`. +Four `CEEditor` files depend on the *protocol* (`EditorRestorer`, `EditorJumpBarMenu`, `EditorLayout+StateRestoration`, the environment key) and **no package imports the implementation**. +Its 22 consumers are app-side, plus 6 test files. +Same shape as `ShellClient` below: contract in Core, adapter in its own target, app composes. +See the I/O norm above for why the merge is worse than it looks. +**`CodeEditDocument` and `CELSP` both stay their own targets** (asked and settled 2026-08-20). +Neither is a leftover, and the two conclusions depend on each other. + +`CEEditor` and `CELSP` reference each other **zero times, in either direction**. +They are siblings. +What keeps them apart is `LanguageServicesProvider`, declared in `CodeEditDocument`, implemented by `CELSP`'s `AppLanguageServicesProvider`, and consumed by `CEEditor` through an environment key. +It even ships a `NoOpLanguageServicesProvider`, so the editor works with no language service at all. +So `CodeEditDocument` is not "the document type plus some bridging": **it is the contract that keeps two features independent.** `CodeFileDocument` imports AppKit, SwiftUI and the editor frameworks, so it cannot live in Core; two features need it, so it cannot live in either. +Its own target is forced, not chosen. + +`CELSP` is not part of the editor either. +Its consumers are the settings UI (installing servers), the utility area (reading logs) and app lifecycle (nothing in `CEEditor` imports it), and **28 of its 78 files are `Registry/`**: package managers, install steps and source parsers for Cargo, NPM, Pip, Go and GitHub. +That is downloading and installing language servers, not editing text. +Folding it into `CEEditor` would make a ~130-file target mixing the two. + +The abstraction is already sound where it counts: `LanguageServer`, `LSPContentCoordinator`, `SemanticTokenHighlightProvider` and `LanguageServerDocumentObjects` are all generic over `LanguageServerDocument`, a protocol requiring only `content`, `languageServerURI` and `getLanguage()`. +Only `LSPService` itself pins the generic to `CodeFileDocument`. +Decoupling that would mean making the service generic and forcing `LSPServiceProtocol` to gain an associated type, breaking its use as an existential for DI, all to delete a five-file target. +A bad trade. +## Keeping I/O out of Core + +**What the norm actually prevents** (asked 2026-08-20): merging `CEWorkspaceFileManager` into Core. +That target holds 50 `FileManager` calls and a complete FSEvents implementation, `FSEventStreamCreate` with a C callback, its own dispatch queue, and start/stop/invalidate/release. +Without this norm the merge looks reasonable, because that target depends on nothing but Core and folding it in removes a target. +With the norm it is obviously wrong: it would put a live filesystem event stream inside the dependency sink all twelve targets rest on, and end Core's filesystem-free tests the same day. +State the norm with this example, not on principle alone. +## The panel contribution seam + +The first extension seam built, kept here in full because it is the worked example the later seams follow. +The reusable pattern is summarised in the guide; the specifics below live with the code as doc comments. + +### Detail + +The navigator, inspector and utility area no longer switch on closed enums (`NavigatorTab`, `InspectorTab`, `UtilityAreaTab`). +Each panel is a list of `WorkspacePanelContribution` values, so a tab is a value, not a case, assembled by one function per panel in `CodeEdit/WorkspaceWindow/WorkspacePanel/PanelContributions.swift`: first-party entries named directly, conditional ones (`InternalDevelopmentInspectorContribution`) as a plain `if`, then extension-provided ones appended through a single adapter call. +The panel that renders the list cannot tell a first-party tab from an extension's. +That indistinguishability is the point; it is what lets a new contribution source arrive without the panel code changing. + +`WorkspacePanelContribution` lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`), not `CodeEditCore`: a contribution vends a `content: AnyView`, and Core's charter (rule 1, above) forbids UI imports outright. +`CodeEditUI`'s own charter (rule 2) is satisfied too: the protocol needs SwiftUI and nothing else. + +A contribution vends two views. +`content` is the tab itself. +`bottomView` is an optional bar pinned below it. +The navigator's filter field and sort controls are the existing examples. +It defaults to `nil` so a tab with no such bar says nothing about it. +It is a *requirement* rather than something the panel is handed deliberately: it arrived from upstream as a `switch` over the retired tab enum, where every new tab had to remember to add its case, and a package could not contribute one at all because the switch lived app-side. +Vended by the contribution, a tab cannot forget, and where the bar is placed stays the panel's business: pre-Tahoe it insets the tab's own content, and from macOS 26 it spans the panel below the tab bar. + +**A panel's tab list changes at runtime**, so a stored selection can outlive the tab it names: the inspector rebuilds its list when a setting changes, and any panel's list changes when an extension is enabled or disabled. +`Collection.reconcilingSelection(_:)` (alongside the protocol) keeps a selection that still resolves and otherwise falls back to the first tab; `WorkspacePanelView` applies it on appear and on every change to the list. +Without it the panel reads "No Selection" until the user clicks something, because the stored id is stale rather than absent and nothing recovers on its own. +The rule lives in the package rather than the view so it holds for every panel and can be tested without one. + +A contribution's owner follows the same placement rule as everything else in [Where does my code go?](#where-does-my-code-go): a feature that owns a tab vends its own contribution from its package, reading whatever it needs (including settings, through the seam) directly rather than having the app assemble it. +`CESearch`'s `FindNavigatorContribution` (`CodeEditModules/Sources/CESearch/FindNavigatorContribution.swift`) replaced an app-side `FindNavigatorTab` wrapper that existed only to shuttle settings values down. +Once the feature could read its own settings, the wrapper had no reason to exist. +The tab's id is now owned by the same package (`FindNavigatorContribution.tabID`); the app's `PanelTabID.search` references it rather than duplicating the literal, so there is exactly one source of truth even though the app still needs a compile-checked constant to select the tab by. + +`ProjectNavigatorContribution` (`CodeEdit/WorkspaceWindow/NavigatorArea/NavigatorContributions.swift`) stays app-side, not because it is a tab (see the [chrome exemption correction](#where-does-my-code-go)) but because the project navigator has no owning package to move to. +The same holds for the file inspector, the internal-development inspector, and the debug and output utility tabs. +`TerminalUtilityContribution` is the one that should move, to `CETerminal`; it has not because it is coupled to app-side utility-area chrome. + +`CESourceControl` is the second feature package to own a panel tab, after `CESearch`. +`SourceControlNavigatorContribution` and `GitHistoryInspectorContribution` (`CodeEditModules/Sources/CESourceControl/SourceControlNavigator/` and `.../HistoryInspector/`) vend `SourceControlNavigatorView` and `HistoryInspectorView` from the package; the app-side `WorkspaceWindow/NavigatorArea/SourceControlNavigator/` and `WorkspaceWindow/InspectorArea/HistoryInspector/` groups no longer exist. +The relocation is the shape a future one follows: whatever the view needs that the package cannot see becomes a **required** initialiser parameter on the contribution (`WorkspaceNavigator` for the navigator tab, `ActiveEditorState` for the inspector tab) rather than an environment key moved down with it; both are non-optional so a missing injection at the call site (`PanelContributions.swift`) is a compile error, not a silently no-op tab. +Each contribution still owns its own `tabID` constant, and the app's `PanelTabID.sourceControl` / `.gitHistory` reference it rather than duplicating the literal, same as `PanelTabID.search`. +A third relocation onto this shape is expected. + +Extensions are the third contribution source. +`ExtensionPanelContribution` (`CodeEdit/WorkspaceWindow/WorkspacePanel/ExtensionPanelContribution.swift`) is the **only** app file, outside the pre-existing extension-management UI under `AuxiliaryWindows/Extensions/`, that may name `AppExtensionIdentity` or `ResolvedSidebar`. +Confining ExtensionKit's vocabulary to this one adapter is what let the app-side panel-contribution list above be written without an ExtensionKit import in sight, and is what would let a second, non-ExtensionKit contribution source arrive later without touching the panels. + +## What the Core charter has produced + +The friction this produces is usually the rule working. +Four worked examples already in this codebase: `FileIcon` is keyed on `URL` rather than a domain type, so it needs neither `CodeEditCore` nor a charter exception (see rule 2 below); `WorkspacePanelContribution` was shaped to need only SwiftUI, so it lives in `CodeEditUI` (`CodeEditModules/Sources/CodeEditUI/WorkspacePanelContribution.swift`); fuzzy matching's concurrency helper was rewritten over `withTaskGroup` rather than admit `CollectionConcurrencyKit` (below); and `ActiveTheme`, the active light/dark theme, observed by the editor and terminal, lives *in* Core, because `ObservableObject` comes from Combine rather than SwiftUI. +That last one is worth remembering: the charter forbids `SwiftUI`/`AppKit`/`Cocoa` specifically, so observation is available in Core and an "it needs to be observable, therefore it needs SwiftUI" argument is simply false. + +The counter-example people will cite: `TextEditingSettings` and `TerminalSettings.Font` carry `NSFont.Weight`, which forces them out of Core. +That is the rule flagging a presentation type inside a settings model, not the rule obstructing a reasonable design. +Storing the weight as a `Double` and converting at the presentation boundary would make both structs Core-eligible with no rule change. +## Singletons that remain + +Recorded 2026-08-16. + +Eight singletons remain. +None is load-bearing; each is listed with the scope it actually has. + +| Singleton | True scope | Note | +| --- | --- | --- | +| `ThemeModel` | process | already takes its settings store via `configure(_:)` at launch | +| `FeedbackModel` | process | ditto | +| `SearchSettingsModel` | process | ditto | +| `ExtensionManager` | process | | +| `ExtensionDiscovery` | process | | +| `InternalDevelopmentOutputSource` | process | debug-only | +| `EditorStateRestoration` | process | a GRDB database; `nonisolated(unsafe)` and optional | +| `TerminalCache` | **workspace** | see below | + +The first seven are process-scoped services that simply have not moved to `AppDependencies`; doing so is mechanical and changes no behaviour. + +`TerminalCache` is different, and is the one worth fixing rather than relocating. +It is a process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. +Eviction is per-terminal only, and nothing clears it when a workspace or window closes, so two open projects share one bag of live terminal views. +It works because UUIDs do not collide, but the lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. \ No newline at end of file From dcbb8915918b6f24e39e64bdae6638268b55bf4f Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 11:34:57 +0200 Subject: [PATCH 325/335] Fix: Isolate SettingsValue to the main actor CI failed to compile CodeEditSettings with four errors in SettingsValue.swift: EnvironmentObject's init and its wrapped value are @MainActor in the SDK, so a nonisolated property wrapper touching them is rejected under Swift 6 strict concurrency. It compiled locally because this machine runs Xcode 26.6 while the runner uses Xcode 16.4, and newer SwiftUI carries @preconcurrency annotations that soften the isolation. The app-side @AppSettings wrapper has the same shape and is unaffected, because the app target is still Swift 5. The isolation is correct on the merits, not just a way to satisfy the compiler. SettingsValue is documented as valid only inside a View, and PersistentSettingsStore.setValue already asserts the main thread. SwiftUI's own @StateObject and @ObservedObject are main-actor wrappers used the same way. Nothing in the branch needs a newer SDK, so the runner is adequate: the 37 #available(macOS 26) guards are version checks that compile against any SDK, GlassEffectView is our own NSViewRepresentable, and the only Apple glass API reference in the tree is commented out. Upgrading the runner would have hidden this rather than fixed it. Cannot be verified locally, since Xcode 26.6 does not produce the error. CI is the check. --- .../Sources/CodeEditSettings/Store/SettingsValue.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift index 89874caed0..a00311574a 100644 --- a/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift +++ b/CodeEditModules/Sources/CodeEditSettings/Store/SettingsValue.swift @@ -102,7 +102,17 @@ public struct SnapshotSettingsReader: SettingsReading { /// signal read correctly and never re-rendered. `@EnvironmentObject` makes both unrepresentable. /// SwiftUI subscribes to the object itself, so there is no second key to forget and nothing to keep /// in sync. +/// +/// **Main-actor isolated deliberately, and it must stay that way.** +/// +/// `EnvironmentObject`'s initialiser and wrapped value are `@MainActor` in the SDK, so a +/// nonisolated wrapper touching them fails under Swift 6 strict concurrency. Newer SwiftUI carries +/// `@preconcurrency` annotations that hide this, which is why it compiled on Xcode 26 and failed on +/// the CI runner's Xcode 16.4. The isolation is also true on the merits: this is documented as valid +/// only inside a `View`, and `PersistentSettingsStore.setValue` already asserts the main thread. +/// Same shape as SwiftUI's own `@StateObject` and `@ObservedObject`. @propertyWrapper +@MainActor public struct SettingsValue: DynamicProperty { @EnvironmentObject private var store: PersistentSettingsStore From 0f85e618470f1a1961215a3eecddcfba2d8877a8 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 11:49:29 +0200 Subject: [PATCH 326/335] Fix: Declare GitClient.Status Sendable It crosses GitClientProtocol, which is Sendable, and holds only [GitChangedFile], which is Sendable too. Every other type returned by that protocol already declares the conformance, so Status was the lone outlier. Xcode 26 accepts the call site without this because its region-based isolation analysis can prove the value is disconnected there. Xcode 16.4, which the CI runner uses, cannot, and asks for the conformance instead. --- .../Sources/CESourceControl/Client/GitClient+Status.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CodeEditModules/Sources/CESourceControl/Client/GitClient+Status.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Status.swift index cd4b3505d7..27936e5c2e 100644 --- a/CodeEditModules/Sources/CESourceControl/Client/GitClient+Status.swift +++ b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Status.swift @@ -27,7 +27,11 @@ import CodeEditCore extension GitClient { /// The parsed result of `git status`: ordinary changes, unmerged (conflicting) paths, and untracked files. - public struct Status { + /// + /// `Sendable` because this crosses ``GitClientProtocol``, which is itself `Sendable`, and every + /// other type returned by that protocol declares the conformance. It holds only + /// `[GitChangedFile]`, which is `Sendable`, so the conformance is free. + public struct Status: Sendable { var changedFiles: [GitChangedFile] var unmergedChanges: [GitChangedFile] var untrackedFiles: [GitChangedFile] From 87b373d5f2011be174acb6cd93e3f201b44a62eb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 11:57:03 +0200 Subject: [PATCH 327/335] Fix: Declare GitClient.CloneProgress Sendable These values are produced by an AsyncSequence and consumed on the main actor, so they cross an isolation boundary. The struct holds a Double and a payload-free internal enum, so it was already Sendable in fact and only lacked the declaration. Same toolchain split as GitClient.Status: Xcode 26 proves the value is disconnected at the call site, while the CI runner's Xcode 16.4 asks for the conformance. --- .../Sources/CESourceControl/Client/GitClient+Clone.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CodeEditModules/Sources/CESourceControl/Client/GitClient+Clone.swift b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Clone.swift index f74e18e48c..48bb404039 100644 --- a/CodeEditModules/Sources/CESourceControl/Client/GitClient+Clone.swift +++ b/CodeEditModules/Sources/CESourceControl/Client/GitClient+Clone.swift @@ -11,7 +11,10 @@ import CodeEditCore extension GitClient { /// A snapshot of clone progress: the total percentage (0-100) and the phase git is currently in. - public struct CloneProgress { + /// + /// `Sendable` because these are produced by an `AsyncSequence` and consumed on the main actor. + /// It holds a `Double` and a payload-free internal enum, so the conformance is free. + public struct CloneProgress: Sendable { let progress: Double let state: GitCloneProgressState } From 036485a3b2099b3877764553bef9e861c953fefc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 12:05:35 +0200 Subject: [PATCH 328/335] Fix: Delete SearchState.replaceRange, which was dead and thread-unsafe The method had no callers anywhere in the repo, was internal to CESearch so nothing outside could reach it, witnessed no protocol requirement, and was not @objc, so no dynamic dispatch could find it either. It also built an NSAlert and called runModal() from a synchronous nonisolated method on a nonisolated class, which presents a modal alert from an arbitrary thread. Xcode 26 accepts this; the CI runner's Xcode 16.4 rejects it, correctly. The three other NSAlert sites in the packages all sit on @MainActor types and are unaffected. Removing this one leaves the file with no AppKit dependency, so that import goes too: its only other NS use, NSString.CompareOptions, is Foundation. --- .../SearchState+FindAndReplace.swift | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/CodeEditModules/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift b/CodeEditModules/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift index 411f694384..f7f36f4a6d 100644 --- a/CodeEditModules/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift +++ b/CodeEditModules/Sources/CESearch/SearchState/SearchState+FindAndReplace.swift @@ -6,7 +6,6 @@ // import Foundation -import AppKit extension SearchState { /// Performs a search and replace operation in a collection of files based on the provided query. @@ -107,63 +106,6 @@ extension SearchState { try updatedContent.write(to: fileURL, atomically: true, encoding: .utf8) } - /// Replaces a specified range of text within a file with a new string. - /// - /// - Parameters: - /// - file: The URL of the file to be modified. - /// - searchTerm: The string to be replaced within the specified range. - /// - replacingTerm: The string to replace the specified searchTerm. - /// - keywordRange: The range within which the replacement should occur. - /// - /// - Note: This function can be utilised for two specific use cases: - /// 1. To replace a particular occurrence of a string within a file, - /// provide the range of the keyword to be replaced. - /// 2. To replace all occurrences of the string within the file, - /// pass the start and end index covering the entire range. - func replaceRange( - file: URL, - searchTerm: String, - replacingTerm: String, - keywordRange: Range - ) { - guard let fileContent = try? String(contentsOf: file, encoding: .utf8) else { - let alert = NSAlert() - alert.messageText = "Error" - alert.informativeText = "An error occurred while reading file contents of: \(file)" - alert.alertStyle = .critical - alert.addButton(withTitle: "OK") - alert.runModal() - - return - } - - var replaceOptions = NSString.CompareOptions() - if selectedMode.second == .RegularExpression { - replaceOptions = [.regularExpression] - } - if !caseSensitive { - replaceOptions = [.caseInsensitive] - } - - let updatedContent = fileContent.replacingOccurrences( - of: searchTerm, - with: replacingTerm, - options: replaceOptions, - range: keywordRange - ) - - do { - try updatedContent.write(to: file, atomically: true, encoding: .utf8) - } catch { - let alert = NSAlert() - alert.messageText = "Error" - alert.informativeText = "An error occurred while writing to: \(error.localizedDescription)" - alert.alertStyle = .critical - alert.addButton(withTitle: "OK") - alert.runModal() - } - } - func setStatus(_ status: FindNavigatorStatus) async { await MainActor.run { self.findNavigatorStatus = status From efb4b6f84e75cc8f9656f98289ff4469291b84dc Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 12:16:02 +0200 Subject: [PATCH 329/335] Fix: Isolate the NSPopUpButton coordinators to the main actor Both coordinators write a main-actor binding on their NSViewRepresentable parent from inside a Combine sink on NSMenu.didSendActionNotification. AppKit posts that notification on the main thread, so the write was always main-thread in practice, but nothing said so. This mirrors the fix already used by FindNavigatorResultList's coordinator in the same package: annotate the coordinator, then state the invariant at the mutation with MainActor.assumeIsolated, which traps if it is ever violated. CESearch is the one CI reported, since it is Swift 6. CEEditor has the identical construct and is silent only because it declares swiftLanguageMode(.v5), so it is fixed here too rather than left to fail when that exception is lifted. --- .../Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift | 7 ++++++- .../Sources/CESearch/FindNavigator/FindModePicker.swift | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift index 766a5d94e1..fca3097218 100644 --- a/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift +++ b/CodeEditModules/Sources/CEEditor/JumpBar/EditorJumpBarComponent.swift @@ -176,6 +176,7 @@ struct EditorJumpBarComponent: View { return Coordinator(self) } + @MainActor class Coordinator: NSObject { var parent: NSPopUpButtonView @@ -192,7 +193,11 @@ struct EditorJumpBarComponent: View { .sink { [weak self] notification in if let menuItem = notification.userInfo?["MenuItem"] as? NSMenuItem, let selection = menuItem as? ItemType { - self?.parent.selection = selection + // AppKit posts `NSMenu.didSendActionNotification` on the main thread, + // so this delivery is main-thread and the binding may be written here. + MainActor.assumeIsolated { + self?.parent.selection = selection + } } } } diff --git a/CodeEditModules/Sources/CESearch/FindNavigator/FindModePicker.swift b/CodeEditModules/Sources/CESearch/FindNavigator/FindModePicker.swift index 963cd98579..625bdca797 100644 --- a/CodeEditModules/Sources/CESearch/FindNavigator/FindModePicker.swift +++ b/CodeEditModules/Sources/CESearch/FindNavigator/FindModePicker.swift @@ -133,6 +133,7 @@ struct FindModePicker: View { return Coordinator(self) } + @MainActor class Coordinator: NSObject { var parent: NSPopUpButtonView @@ -149,7 +150,11 @@ struct FindModePicker: View { .sink { [weak self] notification in if let menuItem = notification.userInfo?["MenuItem"] as? NSMenuItem, let selection = menuItem as? ItemType { - self?.parent.selection = selection + // AppKit posts `NSMenu.didSendActionNotification` on the main thread, + // so this delivery is main-thread and the binding may be written here. + MainActor.assumeIsolated { + self?.parent.selection = selection + } } } } From b5ad67b44a223a4cd2f63c03d6ec1266cce319d4 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 12:21:25 +0200 Subject: [PATCH 330/335] Fix: Isolate SourceControlViewModelTests' setUp and tearDown The class is @MainActor, but both methods override nonisolated declarations on XCTestCase, and an override cannot add isolation the superclass lacks. So they stayed nonisolated while the property they assign and the initialiser they call are main-actor. XCTest runs setUp and tearDown on the main thread for synchronous test cases, so MainActor.assumeIsolated states that invariant instead of weakening the isolation of the view model. Swept the other seventeen test files with these overrides: no other package test is affected. The remaining @MainActor ones live in CodeEditTests, which is Swift 5. --- .../SourceControlViewModelTests.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift b/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift index dd29fe5daf..2ce513da41 100644 --- a/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift +++ b/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift @@ -12,13 +12,20 @@ import XCTest final class SourceControlViewModelTests: XCTestCase { var viewModel: SourceControlViewModel! + // `setUp` and `tearDown` override nonisolated declarations on `XCTestCase`, so they stay + // nonisolated even though this class is `@MainActor`. XCTest runs both on the main thread for + // synchronous test cases, so state that here rather than weakening the isolation. override func setUp() { super.setUp() - viewModel = SourceControlViewModel() + MainActor.assumeIsolated { + viewModel = SourceControlViewModel() + } } override func tearDown() { - viewModel = nil + MainActor.assumeIsolated { + viewModel = nil + } super.tearDown() } From 1d9ffbb3e4c9ccf0499f0bdd0273619668a72dab Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 12:25:07 +0200 Subject: [PATCH 331/335] Fix: Build the test view model lazily instead of in setUp Supersedes the previous attempt on this file. MainActor.assumeIsolated was the wrong tool: its closure captures self, the XCTestCase, which is not Sendable, so sending it into a main-actor closure from a nonisolated override is itself a data race the compiler rejects. A lazy property needs no escape hatch. Its getter is main-actor because the class is, and XCTest creates a fresh test-case instance per test method, so each test still gets its own view model. tearDown only nilled the property, which per-test instances already handle. --- .../SourceControlViewModelTests.swift | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift b/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift index 2ce513da41..8e2209c583 100644 --- a/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift +++ b/CodeEditModules/Tests/CESourceControlTests/SourceControlViewModelTests.swift @@ -10,24 +10,10 @@ import XCTest @MainActor final class SourceControlViewModelTests: XCTestCase { - var viewModel: SourceControlViewModel! - - // `setUp` and `tearDown` override nonisolated declarations on `XCTestCase`, so they stay - // nonisolated even though this class is `@MainActor`. XCTest runs both on the main thread for - // synchronous test cases, so state that here rather than weakening the isolation. - override func setUp() { - super.setUp() - MainActor.assumeIsolated { - viewModel = SourceControlViewModel() - } - } - - override func tearDown() { - MainActor.assumeIsolated { - viewModel = nil - } - super.tearDown() - } + /// Built lazily rather than in `setUp`, which overrides a nonisolated `XCTestCase` method and so + /// cannot touch this `@MainActor` class's state. XCTest creates a fresh test-case instance per + /// test method, so each test still gets its own view model. + private lazy var viewModel = SourceControlViewModel() // MARK: - Operation field reset From 2255475f6e30ddaf6afeef36446ee69452d3e38b Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 13:23:30 +0200 Subject: [PATCH 332/335] Fix: Bridge CodeFileDocument's main-actor isolation NSDocument is main-actor isolated but declares read(from:ofType:) and presentedItemDidChange() nonisolated, since AppKit may call them off the main thread. Both touch main-actor document state, which was silent in the Swift 5 app target and is six errors now the file lives in a Swift 6 package target. The failing lines are byte-identical on main. Three changes, none of them a behaviour change: 1. canConcurrentlyReadDocuments(ofType:) is overridden to false. That is already AppKit's default; stating it pins the invariant the read path relies on, which was previously unwritten. 2. read(from:ofType:) states its main-actor isolation, and registerContentChangeUndo takes a String rather than an NSString so nothing non-Sendable is captured. 3. presentedItemDidChange() consults its main-actor state through the same Thread.isMainThread branch notifyLSPDidOpen() already uses. An unconditional DispatchQueue.main.sync deadlocks, because the tests call this on the main thread while NSFileCoordinator does not. This is a bridge, not a resolution: assumeIsolated states what the compiler cannot check, and the runtime branch stands in for a static guarantee. Recorded as deferred in docs/architecture-decisions.md and in a doc comment, with the redesign sketched. The external-changes section moved to its own file to stay under the 400-line lint limit. --- .../CodeFileDocument+ExternalChanges.swift | 71 ++++++++++++++++++ .../CodeEditDocument/CodeFileDocument.swift | 75 +++++++------------ docs/architecture-decisions.md | 24 +++++- 3 files changed, 119 insertions(+), 51 deletions(-) create mode 100644 CodeEditModules/Sources/CodeEditDocument/CodeFileDocument+ExternalChanges.swift diff --git a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument+ExternalChanges.swift b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument+ExternalChanges.swift new file mode 100644 index 0000000000..665843e3c2 --- /dev/null +++ b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument+ExternalChanges.swift @@ -0,0 +1,71 @@ +// +// CodeFileDocument+ExternalChanges.swift +// CodeEdit +// +// Created by Matthijs Eikelenboom on 22/08/26. +// + +import AppKit + +/// Reacting to the file changing underneath us on disk. +/// +/// **The isolation here is a bridge, not a resolution.** `NSDocument` is main-actor isolated but +/// declares `read(from:ofType:)` and `presentedItemDidChange()` nonisolated, so both override a +/// nonisolated entry point while touching main-actor document state. `MainActor.assumeIsolated` +/// states an invariant the compiler cannot check, and the `Thread.isMainThread` branch below +/// substitutes a runtime test for a static guarantee. +/// +/// The underlying problem is that ``CodeFileDocument`` mixes main-actor UI state (`content` is an +/// `NSTextStorage` SwiftUI observes) with an I/O lifecycle AppKit drives from arbitrary threads. +/// The proper fix separates the two: decode into a `Sendable` value with no isolation, then install +/// it in one main-actor step. That is a redesign of the document's state ownership, deliberately +/// deferred. See `docs/architecture-decisions.md`, "Document isolation is bridged, not solved". +extension CodeFileDocument { + /// Handle the notification that the represented file item changed. + /// + /// We check if a file has been modified and can be read again to display to the user. + /// To determine if a file has changed, we check the modification date. If it's different from the stored one, + /// we continue. + /// To determine if we can reload the file, we check if the document has outstanding edits. If not, we reload the + /// file. + override public func presentedItemDidChange() { + // Unlike reads, this genuinely arrives on the file-presenter thread, while the state it + // consults is main-actor isolated. This blocks the presenter thread intentionally: if we + // don't wait, we'll receive more updates that the file has changed and end up dispatching + // multiple reads. The presenter thread expects this to be synchronous anyway. + + // https://github.com/CodeEditApp/CodeEdit/issues/2091 + // We can't use `.asyncAndWait` on Ventura as it seems the symbol is missing on that + // platform. Could be just for x86 machines. + let reloadIfClean: @MainActor () -> Bool = { [self] in + guard fileModificationDate != getModificationDate(), !isDocumentEdited else { + return false + } + fileModificationDate = getModificationDate() + if let fileURL, let fileType { + try? read(from: fileURL, ofType: fileType) + } + return true + } + + // Callers are not all off-main: `NSFileCoordinator` delivers on the presenter thread, but + // tests (and any future in-process caller) may already be on the main thread, where + // `DispatchQueue.main.sync` would deadlock. Mirrors the branch in ``notifyLSPDidOpen()``. + let handled = Thread.isMainThread + ? MainActor.assumeIsolated { reloadIfClean() } + : DispatchQueue.main.sync { MainActor.assumeIsolated { reloadIfClean() } } + + if !handled { + super.presentedItemDidChange() + } + } + + /// Helper to find the last modified date of the represented file item. + /// + /// Different from `NSDocument.fileModificationDate`. This returns the *current* modification date, whereas the + /// alternative stores the date that existed when we last read the file. + private func getModificationDate() -> Date? { + guard let path = fileURL?.absolutePath else { return nil } + return try? FileManager.default.attributesOfItem(atPath: path)[.modificationDate] as? Date + } +} diff --git a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift index 94e7896ce6..ce45964e31 100644 --- a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift @@ -165,6 +165,16 @@ public final class CodeFileDocument: NSDocument, ObservableObject { // MARK: - Read + /// Never read concurrently. + /// + /// This is AppKit's default, stated explicitly because ``read(from:ofType:)`` depends on it: + /// that method overrides a nonisolated `NSDocument` entry point but touches this document's + /// main-actor state, which is only sound while reads stay on the main thread. Returning `true` + /// here would make that unsound with no compile-time error. + override public static func canConcurrentlyReadDocuments(ofType typeName: String) -> Bool { + false + } + /// This function is used for decoding files. /// It should not throw error as unsupported files can still be opened by QLPreviewView. override public func read(from data: Data, ofType _: String) throws { @@ -183,14 +193,20 @@ public final class CodeFileDocument: NSDocument, ObservableObject { Self.logger.error("Failed to read file from data using encoding: \(rawEncoding)") return } - self.sourceEncoding = validEncoding - if let content { - registerContentChangeUndo(fileURL: fileURL, nsString: nsString, content: content) - content.mutableString.setString(nsString as String) - } else { - self.content = NSTextStorage(string: nsString as String) + // `read(from:ofType:)` overrides a nonisolated `NSDocument` method, but everything below + // touches main-actor state. Reads are main-thread only, which + // `canConcurrentlyReadDocuments(ofType:)` above pins, so stating the isolation is sound. + let text = nsString as String + MainActor.assumeIsolated { + self.sourceEncoding = validEncoding + if let content { + registerContentChangeUndo(fileURL: fileURL, text: text, content: content) + content.mutableString.setString(text) + } else { + self.content = NSTextStorage(string: text) + } + notifyLSPDidOpen() } - notifyLSPDidOpen() } /// The delegate is main-actor isolated, but document reads and closes can happen off the main @@ -219,12 +235,12 @@ public final class CodeFileDocument: NSDocument, ObservableObject { /// - Note: This is inefficient memory-wise. We could do a diff of the file and only register the /// mutations that would recreate the diff. However, that would instead be CPU intensive. /// Tradeoffs. - nonisolated private func registerContentChangeUndo(fileURL: URL?, nsString: NSString, content: NSTextStorage) { + nonisolated private func registerContentChangeUndo(fileURL: URL?, text: String, content: NSTextStorage) { guard let fileURL else { return } // The delegate's undo registry is main-actor isolated. Capture only Sendable primitives and build // the (non-Sendable) `TextMutation` on the main actor so nothing non-Sendable crosses the boundary. // Re-reads reach here on the main thread; mirror the `queue: .main` bridge used for LSP notifications. - let string = nsString as String + let string = text let length = content.length let register: @MainActor () -> Void = { [weak self] in let mutation = TextMutation( @@ -295,47 +311,6 @@ public final class CodeFileDocument: NSDocument, ObservableObject { } } - // MARK: - External Changes - - /// Handle the notification that the represented file item changed. - /// - /// We check if a file has been modified and can be read again to display to the user. - /// To determine if a file has changed, we check the modification date. If it's different from the stored one, - /// we continue. - /// To determine if we can reload the file, we check if the document has outstanding edits. If not, we reload the - /// file. - override public func presentedItemDidChange() { - if fileModificationDate != getModificationDate() { - guard isDocumentEdited else { - fileModificationDate = getModificationDate() - if let fileURL, let fileType { - // This blocks the presented item thread intentionally. If we don't wait, we'll receive more updates - // that the file has changed and we'll end up dispatching multiple reads. - // The presented item thread expects this operation to by synchronous anyways. - - // https://github.com/CodeEditApp/CodeEdit/issues/2091 - // We can't use `.asyncAndWait` on Ventura as it seems the symbol is missing on that platform. - // Could be just for x86 machines. - DispatchQueue.main.sync { - try? self.read(from: fileURL, ofType: fileType) - } - } - return - } - } - - super.presentedItemDidChange() - } - - /// Helper to find the last modified date of the represented file item. - /// - /// Different from `NSDocument.fileModificationDate`. This returns the *current* modification date, whereas the - /// alternative stores the date that existed when we last read the file. - private func getModificationDate() -> Date? { - guard let path = fileURL?.absolutePath else { return nil } - return try? FileManager.default.attributesOfItem(atPath: path)[.modificationDate] as? Date - } - // MARK: - Close override public func close() { diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 41040bc21a..b35beed6fc 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -129,4 +129,26 @@ The first seven are process-scoped services that simply have not moved to `AppDe `TerminalCache` is different, and is the one worth fixing rather than relocating. It is a process-global `[UUID: CELocalShellTerminalView]` holding views that belong to a workspace window. Eviction is per-terminal only, and nothing clears it when a workspace or window closes, so two open projects share one bag of live terminal views. -It works because UUIDs do not collide, but the lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. \ No newline at end of file +It works because UUIDs do not collide, but the lifetime is wrong, and a wrong lifetime surfaces as a leak rather than as a compile error. + +## Document isolation is bridged, not solved + +`CodeFileDocument` moved into `CodeEditDocument` during the package extraction, which put it under Swift 6 strict concurrency for the first time. +That surfaced six pre-existing isolation errors, all in code that is byte-identical on `main` and compiles silently there. + +`NSDocument` is main-actor isolated, but declares `read(from:ofType:)` and `presentedItemDidChange()` nonisolated, because AppKit may call them off the main thread. +Both touch main-actor document state. +Three things now hold that together, and none of them is a static guarantee: + +1. `canConcurrentlyReadDocuments(ofType:)` is overridden to return `false`, pinning AppKit's default so reads stay on the main thread. Returning `true` would make the isolation unsound with no compile error. +2. `read(from:ofType:)` uses `MainActor.assumeIsolated`, which relies on (1). +3. `presentedItemDidChange()` branches on `Thread.isMainThread`, because it genuinely arrives on the file-presenter thread in production but on the main thread from tests. An unconditional `DispatchQueue.main.sync` deadlocks the second case. + +This is accepted as a bridge so the extraction can land, not as the end state. +The real problem is that the type mixes main-actor UI state (`content` is an `NSTextStorage` that SwiftUI observes) with an I/O lifecycle driven from arbitrary threads. +Separating those, so decoding produces a `Sendable` value that a single main-actor step installs, removes all three props at once. +That is a redesign of the document's state ownership and is deliberately deferred. + +The general lesson is worth stating separately, because it applies to every future extraction: **moving a file into `CodeEditModules` is also a Swift 6 migration of that file.** +The app target is `SWIFT_VERSION = 5.0` with no `SWIFT_STRICT_CONCURRENCY` setting, so it defaults to `minimal`; the package is `swift-tools-version: 6.0`, so every target defaults to Swift 6 language mode. +Code that compiled without complaint for years can arrive in a package with a dozen errors, none of them regressions. From 3fa21cd506a91a23dc1a707f90015da57d4214bb Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 13:27:39 +0200 Subject: [PATCH 333/335] Fix: Bind the document once in the autosave timer block The timer block optional-chained self on every access, including inside a MainActor.assumeIsolated closure. Region-based isolation cannot treat self as disconnected there, because the escaping timer block still shares it, so the isolated call reads as sending self. Binding once with a guard gives the analysis a local value and changes no semantics: the capture stays weak, so there is still no retain cycle, and the strong binding lasts one firing, which repeated self? accesses already amounted to. This surfaced only after the type errors in this file were cleared: the sending diagnostic is a SIL pass that runs after type checking succeeds, so it never reached this code before. --- .../Sources/CodeEditDocument/CodeFileDocument.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift index ce45964e31..77b781a4e2 100644 --- a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift @@ -294,13 +294,16 @@ public final class CodeFileDocument: NSDocument, ObservableObject { if self.hasUnautosavedChanges { guard autosaveTimer == nil else { return } autosaveTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] timer in - self?.autosaveTimerLock.withLock { + // Bound once, rather than optional-chained per access: the isolation below + // cannot state itself for a value the escaping timer block still shares. + guard let document = self else { return } + document.autosaveTimerLock.withLock { guard timer.isValid else { return } - self?.autosaveTimer = nil + document.autosaveTimer = nil // Delivered on the main runloop the timer was scheduled on; assert that // rather than hopping, which would fire outside the lock. MainActor.assumeIsolated { - self?.autosave(withDelegate: nil, didAutosave: nil, contextInfo: nil) + document.autosave(withDelegate: nil, didAutosave: nil, contextInfo: nil) } } } From 97af3d7f1db1296d53f405ea3d024c0ad0ba9d71 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 14:51:53 +0200 Subject: [PATCH 334/335] Fix: Make CodeFileDocument's four isolation sites consistent read(from:ofType:) used a bare MainActor.assumeIsolated, justified by canConcurrentlyReadDocuments(ofType:) being pinned to false. That justification was wrong: the pin constrains AppKit's own reads and says nothing about an in-process caller constructing a document off the main actor. Commit 1985839f records that exact shape trapping here before and taking twenty unit tests down with it. All four nonisolated-override sites now branch on Thread.isMainThread and assume isolation only on the main-thread side. read and presentedItemDidChange block with .sync because both must complete before returning; the LSP notifications and undo registration hop with .async as they already did. docs/architecture-decisions.md is corrected too. It previously described the pin as what made the read path sound, which overstated it. --- .../CodeEditDocument/CodeFileDocument.swift | 22 ++++++++++++++----- docs/architecture-decisions.md | 6 ++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift index 77b781a4e2..f98822266c 100644 --- a/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift +++ b/CodeEditModules/Sources/CodeEditDocument/CodeFileDocument.swift @@ -193,20 +193,30 @@ public final class CodeFileDocument: NSDocument, ObservableObject { Self.logger.error("Failed to read file from data using encoding: \(rawEncoding)") return } - // `read(from:ofType:)` overrides a nonisolated `NSDocument` method, but everything below - // touches main-actor state. Reads are main-thread only, which - // `canConcurrentlyReadDocuments(ofType:)` above pins, so stating the isolation is sound. let text = nsString as String - MainActor.assumeIsolated { - self.sourceEncoding = validEncoding + let installContents: @MainActor () -> Void = { [self] in + sourceEncoding = validEncoding if let content { registerContentChangeUndo(fileURL: fileURL, text: text, content: content) content.mutableString.setString(text) } else { - self.content = NSTextStorage(string: text) + content = NSTextStorage(string: text) } notifyLSPDidOpen() } + + // This overrides a nonisolated `NSDocument` method while everything above touches + // main-actor state. `canConcurrentlyReadDocuments(ofType:)` keeps AppKit's own reads on the + // main thread, but that says nothing about an in-process caller constructing a document off + // it, which has happened before and trapped a bare `assumeIsolated` here. So branch, like + // ``notifyLSPDidOpen()`` and ``presentedItemDidChange()`` do. Unlike those, this blocks: + // `NSDocument` requires the document to be loaded by the time `read` returns, so an async + // hop would return an empty document. + if Thread.isMainThread { + MainActor.assumeIsolated { installContents() } + } else { + DispatchQueue.main.sync { MainActor.assumeIsolated { installContents() } } + } } /// The delegate is main-actor isolated, but document reads and closes can happen off the main diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index b35beed6fc..d9b1da0b87 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -140,9 +140,9 @@ That surfaced six pre-existing isolation errors, all in code that is byte-identi Both touch main-actor document state. Three things now hold that together, and none of them is a static guarantee: -1. `canConcurrentlyReadDocuments(ofType:)` is overridden to return `false`, pinning AppKit's default so reads stay on the main thread. Returning `true` would make the isolation unsound with no compile error. -2. `read(from:ofType:)` uses `MainActor.assumeIsolated`, which relies on (1). -3. `presentedItemDidChange()` branches on `Thread.isMainThread`, because it genuinely arrives on the file-presenter thread in production but on the main thread from tests. An unconditional `DispatchQueue.main.sync` deadlocks the second case. +1. `canConcurrentlyReadDocuments(ofType:)` is overridden to return `false`, pinning AppKit's default so its own reads stay on the main thread. Returning `true` would make that half unsound with no compile error. +2. All four sites that touch main-actor state from a nonisolated override branch on `Thread.isMainThread` and use `MainActor.assumeIsolated` on the main-thread side: `read(from:ofType:)`, `presentedItemDidChange()`, `notifyLSPDidOpen()`/`notifyLSPDidClose(_:)`, and `registerContentChangeUndo`. The pin is not load-bearing on its own, because it says nothing about an in-process caller constructing a document off the main actor, which has happened here before and trapped a bare `assumeIsolated`. +3. Two of the four block rather than hop. `read(from:ofType:)` must, because `NSDocument` requires the document loaded by the time it returns; `presentedItemDidChange()` must, or repeated change notifications pile up. Both therefore carry a `DispatchQueue.main.sync`, whose safety rests on no caller blocking the main thread while triggering an off-main read. Nothing enforces that. This is accepted as a bridge so the extraction can land, not as the end state. The real problem is that the type mixes main-actor UI state (`content` is an `NSTextStorage` that SwiftUI observes) with an I/O lifecycle driven from arbitrary threads. From 9b143595743b476fb66ac9ce4b1a6f605d46f951 Mon Sep 17 00:00:00 2001 From: Matthijs Eikelenboom Date: Sat, 22 Aug 2026 15:20:19 +0200 Subject: [PATCH 335/335] Docs: Record why the app is not sandboxed Five build configs carry ENABLE_APP_SANDBOX = NO and the entitlements file has no app-sandbox key, with nothing in the repo saying why. A reviewer had no way to tell a deliberate decision from a leftover. The sandbox blocks Process from spawning subprocesses, which is what ShellClient, CETerminal, and every git path depend on, so this is the project's long-standing configuration rather than a workaround. PR #2147 enabled it by accident in December as part of an unrelated fix and broke git, LSP, the terminal, and package installs; commit a2fff0c9 reverted that. Recording the history so the next person does not repeat it. Also notes the two consequences: App Store distribution is out of scope, and the security-scoped bookmark handling is a deliberate no-op kept for the case where this is revisited. --- docs/architecture-decisions.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index d9b1da0b87..da8bf0b653 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -152,3 +152,23 @@ That is a redesign of the document's state ownership and is deliberately deferre The general lesson is worth stating separately, because it applies to every future extraction: **moving a file into `CodeEditModules` is also a Swift 6 migration of that file.** The app target is `SWIFT_VERSION = 5.0` with no `SWIFT_STRICT_CONCURRENCY` setting, so it defaults to `minimal`; the package is `swift-tools-version: 6.0`, so every target defaults to Swift 6 language mode. Code that compiled without complaint for years can arrive in a package with a dozen errors, none of them regressions. + +## The app is not sandboxed + +`ENABLE_APP_SANDBOX = NO` on all five app and app-hosted-test build configurations, and +`CodeEdit.entitlements` carries no `com.apple.security.app-sandbox` key. +This is deliberate and is the project's long-standing configuration, not a workaround left in place. + +The App Sandbox blocks `Process` from spawning subprocesses, and CodeEdit's core features are built on exactly that. +`ShellClient` spawns `/bin/zsh`; `CETerminal` (`Shell`, `CELocalShellTerminalView`) runs the user's shell; `RepositoryCloner` and the rest of source control shell out to `git`, which itself shims through `xcrun`. +Sandboxed, all of these fail with `xcrun: error: cannot be used within an App Sandbox.` + +The history is worth recording because it has already been changed once by accident. +Community PR #2147 (commit `78c3be9c`, 2025-12-12) enabled the sandbox as part of an unrelated deprecations and memory-leak fix, which broke git, LSP, the terminal, and package installs. +Commit `a2fff0c9` reverted to the pre-#2147 configuration and restored the `com.apple.security.cs.allow-jit` and `com.apple.security.cs.disable-library-validation` exceptions it had removed. +Anyone tempted to enable the sandbox should read this section first: it is not a build-setting toggle. + +Two consequences follow. +Mac App Store distribution is out of scope, since the store requires sandboxing, and getting there would mean rearchitecting every subprocess path rather than flipping a flag. +And the security-scoped bookmark handling added for recents (`WorkspaceFactory`, `Workspace.tearDown`) is a no-op while unsandboxed, because `startAccessingSecurityScopedResource()` returns `false`; it is kept so the code stays correct if this decision is ever revisited. +