@@ -85,8 +85,22 @@ import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
8585@ApiStatus.Experimental
8686@ExperimentalJewelApi
8787public abstract class ScrollingSynchronizer {
88- /* * Scroll the preview to the position that match the given [sourceLine] the best. */
89- public abstract suspend fun scrollToLine (sourceLine : Int , animationSpec : AnimationSpec <Float > = SpringSpec ())
88+ /* *
89+ * Scroll the preview to the position that matches the given [sourceLine] the best.
90+ *
91+ * Don't extend this function, implement [scrollToCoordinate] and [findYCoordinateToScroll] instead. `open` is
92+ * preserved for compatibility reasons and will be removed in the future.
93+ */
94+ @ApiStatus.NonExtendable
95+ public open suspend fun scrollToLine (sourceLine : Int , animationSpec : AnimationSpec <Float > = SpringSpec ()) {
96+ scrollToCoordinate(findYCoordinateToScroll(sourceLine), animationSpec)
97+ }
98+
99+ /* * Scroll the preview to the given vertical position [y] using given [animationSpec]. */
100+ protected abstract suspend fun scrollToCoordinate (y : Int , animationSpec : AnimationSpec <Float > = SpringSpec ())
101+
102+ /* * Find the vertical position in the preview that matches the given [sourceLine] the best. */
103+ protected abstract suspend fun findYCoordinateToScroll (sourceLine : Int ): Int
90104
91105 /* *
92106 * Called when [MarkdownProcessor] processes the raw markdown text. The processing itself is passed as an [action].
@@ -185,28 +199,56 @@ public abstract class ScrollingSynchronizer {
185199 // so this map always keeps relevant information.
186200 private val blocks2TextOffsets = mutableMapOf<MarkdownBlock , List <Int >>()
187201
188- override suspend fun scrollToLine (sourceLine : Int , animationSpec : AnimationSpec <Float >) {
189- val block = findBestBlockForLine(sourceLine) ? : return
190- val y = blocks2Top[block] ? : return
191- if (y < 0 ) return
192- val lineRange = (block as ? LocatableMarkdownBlock )?.lines ? : return
193- val textOffsets = blocks2TextOffsets[block]
202+ override suspend fun scrollToCoordinate (y : Int , animationSpec : AnimationSpec <Float >) {
203+ scrollState.animateScrollTo(y, animationSpec)
204+ }
205+
206+ override suspend fun findYCoordinateToScroll (sourceLine : Int ): Int {
207+ blocksSortedByPreference(sourceLine).forEach { block ->
208+ val positionToScroll = block.positionToScroll(sourceLine)
209+ if (positionToScroll != null ) {
210+ return positionToScroll
211+ }
212+ }
213+ return 0
214+ }
215+
216+ private fun blocksSortedByPreference (sourceLine : Int ) = iterator {
217+ val blockOnLine = lines2Blocks[sourceLine]
218+ if (blockOnLine != null ) {
219+ yield (blockOnLine)
220+ } else {
221+ // If there is no block that covers the line,
222+ // the next best block is the one **after** the line.
223+ // Otherwise, when scrolling down the source,
224+ // on empty lines the preview will scroll
225+ // in the opposite direction
226+ val firstBlockAfterLine = lines2Blocks.higherEntry(sourceLine)?.value
227+ if (firstBlockAfterLine != null ) {
228+ yield (firstBlockAfterLine)
229+ }
230+ }
231+ // Otherwise, look for the closest block positioned before the line.
232+ // This way, the corresponding preview line will be located
233+ // below the viewport's top point (and the user still has a chance
234+ // to see it in the visible area)
235+ val blocksBeforeLine = lines2Blocks.headMap(sourceLine)
236+ val blocksBeforeLineClosestFirst = blocksBeforeLine.values.reversed()
237+ for (block in blocksBeforeLineClosestFirst) {
238+ yield (block)
239+ }
240+ }
241+
242+ private fun MarkdownBlock.positionToScroll (sourceLine : Int ): Int? {
243+ val y = blocks2Top[this ] ? : return null
244+ val lineRange = (this as ? LocatableMarkdownBlock )?.lines ? : return y
245+
194246 // The line may be empty and represent no block,
195247 // in this case scroll to the first line of the first block positioned after the line
196248 val lineIndexInBlock = maxOf(0 , sourceLine - lineRange.first)
197- val lineOffset = textOffsets?.get(lineIndexInBlock) ? : 0
198- scrollState.animateScrollTo(y + lineOffset, animationSpec)
199- }
249+ val textOffsets = blocks2TextOffsets[this ]
200250
201- private fun findBestBlockForLine (line : Int ): MarkdownBlock ? {
202- // The best block is the one **below** the line if there is no block that covers the
203- // line.
204- // Otherwise, when scrolling down the source, on empty lines preview will scroll in the
205- // opposite direction
206- val sm = lines2Blocks.subMap(line, Int .MAX_VALUE )
207- if (sm.isEmpty()) return null
208- // TODO use firstEntry() after switching to JDK 21
209- return sm.getValue(sm.firstKey())
251+ return y + (textOffsets?.getOrNull(lineIndexInBlock) ? : 0 )
210252 }
211253
212254 override fun beforeProcessing () {
0 commit comments