Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions CodeEdit/Features/Editor/Models/EditorInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Void, Never>()

private weak var textViewController: TextViewController?

init() { }
Expand All @@ -136,6 +141,7 @@ class EditorInstance: ObservableObject, Hashable {
if controller.isEditable && controller.isSelectable {
controller.view.window?.makeFirstResponder(controller.textView)
}
controllerDidAppearSubject.send()
}

func destroy() {
Expand All @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion CodeEdit/Features/Editor/Views/CodeFileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ struct CodeFileView: View {
)
},
set: { newState in
editorInstance.cursorPositions = newState.cursorPositions ?? []
if let cursorPositions = newState.cursorPositions {
editorInstance.cursorPositions = cursorPositions
}
Comment thread
jkaunert marked this conversation as resolved.
editorInstance.scrollPosition = newState.scrollPosition
editorInstance.findText = newState.findText
editorInstance.findTextSubject.send(newState.findText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct StatusBarCursorPositionLabel: View {
Group {
if let currentTab = tab {
LineLabel(editorInstance: currentTab)
.id(ObjectIdentifier(currentTab))
} else {
Text("").accessibilityLabel("No Selection")
}
Expand All @@ -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
}
Comment thread
jkaunert marked this conversation as resolved.
}

struct LineLabel: View {
Expand All @@ -54,6 +63,7 @@ struct StatusBarCursorPositionLabel: View {

init(editorInstance: EditorInstance) {
self.editorInstance = editorInstance
self._cursorPositions = State(initialValue: editorInstance.cursorPositions)
}

var body: some View {
Expand All @@ -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 {
Expand All @@ -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 ""
}
Expand Down Expand Up @@ -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: \(cursorPositions[0].range.length)"
}
return "Line: 1 Col: 1"
}
Comment thread
jkaunert marked this conversation as resolved.

return "Line: \(cursorPositions[0].start.line) Col: \(cursorPositions[0].start.column)"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
jkaunert marked this conversation as resolved.
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
Expand All @@ -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)
}
Comment thread
jkaunert marked this conversation as resolved.

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
}
Comment thread
jkaunert marked this conversation as resolved.

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)
}
Comment thread
jkaunert marked this conversation as resolved.
}