@@ -86,50 +86,29 @@ extension TextView {
8686 return
8787 }
8888
89+ // We receive global events because our view received the drag event, but we need to clamp the potentially
90+ // out-of-bounds positions to a position our layout manager can deal with.
91+ let locationInWindow = convert ( event. locationInWindow, from: nil )
92+ let locationInView = CGPoint (
93+ x: max ( 0.0 , min ( locationInWindow. x, frame. width) ) ,
94+ y: max ( 0.0 , min ( locationInWindow. y, frame. height) )
95+ )
96+
8997 if mouseDragAnchor == nil {
90- mouseDragAnchor = convert ( event . locationInWindow , from : nil )
98+ mouseDragAnchor = locationInView
9199 super. mouseDragged ( with: event)
92100 } else {
93101 guard let mouseDragAnchor,
94102 let startPosition = layoutManager. textOffsetAtPoint ( mouseDragAnchor) ,
95- let endPosition = layoutManager. textOffsetAtPoint ( convert ( event . locationInWindow , from : nil ) ) else {
103+ let endPosition = layoutManager. textOffsetAtPoint ( locationInView ) else {
96104 return
97105 }
98106
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- )
107+ let modifierFlags = event. modifierFlags. intersection ( . deviceIndependentFlagsMask)
108+ if modifierFlags. contains ( . option) {
109+ dragColumnSelection ( mouseDragAnchor: mouseDragAnchor, locationInView: locationInView)
110+ } else {
111+ dragSelection ( startPosition: startPosition, endPosition: endPosition, mouseDragAnchor: mouseDragAnchor)
133112 }
134113
135114 setNeedsDisplay ( )
@@ -182,4 +161,81 @@ extension TextView {
182161 mouseDragTimer? . invalidate ( )
183162 mouseDragTimer = nil
184163 }
164+
165+ private func dragSelection( startPosition: Int , endPosition: Int , mouseDragAnchor: CGPoint ) {
166+ switch cursorSelectionMode {
167+ case . character:
168+ selectionManager. setSelectedRange (
169+ NSRange (
170+ location: min ( startPosition, endPosition) ,
171+ length: max ( startPosition, endPosition) - min( startPosition, endPosition)
172+ )
173+ )
174+
175+ case . word:
176+ let startWordRange = findWordBoundary ( at: startPosition)
177+ let endWordRange = findWordBoundary ( at: endPosition)
178+
179+ selectionManager. setSelectedRange (
180+ NSRange (
181+ location: min ( startWordRange. location, endWordRange. location) ,
182+ length: max ( startWordRange. location + startWordRange. length,
183+ endWordRange. location + endWordRange. length) -
184+ min( startWordRange. location, endWordRange. location)
185+ )
186+ )
187+
188+ case . line:
189+ let startLineRange = findLineBoundary ( at: startPosition)
190+ let endLineRange = findLineBoundary ( at: endPosition)
191+
192+ selectionManager. setSelectedRange (
193+ NSRange (
194+ location: min ( startLineRange. location, endLineRange. location) ,
195+ length: max ( startLineRange. location + startLineRange. length,
196+ endLineRange. location + endLineRange. length) -
197+ min( startLineRange. location, endLineRange. location)
198+ )
199+ )
200+ }
201+ }
202+
203+ private func dragColumnSelection( mouseDragAnchor: CGPoint , locationInView: CGPoint ) {
204+ // Drag the selection and select in columns
205+ let start = CGPoint (
206+ x: min ( mouseDragAnchor. x, locationInView. x) ,
207+ y: min ( mouseDragAnchor. y, locationInView. y)
208+ )
209+ let end = CGPoint (
210+ x: max ( mouseDragAnchor. x, locationInView. x) ,
211+ y: max ( mouseDragAnchor. y, locationInView. y)
212+ )
213+
214+ // Collect all overlapping text ranges
215+ var selectedRanges : [ NSRange ] = layoutManager. linesStartingAt ( start. y, until: end. y) . flatMap { textLine in
216+ // Collect fragment ranges
217+ return textLine. data. lineFragments. compactMap { lineFragment -> NSRange ? in
218+ let startOffset = self . layoutManager. textOffsetAtPoint (
219+ start,
220+ fragmentPosition: lineFragment,
221+ linePosition: textLine
222+ )
223+ let endOffset = self . layoutManager. textOffsetAtPoint (
224+ end,
225+ fragmentPosition: lineFragment,
226+ linePosition: textLine
227+ )
228+ guard let startOffset, let endOffset else { return nil }
229+
230+ return NSRange ( start: startOffset, end: endOffset)
231+ }
232+ }
233+
234+ // If we have some non-cursor selections, filter out any cursor selections
235+ if selectedRanges. contains ( where: { !$0. isEmpty } ) {
236+ selectedRanges = selectedRanges. filter ( { !$0. isEmpty } )
237+ }
238+
239+ selectionManager. setSelectedRanges ( selectedRanges)
240+ }
185241}
0 commit comments