From bb6aed3a2b322afa2babb403b8e315e5bc2f3391 Mon Sep 17 00:00:00 2001 From: joshua kaunert <44586402+jkaunert@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:35:54 -0500 Subject: [PATCH 1/2] Fix initial status bar cursor position --- .../Editor/Models/EditorInstance.swift | 16 +++++- .../Features/Editor/Views/CodeFileView.swift | 4 +- .../StatusBarCursorPositionLabel.swift | 24 +++++++- .../ProjectNavigatorUITests.swift | 56 +++++++++++++++++++ 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index fd11333bb7..fd6f9d016f 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -14,6 +14,8 @@ 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 { + private static let defaultCursorPositions = [CursorPosition(line: 1, column: 1)] + /// The file presented in this editor instance. let file: CEWorkspaceFile @@ -43,9 +45,10 @@ class EditorInstance: ObservableObject, Hashable { replaceText = workspace?.searchState?.replaceText replaceTextSubject = PassthroughSubject() - self.cursorPositions = ( - cursorPositions ?? editorState?.editorCursorPositions ?? [CursorPosition(line: 1, column: 1)] - ) + let restoredCursorPositions = editorState?.editorCursorPositions + self.cursorPositions = cursorPositions + ?? (restoredCursorPositions?.isEmpty == false ? restoredCursorPositions : nil) + ?? Self.defaultCursorPositions self.scrollPosition = editorState?.scrollPosition // Setup listeners @@ -124,6 +127,8 @@ class EditorInstance: ObservableObject, Hashable { /// Translates ranges (eg: from a cursor position) to other information like the number of lines in a range. class RangeTranslator: TextViewCoordinator { + let controllerDidAppearSubject = PassthroughSubject() + private weak var textViewController: TextViewController? init() { } @@ -136,6 +141,7 @@ class EditorInstance: ObservableObject, Hashable { if controller.isEditable && controller.isSelectable { controller.view.window?.makeFirstResponder(controller.textView) } + controllerDidAppearSubject.send() } func destroy() { @@ -158,6 +164,10 @@ class EditorInstance: ObservableObject, Hashable { return (endTextLine.index - startTextLine.index) + 1 } + func resolveCursorPosition(_ cursorPosition: CursorPosition) -> CursorPosition { + textViewController?.resolveCursorPosition(cursorPosition) ?? cursorPosition + } + func moveLinesUp() { guard let controller = textViewController else { return } controller.moveLinesUp() diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index f22f6cce3d..058d63ec01 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -158,7 +158,9 @@ struct CodeFileView: View { ) }, set: { newState in - editorInstance.cursorPositions = newState.cursorPositions ?? [] + if let cursorPositions = newState.cursorPositions { + editorInstance.cursorPositions = cursorPositions + } editorInstance.scrollPosition = newState.scrollPosition editorInstance.findText = newState.findText editorInstance.findTextSubject.send(newState.findText) diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift index 6939fca4ec..e29239dd6c 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift @@ -24,6 +24,7 @@ struct StatusBarCursorPositionLabel: View { Group { if let currentTab = tab { LineLabel(editorInstance: currentTab) + .id(ObjectIdentifier(currentTab)) } else { Text("").accessibilityLabel("No Selection") } @@ -35,9 +36,17 @@ struct StatusBarCursorPositionLabel: View { .onAppear { updateSource() } - .onReceive(editorManager.tabBarTabIdSubject) { _ in + .onReceive(editorManager.activeEditor.objectWillChange) { _ in + DispatchQueue.main.async { + updateSource() + } + } + .onReceive(editorManager.$activeEditor) { _ in updateSource() } + .onChange(of: editorManager.activeEditor.selectedTab) { _, newTab in + tab = newTab + } } struct LineLabel: View { @@ -54,6 +63,7 @@ struct StatusBarCursorPositionLabel: View { init(editorInstance: EditorInstance) { self.editorInstance = editorInstance + self._cursorPositions = State(initialValue: editorInstance.cursorPositions) } var body: some View { @@ -64,6 +74,9 @@ struct StatusBarCursorPositionLabel: View { .onReceive(editorInstance.$cursorPositions) { newValue in self.cursorPositions = newValue } + .onReceive(editorInstance.rangeTranslator.controllerDidAppearSubject) { _ in + self.cursorPositions = editorInstance.cursorPositions + } } private var foregroundColor: Color { @@ -84,6 +97,8 @@ struct StatusBarCursorPositionLabel: View { /// Create a label string for cursor positions. /// - Returns: A string describing the user's location in a document. func getLabel() -> String { + let cursorPositions = cursorPositions.map(editorInstance.rangeTranslator.resolveCursorPosition) + if cursorPositions.isEmpty { return "" } @@ -115,6 +130,13 @@ struct StatusBarCursorPositionLabel: View { } // When there's a single cursor, display the line and column. + if cursorPositions[0].start.line <= 0 || cursorPositions[0].start.column <= 0 { + if cursorPositions[0].range != .notFound && cursorPositions[0].range.location > 0 { + return "Char: \(cursorPositions[0].range.location) Len: 0" + } + return "Line: 1 Col: 1" + } + return "Line: \(cursorPositions[0].start.line) Col: \(cursorPositions[0].start.column)" } } diff --git a/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift b/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift index baac35964a..f0da8d7ad4 100644 --- a/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift +++ b/CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift @@ -39,6 +39,25 @@ final class ProjectNavigatorUITests: XCTestCase { XCTAssertTrue(readmeEditor.exists) XCTAssertNotNil(readmeEditor.value as? String) + let cursorPositionLabel = window.staticTexts["CursorPositionLabel"] + XCTAssertTrue(cursorPositionLabel.waitForExistence(timeout: 2.0), "Cursor position label not found") + assertResolvedCursorPosition(cursorPositionLabel) + + let licenseRow = Query.Navigator.getProjectNavigatorRow(fileTitle: "LICENSE.md", navigator) + XCTAssertFalse(Query.Navigator.rowContainsDisclosureIndicator(licenseRow), "File has disclosure indicator") + licenseRow.click() + + let licenseTab = Query.TabBar.getTab(labeled: "LICENSE.md", tabBar) + XCTAssertTrue(licenseTab.exists) + + let licenseEditor = Query.Window.getFirstEditor(window) + let licenseContent = NSPredicate(format: "value CONTAINS %@", "MIT License") + expectation(for: licenseContent, evaluatedWith: licenseEditor) + waitForExpectations(timeout: 2.0) + + assertResolvedCursorPosition(cursorPositionLabel) + assertCursorPositionChanges(in: licenseEditor, cursorPositionLabel) + let rowCount = navigator.descendants(matching: .outlineRow).count // Open a folder @@ -59,4 +78,41 @@ final class ProjectNavigatorUITests: XCTestCase { XCTAssertTrue(newRowCount > finalRowCount, "Rows were not hidden after closing a folder") XCTAssertEqual(rowCount, finalRowCount, "Different Number of rows loaded") } + + private func assertResolvedCursorPosition(_ cursorPositionLabel: XCUIElement) { + let resolvedCursorPosition = NSPredicate( + format: "value CONTAINS %@ AND NOT value CONTAINS %@", + "Line:", + "-1" + ) + expectation(for: resolvedCursorPosition, evaluatedWith: cursorPositionLabel) + waitForExpectations(timeout: 2.0) + } + + private func assertCursorPositionChanges(in editor: XCUIElement, _ cursorPositionLabel: XCUIElement) { + assertCursorPositionChanges(cursorPositionLabel) { + editor.coordinate(withNormalizedOffset: CGVector(dx: 0.15, dy: 0.15)).click() + } + assertCursorPositionChanges(cursorPositionLabel) { + editor.coordinate(withNormalizedOffset: CGVector(dx: 0.75, dy: 0.75)).click() + } + } + + private func assertCursorPositionChanges(_ cursorPositionLabel: XCUIElement, after action: () -> Void) { + guard let originalValue = cursorPositionLabel.value as? String else { + XCTFail("Cursor position label value not found") + return + } + + action() + + let updatedCursorPosition = NSPredicate( + format: "value CONTAINS %@ AND NOT value CONTAINS %@ AND value != %@", + "Line:", + "-1", + originalValue + ) + expectation(for: updatedCursorPosition, evaluatedWith: cursorPositionLabel) + waitForExpectations(timeout: 2.0) + } } From 090a0e0c252eac8d810600a4d6c8f17ee2f19ad2 Mon Sep 17 00:00:00 2001 From: joshua kaunert <44586402+jkaunert@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:33:15 -0500 Subject: [PATCH 2/2] Address status bar fallback length --- .../Views/StatusBarItems/StatusBarCursorPositionLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift index e29239dd6c..512a9563d1 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift @@ -132,7 +132,7 @@ struct StatusBarCursorPositionLabel: View { // When there's a single cursor, display the line and column. if cursorPositions[0].start.line <= 0 || cursorPositions[0].start.column <= 0 { if cursorPositions[0].range != .notFound && cursorPositions[0].range.location > 0 { - return "Char: \(cursorPositions[0].range.location) Len: 0" + return "Char: \(cursorPositions[0].range.location) Len: \(cursorPositions[0].range.length)" } return "Line: 1 Col: 1" }