-
Notifications
You must be signed in to change notification settings - Fork 23
Text Attachment Support #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
thecoolwinter
merged 35 commits into
CodeEditApp:main
from
thecoolwinter:feat/text-attachment-support
May 9, 2025
Merged
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
5807b02
Initial Work
thecoolwinter cd1cfd4
Introduce Layout Manager API
thecoolwinter af713a3
Handle Attachments In Layout Manager Iterator
thecoolwinter 8d967da
Finish Tests, Fix Bugs
thecoolwinter 0c39c02
Fix Iterator Bug, Add Some Tests, Document Method
thecoolwinter 4f2ca59
Rename `attachments(` to `get(`, Clarify Internal Methods
thecoolwinter 2622e51
Whole Bunch of Fixes and Tests
thecoolwinter 4427899
Trailing Spaces
thecoolwinter 2ef1f12
Rearrange Break Strategy into `DisplayData`
thecoolwinter d692ca6
Tests Compile, Still Need To Fix Overridden Heights
thecoolwinter 1607f04
Fix Overridding Delegate
thecoolwinter 639104a
Linter
thecoolwinter 1d09ede
Fix Some Typesetting Bugs, Add `RangeIterator`
thecoolwinter d86b59d
Add Range Iterator Tests
thecoolwinter 7d2c81c
Update Selections, Remove Demo Menu Item
thecoolwinter 61bb469
Delete CodeEditTextViewExample.xcscheme
thecoolwinter b43306c
Rename `Box` to `Any`
thecoolwinter d0f16b8
Reorder
thecoolwinter f8e3fa5
Docs
thecoolwinter ccf2d7b
Docs, Make `attachments` a constant
thecoolwinter 579cd0a
Remove String Reference on `Typesetter`.
thecoolwinter fdf2df1
Remove Bad `Equatable` Conformance
thecoolwinter 40e2a0f
Remove `Buh`
thecoolwinter a23d196
Update LineFragmentTypesetContext.swift
thecoolwinter 1c811fd
Update TypesetContext.swift
thecoolwinter 85cf92d
FIx Infinite Loop When Zero-Width
thecoolwinter 4d35e1b
Merge branch 'feat/text-attachment-support' of https://github.com/the…
thecoolwinter 2486baa
Document Iterator Structs, Recursion Depth Limit
thecoolwinter c5c7e46
Remove Date Doc Comments
thecoolwinter ac763fb
Comment Too Long
thecoolwinter 1090c23
Finish Cutoff Doc Comment
thecoolwinter ef4e68f
Move Unnecessary NSAttributedString Extension
thecoolwinter 2163fae
Rename Similar Method Names For Clarity
thecoolwinter b335af7
Update Tests Target After Rename
thecoolwinter 3645aab
Fix Small Positioning Bug
thecoolwinter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
Sources/CodeEditTextView/Extensions/CTTypesetter+SuggestLineBreak.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // | ||
| // CTTypesetter+SuggestLineBreak.swift | ||
| // CodeEditTextView | ||
| // | ||
| // Created by Khan Winter on 4/24/25. | ||
| // | ||
|
|
||
| import AppKit | ||
|
|
||
| extension CTTypesetter { | ||
| /// Suggest a line break for the given line break strategy. | ||
| /// - Parameters: | ||
| /// - typesetter: The typesetter to use. | ||
| /// - strategy: The strategy that determines a valid line break. | ||
| /// - startingOffset: Where to start breaking. | ||
| /// - constrainingWidth: The available space for the line. | ||
| /// - Returns: An offset relative to the entire string indicating where to break. | ||
| func suggestLineBreak( | ||
| using string: NSAttributedString, | ||
| strategy: LineBreakStrategy, | ||
| subrange: NSRange, | ||
| constrainingWidth: CGFloat | ||
| ) -> Int { | ||
| switch strategy { | ||
| case .character: | ||
| return suggestLineBreakForCharacter( | ||
| string: string, | ||
| startingOffset: subrange.location, | ||
| constrainingWidth: constrainingWidth | ||
| ) | ||
| case .word: | ||
| return suggestLineBreakForWord( | ||
| string: string, | ||
| subrange: subrange, | ||
| constrainingWidth: constrainingWidth | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /// Suggest a line break for the character break strategy. | ||
| /// - Parameters: | ||
| /// - typesetter: The typesetter to use. | ||
| /// - startingOffset: Where to start breaking. | ||
| /// - constrainingWidth: The available space for the line. | ||
| /// - Returns: An offset relative to the entire string indicating where to break. | ||
| private func suggestLineBreakForCharacter( | ||
| string: NSAttributedString, | ||
| startingOffset: Int, | ||
| constrainingWidth: CGFloat | ||
| ) -> Int { | ||
| var breakIndex: Int | ||
| // Check if we need to skip to an attachment | ||
|
|
||
| breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(self, startingOffset, constrainingWidth) | ||
| guard breakIndex < string.length else { | ||
| return breakIndex | ||
| } | ||
| let substring = string.attributedSubstring(from: NSRange(location: breakIndex - 1, length: 2)).string | ||
| if substring == LineEnding.carriageReturnLineFeed.rawValue { | ||
| // Breaking in the middle of the clrf line ending | ||
| breakIndex += 1 | ||
| } | ||
|
|
||
| return breakIndex | ||
| } | ||
|
|
||
| /// Suggest a line break for the word break strategy. | ||
| /// - Parameters: | ||
| /// - typesetter: The typesetter to use. | ||
| /// - startingOffset: Where to start breaking. | ||
| /// - constrainingWidth: The available space for the line. | ||
| /// - Returns: An offset relative to the entire string indicating where to break. | ||
| private func suggestLineBreakForWord( | ||
| string: NSAttributedString, | ||
| subrange: NSRange, | ||
| constrainingWidth: CGFloat | ||
| ) -> Int { | ||
| var breakIndex = subrange.location + CTTypesetterSuggestClusterBreak(self, subrange.location, constrainingWidth) | ||
| let isBreakAtEndOfString = breakIndex >= subrange.max | ||
|
|
||
| let isNextCharacterCarriageReturn = checkIfLineBreakOnCRLF(breakIndex, for: string) | ||
| if isNextCharacterCarriageReturn { | ||
| breakIndex += 1 | ||
| } | ||
|
|
||
| let canLastCharacterBreak = (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1, for: string)) | ||
|
|
||
| if isBreakAtEndOfString || canLastCharacterBreak { | ||
| // Breaking either at the end of the string, or on a whitespace. | ||
| return breakIndex | ||
| } else if breakIndex - 1 > 0 { | ||
| // Try to walk backwards until we hit a whitespace or punctuation | ||
| var index = breakIndex - 1 | ||
|
|
||
| while breakIndex - index < 100 && index > subrange.location { | ||
| if ensureCharacterCanBreakLine(at: index, for: string) { | ||
| return index + 1 | ||
| } | ||
| index -= 1 | ||
| } | ||
| } | ||
|
|
||
| return breakIndex | ||
| } | ||
|
|
||
| /// Ensures the character at the given index can break a line. | ||
| /// - Parameter index: The index to check at. | ||
| /// - Returns: True, if the character is a whitespace or punctuation character. | ||
| private func ensureCharacterCanBreakLine(at index: Int, for string: NSAttributedString) -> Bool { | ||
| let subrange = (string.string as NSString).rangeOfComposedCharacterSequence(at: index) | ||
| let set = CharacterSet(charactersIn: (string.string as NSString).substring(with: subrange)) | ||
| return set.isSubset(of: .whitespacesAndNewlines) || set.isSubset(of: .punctuationCharacters) | ||
| } | ||
|
|
||
| /// Check if the break index is on a CRLF (`\r\n`) character, indicating a valid break position. | ||
| /// - Parameter breakIndex: The index to check in the string. | ||
| /// - Returns: True, if the break index lies after the `\n` character in a `\r\n` sequence. | ||
| private func checkIfLineBreakOnCRLF(_ breakIndex: Int, for string: NSAttributedString) -> Bool { | ||
| guard breakIndex - 1 > 0 && breakIndex + 1 <= string.length else { | ||
| return false | ||
| } | ||
| let substringRange = NSRange(location: breakIndex - 1, length: 2) | ||
| let substring = string.attributedSubstring(from: substringRange).string | ||
|
|
||
| return substring == LineEnding.carriageReturnLineFeed.rawValue | ||
| } | ||
| } |
31 changes: 31 additions & 0 deletions
31
Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // | ||
| // TextAttachment.swift | ||
| // CodeEditTextView | ||
| // | ||
| // Created by Khan Winter on 4/24/25. | ||
| // | ||
|
|
||
| import AppKit | ||
|
|
||
| /// Represents an attachment type. Attachments take up some set width, and draw their contents in a receiver view. | ||
| public protocol TextAttachment: AnyObject { | ||
| var width: CGFloat { get } | ||
| func draw(in context: CGContext, rect: NSRect) | ||
| } | ||
|
|
||
| /// Type-erasing type for ``TextAttachment`` that also contains range information about the attachment. | ||
| /// | ||
| /// This type cannot be initialized outside of `CodeEditTextView`, but will be received when interrogating | ||
| /// the ``TextAttachmentManager``. | ||
| public struct AnyTextAttachment: Equatable { | ||
| var range: NSRange | ||
| let attachment: any TextAttachment | ||
|
|
||
| var width: CGFloat { | ||
| attachment.width | ||
| } | ||
|
|
||
| public static func == (_ lhs: AnyTextAttachment, _ rhs: AnyTextAttachment) -> Bool { | ||
| lhs.range == rhs.range && lhs.attachment === rhs.attachment | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.