From a193cfb941761a925b6e72fd489fca656d404107 Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sat, 24 Jun 2023 14:36:40 +1200 Subject: [PATCH 01/12] wrap-perf: cache wrapped line heights --- .../ui/rsyntaxtextarea/WrappedSyntaxView.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index ef82a64a0..953e335c5 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -18,6 +18,8 @@ import java.awt.Insets; import java.awt.Rectangle; import java.awt.Shape; +import java.util.Map.Entry; +import java.util.TreeMap; import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; @@ -1156,6 +1158,14 @@ class WrappedLine extends View { private int nlines; private boolean widthChangePending; + /** + * A temporary lookup for a character position at the start of a line to the y offset of that line, + * to improve the performance when wrapping long lines. + */ + private transient TreeMap posToHeightLookup; + + private transient int posToHeightLookupWidth; + WrappedLine(Element elem) { super(elem); } @@ -1289,6 +1299,21 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) Token tokenList = doc.getTokenListForLine(line); float x0 = alloc.x;//0; + final int width = getWidth(); + + if (posToHeightLookup != null) { + if (posToHeightLookupWidth == width) { + Entry floorEntry = posToHeightLookup.floorEntry(pos); + if (floorEntry != null) { + p0 = floorEntry.getKey(); + alloc.y = floorEntry.getValue(); + } + } else { + posToHeightLookup = null; + } + } + + int loops = 0; while (p0 < p1) { TokenSubList subList = TokenUtils.getSubTokenList(tokenList, p0, WrappedSyntaxView.this, textArea, x0, lineCountTempToken); @@ -1320,7 +1345,15 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) p0 = (p == p0) ? p1 : p; //System.err.println("... ... Incrementing y"); alloc.y += alloc.height; + loops++; + if (loops > 10) { + if (posToHeightLookup == null) { + posToHeightLookup = new TreeMap<>(); + posToHeightLookupWidth = width; + } + posToHeightLookup.put(p0, alloc.y); + } } throw new BadLocationException(null, pos); From bc67c46df163448da25cbfb48478869f54997e2f Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sat, 24 Jun 2023 14:38:12 +1200 Subject: [PATCH 02/12] wrap-perf: only measure width when we have to, and only up to the maximum width we have to --- .../ui/rsyntaxtextarea/RSyntaxUtilities.java | 2 +- .../org/fife/ui/rsyntaxtextarea/Token.java | 30 ++++++++++++ .../fife/ui/rsyntaxtextarea/TokenImpl.java | 48 +++++++++++++++++++ .../fife/ui/rsyntaxtextarea/TokenUtils.java | 17 +++++-- .../ui/rsyntaxtextarea/WrappedSyntaxView.java | 8 ++-- 5 files changed, 96 insertions(+), 9 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxUtilities.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxUtilities.java index aef8441d4..d2b188812 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxUtilities.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxUtilities.java @@ -371,7 +371,7 @@ else if (p1>doc.getLength()) { // token types, etc.), and get the x-location (in pixels) of the // beginning of this new token list. TokenSubList subList = TokenUtils.getSubTokenList(t, p0, e, textArea, - 0, TEMP_TOKEN); + 0, TEMP_TOKEN, false, 0); t = subList.tokenList; rect = t.listOffsetToView(textArea, e, p1, x0, rect); diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/Token.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/Token.java index b40117668..765c09b0c 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/Token.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/Token.java @@ -301,6 +301,19 @@ int getOffsetBeforeX(RSyntaxTextArea textArea, TabExpander e, */ float getWidth(RSyntaxTextArea textArea, TabExpander e, float x0); + /** + * Returns the width of this token given the specified parameters. + * + * @param textArea The text area in which the token is being painted. + * @param e Describes how to expand tabs. This parameter cannot be + * null. + * @param x0 The pixel-location at which the token begins. This is needed + * because of tabs. + * @param maxWidth the maximum width we care about measuring + * @return The width of the token, in pixels. + * @see #getWidthUpTo + */ + float getWidth(RSyntaxTextArea textArea, TabExpander e, float x0, float maxWidth); /** * Returns the width of a specified number of characters in this token. @@ -318,6 +331,23 @@ int getOffsetBeforeX(RSyntaxTextArea textArea, TabExpander e, float getWidthUpTo(int numChars, RSyntaxTextArea textArea, TabExpander e, float x0); + /** + * Returns the width of a specified number of characters in this token. + * For example, for the token "while", specifying a value of 3 + * here returns the width of the "whi" portion of the token. + * + * @param numChars The number of characters for which to get the width. + * @param textArea The text area in which the token is being painted. + * @param e How to expand tabs. This value cannot be null. + * @param x0 The pixel-location at which this token begins. This is needed + * because of tabs. + * @param maxWidth the maximum width we care about measuring + * @return The width of the specified number of characters in this token. + * @see #getWidth + */ + float getWidthUpTo(int numChars, RSyntaxTextArea textArea, + TabExpander e, float x0, float maxWidth); + /** * Returns whether this token's lexeme matches a specific character array. diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java index 2a3b12533..a982222aa 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java @@ -570,6 +570,10 @@ public float getWidth(RSyntaxTextArea textArea, TabExpander e, float x0) { return getWidthUpTo(textCount, textArea, e, x0); } + @Override + public float getWidth(RSyntaxTextArea textArea, TabExpander e, float x0, float maxWidth) { + return getWidthUpTo(textCount, textArea, e, x0, maxWidth); + } @Override public float getWidthUpTo(int numChars, RSyntaxTextArea textArea, @@ -603,6 +607,50 @@ public float getWidthUpTo(int numChars, RSyntaxTextArea textArea, return width - x0; } + public float getWidthUpTo(int numChars, RSyntaxTextArea textArea, + TabExpander e, float x0, float maxWidth) { + float width = x0; + FontMetrics fm = textArea.getFontMetricsForTokenType(getType()); + if (fm != null) { + int w; + int currentStart = textOffset; + final int endBefore = textOffset + numChars; + for (int i = currentStart; i < endBefore; i++) { + if (text[i] == '\t') { + // Since TokenMaker implementations usually group all + // adjacent whitespace into a single token, there + // aren't usually any characters to compute a width + // for here, so we check before calling. + w = i - currentStart; + if (w > 0) { + width += fm.charsWidth(text, currentStart, w); + } + currentStart = i + 1; + width = e.nextTabStop(width, 0); + + if (width - x0 >= maxWidth) { + return maxWidth; + } + } else if ((i - currentStart) > 50) { + w = i - currentStart + 1; + width += fm.charsWidth(text, currentStart, w); + currentStart = i + 1; + + if (width - x0 >= maxWidth) { + return maxWidth; + } + } + } + + // Most (non-whitespace) tokens will have characters at this + // point to get the widths for, so we don't check for w>0 (mini- + // optimization). + w = endBefore - currentStart; + width += fm.charsWidth(text, currentStart, w); + } + return width - x0; + } + @Override public int hashCode() { diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenUtils.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenUtils.java index 9e12062b9..8b2a4904e 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenUtils.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenUtils.java @@ -62,7 +62,7 @@ public static TokenSubList getSubTokenList(Token tokenList, int pos, TabExpander e, final RSyntaxTextArea textArea, float x0) { - return getSubTokenList(tokenList, pos, e, textArea, x0, null); + return getSubTokenList(tokenList, pos, e, textArea, x0, null, true, Float.MAX_VALUE); } @@ -100,6 +100,8 @@ public static TokenSubList getSubTokenList(Token tokenList, int pos, * @param tempToken A temporary token to use when creating the token list * result. This may be null but callers can pass in * a "buffer" token for performance if desired. + * @param careAboutWidth whether we need to know the width or the result or not + * @param maxWidth the maximum width we care about measuring, if we care about width * @return Information about the "sub" token list. This will be * null if pos was not a valid offset * into the token list. @@ -109,7 +111,10 @@ public static TokenSubList getSubTokenList(Token tokenList, int pos, TabExpander e, final RSyntaxTextArea textArea, float x0, - TokenImpl tempToken) { + TokenImpl tempToken, + boolean careAboutWidth, + float maxWidth) { + if (tempToken==null) { tempToken = new TokenImpl(); @@ -119,7 +124,9 @@ public static TokenSubList getSubTokenList(Token tokenList, int pos, // Loop through the token list until you find the one that contains // pos. Remember the cumulative width of all of these tokens. while (t!=null && t.isPaintable() && !t.containsPosition(pos)) { - x0 += t.getWidth(textArea, e, x0); + if (careAboutWidth) { + x0 += t.getWidth(textArea, e, x0, maxWidth); + } t = t.getNextToken(); } @@ -129,7 +136,9 @@ public static TokenSubList getSubTokenList(Token tokenList, int pos, if (t.getOffset()!=pos) { // Number of chars between p0 and token start. int difference = pos - t.getOffset(); - x0 += t.getWidthUpTo(t.length()-difference+1, textArea, e, x0); + if (careAboutWidth) { + x0 += t.getWidthUpTo(t.length()-difference+1, textArea, e, x0, maxWidth); + } tempToken.copyFrom(t); tempToken.makeStartAt(pos); diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index 953e335c5..9ada6e455 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -126,7 +126,7 @@ protected int calculateBreakPosition(int p0, Token tokenList, float x0) { // FIXME: Replace the code below with the commented-out line below. This will // allow long tokens to be broken at embedded spaces (such as MLC's). But it // currently throws BadLocationExceptions sometimes... - float tokenWidth = t.getWidth(textArea, this, x0); + float tokenWidth = t.getWidth(textArea, this, x0, currentWidth + 1); if (tokenWidth>currentWidth) { // If the current token alone is too long for this line, // break at a character boundary. @@ -1196,7 +1196,7 @@ final int calculateLineCount() { //System.err.println("... ... " + p0 + ", " + p1); nlines += 1; TokenSubList subList = TokenUtils.getSubTokenList(tokenList, p0, - WrappedSyntaxView.this, textArea, x0, lineCountTempToken); + WrappedSyntaxView.this, textArea, x0, lineCountTempToken, false, 0); x0 = subList!=null ? subList.x : x0; tokenList = subList!=null ? subList.tokenList : null; int p = calculateBreakPosition(p0, tokenList, x0); @@ -1316,7 +1316,7 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) int loops = 0; while (p0 < p1) { TokenSubList subList = TokenUtils.getSubTokenList(tokenList, p0, - WrappedSyntaxView.this, textArea, x0, lineCountTempToken); + WrappedSyntaxView.this, textArea, x0, lineCountTempToken, true, width + 1); x0 = subList!=null ? subList.x : x0; tokenList = subList!=null ? subList.tokenList : null; int p = calculateBreakPosition(p0, tokenList, x0); @@ -1418,7 +1418,7 @@ else if (y > alloc.y + alloc.height) { // lines so they start at the beginning of a physical // line. TokenSubList subList = TokenUtils.getSubTokenList(tlist, p0, - WrappedSyntaxView.this, textArea, alloc.x, lineCountTempToken); + WrappedSyntaxView.this, textArea, alloc.x, lineCountTempToken, false, 0); tlist = subList!=null ? subList.tokenList : null; int p = calculateBreakPosition(p0, tlist, alloc.x); From 47a081c9d53cc49c1050807c04ccff460114aa1d Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sat, 24 Jun 2023 15:01:46 +1200 Subject: [PATCH 03/12] wrap-pref: fix selection showing whole lines selected --- .../org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index 9ada6e455..e9b0d2746 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -1287,8 +1287,6 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) alloc.width = 1; int p0 = getStartOffset(); int p1 = getEndOffset(); - int testP = (b == Position.Bias.Forward) ? pos : - Math.max(p0, pos - 1); // Get the token list for this line so we don't have to keep // recomputing it if this logical line spans multiple physical @@ -1301,9 +1299,10 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) final int width = getWidth(); + /* Use lookup table to improve performance */ if (posToHeightLookup != null) { if (posToHeightLookupWidth == width) { - Entry floorEntry = posToHeightLookup.floorEntry(pos); + Entry floorEntry = posToHeightLookup.floorEntry(pos - 1); if (floorEntry != null) { p0 = floorEntry.getKey(); alloc.y = floorEntry.getValue(); @@ -1313,6 +1312,9 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) } } + int testP = (b == Position.Bias.Forward) ? pos : + Math.max(p0, pos - 1); + int loops = 0; while (p0 < p1) { TokenSubList subList = TokenUtils.getSubTokenList(tokenList, p0, From 2842e6eb0e0bef3eff5be7ae9d2eda95a40ee8d5 Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sat, 24 Jun 2023 15:03:48 +1200 Subject: [PATCH 04/12] wrap-perf: add a lookup table for viewToModel as well --- .../ui/rsyntaxtextarea/WrappedSyntaxView.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index e9b0d2746..bb99dd46c 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -1163,8 +1163,12 @@ class WrappedLine extends View { * to improve the performance when wrapping long lines. */ private transient TreeMap posToHeightLookup; + private transient TreeMap heightToPosLookup; - private transient int posToHeightLookupWidth; + /** + * The width used when the lookup tables were calculated, so they can be reset when the width changes. + */ + private transient int widthForLookups; WrappedLine(Element elem) { super(elem); @@ -1301,7 +1305,7 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) /* Use lookup table to improve performance */ if (posToHeightLookup != null) { - if (posToHeightLookupWidth == width) { + if (widthForLookups == width) { Entry floorEntry = posToHeightLookup.floorEntry(pos - 1); if (floorEntry != null) { p0 = floorEntry.getKey(); @@ -1352,7 +1356,7 @@ public Shape modelToView(int pos, Shape a, Position.Bias b) if (loops > 10) { if (posToHeightLookup == null) { posToHeightLookup = new TreeMap<>(); - posToHeightLookupWidth = width; + widthForLookups = width; } posToHeightLookup.put(p0, alloc.y); } @@ -1413,6 +1417,23 @@ else if (y > alloc.y + alloc.height) { int line = map.getElementIndex(p0); Token tlist = doc.getTokenListForLine(line); + final int width = getWidth(); + + /* Use lookup table to improve performance */ + if (heightToPosLookup != null) { + if (widthForLookups == width) { + Entry floorEntry = heightToPosLookup.floorEntry((int) fy); + if (floorEntry != null) { + alloc.y = floorEntry.getKey(); + p0 = floorEntry.getValue(); + } + } else { + heightToPosLookup = null; + } + } + + int loops = 0; + // Look at each physical line-chunk of this logical line. while (p0 10) { + if (heightToPosLookup == null) { + heightToPosLookup = new TreeMap<>(); + widthForLookups = width; + } + heightToPosLookup.put(alloc.y, p0); + } } // End of while (p0 Date: Sat, 24 Jun 2023 15:10:00 +1200 Subject: [PATCH 05/12] wrap-perf: checkstyle --- .../src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java | 1 + .../java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java index a982222aa..c7e5c9a39 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TokenImpl.java @@ -607,6 +607,7 @@ public float getWidthUpTo(int numChars, RSyntaxTextArea textArea, return width - x0; } + @Override public float getWidthUpTo(int numChars, RSyntaxTextArea textArea, TabExpander e, float x0, float maxWidth) { float width = x0; diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index bb99dd46c..be03ba8c0 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -19,6 +19,7 @@ import java.awt.Rectangle; import java.awt.Shape; import java.util.Map.Entry; +import java.util.NavigableMap; import java.util.TreeMap; import javax.swing.event.DocumentEvent; @@ -1162,8 +1163,8 @@ class WrappedLine extends View { * A temporary lookup for a character position at the start of a line to the y offset of that line, * to improve the performance when wrapping long lines. */ - private transient TreeMap posToHeightLookup; - private transient TreeMap heightToPosLookup; + private transient NavigableMap posToHeightLookup; + private transient NavigableMap heightToPosLookup; /** * The width used when the lookup tables were calculated, so they can be reset when the width changes. From 291ded30b6007cc534ced1aa96114a9b045e18b3 Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sat, 24 Jun 2023 22:25:41 +1200 Subject: [PATCH 06/12] wrap-perf: clip the tokens we paint in a View A View can be enormous in the case of a massive unbroken line. --- .../ui/rsyntaxtextarea/WrappedSyntaxView.java | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index be03ba8c0..0f1d27e89 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -215,7 +215,7 @@ private void childAllocation2(int line, int y, Rectangle alloc) { * @param fontHeight The height of the font being used. * @param y The y-coordinate at which to begin painting. */ - protected void drawView(TokenPainter painter, Graphics2D g, Rectangle r, + protected void drawView(TokenPainter painter, Graphics2D g, Rectangle r, Rectangle clip, View view, int fontHeight, int y, int line) { float x = r.x; @@ -249,11 +249,17 @@ protected void drawView(TokenPainter painter, Graphics2D g, Rectangle r, int p = calculateBreakPosition(p0, token, x); x = r.x; - h.paintLayeredHighlights(g, p0,p, r, host, this); + /* NB: for text drawing y is the baseline */ + final boolean paintThisLine = (y >= clip.getMinY()); + if (paintThisLine) { + h.paintLayeredHighlights(g, p0,p, r, host, this); + } while (token!=null && token.isPaintable() && token.getEndOffset()-1 clip.getMaxY()) { + break; + } + y += fontHeight; } // End of while (token!=null && token.isPaintable()). @@ -926,7 +941,7 @@ public void paint(Graphics g, Shape a) { View view = getView(i); if (selStart==selEnd || startOffset>=selEnd || endOffset Date: Sat, 24 Jun 2023 22:49:16 +1200 Subject: [PATCH 07/12] wrap-perf: clip the tokens we paint selected in a View --- .../ui/rsyntaxtextarea/WrappedSyntaxView.java | 200 ++++++++++-------- 1 file changed, 108 insertions(+), 92 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index 0f1d27e89..c1716faf6 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -285,6 +285,7 @@ protected void drawView(TokenPainter painter, Graphics2D g, Rectangle r, Rectang p0 = (p==p0) ? p1 : p; + /* NB: y is the baseline, so we check it before we add fontHeight */ if (y > clip.getMaxY()) { break; } @@ -319,7 +320,7 @@ protected void drawView(TokenPainter painter, Graphics2D g, Rectangle r, Rectang * @param selEnd The end of the selection. */ protected void drawViewWithSelection(TokenPainter painter, Graphics2D g, - Rectangle r, View view, int fontHeight, int y, int selStart, + Rectangle r, Rectangle clip, View view, int fontHeight, int y, int selStart, int selEnd) { float x = r.x; @@ -354,66 +355,71 @@ protected void drawViewWithSelection(TokenPainter painter, Graphics2D g, int p = calculateBreakPosition(p0, token, x); x = r.x; - h.paintLayeredHighlights(g, p0,p, r, host, this); + /* NB: for text drawing y is the baseline */ + final boolean paintThisLine = (y >= clip.getMinY()); - while (token!=null && token.isPaintable() && token.getEndOffset()-1token.getOffset()) { + tempToken.copyFrom(token); + tempToken.textCount = selStart - tempToken.getOffset(); + x = painter.paint(tempToken,g,x,y,host, this); + tempToken.textCount = token.length(); + tempToken.makeStartAt(selStart); + // Clone required since token and tempToken must be + // different tokens for else statement below + token = new TokenImpl(tempToken); + } + int selCount = Math.min(token.length(), selEnd-token.getOffset()); + if (selCount==token.length()) { + x = painter.paintSelected(token, g, x,y, host, this, + useSTC); + } + else { + tempToken.copyFrom(token); + tempToken.textCount = selCount; + x = painter.paintSelected(tempToken, g, x,y, host, this, + useSTC); + tempToken.textCount = token.length(); + tempToken.makeStartAt(token.getOffset() + selCount); + token = tempToken; + x = painter.paint(token, g, x,y, host, this); + } - if (selStart>token.getOffset()) { - tempToken.copyFrom(token); - tempToken.textCount = selStart - tempToken.getOffset(); - x = painter.paint(tempToken,g,x,y,host, this); - tempToken.textCount = token.length(); - tempToken.makeStartAt(selStart); - // Clone required since token and tempToken must be - // different tokens for else statement below - token = new TokenImpl(tempToken); } - int selCount = Math.min(token.length(), selEnd-token.getOffset()); - if (selCount==token.length()) { - x = painter.paintSelected(token, g, x,y, host, this, - useSTC); - } - else { + // Selection ends in this token + else if (token.containsPosition(selEnd)) { tempToken.copyFrom(token); - tempToken.textCount = selCount; + tempToken.textCount = selEnd - tempToken.getOffset(); x = painter.paintSelected(tempToken, g, x,y, host, this, useSTC); tempToken.textCount = token.length(); - tempToken.makeStartAt(token.getOffset() + selCount); + tempToken.makeStartAt(selEnd); token = tempToken; x = painter.paint(token, g, x,y, host, this); } - } - - // Selection ends in this token - else if (token.containsPosition(selEnd)) { - tempToken.copyFrom(token); - tempToken.textCount = selEnd - tempToken.getOffset(); - x = painter.paintSelected(tempToken, g, x,y, host, this, - useSTC); - tempToken.textCount = token.length(); - tempToken.makeStartAt(selEnd); - token = tempToken; - x = painter.paint(token, g, x,y, host, this); - } - - // This token is entirely selected - else if (token.getOffset()>=selStart && - token.getEndOffset()<=selEnd) { - x = painter.paintSelected(token, g, x,y, host, this,useSTC); - } + // This token is entirely selected + else if (token.getOffset()>=selStart && + token.getEndOffset()<=selEnd) { + x = painter.paintSelected(token, g, x,y, host, this,useSTC); + } - // This token is entirely unselected - else { - x = painter.paint(token, g, x,y, host, this); + // This token is entirely unselected + else { + x = painter.paint(token, g, x,y, host, this); + } } - token = token.getNextToken(); } @@ -427,59 +433,61 @@ else if (token.getOffset()>=selStart && tokenOffset, token.getType(), token.getLanguageIndex()); token.setLanguageIndex(token.getLanguageIndex()); - // Selection starts in this token - if (token.containsPosition(selStart)) { + if (paintThisLine) { + // Selection starts in this token + if (token.containsPosition(selStart)) { + + if (selStart>token.getOffset()) { + tempToken.copyFrom(token); + tempToken.textCount = selStart - tempToken.getOffset(); + x = painter.paint(tempToken,g,x,y,host, this); + tempToken.textCount = token.length(); + tempToken.makeStartAt(selStart); + // Clone required since token and tempToken must be + // different tokens for else statement below + token = new TokenImpl(tempToken); + } - if (selStart>token.getOffset()) { - tempToken.copyFrom(token); - tempToken.textCount = selStart - tempToken.getOffset(); - x = painter.paint(tempToken,g,x,y,host, this); - tempToken.textCount = token.length(); - tempToken.makeStartAt(selStart); - // Clone required since token and tempToken must be - // different tokens for else statement below - token = new TokenImpl(tempToken); - } + int selCount = Math.min(token.length(), selEnd-token.getOffset()); + if (selCount==token.length()) { + x = painter.paintSelected(token, g, x,y, host, this, + useSTC); + } + else { + tempToken.copyFrom(token); + tempToken.textCount = selCount; + x = painter.paintSelected(tempToken, g, x,y, host, + this, useSTC); + tempToken.textCount = token.length(); + tempToken.makeStartAt(token.getOffset() + selCount); + token = tempToken; + x = painter.paint(token, g, x,y, host, this); + } - int selCount = Math.min(token.length(), selEnd-token.getOffset()); - if (selCount==token.length()) { - x = painter.paintSelected(token, g, x,y, host, this, - useSTC); } - else { + + // Selection ends in this token + else if (token.containsPosition(selEnd)) { tempToken.copyFrom(token); - tempToken.textCount = selCount; - x = painter.paintSelected(tempToken, g, x,y, host, - this, useSTC); + tempToken.textCount = selEnd - tempToken.getOffset(); + x = painter.paintSelected(tempToken, g, x,y, host, this, + useSTC); tempToken.textCount = token.length(); - tempToken.makeStartAt(token.getOffset() + selCount); + tempToken.makeStartAt(selEnd); token = tempToken; x = painter.paint(token, g, x,y, host, this); } - } - - // Selection ends in this token - else if (token.containsPosition(selEnd)) { - tempToken.copyFrom(token); - tempToken.textCount = selEnd - tempToken.getOffset(); - x = painter.paintSelected(tempToken, g, x,y, host, this, - useSTC); - tempToken.textCount = token.length(); - tempToken.makeStartAt(selEnd); - token = tempToken; - x = painter.paint(token, g, x,y, host, this); - } - - // This token is entirely selected - else if (token.getOffset()>=selStart && - token.getEndOffset()<=selEnd) { - x = painter.paintSelected(token, g, x,y, host, this,useSTC); - } + // This token is entirely selected + else if (token.getOffset()>=selStart && + token.getEndOffset()<=selEnd) { + x = painter.paintSelected(token, g, x,y, host, this,useSTC); + } - // This token is entirely unselected - else { - x = painter.paint(token, g, x,y, host, this); + // This token is entirely unselected + else { + x = painter.paint(token, g, x,y, host, this); + } } token = new TokenImpl(orig); @@ -487,11 +495,19 @@ else if (token.getOffset()>=selStart && } - // Paint parser (e.g. squiggle underline) highlights after - // text and selection - h.paintParserHighlights(g, p0,p, r, host, this); + if (paintThisLine) { + // Paint parser (e.g. squiggle underline) highlights after + // text and selection + h.paintParserHighlights(g, p0,p, r, host, this); + } p0 = (p==p0) ? p1 : p; + + /* NB: y is the baseline, so we check it before we add fontHeight */ + if (y > clip.getMaxY()) { + break; + } + y += fontHeight; } // End of while (token!=null && token.isPaintable()). @@ -946,7 +962,7 @@ public void paint(Graphics g, Shape a) { } else { //System.out.println("Drawing line with selection: " + i); - drawViewWithSelection(painter, g2d, alloc, view, fontHeight, + drawViewWithSelection(painter, g2d, alloc, clip, view, fontHeight, tempRect.y+ascent, selStart, selEnd); } } From ec41cf01f54eac6bf98abeff78c129fb3beec656 Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sat, 24 Jun 2023 22:50:14 +1200 Subject: [PATCH 08/12] wrap-perf: avoid spurious resizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’ve observed ongoing one pixel changes backwards and forwards in width in the setSize method from its two different call-sites. --- .../java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index c1716faf6..6b9c99e04 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -1043,7 +1043,7 @@ private void setSegment(int p0, int p1, Document document, @Override public void setSize(float width, float height) { updateMetrics(); - if ((int) width != getWidth()) { + if (Math.abs((int) width - getWidth()) > 1) { // invalidate the view itself since the children's // desired widths will be based upon this view's width. preferenceChanged(null, true, true); From f5ae6bae98ace818d26d4fc985844428bdee3589 Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Sun, 25 Jun 2023 08:27:56 +1200 Subject: [PATCH 09/12] fold-perf: improve fold performance on large files --- .../folding/DefaultFoldManager.java | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java index 751fe2b89..2da314c61 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java @@ -11,7 +11,9 @@ import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -53,6 +55,7 @@ public class DefaultFoldManager implements FoldManager { private Parser rstaParser; private FoldParser foldParser; private List folds; + private Map foldsPerLine; private boolean codeFoldingEnabled; private PropertyChangeSupport support; private Listener l; @@ -71,6 +74,7 @@ public DefaultFoldManager(RSyntaxTextArea textArea) { textArea.addPropertyChangeListener(RSyntaxTextArea.SYNTAX_STYLE_PROPERTY, l); textArea.addPropertyChangeListener("document", l); folds = new ArrayList<>(); + foldsPerLine = new HashMap<>(); updateFoldParser(); } @@ -84,6 +88,7 @@ public void addPropertyChangeListener(PropertyChangeListener l) { @Override public void clear() { folds.clear(); + foldsPerLine.clear(); } @@ -163,7 +168,13 @@ public int getFoldCount() { @Override public Fold getFoldForLine(int line) { - return getFoldForLineImpl(null, folds, line); + if (foldsPerLine.containsKey(line)) { + return foldsPerLine.get(line); + } + + Fold result = getFoldForLineImpl(null, folds, line); + foldsPerLine.put(line, result); + return result; } @@ -364,33 +375,44 @@ public boolean isFoldStartLine(int line) { @Override public boolean isLineHidden(int line) { - for (Fold fold : folds) { - if (fold.containsLine(line)) { - if (fold.isCollapsed()) { - return true; - } - else { - return isLineHiddenImpl(fold, line); - } - } - } - return false; + return isLineHiddenImpl(null, folds, line); } + private boolean isLineHiddenImpl(Fold parent, List folds, int line) { + if (folds == null) { + return false; + } + + int low = 0; + int high = folds.size() - 1; - private boolean isLineHiddenImpl(Fold parent, int line) { - for (int i=0; i> 1; + Fold midFold = folds.get(mid); + int startLine = midFold.getStartLine(); + if (line==startLine) { + /* The first line of a fold is never hidden */ + return false; + } + else if (lineendLine) { + low = mid + 1; } - else { - return isLineHiddenImpl(child, line); + else { // line>startLine && line<=endLine + if (midFold.isCollapsed()) { + return true; + } else { + return isLineHiddenImpl(midFold, midFold.getChildren(), line); + } } } } - return false; + + return false; // No fold for this line } @@ -450,6 +472,7 @@ public void reparse() { keepFoldStates(newFolds, folds); } folds = newFolds; + foldsPerLine = new HashMap<>(); // Let folks (gutter, etc.) know that folds have been updated. support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, folds); @@ -458,6 +481,7 @@ public void reparse() { } else { folds.clear(); + foldsPerLine.clear(); } } @@ -484,6 +508,7 @@ public ParseResult parse(RSyntaxDocument doc, String style) { } else { folds = Collections.emptyList(); + foldsPerLine = new HashMap<>(); textArea.repaint(); support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null); } @@ -494,6 +519,7 @@ public ParseResult parse(RSyntaxDocument doc, String style) { @Override public void setFolds(List folds) { this.folds = folds; + this.foldsPerLine = new HashMap<>(); } From 2634e0da736f43093f5b16eec50946594353763e Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Mon, 26 Jun 2023 21:58:30 +1200 Subject: [PATCH 10/12] fold-perf: rename lookup table and document format --- .../folding/DefaultFoldManager.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java index 2da314c61..517ec1e48 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java @@ -55,7 +55,10 @@ public class DefaultFoldManager implements FoldManager { private Parser rstaParser; private FoldParser foldParser; private List folds; - private Map foldsPerLine; + /** + * A {@link Map} from line number to {@link Fold}. + */ + private Map foldsByLine; private boolean codeFoldingEnabled; private PropertyChangeSupport support; private Listener l; @@ -74,7 +77,7 @@ public DefaultFoldManager(RSyntaxTextArea textArea) { textArea.addPropertyChangeListener(RSyntaxTextArea.SYNTAX_STYLE_PROPERTY, l); textArea.addPropertyChangeListener("document", l); folds = new ArrayList<>(); - foldsPerLine = new HashMap<>(); + foldsByLine = new HashMap<>(); updateFoldParser(); } @@ -88,7 +91,7 @@ public void addPropertyChangeListener(PropertyChangeListener l) { @Override public void clear() { folds.clear(); - foldsPerLine.clear(); + foldsByLine.clear(); } @@ -168,12 +171,12 @@ public int getFoldCount() { @Override public Fold getFoldForLine(int line) { - if (foldsPerLine.containsKey(line)) { - return foldsPerLine.get(line); + if (foldsByLine.containsKey(line)) { + return foldsByLine.get(line); } Fold result = getFoldForLineImpl(null, folds, line); - foldsPerLine.put(line, result); + foldsByLine.put(line, result); return result; } @@ -472,7 +475,7 @@ public void reparse() { keepFoldStates(newFolds, folds); } folds = newFolds; - foldsPerLine = new HashMap<>(); + foldsByLine = new HashMap<>(); // Let folks (gutter, etc.) know that folds have been updated. support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, folds); @@ -481,7 +484,7 @@ public void reparse() { } else { folds.clear(); - foldsPerLine.clear(); + foldsByLine.clear(); } } @@ -508,7 +511,7 @@ public ParseResult parse(RSyntaxDocument doc, String style) { } else { folds = Collections.emptyList(); - foldsPerLine = new HashMap<>(); + foldsByLine = new HashMap<>(); textArea.repaint(); support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null); } @@ -519,7 +522,7 @@ public ParseResult parse(RSyntaxDocument doc, String style) { @Override public void setFolds(List folds) { this.folds = folds; - this.foldsPerLine = new HashMap<>(); + this.foldsByLine = new HashMap<>(); } From 30522854cdd45c453f3e89afafe655aad2e9719f Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Thu, 6 Jul 2023 18:26:55 +1200 Subject: [PATCH 11/12] wrap-perf: clear lookup tables when the document changes --- .../java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java index 6b9c99e04..ca8fb2fd1 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/WrappedSyntaxView.java @@ -1533,6 +1533,10 @@ else if (tlist != null) { private void handleDocumentEvent(DocumentEvent e, Shape a, ViewFactory f) { + /* Clear lookup tables */ + posToHeightLookup = null; + heightToPosLookup = null; + int n = calculateLineCount(); if (this.nlines != n) { this.nlines = n; From e04f26b13596e0b169076b6663292e2d72df903f Mon Sep 17 00:00:00 2001 From: Karl von Randow Date: Thu, 6 Jul 2023 18:56:53 +1200 Subject: [PATCH 12/12] fold-perf: improve performance when there are lots of nested folds --- .../fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java index 517ec1e48..2e9570b42 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/folding/DefaultFoldManager.java @@ -408,8 +408,10 @@ else if (linestartLine && line<=endLine if (midFold.isCollapsed()) { return true; - } else { + } else if (midFold.getCollapsedLineCount() != 0) { return isLineHiddenImpl(midFold, midFold.getChildren(), line); + } else { + return false; } } }