File tree Expand file tree Collapse file tree
Sources/Textual/Internal/TextInteraction/Shared
Tests/TextualTests/Internal/TextInteraction Expand file tree Collapse file tree Original file line number Diff line number Diff line change 124124 }
125125
126126 func isPositionAtBlockBoundary( _ position: TextPosition ) -> Bool {
127+ guard contains ( position) else {
128+ return false
129+ }
130+
127131 if position
128132 == TextPosition (
129133 indexPath: . init( layout: position. indexPath. layout) ,
Original file line number Diff line number Diff line change 2929 }
3030
3131 func position( from position: TextPosition , offset: Int ) -> TextPosition ? {
32- let from = characterIndex ( at: position)
32+ guard let from = characterIndex ( at: position) else {
33+ return nil
34+ }
3335 let target = from + offset
3436
3537 guard ( 0 ... stringLength) . contains ( target) else {
5961 return self . position ( at: layout, localCharacterIndex: localTarget)
6062 }
6163
62- func characterIndex( at position: TextPosition ) -> Int {
64+ func characterIndex( at position: TextPosition ) -> Int ? {
65+ guard contains ( position) else {
66+ return nil
67+ }
6368 let base = layouts. prefix ( position. indexPath. layout)
6469 . map ( \. attributedString. length)
6570 . reduce ( 0 , + )
6671 return base + localCharacterIndex( at: position)
6772 }
6873
74+ func contains( _ position: TextPosition ) -> Bool {
75+ let indexPath = position. indexPath
76+ guard
77+ indexPath. count == 4 ,
78+ layouts. indices. contains ( indexPath. layout)
79+ else {
80+ return false
81+ }
82+
83+ let layout = layouts [ indexPath. layout]
84+ guard layout. lines. indices. contains ( indexPath. line) else {
85+ return false
86+ }
87+
88+ let line = layout. lines [ indexPath. line]
89+ guard line. runs. indices. contains ( indexPath. run) else {
90+ return false
91+ }
92+
93+ return line. runs [ indexPath. run] . slices. indices. contains ( indexPath. runSlice)
94+ }
95+
6996 func localCharacterIndex( at position: TextPosition ) -> Int {
7097 let range = localCharacterRange ( at: position. indexPath)
7198 switch position. affinity {
Original file line number Diff line number Diff line change 111111 }
112112
113113 func offset( from: TextPosition , to: TextPosition ) -> Int {
114- layoutCollection. characterIndex ( at: to) - layoutCollection. characterIndex ( at: from)
114+ guard
115+ let fromIndex = layoutCollection. characterIndex ( at: from) ,
116+ let toIndex = layoutCollection. characterIndex ( at: to)
117+ else {
118+ return 0
119+ }
120+ return toIndex - fromIndex
115121 }
116122
117123 func firstRect( for range: TextRange ) -> CGRect {
Original file line number Diff line number Diff line change 198198 #expect( offset == - 38 )
199199 }
200200
201+ @Test
202+ func stalePositionsAfterLayoutBecomesEmpty( ) throws {
203+ // given
204+ let model = try TextSelectionModel ( fixtureName: " two-paragraphs-bidi " )
205+ let staleStart = model. startPosition
206+ let staleEnd = model. endPosition
207+
208+ // when
209+ model. setLayoutCollection ( EmptyTextLayoutCollection ( ) )
210+
211+ // then
212+ #expect( model. position ( from: staleStart, offset: 0 ) == nil )
213+ #expect( model. offset ( from: staleStart, to: staleEnd) == 0 )
214+ #expect( model. isPositionAtBlockBoundary ( staleStart) == false )
215+ #expect( model. isPositionAtBlockBoundary ( staleEnd) == false )
216+ }
217+
201218 @Test
202219 func firstRectCollapsedRange( ) throws {
203220 // given
You can’t perform that action at this time.
0 commit comments