Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ An expander offers an up arrow (lines above the next hunk), a down arrow (lines

Expanders only appear once `contextProvider` is set. Errors it throws are reported through `expansionFailureHandler`, and tapping a file header collapses the file and calls `fileCollapseHandler`.

Line selection works like `MarkdownTextView`'s: setting `lineSelectionHandler` / `lineSelectionEndedHandler` enables tap and drag selection over the hunks, and the callbacks receive the file's display path alongside the `LineSelectionInfo` (`nil` when the selection clears). A selection spans one hunk at a time — selecting in another hunk clears the previous one.

## Architecture

The library is split into two modules:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ import Foundation
/// the file.
public var fileCollapseHandler: ((String, Bool) -> Void)?

/// Live line-selection updates: the display path of the file being
/// selected in, and the selection (`nil` when it clears). A selection
/// spans one hunk; selecting in another hunk clears the previous one.
/// Line selection is enabled while either selection handler is set.
public var lineSelectionHandler: ((String, LineSelectionInfo?) -> Void)? {
didSet { reloadEverything() }
}

/// Fires once the selection gesture settles (tap completes, drag ends),
/// with the same arguments as `lineSelectionHandler`.
public var lineSelectionEndedHandler: ((String, LineSelectionInfo?) -> Void)? {
didSet { reloadEverything() }
}

public private(set) var patch: String = ""

private var document = DiffPatchDocument(files: [], language: nil)
Expand All @@ -95,6 +109,9 @@ import Foundation
private var documentGeneration = 0
private var collapsedFileIDs: Set<Int> = []
private var loadingExpansions: Set<ExpansionKey> = []
/// The hunk cell owning the active line selection. `DiffView` clears
/// exclusively within one view; across hunk cells it's enforced here.
private weak var selectionCell: DiffHunkCell?

private struct ExpansionKey: Hashable {
let expander: DiffExpander
Expand Down Expand Up @@ -126,6 +143,7 @@ import Foundation
documentGeneration += 1
collapsedFileIDs = []
loadingExpansions = []
selectionCell = nil
guard isViewLoaded else { return }
applySnapshot(animated: false)
}
Expand Down Expand Up @@ -214,8 +232,15 @@ import Foundation
else { return }
cell.configure(
renderBlock: file.renderBlock(forHunkAt: hunkIndex),
theme: theme
theme: theme,
selectionEnabled: isLineSelectionEnabled
)
cell.onSelectionChanged = { [weak self] cell, info in
self?.hunkSelectionChanged(cell, fileID: fileID, info: info)
}
cell.onSelectionEnded = { [weak self] cell, info in
self?.hunkSelectionEnded(cell, fileID: fileID, info: info)
}
}

let expanderRegistration = UICollectionView.CellRegistration<DiffExpanderCell, DiffFileItem> {
Expand Down Expand Up @@ -347,6 +372,41 @@ import Foundation
}
}

// MARK: - Line selection

private var isLineSelectionEnabled: Bool {
lineSelectionHandler != nil || lineSelectionEndedHandler != nil
}

private func hunkSelectionChanged(_ cell: DiffHunkCell, fileID: Int, info: LineSelectionInfo?) {
guard let path = document.file(withID: fileID)?.displayPath else { return }
if info != nil {
if let previous = selectionCell, previous !== cell {
previous.clearSelection()
}
selectionCell = cell
lineSelectionHandler?(path, info)
return
}
// Only a clear from the active cell counts — a reused or
// reconfigured other cell must not cancel the live selection.
guard selectionCell == nil || selectionCell === cell else { return }
selectionCell = nil
lineSelectionHandler?(path, nil)
}

private func hunkSelectionEnded(_ cell: DiffHunkCell, fileID: Int, info: LineSelectionInfo?) {
guard let path = document.file(withID: fileID)?.displayPath else { return }
if info != nil {
selectionCell = cell
lineSelectionEndedHandler?(path, info)
return
}
guard selectionCell == nil || selectionCell === cell else { return }
selectionCell = nil
lineSelectionEndedHandler?(path, nil)
}

// MARK: - Context expansion

private func loadingDirections(for expander: DiffExpander) -> Set<DiffExpander.Direction> {
Expand Down
27 changes: 26 additions & 1 deletion Sources/MarkdownView/Components/DiffFilesView/DiffHunkCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import Foundation
private lazy var diffView: DiffView = .init()
private var heightConstraint: NSLayoutConstraint?

var onSelectionChanged: ((DiffHunkCell, LineSelectionInfo?) -> Void)?
var onSelectionEnded: ((DiffHunkCell, LineSelectionInfo?) -> Void)?

override init(frame: CGRect) {
super.init(frame: frame)
diffView.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -31,14 +34,36 @@ import Foundation
fatalError("init(coder:) has not been implemented")
}

func configure(renderBlock: DiffRenderBlock, theme: MarkdownTheme) {
func configure(renderBlock: DiffRenderBlock, theme: MarkdownTheme, selectionEnabled: Bool = false) {
let hunkTheme = DiffFilesViewConfiguration.hunkTheme(from: theme)
diffView.theme = hunkTheme
// Setting the render block clears any selection left by a reused cell.
diffView.renderBlock = renderBlock
heightConstraint?.constant = DiffViewConfiguration.intrinsicHeight(
for: renderBlock,
theme: hunkTheme
)

diffView.isLineSelectionEnabled = selectionEnabled
if selectionEnabled {
diffView.lineSelectionHandler = { [weak self] info in
guard let self else { return }
onSelectionChanged?(self, info)
}
diffView.lineSelectionEndedHandler = { [weak self] info in
guard let self else { return }
onSelectionEnded?(self, info)
}
} else {
diffView.lineSelectionHandler = nil
diffView.lineSelectionEndedHandler = nil
}
}

/// Drops the highlight without firing the selection handlers — used by
/// the controller to enforce one selection across hunk cells.
func clearSelection() {
diffView.clearLineSelection()
}
}
#endif
Loading