Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5807b02
Initial Work
thecoolwinter Apr 25, 2025
cd1cfd4
Introduce Layout Manager API
thecoolwinter Apr 25, 2025
af713a3
Handle Attachments In Layout Manager Iterator
thecoolwinter May 2, 2025
8d967da
Finish Tests, Fix Bugs
thecoolwinter May 3, 2025
0c39c02
Fix Iterator Bug, Add Some Tests, Document Method
thecoolwinter May 5, 2025
4f2ca59
Rename `attachments(` to `get(`, Clarify Internal Methods
thecoolwinter May 5, 2025
2622e51
Whole Bunch of Fixes and Tests
thecoolwinter May 5, 2025
4427899
Trailing Spaces
thecoolwinter May 5, 2025
2ef1f12
Rearrange Break Strategy into `DisplayData`
thecoolwinter May 5, 2025
d692ca6
Tests Compile, Still Need To Fix Overridden Heights
thecoolwinter May 5, 2025
1607f04
Fix Overridding Delegate
thecoolwinter May 5, 2025
639104a
Linter
thecoolwinter May 5, 2025
1d09ede
Fix Some Typesetting Bugs, Add `RangeIterator`
thecoolwinter May 5, 2025
d86b59d
Add Range Iterator Tests
thecoolwinter May 5, 2025
7d2c81c
Update Selections, Remove Demo Menu Item
thecoolwinter May 5, 2025
61bb469
Delete CodeEditTextViewExample.xcscheme
thecoolwinter May 5, 2025
b43306c
Rename `Box` to `Any`
thecoolwinter May 5, 2025
d0f16b8
Reorder
thecoolwinter May 5, 2025
f8e3fa5
Docs
thecoolwinter May 5, 2025
ccf2d7b
Docs, Make `attachments` a constant
thecoolwinter May 5, 2025
579cd0a
Remove String Reference on `Typesetter`.
thecoolwinter May 5, 2025
fdf2df1
Remove Bad `Equatable` Conformance
thecoolwinter May 5, 2025
40e2a0f
Remove `Buh`
thecoolwinter May 5, 2025
a23d196
Update LineFragmentTypesetContext.swift
thecoolwinter May 7, 2025
1c811fd
Update TypesetContext.swift
thecoolwinter May 7, 2025
85cf92d
FIx Infinite Loop When Zero-Width
thecoolwinter May 7, 2025
4d35e1b
Merge branch 'feat/text-attachment-support' of https://github.com/the…
thecoolwinter May 7, 2025
2486baa
Document Iterator Structs, Recursion Depth Limit
thecoolwinter May 7, 2025
c5c7e46
Remove Date Doc Comments
thecoolwinter May 7, 2025
ac763fb
Comment Too Long
thecoolwinter May 7, 2025
1090c23
Finish Cutoff Doc Comment
thecoolwinter May 7, 2025
ef4e68f
Move Unnecessary NSAttributedString Extension
thecoolwinter May 7, 2025
2163fae
Rename Similar Method Names For Clarity
thecoolwinter May 7, 2025
b335af7
Update Tests Target After Rename
thecoolwinter May 7, 2025
3645aab
Fix Small Positioning Bug
thecoolwinter May 8, 2025
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
Prev Previous commit
Next Next commit
Whole Bunch of Fixes and Tests
  • Loading branch information
thecoolwinter committed May 5, 2025
commit 2622e51cf05bda79b521895f6cd5d19feeef047b
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public final class TextAttachmentManager {
let insertIndex = findInsertionIndex(for: range.location)
orderedAttachments.insert(box, at: insertIndex)
layoutManager?.lineStorage.linesInRange(range).dropFirst().forEach {
layoutManager?.lineStorage.update(atOffset: $0.range.location, delta: 0, deltaHeight: -$0.height)
if $0.height != 0 {
layoutManager?.lineStorage.update(atOffset: $0.range.location, delta: 0, deltaHeight: -$0.height)
}
}
layoutManager?.invalidateLayoutForRange(range)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,23 @@ public extension TextLayoutManager {
for originalPosition: TextLineStorage<TextLine>.TextLinePosition?
) -> (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>)? {
guard let originalPosition else { return nil }
return determineVisiblePosition(for: (originalPosition, originalPosition.index...originalPosition.index))
}

let attachments = attachments.get(overlapping: originalPosition.range)
func determineVisiblePosition(
for originalPosition: (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>)
) -> (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>)? {
let attachments = attachments.get(overlapping: originalPosition.position.range)
guard let firstAttachment = attachments.first, let lastAttachment = attachments.last else {
// No change, either no attachments or attachment doesn't span multiple lines.
return (originalPosition, originalPosition.index...originalPosition.index)
return originalPosition
}

var minIndex = originalPosition.index
var maxIndex = originalPosition.index
var newPosition = originalPosition
var minIndex = originalPosition.indexRange.lowerBound
var maxIndex = originalPosition.indexRange.upperBound
var newPosition = originalPosition.position

if firstAttachment.range.location < originalPosition.range.location,
if firstAttachment.range.location < originalPosition.position.range.location,
let extendedLinePosition = lineStorage.getLine(atOffset: firstAttachment.range.location) {
newPosition = TextLineStorage<TextLine>.TextLinePosition(
data: extendedLinePosition.data,
Expand All @@ -125,23 +130,24 @@ public extension TextLayoutManager {
minIndex = min(minIndex, newPosition.index)
}

if lastAttachment.range.max > originalPosition.range.max,
if lastAttachment.range.max > originalPosition.position.range.max,
let extendedLinePosition = lineStorage.getLine(atOffset: lastAttachment.range.max) {
newPosition = TextLineStorage<TextLine>.TextLinePosition(
data: newPosition.data,
range: NSRange(start: newPosition.range.location, end: extendedLinePosition.range.max),
yPos: newPosition.yPos,
height: newPosition.height,
index: newPosition.index
index: newPosition.index // We want to keep the minimum index.
)
maxIndex = max(maxIndex, newPosition.index)
maxIndex = max(maxIndex, extendedLinePosition.index)
}

if newPosition == originalPosition {
// Base case, we haven't updated anything
if minIndex...maxIndex == originalPosition.indexRange {
return (newPosition, minIndex...maxIndex)
} else {
// Recurse, to make sure we combine all necessary lines.
return determineVisiblePosition(for: newPosition)
return determineVisiblePosition(for: (newPosition, minIndex...maxIndex))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,95 @@ struct TextLayoutManagerAttachmentsTests {
let layoutManager: TextLayoutManager

init() throws {
textView = TextView(string: "A\nB\nC\nD")
textView = TextView(string: "12\n45\n78\n01\n")
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
textStorage = textView.textStorage
layoutManager = try #require(textView.layoutManager)
}

@Test
func addAndGetAttachments() throws {
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 2, end: 8))
#expect(layoutManager.attachments.get(overlapping: textView.documentRange).count == 1)
#expect(layoutManager.attachments.get(overlapping: NSRange(start: 0, end: 3)).count == 1)
#expect(layoutManager.attachments.get(startingIn: NSRange(start: 0, end: 3)).count == 1)
}

// MARK: - Determine Visible Line Tests

@Test
func determineVisibleLinesMovesForwards() {
layoutManager.attachments.attachments(overlapping: <#T##NSRange#>)
func determineVisibleLinesMovesForwards() throws {
// From middle of the first line, to middle of the third line
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 2, end: 8))

// Start with the first line, should extend to the third line
let originalPosition = try #require(layoutManager.lineStorage.getLine(atIndex: 0)) // zero-indexed
let newPosition = try #require(layoutManager.determineVisiblePosition(for: originalPosition))

#expect(newPosition.indexRange == 0...2)
#expect(newPosition.position.range == NSRange(start: 0, end: 9)) // Lines one -> three
}

@Test
func determineVisibleLinesMovesBackwards() throws {
// From middle of the first line, to middle of the third line
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 2, end: 8))

// Start with the third line, should extend back to the first line
let originalPosition = try #require(layoutManager.lineStorage.getLine(atIndex: 2)) // zero-indexed
let newPosition = try #require(layoutManager.determineVisiblePosition(for: originalPosition))

#expect(newPosition.indexRange == 0...2)
#expect(newPosition.position.range == NSRange(start: 0, end: 9)) // Lines one -> three
}

@Test
func determineVisibleLinesMovesBackwards() {
func determineVisibleLinesMergesMultipleAttachments() throws {
// Two attachments, meeting at the third line. `determineVisiblePosition` should merge all four lines.
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 2, end: 7))
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 7, end: 11))

let originalPosition = try #require(layoutManager.lineStorage.getLine(atIndex: 2)) // zero-indexed
let newPosition = try #require(layoutManager.determineVisiblePosition(for: originalPosition))

#expect(newPosition.indexRange == 0...3)
#expect(newPosition.position.range == NSRange(start: 0, end: 12)) // Lines one -> four
}

@Test
func determineVisibleLinesMergesMultipleAttachments() {
func determineVisibleLinesMergesOverlappingAttachments() throws {
// Two attachments, overlapping at the third line. `determineVisiblePosition` should merge all four lines.
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 2, end: 7))
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 5, end: 11))

let originalPosition = try #require(layoutManager.lineStorage.getLine(atIndex: 2)) // zero-indexed
let newPosition = try #require(layoutManager.determineVisiblePosition(for: originalPosition))

#expect(newPosition.indexRange == 0...3)
#expect(newPosition.position.range == NSRange(start: 0, end: 12)) // Lines one -> four
}

// MARK: - Iterator Tests

@Test
func iterateWithAttachments() {
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 1, end: 2))

let lines = layoutManager.linesStartingAt(0, until: 1000)

// Line "5" is from the trailing newline. That shows up as an empty line in the view.
#expect(lines.map { $0.index } == [0, 1, 2, 3, 4])
}

@Test
func iterateWithMultilineAttachments() {
// Two attachments, meeting at the third line.
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 2, end: 7))
layoutManager.attachments.add(DemoTextAttachment(), for: NSRange(start: 7, end: 11))

let lines = layoutManager.linesStartingAt(0, until: 1000)

// Line "5" is from the trailing newline. That shows up as an empty line in the view.
#expect(lines.map { $0.index } == [0, 4])
}
}
Loading