Skip to content

Commit a763fa5

Browse files
committed
Make CursorPosition More Flexible
1 parent 22cb5be commit a763fa5

File tree

6 files changed

+115
-63
lines changed

6 files changed

+115
-63
lines changed

Package.resolved

Lines changed: 2 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/CodeEditSourceEditor/Controller/TextViewController+Cursor.swift

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,30 @@ extension TextViewController {
1515
if isPostingCursorNotification { return }
1616
var newSelectedRanges: [NSRange] = []
1717
for position in positions {
18-
let line = position.line
19-
let column = position.column
20-
guard (line > 0 && column > 0) || (position.range != .notFound) else { continue }
18+
guard (position.start.isPositive && position.end?.isPositive ?? true)
19+
|| (position.range != .notFound) else {
20+
continue
21+
}
2122

2223
if position.range == .notFound {
2324
if textView.textStorage.length == 0 {
2425
// If the file is blank, automatically place the cursor in the first index.
2526
newSelectedRanges.append(NSRange(location: 0, length: 0))
26-
} else if let linePosition = textView.layoutManager.textLineForIndex(line - 1) {
27+
} else if let linePosition = textView.layoutManager.textLineForIndex(position.start.line - 1) {
2728
// If this is a valid line, set the new position
28-
let index = linePosition.range.lowerBound + min(linePosition.range.upperBound, column - 1)
29-
newSelectedRanges.append(NSRange(location: index, length: 0))
29+
let startCharacter = linePosition.range.lowerBound + min(
30+
linePosition.range.upperBound,
31+
position.start.column - 1
32+
)
33+
if let end = position.end, let endLine = textView.layoutManager.textLineForIndex(end.line - 1) {
34+
let endCharacter = endLine.range.lowerBound + min(
35+
endLine.range.upperBound,
36+
end.column - 1
37+
)
38+
newSelectedRanges.append(NSRange(start: startCharacter, end: endCharacter))
39+
} else {
40+
newSelectedRanges.append(NSRange(location: startCharacter, length: 0))
41+
}
3042
}
3143
} else {
3244
newSelectedRanges.append(position.range)
@@ -46,9 +58,20 @@ extension TextViewController {
4658
guard let linePosition = textView.layoutManager.textLineForOffset(selectedRange.range.location) else {
4759
continue
4860
}
49-
let column = (selectedRange.range.location - linePosition.range.location) + 1
50-
let line = linePosition.index + 1
51-
positions.append(CursorPosition(range: selectedRange.range, line: line, column: column))
61+
let start = CursorPosition.Position(
62+
line: linePosition.index + 1,
63+
column: (selectedRange.range.location - linePosition.range.location) + 1
64+
)
65+
let end = if !selectedRange.range.isEmpty,
66+
let endPosition = textView.layoutManager.textLineForOffset(selectedRange.range.max) {
67+
CursorPosition.Position(
68+
line: endPosition.index + 1,
69+
column: selectedRange.range.max - endPosition.range.location + 1
70+
)
71+
} else {
72+
CursorPosition.Position?.none
73+
}
74+
positions.append(CursorPosition(range: selectedRange.range, start: start, end: end))
5275
}
5376

5477
isPostingCursorNotification = true
@@ -66,26 +89,43 @@ extension TextViewController {
6689

6790
/// Fills out all properties on the given cursor position if it's missing either the range or line/column
6891
/// information.
69-
func resolveCursorPosition(_ position: CursorPosition) -> CursorPosition? {
92+
public func resolveCursorPosition(_ position: CursorPosition) -> CursorPosition? {
7093
var range = position.range
7194
if range == .notFound {
72-
guard position.line > 0, position.column > 0,
73-
let linePosition = textView.layoutManager.textLineForIndex(position.line - 1) else {
95+
guard position.start.line > 0, position.start.column > 0,
96+
let linePosition = textView.layoutManager.textLineForIndex(position.start.line - 1) else {
7497
return nil
7598
}
76-
range = NSRange(location: linePosition.range.location + position.column, length: 0)
99+
if let end = position.end, let endPosition = textView.layoutManager.textLineForIndex(end.line - 1) {
100+
range = NSRange(
101+
location: linePosition.range.location + position.start.column,
102+
length: linePosition.range.max
103+
)
104+
} else {
105+
range = NSRange(location: linePosition.range.location + position.start.column, length: 0)
106+
}
77107
}
78108

79-
var line = position.line
80-
var column = position.column
81-
if position.line <= 0 || position.column <= 0 {
82-
guard range != .notFound, let linePosition = textView.layoutManager.textLineForOffset(range.location) else {
83-
return nil
84-
}
85-
column = (range.location - linePosition.range.location) + 1
86-
line = linePosition.index + 1
109+
var start: CursorPosition.Position
110+
var end: CursorPosition.Position?
111+
112+
guard let startLinePosition = textView.layoutManager.textLineForOffset(range.location) else {
113+
return nil
114+
}
115+
116+
start = CursorPosition.Position(
117+
line: startLinePosition.index + 1,
118+
column: (range.location - startLinePosition.range.location) + 1
119+
)
120+
121+
if !range.isEmpty {
122+
guard let endLinePosition = textView.layoutManager.textLineForOffset(range.max) else { return nil }
123+
end = CursorPosition.Position(
124+
line: endLinePosition.index + 1,
125+
column: (range.max - endLinePosition.range.location) + 1
126+
)
87127
}
88128

89-
return CursorPosition(range: range, line: line, column: column)
129+
return CursorPosition(range: range, start: start, end: end)
90130
}
91131
}

Sources/CodeEditSourceEditor/JumpToDefinition/JumpToDefinitionDelegate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
import Foundation
99

1010
public protocol JumpToDefinitionDelegate: AnyObject {
11-
func queryLinks(forRange range: NSRange) async -> [JumpToDefinitionLink]?
12-
func openLink(url: URL, targetRange: NSRange)
11+
func queryLinks(forRange range: NSRange, textView: TextViewController) async -> [JumpToDefinitionLink]?
12+
func openLink(link: JumpToDefinitionLink)
1313
}

Sources/CodeEditSourceEditor/JumpToDefinition/JumpToDefinitionLink.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,30 @@ public struct JumpToDefinitionLink: Identifiable, Sendable, CodeSuggestionEntry
1212
public var id: String { url?.absoluteString ?? "\(targetRange)" }
1313
/// Leave as `nil` if the link is in the same document.
1414
public let url: URL?
15-
public let targetPosition: CursorPosition?
16-
public let targetRange: NSRange
15+
public var targetPosition: CursorPosition? {
16+
targetRange
17+
}
18+
public let targetRange: CursorPosition
1719

1820
public let label: String
1921
public let sourcePreview: String?
2022

2123
public let image: Image
2224
public let imageColor: Color
2325

24-
public var detail: String? { nil }
26+
public var detail: String? { url?.lastPathComponent }
2527
public var pathComponents: [String]? { url?.pathComponents ?? [] }
2628
public var deprecated: Bool { false }
2729

2830
public init(
2931
url: URL?,
30-
targetPosition: CursorPosition,
31-
targetRange: NSRange,
32+
targetRange: CursorPosition,
3233
typeName: String,
3334
sourcePreview: String,
3435
image: Image = Image(systemName: "dot.square.fill"),
3536
imageColor: Color = Color(NSColor.lightGray)
3637
) {
3738
self.url = url
38-
self.targetPosition = targetPosition
3939
self.targetRange = targetRange
4040
self.label = typeName
4141
self.sourcePreview = sourcePreview

Sources/CodeEditSourceEditor/JumpToDefinition/JumpToDefinitionModel.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ final class JumpToDefinitionModel {
6262
jumpRequestTask?.cancel()
6363
jumpRequestTask = Task {
6464
currentLinks = nil
65-
guard let links = await delegate?.queryLinks(forRange: location),
65+
guard let controller,
66+
let links = await delegate?.queryLinks(forRange: location, textView: controller),
6667
!links.isEmpty else {
6768
NSSound.beep()
6869
if let textView {
@@ -72,10 +73,10 @@ final class JumpToDefinitionModel {
7273
}
7374
if links.count == 1 {
7475
let link = links[0]
75-
if let url = link.url {
76-
delegate?.openLink(url: url, targetRange: link.targetRange)
76+
if link.url != nil {
77+
delegate?.openLink(link: link)
7778
} else {
78-
textView?.selectionManager.setSelectedRange(link.targetRange)
79+
textView?.selectionManager.setSelectedRange(link.targetRange.range)
7980
}
8081

8182
textView?.scrollSelectionToVisible()
@@ -106,8 +107,10 @@ final class JumpToDefinitionModel {
106107
// MARK: - Local Link
107108

108109
private func openLocalLink(link: JumpToDefinitionLink) {
109-
guard let controller = controller else { return }
110-
controller.textView.selectionManager.setSelectedRange(link.targetRange)
110+
guard let controller = controller, let range = controller.resolveCursorPosition(link.targetRange) else {
111+
return
112+
}
113+
controller.textView.selectionManager.setSelectedRange(range.range)
111114
controller.textView.scrollSelectionToVisible()
112115
}
113116

@@ -183,7 +186,7 @@ extension JumpToDefinitionModel: CodeSuggestionDelegate {
183186
) {
184187
guard let link = item as? JumpToDefinitionLink else { return }
185188
if let url = link.url {
186-
delegate?.openLink(url: url, targetRange: link.targetRange)
189+
delegate?.openLink(link: link)
187190
} else {
188191
openLocalLink(link: link)
189192
}

Sources/CodeEditSourceEditor/Utils/CursorPosition.swift

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ import Foundation
1717
/// controller.
1818
///
1919
public struct CursorPosition: Sendable, Codable, Equatable, Hashable {
20+
public struct Position: Sendable, Codable, Equatable, Hashable {
21+
/// The line the cursor is located at. 1-indexed.
22+
/// If ``CursorPosition/range`` is not empty, this is the line at the beginning of the selection.
23+
public let line: Int
24+
/// The column the cursor is located at. 1-indexed.
25+
/// If ``CursorPosition/range`` is not empty, this is the column at the beginning of the selection.
26+
public let column: Int
27+
28+
public init(line: Int, column: Int) {
29+
self.line = line
30+
self.column = column
31+
}
32+
33+
var isPositive: Bool { line > 0 && column > 0 }
34+
}
35+
2036
/// Initialize a cursor position.
2137
///
2238
/// When this initializer is used, ``CursorPosition/range`` will be initialized to `NSNotFound`.
@@ -28,8 +44,14 @@ public struct CursorPosition: Sendable, Codable, Equatable, Hashable {
2844
/// - column: The column of the cursor position, 1-indexed.
2945
public init(line: Int, column: Int) {
3046
self.range = .notFound
31-
self.line = line
32-
self.column = column
47+
self.start = Position(line: line, column: column)
48+
self.end = nil
49+
}
50+
51+
public init(start: Position, end: Position) {
52+
self.range = .notFound
53+
self.start = start
54+
self.end = end
3355
}
3456

3557
/// Initialize a cursor position.
@@ -41,27 +63,23 @@ public struct CursorPosition: Sendable, Codable, Equatable, Hashable {
4163
/// - Parameter range: The range of the cursor position.
4264
public init(range: NSRange) {
4365
self.range = range
44-
self.line = -1
45-
self.column = -1
66+
self.start = Position(line: -1, column: -1)
67+
self.end = nil
4668
}
4769

4870
/// Private initializer.
4971
/// - Parameters:
5072
/// - range: The range of the position.
51-
/// - line: The line of the position.
52-
/// - column: The column of the position.
53-
package init(range: NSRange, line: Int, column: Int) {
73+
/// - start: The start position of the range.
74+
/// - end: The end position of the range.
75+
package init(range: NSRange, start: Position, end: Position?) {
5476
self.range = range
55-
self.line = line
56-
self.column = column
77+
self.start = start
78+
self.end = end
5779
}
5880

5981
/// The range of the selection.
6082
public let range: NSRange
61-
/// The line the cursor is located at. 1-indexed.
62-
/// If ``CursorPosition/range`` is not empty, this is the line at the beginning of the selection.
63-
public let line: Int
64-
/// The column the cursor is located at. 1-indexed.
65-
/// If ``CursorPosition/range`` is not empty, this is the column at the beginning of the selection.
66-
public let column: Int
83+
public let start: Position
84+
public let end: Position?
6785
}

0 commit comments

Comments
 (0)