1818import org .fxmisc .flowless .VirtualFlow ;
1919import org .fxmisc .richtext .CodeArea ;
2020import org .fxmisc .richtext .GenericStyledArea ;
21+ import org .fxmisc .richtext .StyleActions ;
2122import org .fxmisc .richtext .model .PlainTextChange ;
2223import org .fxmisc .richtext .model .ReadOnlyStyledDocument ;
2324import org .fxmisc .richtext .model .StyleSpans ;
@@ -177,7 +178,7 @@ else if (e.getCode() == KeyCode.ENTER)
177178 int start = range .start ();
178179 int end = range .end ();
179180 return new StyleResult (syntaxHighlighter .createStyleSpans (text , start , end ), start );
180- }, result -> codeArea . setStyleSpans (result .position (), result .spans ()));
181+ }, result -> setStyleSpans (result .position (), result .spans ()));
181182 }
182183 }
183184
@@ -248,7 +249,29 @@ public void showParagraphAtCenter(int paragraph) {
248249 // - Assuming all cells are the same height, we can compute the offset needed to center the paragraph.
249250 int count = 1 + Math .max (virtualFlow .getLastVisibleIndex () - virtualFlow .getFirstVisibleIndex (), 0 );
250251 double paragraphHeight = getHeight () / count ;
251- virtualFlow .showAtOffset (paragraph , count /2.0 * paragraphHeight /2.0 );
252+ virtualFlow .showAtOffset (paragraph , count / 2.0 * paragraphHeight / 2.0 );
253+ }
254+
255+ /**
256+ * Delegates to {@link StyleActions#setStyleSpans(int, StyleSpans)} but with some scroll-position preservation logic.
257+ *
258+ * @param from
259+ * Text position to start applying styles at.
260+ * @param spans
261+ * Style spans to apply.
262+ */
263+ private void setStyleSpans (int from , @ Nonnull StyleSpans <Collection <String >> spans ) {
264+ // Updating the styles can cause the 'Navigator' to set its target position back to zero for... some reason.
265+ // See Navigator:
266+ // - setTargetPosition
267+ // - scrollCurrentPositionBy
268+ // To prevent jank, we record the first visible index before the update, and restore it after.
269+ //
270+ // We use the CodeArea's "showParagraphAtTop" instead of our direct one on the VirtualFlow, because the CodeArea's
271+ // suspension handling is necessary to keep event ordering correct (where the restoration happens after the janky reset).
272+ int virtualFlowFirst = virtualFlow .getFirstVisibleIndex ();
273+ codeArea .setStyleSpans (from , spans );
274+ codeArea .showParagraphAtTop (virtualFlowFirst );
252275 }
253276
254277 /**
@@ -272,7 +295,7 @@ public CompletableFuture<Void> restyleAtPosition(int position, int length) {
272295 int start = range .start ();
273296 int end = range .end ();
274297 return new StyleResult (syntaxHighlighter .createStyleSpans (getText (), start , end ), start );
275- }, result -> codeArea . setStyleSpans (result .position (), result .spans ()));
298+ }, result -> setStyleSpans (result .position (), result .spans ()));
276299 }
277300 return CompletableFuture .completedFuture (null );
278301 }
@@ -457,7 +480,7 @@ public void setSyntaxHighlighter(@Nullable SyntaxHighlighter syntaxHighlighter)
457480 syntaxHighlighter .install (this );
458481 String text = getText ();
459482 if (!text .isBlank ())
460- codeArea . setStyleSpans (0 , syntaxHighlighter .createStyleSpans (text , 0 , getTextLength ()));
483+ setStyleSpans (0 , syntaxHighlighter .createStyleSpans (text , 0 , getTextLength ()));
461484 }
462485 }
463486
0 commit comments