Skip to content

Commit 4f78bc9

Browse files
committed
Column Selection
1 parent 7d63a64 commit 4f78bc9

File tree

2 files changed

+102
-43
lines changed

2 files changed

+102
-43
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,22 @@ extension TextLayoutManager {
7575
) else {
7676
return nil
7777
}
78-
let fragment = fragmentPosition.data
7978

79+
return textOffsetAtPoint(point, fragmentPosition: fragmentPosition, linePosition: linePosition)
80+
}
81+
82+
func textOffsetAtPoint(
83+
_ point: CGPoint,
84+
fragmentPosition: TextLineStorage<LineFragment>.TextLinePosition,
85+
linePosition: TextLineStorage<TextLine>.TextLinePosition
86+
) -> Int? {
87+
let fragment = fragmentPosition.data
8088
if fragment.width == 0 {
8189
return linePosition.range.location + fragmentPosition.range.location
8290
} else if fragment.width <= point.x - edgeInsets.left {
8391
return findOffsetAfterEndOf(fragmentPosition: fragmentPosition, in: linePosition)
8492
} else {
85-
return findOffsetAtPoint(inFragment: fragment, point: point, inLine: linePosition)
93+
return findOffsetAtPoint(inFragment: fragment, xPos: point.x, inLine: linePosition)
8694
}
8795
}
8896

@@ -125,23 +133,23 @@ extension TextLayoutManager {
125133
/// Finds a document offset for a point that lies in a line fragment.
126134
/// - Parameters:
127135
/// - fragment: The fragment the point lies in.
128-
/// - point: The point being queried, relative to the text view.
136+
/// - xPos: The point being queried, relative to the text view.
129137
/// - linePosition: The position that contains the `fragment`.
130138
/// - Returns: The offset (relative to the document) that's closest to the given point, or `nil` if it could not be
131139
/// found.
132-
private func findOffsetAtPoint(
140+
func findOffsetAtPoint(
133141
inFragment fragment: LineFragment,
134-
point: CGPoint,
142+
xPos: CGFloat,
135143
inLine linePosition: TextLineStorage<TextLine>.TextLinePosition
136144
) -> Int? {
137-
guard let (content, contentPosition) = fragment.findContent(atX: point.x - edgeInsets.left) else {
145+
guard let (content, contentPosition) = fragment.findContent(atX: xPos - edgeInsets.left) else {
138146
return nil
139147
}
140148
switch content.data {
141149
case .text(let ctLine):
142150
let fragmentIndex = CTLineGetStringIndexForPosition(
143151
ctLine,
144-
CGPoint(x: point.x - edgeInsets.left - contentPosition.xPos, y: fragment.height/2)
152+
CGPoint(x: xPos - edgeInsets.left - contentPosition.xPos, y: fragment.height/2)
145153
)
146154
return fragmentIndex + contentPosition.offset + linePosition.range.location
147155
case .attachment:

Sources/CodeEditTextView/TextView/TextView+Mouse.swift

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ extension TextView {
4141
super.mouseDown(with: event)
4242
return
4343
}
44-
if event.modifierFlags.intersection(.deviceIndependentFlagsMask).isSuperset(of: [.control, .shift]) {
44+
let eventFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
45+
if eventFlags == [.control, .shift] {
4546
unmarkText()
4647
selectionManager.addSelectedRange(NSRange(location: offset, length: 0))
47-
} else if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.shift) {
48+
} else if eventFlags.contains(.shift) {
4849
unmarkText()
4950
shiftClickExtendSelection(to: offset)
5051
} else {
@@ -96,40 +97,11 @@ extension TextView {
9697
return
9798
}
9899

99-
switch cursorSelectionMode {
100-
case .character:
101-
selectionManager.setSelectedRange(
102-
NSRange(
103-
location: min(startPosition, endPosition),
104-
length: max(startPosition, endPosition) - min(startPosition, endPosition)
105-
)
106-
)
107-
108-
case .word:
109-
let startWordRange = findWordBoundary(at: startPosition)
110-
let endWordRange = findWordBoundary(at: endPosition)
111-
112-
selectionManager.setSelectedRange(
113-
NSRange(
114-
location: min(startWordRange.location, endWordRange.location),
115-
length: max(startWordRange.location + startWordRange.length,
116-
endWordRange.location + endWordRange.length) -
117-
min(startWordRange.location, endWordRange.location)
118-
)
119-
)
120-
121-
case .line:
122-
let startLineRange = findLineBoundary(at: startPosition)
123-
let endLineRange = findLineBoundary(at: endPosition)
124-
125-
selectionManager.setSelectedRange(
126-
NSRange(
127-
location: min(startLineRange.location, endLineRange.location),
128-
length: max(startLineRange.location + startLineRange.length,
129-
endLineRange.location + endLineRange.length) -
130-
min(startLineRange.location, endLineRange.location)
131-
)
132-
)
100+
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
101+
if modifierFlags.contains(.option) {
102+
dragColumnSelection(mouseDragAnchor: mouseDragAnchor, event: event)
103+
} else {
104+
dragSelection(startPosition: startPosition, endPosition: endPosition, mouseDragAnchor: mouseDragAnchor)
133105
}
134106

135107
setNeedsDisplay()
@@ -182,4 +154,83 @@ extension TextView {
182154
mouseDragTimer?.invalidate()
183155
mouseDragTimer = nil
184156
}
157+
158+
private func dragSelection(startPosition: Int, endPosition: Int, mouseDragAnchor: CGPoint) {
159+
switch cursorSelectionMode {
160+
case .character:
161+
selectionManager.setSelectedRange(
162+
NSRange(
163+
location: min(startPosition, endPosition),
164+
length: max(startPosition, endPosition) - min(startPosition, endPosition)
165+
)
166+
)
167+
168+
case .word:
169+
let startWordRange = findWordBoundary(at: startPosition)
170+
let endWordRange = findWordBoundary(at: endPosition)
171+
172+
selectionManager.setSelectedRange(
173+
NSRange(
174+
location: min(startWordRange.location, endWordRange.location),
175+
length: max(startWordRange.location + startWordRange.length,
176+
endWordRange.location + endWordRange.length) -
177+
min(startWordRange.location, endWordRange.location)
178+
)
179+
)
180+
181+
case .line:
182+
let startLineRange = findLineBoundary(at: startPosition)
183+
let endLineRange = findLineBoundary(at: endPosition)
184+
185+
selectionManager.setSelectedRange(
186+
NSRange(
187+
location: min(startLineRange.location, endLineRange.location),
188+
length: max(startLineRange.location + startLineRange.length,
189+
endLineRange.location + endLineRange.length) -
190+
min(startLineRange.location, endLineRange.location)
191+
)
192+
)
193+
}
194+
}
195+
196+
private func dragColumnSelection(mouseDragAnchor: CGPoint, event: NSEvent) {
197+
// Drag the selection and select in columns
198+
let eventLocation = convert(event.locationInWindow, from: nil)
199+
200+
let start = CGPoint(
201+
x: min(mouseDragAnchor.x, eventLocation.x),
202+
y: min(mouseDragAnchor.y, eventLocation.y)
203+
)
204+
let end = CGPoint(
205+
x: max(mouseDragAnchor.x, eventLocation.x),
206+
y: max(mouseDragAnchor.y, eventLocation.y)
207+
)
208+
209+
// Collect all overlapping text ranges
210+
var selectedRanges: [NSRange] = layoutManager.linesStartingAt(start.y, until: end.y).flatMap { textLine in
211+
// Collect fragment ranges
212+
return textLine.data.lineFragments.compactMap { lineFragment -> NSRange? in
213+
let startOffset = self.layoutManager.textOffsetAtPoint(
214+
start,
215+
fragmentPosition: lineFragment,
216+
linePosition: textLine
217+
)
218+
let endOffset = self.layoutManager.textOffsetAtPoint(
219+
end,
220+
fragmentPosition: lineFragment,
221+
linePosition: textLine
222+
)
223+
guard let startOffset, let endOffset else { return nil }
224+
225+
return NSRange(start: startOffset, end: endOffset)
226+
}
227+
}
228+
229+
// If we have some non-cursor selections, filter out any cursor selections
230+
if selectedRanges.contains(where: { !$0.isEmpty }) {
231+
selectedRanges = selectedRanges.filter({ !$0.isEmpty })
232+
}
233+
234+
selectionManager.setSelectedRanges(selectedRanges)
235+
}
185236
}

0 commit comments

Comments
 (0)