Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct CodeEditTextViewExampleDocument: FileDocument {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
text = String(decoding: data, as: UTF8.self)
text = String(bytes: data, encoding: .utf8)
}

func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
Expand Down
32 changes: 16 additions & 16 deletions Sources/CodeEditTextView/EmphasizeAPI/EmphasizeAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import AppKit
public class EmphasizeAPI {
// MARK: - Properties

private var highlightedRanges: [EmphasizedRange] = []
private var emphasizedRangeIndex: Int?
public private(set) var emphasizedRanges: [EmphasizedRange] = []
public private(set) var emphasizedRangeIndex: Int?
private let activeColor: NSColor = NSColor(hex: 0xFFFB00, alpha: 1)
private let inactiveColor: NSColor = NSColor.lightGray.withAlphaComponent(0.4)

Expand All @@ -23,8 +23,8 @@ public class EmphasizeAPI {
}

// MARK: - Structs
private struct EmphasizedRange {
var range: NSRange
public struct EmphasizedRange {
public var range: NSRange
var layer: CAShapeLayer
}

Expand Down Expand Up @@ -61,18 +61,18 @@ public class EmphasizeAPI {
let layer = createEmphasizeLayer(shapePath: shapePath, active: active)
textView?.layer?.insertSublayer(layer, at: 1)

highlightedRanges.append(EmphasizedRange(range: range, layer: layer))
emphasizedRanges.append(EmphasizedRange(range: range, layer: layer))
}

/// Removes the highlight for a specific range.
/// - Parameter range: The range to remove.
public func removeHighlightForRange(_ range: NSRange) {
guard let index = highlightedRanges.firstIndex(where: { $0.range == range }) else { return }
guard let index = emphasizedRanges.firstIndex(where: { $0.range == range }) else { return }

let removedLayer = highlightedRanges[index].layer
let removedLayer = emphasizedRanges[index].layer
removedLayer.removeFromSuperlayer()

highlightedRanges.remove(at: index)
emphasizedRanges.remove(at: index)

// Adjust the active highlight index
if let currentIndex = emphasizedRangeIndex {
Expand Down Expand Up @@ -105,8 +105,8 @@ public class EmphasizeAPI {

/// Removes all emphasised ranges.
public func removeEmphasizeLayers() {
highlightedRanges.forEach { $0.layer.removeFromSuperlayer() }
highlightedRanges.removeAll()
emphasizedRanges.forEach { $0.layer.removeFromSuperlayer() }
emphasizedRanges.removeAll()
emphasizedRangeIndex = nil
}

Expand Down Expand Up @@ -147,29 +147,29 @@ public class EmphasizeAPI {
/// - Returns: An optional `NSRange` representing the newly active highlight, colored in the active color.
/// Returns `nil` if no change occurred (e.g., if there are no highlighted ranges).
private func shiftActiveHighlight(amount: Int) -> NSRange? {
guard !highlightedRanges.isEmpty else { return nil }
guard !emphasizedRanges.isEmpty else { return nil }

var currentIndex = emphasizedRangeIndex ?? -1
currentIndex = (currentIndex + amount + highlightedRanges.count) % highlightedRanges.count
currentIndex = (currentIndex + amount + emphasizedRanges.count) % emphasizedRanges.count

guard currentIndex < highlightedRanges.count else { return nil }
guard currentIndex < emphasizedRanges.count else { return nil }

// Reset the previously active layer
if let currentIndex = emphasizedRangeIndex {
let previousLayer = highlightedRanges[currentIndex].layer
let previousLayer = emphasizedRanges[currentIndex].layer
previousLayer.fillColor = inactiveColor.cgColor
previousLayer.shadowOpacity = 0.0
}

// Set the new active layer
let newLayer = highlightedRanges[currentIndex].layer
let newLayer = emphasizedRanges[currentIndex].layer
newLayer.fillColor = activeColor.cgColor
newLayer.shadowOpacity = 0.3

applyPopAnimation(to: newLayer)
emphasizedRangeIndex = currentIndex

return highlightedRanges[currentIndex].range
return emphasizedRanges[currentIndex].range
}

private func applyPopAnimation(to layer: CALayer) {
Expand Down
64 changes: 64 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import AppKit

extension TextView {
fileprivate typealias Direction = TextSelectionManager.Direction
Expand Down Expand Up @@ -36,6 +37,69 @@ extension TextView {
}
}

/// Scrolls the view to the specified range.
///
/// - Parameters:
/// - range: The range to scroll to.
/// - center: A flag that determines if the range should be centered in the view. Defaults to `true`.
///
/// If `center` is `true`, the range will be centered in the visible area.
/// If `center` is `false`, the range will be aligned at the top-left of the view.
public func scrollToRange(_ range: NSRange, center: Bool = true) {
guard let scrollView else { return }

guard let boundingRect = layoutManager.rectForOffset(range.location) else { return }

// Check if the range is already visible
if visibleRect.contains(boundingRect) {
return // No scrolling needed
}

// Calculate the target offset based on the center flag
let targetOffset: CGPoint
if center {
targetOffset = CGPoint(
x: max(boundingRect.midX - visibleRect.width / 2, 0),
y: max(boundingRect.midY - visibleRect.height / 2, 0)
)
} else {
targetOffset = CGPoint(
x: max(boundingRect.origin.x, 0),
y: max(boundingRect.origin.y, 0)
)
}

var lastFrame: CGRect = .zero

// Set a timeout to avoid an infinite loop
let timeout: TimeInterval = 0.5
let startTime = Date()

// Adjust layout until stable
while let newRect = layoutManager.rectForOffset(range.location),
lastFrame != newRect,
Date().timeIntervalSince(startTime) < timeout {
lastFrame = newRect
layoutManager.layoutLines()
selectionManager.updateSelectionViews()
selectionManager.drawSelections(in: visibleRect)
}

// Scroll to make the range appear at the desired position
if lastFrame != .zero {
let animated = false // feature flag
if animated {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.15 // Adjust duration as needed
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
scrollView.contentView.animator().setBoundsOrigin(targetOffset)
}
} else {
scrollView.contentView.scroll(to: targetOffset)
}
}
}

/// Get the selection that should be scrolled to visible for the current text selection.
/// - Returns: The the selection to scroll to.
private func getSelection() -> TextSelection? {
Expand Down