Skip to content

Commit

Permalink
Shift-Click to Extend Selection
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter committed Aug 21, 2024
1 parent eb1d382 commit 12be418
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 78 deletions.
15 changes: 15 additions & 0 deletions Sources/CodeEditTextView/Extensions/NSRange+/NSRange+init.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// NSRange.swift
// CodeEditTextView
//
// Created by Khan Winter on 8/20/24.
//

import Foundation

extension NSRange {
@inline(__always)
init(start: Int, end: Int) {
self.init(location: start, length: end - start)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension NSString {
var contentsEnd: Int = NSNotFound
self.getLineStart(nil, end: &end, contentsEnd: &contentsEnd, for: range)
if end != NSNotFound && contentsEnd != NSNotFound && end != contentsEnd {
return NSRange(location: contentsEnd, length: end - contentsEnd)
return NSRange(start: contentsEnd, end: end)
} else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
if !string.isEmpty {
var index = 0
while let nextLine = (string as NSString).getNextLine(startingAt: index) {
let lineRange = NSRange(location: index, length: nextLine.max - index)
let lineRange = NSRange(start: index, end: nextLine.max)
applyLineInsert((string as NSString).substring(with: lineRange) as NSString, at: range.location + index)
index = nextLine.max
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,7 @@ extension TextLayoutManager {
let originalHeight = lineStorage.height

for linePosition in lineStorage.linesInRange(
NSRange(
location: startingLinePosition.range.location,
length: linePosition.range.max - startingLinePosition.range.location
)
NSRange(start: startingLinePosition.range.location, end: linePosition.range.max)
) {
let height = ensureLayoutFor(position: linePosition)
if height != linePosition.height {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodeEditTextView/TextLine/Typesetter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ final class Typesetter {
constrainingWidth: maxWidth
)
let lineFragment = typesetLine(
range: NSRange(location: startIndex, length: lineBreak - startIndex),
range: NSRange(start: startIndex, end: lineBreak),
lineHeightMultiplier: lineHeightMultiplier
)
lines.append(.init(
Expand Down
17 changes: 17 additions & 0 deletions Sources/CodeEditTextView/TextSelectionManager/Destination.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Destination.swift
// CodeEditTextView
//
// Created by Khan Winter on 8/20/24.
//

public extension TextSelectionManager {
public enum Destination {
case character
case word
case line
case visualLine
case page
case document
}
}
15 changes: 15 additions & 0 deletions Sources/CodeEditTextView/TextSelectionManager/Direction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Direction.swift
// CodeEditTextView
//
// Created by Khan Winter on 8/20/24.
//

public extension TextSelectionManager {
public enum Direction {
case up
case down
case forward
case backward
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ package extension TextSelectionManager {
return extendSelectionVisualLine(string: string, from: offset, delta: delta)
case .document:
if delta > 0 {
return NSRange(location: offset, length: string.length - offset)
return NSRange(start: offset, end: string.length)
} else {
return NSRange(location: 0, length: offset)
}
Expand Down Expand Up @@ -194,8 +194,8 @@ package extension TextSelectionManager {
delta: Int
) -> NSRange {
var foundRange = NSRange(
location: min(lineBound, offset),
length: max(lineBound, offset) - min(lineBound, offset)
start: min(lineBound, offset),
end: max(lineBound, offset)
)
let originalFoundRange = foundRange

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ package extension TextSelectionManager {
if up && layoutManager?.lineStorage.first?.range.contains(offset) ?? false {
return NSRange(location: 0, length: offset)
} else if !up && layoutManager?.lineStorage.last?.range.contains(offset) ?? false {
return NSRange(location: offset, length: (textStorage?.length ?? 0) - offset)
return NSRange(start: offset, end: (textStorage?.length ?? offset))
}

switch destination {
Expand All @@ -42,7 +42,7 @@ package extension TextSelectionManager {
if up {
return NSRange(location: 0, length: offset)
} else {
return NSRange(location: offset, length: (textStorage?.length ?? 0) - offset)
return NSRange(start: offset, end: (textStorage?.length ?? offset))
}
}
}
Expand Down Expand Up @@ -107,10 +107,8 @@ package extension TextSelectionManager {
return NSRange(location: offset, length: 0)
}
return NSRange(
location: up ? nextLine.range.location : offset,
length: up
? offset - nextLine.range.location
: nextLine.range.max - offset - (layoutManager?.detectedLineEnding.length ?? 0)
start: up ? nextLine.range.location : offset,
end: up ? offset : nextLine.range.max - (layoutManager?.detectedLineEnding.length ?? 0)
)
}
}
Expand Down Expand Up @@ -142,7 +140,7 @@ package extension TextSelectionManager {
}

if delta > 0 {
return NSRange(location: nextPageOffset, length: offset - nextPageOffset)
return NSRange(start: nextPageOffset, end: offset)
} else {
return NSRange(location: offset, length: nextPageOffset - offset)
}
Expand Down
46 changes: 46 additions & 0 deletions Sources/CodeEditTextView/TextSelectionManager/TextSelection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// TextSelection.swift
// CodeEditTextView
//
// Created by Khan Winter on 8/20/24.
//

import Foundation
import AppKit

public extension TextSelectionManager {
public class TextSelection: Hashable, Equatable {
public var range: NSRange
weak var view: NSView?
var boundingRect: CGRect = .zero
var suggestedXPos: CGFloat?
/// The position this selection should 'rotate' around when modifying selections.
var pivot: Int?

init(range: NSRange, view: CursorView? = nil) {
self.range = range
self.view = view
}

var isCursor: Bool {
range.length == 0
}

public func hash(into hasher: inout Hasher) {
hasher.combine(range)
}

public static func == (lhs: TextSelection, rhs: TextSelection) -> Bool {
lhs.range == rhs.range
}
}
}

private extension TextSelectionManager.TextSelection {
func didInsertText(length: Int, retainLength: Bool = false) {
if !retainLength {
range.length = 0
}
range.location += length
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,50 +19,6 @@ public protocol TextSelectionManagerDelegate: AnyObject {
/// Draws selections using a draw method similar to the `TextLayoutManager` class, and adds cursor views when
/// appropriate.
public class TextSelectionManager: NSObject {
// MARK: - TextSelection

public class TextSelection: Hashable, Equatable {
public var range: NSRange
weak var view: NSView?
var boundingRect: CGRect = .zero
var suggestedXPos: CGFloat?
/// The position this selection should 'rotate' around when modifying selections.
var pivot: Int?

init(range: NSRange, view: CursorView? = nil) {
self.range = range
self.view = view
}

var isCursor: Bool {
range.length == 0
}

public func hash(into hasher: inout Hasher) {
hasher.combine(range)
}

public static func == (lhs: TextSelection, rhs: TextSelection) -> Bool {
lhs.range == rhs.range
}
}

public enum Destination {
case character
case word
case line
case visualLine
case page
case document
}

public enum Direction {
case up
case down
case forward
case backward
}

// MARK: - Properties

// swiftlint:disable:next line_length
Expand Down Expand Up @@ -107,7 +63,9 @@ public class TextSelectionManager: NSObject {
}

// MARK: - Selected Ranges


Check failure on line 66 in Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
/// Set the selected ranges to a single range. Overrides any existing selections.
/// - Parameter range: The range to set.
public func setSelectedRange(_ range: NSRange) {
textSelections.forEach { $0.view?.removeFromSuperview() }
let selection = TextSelection(range: range)
Expand All @@ -119,6 +77,8 @@ public class TextSelectionManager: NSObject {
}
}

/// Set the selected ranges to new ranges. Overrides any existing selections.
/// - Parameter range: The selected ranges to set.
public func setSelectedRanges(_ ranges: [NSRange]) {
textSelections.forEach { $0.view?.removeFromSuperview() }
// Remove duplicates, invalid ranges, update suggested X position.
Expand All @@ -137,7 +97,9 @@ public class TextSelectionManager: NSObject {
NotificationCenter.default.post(Notification(name: Self.selectionChangedNotification, object: self))
}
}


Check failure on line 100 in Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
/// Append a new selected range to the existing ones.
/// - Parameter range: The new range to add.
public func addSelectedRange(_ range: NSRange) {
let newTextSelection = TextSelection(range: range)
var didHandle = false
Expand Down Expand Up @@ -336,14 +298,3 @@ public class TextSelectionManager: NSObject {
context.restoreGState()
}
}

// MARK: - Private TextSelection

private extension TextSelectionManager.TextSelection {
func didInsertText(length: Int, retainLength: Bool = false) {
if !retainLength {
range.length = 0
}
range.location += length
}
}
41 changes: 41 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Mouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,35 @@ extension TextView {

switch event.clickCount {
case 1:
// Single click, if control-shift we add a cursor
// if shift, we extend the selection to the click location
// else we set the cursor
guard isEditable else {
super.mouseDown(with: event)
return
}
if event.modifierFlags.intersection(.deviceIndependentFlagsMask).isSuperset(of: [.control, .shift]) {
unmarkText()
selectionManager.addSelectedRange(NSRange(location: offset, length: 0))
} else if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.shift) {
unmarkText()
shiftClickExtendSelection(to: offset)
} else {
selectionManager.setSelectedRange(NSRange(location: offset, length: 0))
unmarkTextIfNeeded()
}
case 2:
guard !event.modifierFlags.contains(.shift) else {
super.mouseDown(with: event)
return
}
unmarkText()
selectWord(nil)
case 3:
guard !event.modifierFlags.contains(.shift) else {
super.mouseDown(with: event)
return
}
unmarkText()
selectLine(nil)
default:
Expand Down Expand Up @@ -81,4 +95,31 @@ extension TextView {
self.autoscroll(with: event)
}
}

Check failure on line 98 in Sources/CodeEditTextView/TextView/TextView+Mouse.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
/// Extends the current selection to the offset. Only used when the user shift-clicks a location in the document.
///
/// If the offset is within the selection, trims the selection from the nearest edge (start or end) towards the
/// clicked offset.
/// Otherwise, extends the selection to the clicked offset.
///
/// - Parameter offset: The offset clicked on.
fileprivate func shiftClickExtendSelection(to offset: Int) {
// Use the last added selection, this is behavior copied from Xcode.
guard var selectedRange = selectionManager.textSelections.last?.range else { return }
if selectedRange.contains(offset) {
if offset - selectedRange.location <= selectedRange.max - offset {
selectedRange.length -= offset - selectedRange.location
selectedRange.location = offset
} else {
selectedRange.length -= selectedRange.max - offset
}
} else {
selectedRange.formUnion(NSRange(
start: min(offset, selectedRange.location),
end: max(offset, selectedRange.max)
))
}
selectionManager.setSelectedRange(selectedRange)
setNeedsDisplay()
}
}
5 changes: 1 addition & 4 deletions Sources/CodeEditTextView/TextView/TextView+Select.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ extension TextView {
.findNextOccurrenceOfCharacter(in: characterSet.inverted, from: textSelection.range.max) else {
return nil
}
return NSRange(
location: start,
length: end - start
)
return NSRange(start: start, end: end)
}
selectionManager.setSelectedRanges(newSelections)
unmarkTextIfNeeded()
Expand Down

0 comments on commit 12be418

Please sign in to comment.