package bluej.editor.moe;

import bluej.Config;
import bluej.editor.moe.MoeSyntaxDocument.Element;
import bluej.editor.moe.MoeSyntaxEvent.NodeChangeRecord;
import bluej.editor.moe.Token.TokenType;
import bluej.parser.nodes.NodeTree.NodeAndPosition;
import bluej.parser.nodes.ParsedCUNode;
import bluej.parser.nodes.ParsedNode;
import bluej.prefmgr.PrefMgr;
import bluej.utility.Debug;
import bluej.utility.javafx.FXCache;
import bluej.utility.javafx.JavaFXUtil;
import com.google.common.collect.ImmutableSet;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.fxmisc.richtext.model.TwoDimensional.Position;
import threadchecker.OnThread;
import threadchecker.Tag;

import java.util.*;
import java.util.Map.Entry;
import javax.swing.text.Segment;


| A Swing view implementation that does syntax colouring and adds some utility. | | <p>A BlueJSyntaxView (or subclass) instance is normally created by an implementation of | the EditorKit interface. | | <p>The original version of this class was based on SyntaxView from JEdit. Little of | that code remains. | | @author Slava Pestov | @author Bruce Quig | @author Michael Kolling | @author Davin McCall | @OnThread(Tag.FXPlatform) public class BlueJSyntaxView {
| (NaviView) Paint method inner scope? if false, whole method will be highlighted as a single block | private static final boolean PAINT_METHOD_INNER = false; private static final int LEFT_INNER_SCOPE_MARGIN = 5; private static final int LEFT_OUTER_SCOPE_MARGIN = 2; private static final int RIGHT_SCOPE_MARGIN = 4; private static final int CURVED_CORNER_SIZE = 4; private static final int PARAGRAPH_MARGIN = 24; @OnThread(Tag.FX) private static final int[][] CORNER_TEMPLATE = new int[][] { {0, 0, 1, 1 }, {0, 1, 2, 2 }, {1, 2, 2, 2 }, {1, 2, 2, 2 } }; private final MoeSyntaxDocument document; private final FXCache<ScopeInfo, Image> imageCache; private final ScopeColors scopeColors; private final BooleanExpression syntaxHighlighting; private int imageCacheLineHeight; private ReadOnlyDoubleProperty widthProperty; private MoeEditorPane editorPane; @OnThread(Tag.FX) private boolean small = false;
| Scope painting colours | | The following are initialized by resetColors() | private Color BK; private Color C1; private Color C2; private Color C3; private Color M1; private Color M2; private Color S1; private Color S2; private Color I1; private Color I2; private final List<Double> cachedSpaceSizes = new ArrayList<>(); public static enum ParagraphAttribute { STEP_MARK("bj-step-mark"), BREAKPOINT("bj-breakpoint"), ERROR("bj-error"); private final String pseudoClass; ParagraphAttribute(String pseudoClass) { this.pseudoClass = pseudoClass; } public String getPseudoclass() { return pseudoClass; } }
| Cached indents for ParsedNode items. Maps a node to an indent (in pixels) | When this is zero, it means it is not yet fully valid as the editor has not | yet appeared on screen. | private final Map<ParsedNode,Integer> nodeIndents = new HashMap<ParsedNode,Integer>();
| Are we in the middle of an update which comes from the RichTextFX stream of changes? | If so, we must not ask for character bounds because the offset calculations | are all wrong, and a layout may be forced resulting in inconsistent state in | RichTextFX (and an exception being thrown: catching the exception is not enough | to avoid the incorrect state). | private boolean duringUpdate;
| Creates a new BlueJSyntaxView. | public BlueJSyntaxView(MoeSyntaxDocument document, ScopeColors scopeColors) { this.document = document; this.syntaxHighlighting = PrefMgr.flagProperty(PrefMgr.HIGHLIGHTING); this.imageCache = new FXCache<>(s -> drawImageFor(s, imageCacheLineHeight), 40); this.scopeColors = scopeColors; resetColors(); JavaFXUtil.addChangeListenerPlatform(PrefMgr.getScopeHighlightStrength(), str -> { resetColors(); imageCache.clear(); document.recalculateAllScopes(); }); JavaFXUtil.addChangeListenerPlatform(syntaxHighlighting, syn -> { document.recalculateAllScopes(); }); JavaFXUtil.addChangeListenerPlatform(PrefMgr.getEditorFontSize(), sz -> { imageCache.clear(); nodeIndents.clear(); cachedSpaceSizes.clear(); document.recalculateAllScopes(); }); JavaFXUtil.addChangeListenerPlatform(scopeColors.scopeClassColorProperty(), str -> { if (!document.isPrinting()) { JavaFXUtil.runAfterCurrent(() -> { resetColors(); imageCache.clear(); document.recalculateAllScopes(); }); } }); }
| Get the editor pane that this view is associated with. | public MoeEditorPane getEditorPane() { return editorPane; }
| Mark the syntax view as being during an update. Don't forget | to match every true call with a later false call. | @param duringUpdate | public void setDuringUpdate(boolean duringUpdate) { this.duringUpdate = duringUpdate; }
| Gets the syntax token styles for a given line of code. | | Returns null if there are no styles to apply (e.g. on a blank line or one with only whitespace). | protected final StyleSpans> getTokenStylesFor(int lineIndex, MoeSyntaxDocument document) { StyleSpansBuilder<ImmutableSet<String>> lineStyle = new StyleSpansBuilder<>(); Token tokens = document.getTokensForLine(lineIndex); boolean addedAny = false; for (;;) { TokenType id = tokens.id; if (id == TokenType.END) break; lineStyle.add(syntaxHighlighting.get() ? ImmutableSet.of(id.getCSSClass()) : ImmutableSet.of(), tokens.length); addedAny = true; tokens = tokens.next; } if (addedAny) return lineStyle.create(); else{ return null; } }
| Recalculate scope margins in the given line range. All line numbers are 0-based. | | @param pendingScopes map to store updated scope margin information. | @param firstLineIncl the first line in the range to update (inclusive). | @param lastLineIncl the last line in the range to update (inclusive). | public void recalculateScopes(Map<Integer, ScopeInfo> pendingScopes, int firstLineIncl, int lastLineIncl) { if (editorPane == null) return; recalcScopeMarkers(pendingScopes, (widthProperty == null || widthProperty.get() == 0) ? 200 : ((int)widthProperty.get() - PARAGRAPH_MARGIN), firstLineIncl, lastLineIncl, false); } public Image getImageFor(ScopeInfo s, int lineHeight) { if (lineHeight == 0) { return new WritableImage(1, 1); } if (lineHeight != imageCacheLineHeight) { imageCache.clear(); imageCacheLineHeight = lineHeight; } return copy(imageCache.get(s)); } private static Image copy(Image original) { return new WritableImage(original.getPixelReader(), (int)original.getWidth(), (int)original.getHeight()); } @OnThread(Tag.FX) private Image drawImageFor(ScopeInfo s, int lineHeight) { WritableImage image = new WritableImage(s.nestedScopes.stream() .mapToInt(n -> n.leftRight.rhs + 1).max().orElse(1) + 1, lineHeight); for (ScopeInfo.SingleNestedScope singleNestedScope : s.nestedScopes) { LeftRight leftRight = singleNestedScope.leftRight; int padding = small ? 0 : CURVED_CORNER_SIZE; int sideTopMargin = leftRight.starts ? padding : 0; int sideBottomMargin = leftRight.ends ? padding : 0; fillRect(image.getPixelWriter(), leftRight.lhs, 0 + sideTopMargin, padding, lineHeight - sideBottomMargin - sideTopMargin, leftRight.fillColor); for (int y = sideTopMargin; y < lineHeight - sideBottomMargin; y++) { image.getPixelWriter().setColor(leftRight.lhs, y, leftRight.edgeColor); } if (leftRight.starts) { for (int x = 0; x < padding; x++) { for (int y = 0; y < padding; y++) { if (CORNER_TEMPLATE[y][x] != 0) { Color c = (CORNER_TEMPLATE[y][x] == 1) ? leftRight.edgeColor : leftRight.fillColor; image.getPixelWriter().setColor(leftRight.lhs + x, y, c); } } } } if (leftRight.ends) { for (int x = 0; x < padding; x++) { for (int y = 0; y < padding; y++) { if (CORNER_TEMPLATE[y][x] != 0) { Color c = (CORNER_TEMPLATE[y][x] == 1) ? leftRight.edgeColor : leftRight.fillColor; image.getPixelWriter().setColor(leftRight.lhs + x, lineHeight - 1 - y, c); } } } } Middle middle = singleNestedScope.middle; fillRect(image.getPixelWriter(), middle.lhs + padding, 0, middle.rhs - middle.lhs - padding, lineHeight, middle.bodyColor); if (middle.topColor != null) { for (int x = middle.lhs + padding; x < middle.rhs; x++) { image.getPixelWriter().setColor(x, 0, middle.topColor); } } if (middle.bottomColor != null) { for (int x = middle.lhs + padding; x < middle.rhs; x++) { image.getPixelWriter().setColor(x, lineHeight - 1, middle.bottomColor); } } fillRect(image.getPixelWriter(), leftRight.rhs - padding, 0 + sideTopMargin, padding, lineHeight - sideBottomMargin - sideTopMargin, leftRight.fillColor); for (int y = sideTopMargin; y < lineHeight - sideBottomMargin; y++) { image.getPixelWriter().setColor(leftRight.rhs, y, leftRight.edgeColor); } if (leftRight.starts && leftRight.rhs > padding) { for (int x = 0; x < padding; x++) { for (int y = 0; y < padding; y++) { if (CORNER_TEMPLATE[y][x] != 0) { Color c = (CORNER_TEMPLATE[y][x] == 1) ? leftRight.edgeColor : leftRight.fillColor; image.getPixelWriter().setColor(leftRight.rhs - x, y, c); } } } } if (leftRight.ends && leftRight.rhs > padding) { for (int x = 0; x < padding; x++) { for (int y = 0; y < padding; y++) { if (CORNER_TEMPLATE[y][x] != 0) { Color c = (CORNER_TEMPLATE[y][x] == 1) ? leftRight.edgeColor : leftRight.fillColor; image.getPixelWriter().setColor(leftRight.rhs - x, lineHeight - 1 - y, c); } } } } } if (s.getAttributes().contains(ParagraphAttribute.STEP_MARK)) { blend(image, scopeColors.stepMarkOverlayColorProperty().get()); } else if (s.getAttributes().contains(ParagraphAttribute.BREAKPOINT)) { blend(image, scopeColors.breakpointOverlayColorProperty().get()); } return image; } @OnThread(Tag.FX) private static void blend(WritableImage image, Color rgba) { Color c = new Color(rgba.getRed(), rgba.getGreen(), rgba.getBlue(), 1.0); for (int x = 0; x < image.getWidth(); x++) { for (int y = 0; y < image.getHeight(); y++) { Color prev = image.getPixelReader().getColor(x, y); image.getPixelWriter().setColor(x, y, prev.interpolate(c, rgba.getOpacity())); } } } @OnThread(Tag.FX) private static void fillRect(PixelWriter pixelWriter, int x, int y, int w, int h, Color c) { if (x < 0) { w -= -x; x = 0; } if (y < 0) { h -= -y; y = 0; } for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { pixelWriter.setColor(x + i, y + j, c); } } } public void setEditorPane(MoeEditorPane editorPane) { this.editorPane = editorPane; this.widthProperty = editorPane.widthProperty(); JavaFXUtil.addChangeListenerPlatform(widthProperty, w -> { document.fireChangedUpdate(null); }); JavaFXUtil.addChangeListenerPlatform(editorPane.showLineNumbersProperty(), showLineNumbers -> { editorPane.setParagraphGraphicFactory(this::getParagraphicGraphic); }); }
| A container for three line segments and elements: the previous (or above) line, the | current line, and the next (or below) line. | private class ThreeLines { Segment aboveLineSeg; Segment thisLineSeg; Segment belowLineSeg; Element aboveLineEl; Element thisLineEl; Element belowLineEl; }
| Re-calculate scope margins for the given lines, and add changed margin information to the given | map. Line numbers are 0-based. | | @param pendingScopes a map of (line number : scope information) for updated scope margins | @param fullWidth the full width of the view, used for determining right margin | @param firstLine the first line in the range to process (inclusive). | @param lastLine the last line in the range to process (inclusive). | @param onlyMethods true if only methods should be scope highlighted and not constructs inside. | protected void recalcScopeMarkers(Map<Integer, ScopeInfo> pendingScopes, int fullWidth, int firstLine, int lastLine, boolean onlyMethods) { Element map = document.getDefaultRootElement(); ParsedNode rootNode = document.getParsedNode(); if (rootNode == null) { return; } int aboveLine = firstLine - 1; List<NodeAndPosition<ParsedNode>> prevScopeStack = new LinkedList<NodeAndPosition<ParsedNode>>(); int curLine = firstLine; ThreeLines lines = new ThreeLines(); lines.aboveLineSeg = new Segment(); lines.thisLineSeg = new Segment(); lines.belowLineSeg = new Segment(); lines.aboveLineEl = null; if (aboveLine >= 0) { lines.aboveLineEl = map.getElement(aboveLine); document.getText(lines.aboveLineEl.getStartOffset(), lines.aboveLineEl.getEndOffset() - lines.aboveLineEl.getStartOffset(), lines.aboveLineSeg); } lines.belowLineEl = null; if (firstLine + 1 < map.getElementCount()) { lines.belowLineEl = map.getElement(firstLine + 1); document.getText(lines.belowLineEl.getStartOffset(), lines.belowLineEl.getEndOffset() - lines.belowLineEl.getStartOffset(), lines.belowLineSeg); } lines.thisLineEl = map.getElement(firstLine); document.getText(lines.thisLineEl.getStartOffset(), lines.thisLineEl.getEndOffset() - lines.thisLineEl.getStartOffset(), lines.thisLineSeg); getScopeStackAfter(rootNode, 0, lines.thisLineEl.getStartOffset(), prevScopeStack); while (curLine <= lastLine){ ScopeInfo scope = new ScopeInfo(getParagraphAttributes(curLine + 1)); if (prevScopeStack.isEmpty()) { break; } drawScopes(fullWidth, scope, document, lines, prevScopeStack, onlyMethods, 0); if (! scope.equals(document.getDocument().getParagraphStyle(curLine))) { pendingScopes.put(curLine, scope); } else { pendingScopes.remove(curLine); } curLine++; if (curLine <= lastLine) { lines.aboveLineEl = lines.thisLineEl; lines.thisLineEl = lines.belowLineEl; if (curLine + 1 < map.getElementCount()) { lines.belowLineEl = map.getElement(curLine + 1); } else { lines.belowLineEl = null; } Segment oldAbove = lines.aboveLineSeg; lines.aboveLineSeg = lines.thisLineSeg; lines.thisLineSeg = lines.belowLineSeg; lines.belowLineSeg = oldAbove; if (lines.belowLineEl != null) { document.getText(lines.belowLineEl.getStartOffset(), lines.belowLineEl.getEndOffset() - lines.belowLineEl.getStartOffset(), lines.belowLineSeg); } } } } private class DrawInfo { final ScopeInfo scopes; ThreeLines lines; ParsedNode node; boolean starts; boolean ends; Color color1; Color color2; private DrawInfo(ScopeInfo scopes) { this.scopes = scopes; } }
| Draw the scope highlighting for one line of the document. | | @param fullWidth the width of the editor view | @param g the graphics context to render to | @param document the document | @param lines the previous, current and next lines (segments and elements) | @param prevScopeStack the stack of nodes (from outermost to innermost) at the beginning of the current line | private void drawScopes(int fullWidth, ScopeInfo scopes, MoeSyntaxDocument document, ThreeLines lines, List<NodeAndPosition<ParsedNode>> prevScopeStack, boolean onlyMethods, int nodeDepth) { int rightMargin = small ? 0 : 10; ListIterator<NodeAndPosition<ParsedNode>> li = prevScopeStack.listIterator(); DrawInfo drawInfo = new DrawInfo(scopes); drawInfo.lines = lines; while (li.hasNext()){ NodeAndPosition<ParsedNode> nap = li.next(); int napPos = nap.getPosition(); int napEnd = nap.getEnd(); if (napPos >= lines.thisLineEl.getEndOffset()) { return; } if (! drawNode(drawInfo, nap, onlyMethods)) { continue; } if (nodeSkipsEnd(napPos, napEnd, lines.thisLineEl, lines.thisLineSeg)) { nodeDepth++; break; } int xpos = getNodeIndent(document, nap, lines.thisLineEl, lines.thisLineSeg); if (xpos != - 1 && xpos <= fullWidth) { boolean starts = nodeSkipsStart(nap, lines.aboveLineEl, lines.aboveLineSeg); boolean ends = nodeSkipsEnd(napPos, napEnd, lines.belowLineEl, lines.belowLineSeg); int rbound = getNodeRBound(nap, fullWidth - rightMargin, nodeDepth, lines.thisLineEl, lines.thisLineSeg); drawInfo.node = nap.getNode(); drawInfo.starts = starts; drawInfo.ends = ends; Color[] colors = colorsForNode(drawInfo.node); drawInfo.color1 = colors[0]; drawInfo.color2 = colors[1]; drawInfo.scopes.nestedScopes.add(calculatedNestedScope(drawInfo, xpos, rbound)); } else if (xpos == -1) { drawInfo.scopes.incomplete = true; } nodeDepth++; } nodeDepth--; li = prevScopeStack.listIterator(prevScopeStack.size()); NodeAndPosition<ParsedNode> nap = li.previous(); int napPos = nap.getPosition(); int napEnd = napPos + nap.getSize(); while (napEnd <= lines.thisLineEl.getEndOffset()){ li.remove(); if (drawNode(drawInfo, nap, onlyMethods)) { nodeDepth--; } if (! li.hasPrevious()) return; NodeAndPosition<ParsedNode> napParent = li.previous(); li.next(); NodeAndPosition<ParsedNode> nextNap = nap.nextSibling(); napPos = napParent.getPosition(); napEnd = napPos + napParent.getSize(); nap = napParent; while (nextNap != null){ li.add(nextNap); li.previous(); li.next(); napPos = nextNap.getPosition(); napEnd = napPos + nextNap.getSize(); if (napPos < lines.thisLineEl.getEndOffset() && ! nodeSkipsStart(nextNap, lines.thisLineEl, lines.thisLineSeg)) { if (drawNode(drawInfo, nextNap, onlyMethods)) { nodeDepth++; int xpos = getNodeIndent(document, nextNap, lines.thisLineEl, lines.thisLineSeg); int rbound = getNodeRBound(nextNap, fullWidth - rightMargin, nodeDepth, lines.thisLineEl, lines.thisLineSeg); drawInfo.node = nextNap.getNode(); Color [] colors = colorsForNode(drawInfo.node); drawInfo.color1 = colors[0]; drawInfo.color2 = colors[1]; drawInfo.starts = nodeSkipsStart(nextNap, lines.aboveLineEl, lines.aboveLineSeg); drawInfo.ends = nodeSkipsEnd(napPos, napEnd, lines.belowLineEl, lines.belowLineSeg); if (xpos != -1 && xpos <= fullWidth) { drawInfo.scopes.nestedScopes.add(calculatedNestedScope(drawInfo, xpos, rbound)); } } } nap = nextNap; nextNap = nextNap.getNode().findNodeAtOrAfter(napPos, napPos); } } }
| Gets the left edge of the character at the given offset into the document, if we can calculate it. | | @param startOffset The offset into the document of the character we want the left edge for | @return If available, Optional.of(left-edge-X-in-pixels-in-local-coords). If it is not available | (which is very possible: *always* check for Optional.empty), then Optional.empty will be returned. | private OptionalInt getLeftEdge(int startOffset) { if (editorPane == null) { return OptionalInt.empty(); } Position position = document.offsetToPosition(startOffset); int column = position.getMinor(); String lineText = document.getDocument().getParagraph(position.getMajor()).getText(); boolean allSpaces = (column == 0) || lineText.lastIndexOf(' ', column - 1) == 0; if (!editorPane.lineIsVisible(position.getMajor()) && (!allSpaces || cachedSpaceSizes.size() <= 4)) { if (document.isPrinting()) { TextField field = new TextField(); field.styleProperty().bind(editorPane.styleProperty()); @SuppressWarnings("unused") Scene s = new Scene(new BorderPane(field)); field.applyCss(); double singleSpaceWidth = JavaFXUtil.measureString(field, " ", false, false) / 10.0; int positionSpaceWidth = (int)(singleSpaceWidth * position.getMinor() * 1.05); return OptionalInt.of(positionSpaceWidth + PARAGRAPH_MARGIN); } else { return OptionalInt.empty(); } }
| | So, if a character is on screen, it's trivial to calculate the indent in pixels, we just ask the | editor pane. If the character is not on screen, the editor pane won't tell us the indent. Which | would prevent us drawing the scope boxes correctly. Consider this case: | <----- This is the top of the viewport | class A | {} * public void method foo() | {} * | <----- This is the bottom of the viewport | } //line X | } | | Because we can't calculate the indent for line X, we would draw the scope box for the method with | its left-hand edge 4 spaces in, rather than 1 as it should be, and when we scrolled down, it would | either be wrong or we would need a redraw, which would look ugly. | | However, here's the trick. If the line off-screen is *more* indented than any line on screen, it | won't affect our scope drawing (because we are looking for the left edge). If the line off-screen | is indented *less* than any of the lines on screen then we can use the line on-screen with the highest | indent to calculate the scope that we need. This is because if you've got a line with say 20 spaces, | and you need to know the indent for an 8-space line, then you can just calculate it by asking for | the 8th character position on the 20-space line. So as long as we want an indent with less spaces | than the largest line on screen, we can calculate it. | | We store cached indent sizes (to cut down on continual recalculation) in the cachedSpaceSizes | array, where item at index 0 is the pixel indent for 0 spaces, item 1 is the pixel indent for 1 spaces, etc. | We are making the assumption that spaces are always the same sizes on each line, but I can't | think of any situation where that is not the case. We clear the array when the font size changes. | if (allSpaces) { int numberOfSpaces = column; while (numberOfSpaces >= cachedSpaceSizes.size()) { Optional<Bounds> screenBounds = Optional.empty(); if (!duringUpdate) { screenBounds = editorPane.getCharacterBoundsOnScreen( startOffset - numberOfSpaces + cachedSpaceSizes.size(), startOffset - numberOfSpaces + cachedSpaceSizes.size() + 1); } if (!screenBounds.isPresent()) { if (cachedSpaceSizes.size() >= 4) { int highestSpaces = cachedSpaceSizes.size() - 1; double highestWidth = cachedSpaceSizes.get(highestSpaces) - cachedSpaceSizes.get(0); return OptionalInt.of((int)(highestWidth / highestSpaces * numberOfSpaces + cachedSpaceSizes.get(0))); } return OptionalInt.empty(); } double indent = editorPane.screenToLocal(screenBounds.get()).getMinX(); cachedSpaceSizes.add(indent); } return OptionalInt.of(cachedSpaceSizes.get(numberOfSpaces).intValue()); } else { try { Optional<Bounds> screenBounds = Optional.empty(); if (!duringUpdate && startOffset + 1 < editorPane.getLength()) { screenBounds = editorPane.getCharacterBoundsOnScreen(startOffset, startOffset + 1); } if (screenBounds.isPresent()) { double indent = editorPane.screenToLocal(screenBounds.get()).getMinX(); return OptionalInt.of((int) indent); } } catch (IllegalArgumentException | IndexOutOfBoundsException e) { Debug.reportError(e); } return OptionalInt.empty(); } }
| Check whether a node needs to be drawn. | @param info | @param node | @return | private boolean drawNode(DrawInfo info, NodeAndPosition<ParsedNode> nap, boolean onlyMethods) { int napPos = nap.getPosition(); int napEnd = napPos + nap.getSize(); if (napPos >= info.lines.thisLineEl.getEndOffset()) { return false; } if (! nap.getNode().isContainer() && ! nap.getNode().isInner()) { return false; } if (onlyMethods) { if (nap.getNode().getNodeType() == ParsedNode.NODETYPE_METHODDEF) { return true; } if (! PAINT_METHOD_INNER) { return false; } } if (nodeSkipsStart(nap, info.lines.thisLineEl, info.lines.thisLineSeg)) { return false; } return !nodeSkipsEnd(napPos, napEnd, info.lines.thisLineEl, info.lines.thisLineSeg); } private Color getBackgroundColor() { return BK; }
| Get the scope highlighting colours for a given node. | private Color[] colorsForNode(ParsedNode node) { if (node.isInner()) { return new Color[] { C3, getBackgroundColor() }; } else { if (node.getNodeType() == ParsedNode.NODETYPE_METHODDEF) { return new Color[] { M1, M2 }; } if (node.getNodeType() == ParsedNode.NODETYPE_ITERATION) { return new Color[] { I1, I2 }; } if (node.getNodeType() == ParsedNode.NODETYPE_SELECTION || node.getNodeType() == ParsedNode.NODETYPE_NONE) { |
return new Color[] { S1, S2 }; } return new Color[] { C1, C2 }; } }
| Create a nested scope record based on the supplied information. | private ScopeInfo.SingleNestedScope calculatedNestedScope(DrawInfo info, int xpos, int rbound) { if (! small) { xpos -= info.node.isInner() ? LEFT_INNER_SCOPE_MARGIN : LEFT_OUTER_SCOPE_MARGIN; } return new ScopeInfo.SingleNestedScope( new LeftRight(xpos, rbound, info.starts, info.ends, info.color2, info.color1), getScopeMiddle(info, xpos, rbound)); }
| Draw the center part of a scope (not the left or right edge, but the bit in between) | @param info general drawing information | @param xpos the leftmost x-coordinate to draw from | @param rbounds the rightmost x-coordinate to draw to | private Middle getScopeMiddle(DrawInfo info, int xpos, int rbounds) { Color color1 = info.color1; Color color2 = info.color2; boolean startsThisLine = info.starts; boolean endsThisLine = info.ends; Middle middle = new Middle(color2, xpos, rbounds - 1); if (startsThisLine) { middle.drawTop(color1); } if (endsThisLine) { middle.drawBottom(color1); } return middle; }
| Find the rightmost bound of a node on a particular line. | | @param napEnd The end of the node (position in the document just beyond the node) | @param fullWidth The full width to draw to (for the outermost mode) | @param nodeDepth The node depth | @param lineEl line element of the line to find the bound for | @param lineSeg Segment containing text of the current line | private int getNodeRBound(NodeAndPosition<ParsedNode> nap, int fullWidth, int nodeDepth, Element lineEl, Segment lineSeg) { int napEnd = nap.getEnd(); int rbound = fullWidth - nodeDepth * (small ? 0 : RIGHT_SCOPE_MARGIN); if (lineEl == null || napEnd >= lineEl.getEndOffset()) { return rbound; } if (napEnd < lineEl.getStartOffset()) { return rbound; } int nwsb = findNonWhitespaceComment(nap, lineEl, lineSeg, napEnd - lineEl.getStartOffset()); if (nwsb != -1) { OptionalInt eboundsX = getLeftEdge(napEnd); if (eboundsX.isPresent()) return Math.min(rbound, eboundsX.getAsInt()); else{ return rbound; } } return rbound; }
| Checks whether the given node should be skipped on the given line (because it | starts later). This takes into account that the node may "officially" start on the |* line, but only have white space, in which case it can be moved down to the next line. */ private boolean nodeSkipsStart(NodeAndPosition<ParsedNode> nap, Element lineEl, Segment segment) | |{ | |if (lineEl == null) { | |return true; | |} | |int napPos = nap.getPosition(); | |int napEnd = nap.getEnd(); | |if (napPos > lineEl.getStartOffset() && napEnd > lineEl.getEndOffset()) { | |// The node officially starts on this line, but might have no text on this | |// line. In that case, we probably want to move its start down to the next line. | |if (napPos >= lineEl.getEndOffset()) { | |return true; | |} | |int nws = findNonWhitespaceComment(nap, lineEl, segment, napPos - lineEl.getStartOffset()); | |if (nws == -1) { | |return true; | |} | |} | |return false; | |} | |/** | Check whether a node which overlaps a line of the document actually finishes on the | previous line, by way of not having any actual text on this line. Return true if | so. | private boolean nodeSkipsEnd(int napPos, int napEnd, Element lineEl, Segment segment) { if (lineEl == null) { return true; } if (napEnd < lineEl.getEndOffset() && napPos < lineEl.getStartOffset()) { if (napEnd <= lineEl.getStartOffset()) { return true; } if (napEnd >= lineEl.getEndOffset()) { return false; } int nws = findNonWhitespace(segment, 0); if (nws == -1 || lineEl.getStartOffset() + nws >= napEnd) { return true; } } return false; }
| Get a node's indent amount (in component co-ordinate space, minus left margin) for a given line. | If the node isn't present on the line, returns Integer.MAX_VALUE. A cached value | is used if available. | private int getNodeIndent(MoeSyntaxDocument doc, NodeAndPosition<ParsedNode> nap, Element lineEl, Segment segment) { if (lineEl == null) { return Integer.MAX_VALUE; } int napPos = nap.getPosition(); int napEnd = nap.getEnd(); if (napPos >= lineEl.getEndOffset()) { return Integer.MAX_VALUE; } if (napEnd <= lineEl.getStartOffset()) { return Integer.MAX_VALUE; } if (nodeSkipsStart(nap, lineEl, segment) || nodeSkipsEnd(napPos, napEnd, lineEl, segment)) { return Integer.MAX_VALUE; } Integer indent = nodeIndents.get(nap.getNode()); if (indent == null || indent <= 0) { if (editorPane != null && (editorPane.lineIsVisible(doc.offsetToPosition(lineEl.getStartOffset()).getMajor()) || doc.isPrinting())) { indent = getNodeIndent(doc, nap); nodeIndents.put(nap.getNode(), indent); } else { indent = -1; } } int xpos = indent; if (napPos > lineEl.getStartOffset()) { int nws = findNonWhitespaceBwards(segment, napPos - lineEl.getStartOffset() - 1, 0); if (nws != -1) { OptionalInt lboundsX = getLeftEdge(lineEl.getStartOffset() + nws + 1); if (lboundsX.isPresent()) { xpos = Math.max(xpos, lboundsX.getAsInt() - PARAGRAPH_MARGIN); } } } return xpos; }
| Calculate the indent for a node. | private int getNodeIndent(MoeSyntaxDocument doc, NodeAndPosition<ParsedNode> nap) { try { int indent = Integer.MAX_VALUE; int curpos = nap.getPosition(); int napEnd = nap.getEnd(); Element map = doc.getDefaultRootElement(); Stack<NodeAndPosition<ParsedNode>> scopeStack = new Stack<NodeAndPosition<ParsedNode>>(); scopeStack.add(nap); outer: while (curpos < napEnd){ NodeAndPosition<ParsedNode> top = scopeStack.get(scopeStack.size() - 1); while (top.getEnd() <= curpos){ scopeStack.remove(scopeStack.size() - 1); top = scopeStack.get(scopeStack.size() - 1); } NodeAndPosition<ParsedNode> nextChild = top.getNode().findNodeAt(curpos + 1, top.getPosition()); while (nextChild != null){ if (nextChild.getPosition() > curpos) break; if (nextChild.getNode().isInner()) { curpos = nextChild.getEnd(); continue outer; } scopeStack.add(nextChild); top = nextChild; nextChild = top.getNode().findNodeAt(curpos + 1, top.getPosition()); } int line = map.getElementIndex(curpos); Element lineEl = map.getElement(line); Segment segment = new Segment(); doc.getText(lineEl.getStartOffset(), lineEl.getEndOffset() - lineEl.getStartOffset(), segment); int lineOffset = curpos - lineEl.getStartOffset(); int nws; if (lineEl.getStartOffset() < nap.getPosition() && nap.getNode().isInner()) { nws = findNonWhitespaceComment(nap, lineEl, segment, lineOffset); } else { nws = findNonWhitespace(segment, lineOffset); } if (nws == lineOffset) { OptionalInt cboundsX = getLeftEdge(curpos); if (cboundsX.isPresent()) { indent = Math.min(indent, cboundsX.getAsInt() - PARAGRAPH_MARGIN); } curpos = lineEl.getEndOffset(); } else if (nws == -1) { curpos = lineEl.getEndOffset(); } else { curpos += nws - lineOffset; } } return indent == Integer.MAX_VALUE ? -1 : indent; } catch (IndexOutOfBoundsException e) { return -1; } } private int[] reassessIndentsAdd(int dmgStart, int dmgEnd) { MoeSyntaxDocument doc = document; ParsedCUNode pcuNode = doc.getParsedNode(); if (pcuNode == null) { return new int[] {dmgStart, dmgEnd }; } Element map = doc.getDefaultRootElement(); int ls = map.getElementIndex(dmgStart); int le = map.getElementIndex(dmgEnd); Segment segment = new Segment(); try { int [] dmgRange = new int[2]; dmgRange[0] = dmgStart; dmgRange[1] = dmgEnd; int i = ls; List<NodeAndPosition<ParsedNode>> scopeStack = new LinkedList<NodeAndPosition<ParsedNode>>(); int lineEndPos = map.getElement(le).getEndOffset(); Element lineEl = map.getElement(ls); NodeAndPosition<ParsedNode> top = pcuNode.findNodeAtOrAfter(lineEl.getStartOffset(), 0); while (top != null && top.getEnd() == lineEl.getStartOffset()){ top = top.nextSibling(); } if (top == null) { return dmgRange; } if (top.getPosition() >= lineEl.getEndOffset()) { i = map.getElementIndex(top.getPosition()); if (i > le) { return dmgRange; } } scopeStack.add(top); NodeAndPosition<ParsedNode> nap = top.getNode().findNodeAtOrAfter(lineEl.getStartOffset() + 1, top.getPosition()); while (nap != null){ scopeStack.add(nap); nap = nap.getNode().findNodeAtOrAfter(lineEl.getStartOffset() + 1, nap.getPosition()); } outer: while (true){ doc.getText(lineEl.getStartOffset(), lineEl.getEndOffset() - lineEl.getStartOffset(), segment); int nws = findNonWhitespace(segment, 0); while (nws == -1){ if (++i > le) { break outer; } lineEl = map.getElement(i); doc.getText(lineEl.getStartOffset(), lineEl.getEndOffset() - lineEl.getStartOffset(), segment); nws = findNonWhitespace(segment, 0); } int curpos = lineEl.getStartOffset() + nws; ListIterator<NodeAndPosition<ParsedNode>> j = scopeStack.listIterator(scopeStack.size()); NodeAndPosition<ParsedNode> topNap = null; do { nap = j.previous(); if (nap.getEnd() > curpos) { break; } topNap = nap; j.remove(); } while (j.hasPrevious()){; if (topNap != null) { do { topNap = topNap.nextSibling(); } } while (topNap != null && topNap.getEnd() <= curpos){; while (topNap != null && topNap.getPosition() < lineEndPos) { scopeStack.add(topNap); } topNap = topNap.getNode().findNodeAtOrAfter(curpos + 1, topNap.getPosition()); } } if (scopeStack.isEmpty()) { break; } OptionalInt cboundsX = getLeftEdge(lineEl.getStartOffset() + nws); int indent = cboundsX.orElse(PARAGRAPH_MARGIN); for (j = scopeStack.listIterator(scopeStack.size()); j.hasPrevious(); ) { NodeAndPosition<ParsedNode> next = j.previous(); if (next.getPosition() <= curpos) { updateNodeIndent(next, indent - PARAGRAPH_MARGIN, nodeIndents.get(next.getNode()), dmgRange); } else if (next.getPosition() < lineEl.getEndOffset()) { nws = findNonWhitespace(segment, next.getPosition() - lineEl.getStartOffset()); Integer oindent = nodeIndents.get(next.getNode()); if (oindent != null && nws != -1) { cboundsX = getLeftEdge(lineEl.getStartOffset() + nws); indent = cboundsX.orElse(PARAGRAPH_MARGIN); updateNodeIndent(next, indent - PARAGRAPH_MARGIN, oindent, dmgRange); } } else { continue; } if (next.getNode().isInner()) { break; } } j = scopeStack.listIterator(scopeStack.size()); while (j.hasPrevious()){ nap = j.previous(); if (nap.getEnd() > lineEl.getEndOffset()) { break; } nap = nap.nextSibling(); j.remove(); if (nap != null) { do { scopeStack.add(nap); if (nap.getPosition() < lineEl.getEndOffset()) { int spos = nap.getPosition() - lineEl.getStartOffset(); nws = findNonWhitespace(segment, spos); Integer oindent = nodeIndents.get(nap.getNode()); if (oindent != null && nws != -1) { cboundsX = getLeftEdge(lineEl.getStartOffset() + nws); indent = cboundsX.orElse(PARAGRAPH_MARGIN); updateNodeIndent(nap, indent - PARAGRAPH_MARGIN, oindent, dmgRange); } } nap = nap.getNode().findNodeAtOrAfter(nap.getPosition(), nap.getPosition()); } while (nap != null){; j = scopeStack.listIterator(scopeStack.size()); } } } if (++i > le) { break; } lineEl = map.getElement(i); } return dmgRange; } finally { } } private int[] reassessIndentsRemove(int dmgPoint, boolean multiLine) { MoeSyntaxDocument doc = document; ParsedCUNode pcuNode = doc.getParsedNode(); int [] dmgRange = new int[2]; dmgRange[0] = dmgPoint; dmgRange[1] = dmgPoint; if (pcuNode == null) { return dmgRange; } Element map = doc.getDefaultRootElement(); int ls = map.getElementIndex(dmgPoint); Element lineEl = map.getElement(ls); NodeAndPosition<ParsedNode> top = pcuNode.findNodeAtOrAfter(lineEl.getStartOffset(), 0); while (top != null && top.getEnd() == lineEl.getStartOffset()){ top = top.nextSibling(); } if (top == null) { return dmgRange; } if (top.getPosition() >= lineEl.getEndOffset()) { return dmgRange; } try { Segment segment = new Segment(); doc.getText(lineEl.getStartOffset(), lineEl.getEndOffset() - lineEl.getStartOffset(), segment); List<NodeAndPosition<ParsedNode>> rscopeStack = new LinkedList<NodeAndPosition<ParsedNode>>(); getScopeStackAfter(doc.getParsedNode(), 0, dmgPoint, rscopeStack); rscopeStack.remove(0); boolean doContinue = true; OptionalInt cboundsX = getLeftEdge(dmgPoint); int dpI = cboundsX.orElse(PARAGRAPH_MARGIN) - PARAGRAPH_MARGIN; while (doContinue && ! rscopeStack.isEmpty()){ NodeAndPosition<ParsedNode> rtop = rscopeStack.remove(rscopeStack.size() - 1); while (rtop != null && rtop.getPosition() < lineEl.getEndOffset()){ if (rtop.getPosition() <= dmgPoint && rtop.getEnd() >= lineEl.getEndOffset()) { doContinue &= ! rtop.getNode().isInner(); } Integer cachedIndent = nodeIndents.get(rtop.getNode()); if (cachedIndent == null) { rtop = rtop.nextSibling(); continue; } if (!multiLine && cachedIndent < dpI) { rtop = rtop.nextSibling(); continue; } if (nodeSkipsStart(rtop, lineEl, segment)) { if (rtop.getPosition() <= dmgPoint) { nodeIndents.remove(rtop.getNode()); dmgRange[0] = Math.min(dmgRange[0], rtop.getPosition()); dmgRange[1] = Math.max(dmgRange[1], rtop.getEnd()); } break; } int nwsP = Math.max(lineEl.getStartOffset(), rtop.getPosition()); int nws = findNonWhitespace(segment, nwsP - lineEl.getStartOffset()); if (nws == -1 || nws + lineEl.getStartOffset() >= rtop.getEnd()) { if (rtop.getPosition() <= dmgPoint) { nodeIndents.remove(rtop.getNode()); dmgRange[0] = Math.min(dmgRange[0], rtop.getPosition()); dmgRange[1] = Math.max(dmgRange[1], rtop.getEnd()); } rtop = rtop.nextSibling(); continue; } cboundsX = getLeftEdge(nws + lineEl.getStartOffset()); int newIndent = cboundsX.orElse(PARAGRAPH_MARGIN) - PARAGRAPH_MARGIN; if (newIndent < cachedIndent) { nodeIndents.put(rtop.getNode(), newIndent); dmgRange[0] = Math.min(dmgRange[0], rtop.getPosition()); dmgRange[1] = Math.max(dmgRange[1], rtop.getEnd()); } else if (newIndent > cachedIndent) { if (rtop.getPosition() <= dmgPoint) { nodeIndents.remove(rtop.getNode()); dmgRange[0] = Math.min(dmgRange[0], rtop.getPosition()); dmgRange[1] = Math.max(dmgRange[1], rtop.getEnd()); } } rtop = rtop.nextSibling(); } } return dmgRange; } finally { } }
| Update an existing indent, in the case where we have found a line where the indent | may now be smaller due to an edit. | @param nap The node whose cached indent value is to be updated | @param indent The indent, on some line | @param oindent The old indent value (may be null) | @param dmgRange The range of positions which must be repainted. This is updated by | if necessary. | private void updateNodeIndent(NodeAndPosition<ParsedNode> nap, int indent, Integer oindent, int [] dmgRange) { int dmgStart = dmgRange[0]; int dmgEnd = dmgRange[1]; if (oindent != null) { int noindent = oindent; if (indent < noindent) { nodeIndents.put(nap.getNode(), indent); } else if (indent != noindent) { nodeIndents.remove(nap.getNode()); } if (indent != noindent) { dmgStart = Math.min(dmgStart, nap.getPosition()); dmgEnd = Math.max(dmgEnd, nap.getEnd()); dmgRange[0] = dmgStart; dmgRange[1] = dmgEnd; } } }
| Get a stack of ParsedNodes which overlap or follow a particular document position. The stack shall | contain the outermost node (at the bottom of the stack) through to the innermost node which overlaps | (but does not end at) or which is the node first following the specified position. | | @param root The root node | @param rootPos The position of the root node | @param position The position for which to build the scope stack | @param list The list into which to store the stack. Items are added to the end of the list. | private void getScopeStackAfter(ParsedNode root, int rootPos, int position, List<NodeAndPosition<ParsedNode>> list) { list.add(new NodeAndPosition<ParsedNode>(root, 0, root.getSize())); int curpos = rootPos; NodeAndPosition<ParsedNode> nap = root.findNodeAtOrAfter(position + 1, curpos); while (nap != null){ list.add(nap); curpos = nap.getPosition(); nap = nap.getNode().findNodeAtOrAfter(position + 1, curpos); } }
| Search for a non-whitespace character, starting from the given offset | (0 = start of the segment). Returns -1 if no such character can be found. | private int findNonWhitespace(Segment segment, int startPos) { int endpos = segment.offset + segment.count; for (int i = segment.offset + startPos; i < endpos; i++) { char c = segment.array[i]; if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return i - segment.offset; } } return -1; }
| Search for a non-whitespace character, starting from the given offset in the segment; treat | single-line comments as whitespace. Returns -1 if the line consists only of whitespace. | private int findNonWhitespaceComment(NodeAndPosition<ParsedNode> nap, Element lineEl, Segment segment, int startPos) { int nws = findNonWhitespace(segment, startPos); if (nws != -1) { int pos = nws + lineEl.getStartOffset(); if (nap.getEnd() > pos) { NodeAndPosition<ParsedNode> inNap = nap.getNode().findNodeAt(pos, nap.getPosition()); if (inNap != null && inNap.getNode().getNodeType() == ParsedNode.NODETYPE_COMMENT && inNap.getPosition() == pos && inNap.getEnd() == lineEl.getEndOffset() - 1) { return -1; } } else { NodeAndPosition<ParsedNode> nnap = nap.nextSibling(); if (nnap != null && nnap.getNode().getNodeType() == ParsedNode.NODETYPE_COMMENT && nnap.getPosition() == pos && nnap.getEnd() == lineEl.getEndOffset() - 1) { return -1; } } } return nws; }
| Search backwards for a non-whitespace character. If no such character | is found, returns (endPos - 1). | private int findNonWhitespaceBwards(Segment segment, int startPos, int endPos) { int lastP = segment.offset + endPos; int i; for (i = segment.offset + startPos; i > lastP; i--) { char c = segment.array[i]; if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return i - segment.offset; } } return endPos - 1; }
| | Need to override this method to handle node updates. If a node indentation changes, | the whole node needs to be repainted. | protected void updateDamage(MoeSyntaxEvent changes) { if (changes == null) { nodeIndents.clear(); imageCache.clear(); document.recalculateAllScopes(); return; } int damageStart = document.getLength(); int damageEnd = 0; MoeSyntaxEvent mse = changes; for (NodeAndPosition<ParsedNode> node : mse.getRemovedNodes()) { nodeRemoved(node.getNode()); damageStart = Math.min(damageStart, node.getPosition()); damageEnd = Math.max(damageEnd, node.getEnd()); NodeAndPosition<ParsedNode> nap = node; int [] r = clearNap(nap, document, damageStart, damageEnd); damageStart = r[0]; damageEnd = r[1]; } for (NodeChangeRecord record : mse.getChangedNodes()) { NodeAndPosition<ParsedNode> nap = record.nap; nodeIndents.remove(nap.getNode()); damageStart = Math.min(damageStart, nap.getPosition()); damageStart = Math.min(damageStart, record.originalPos); damageEnd = Math.max(damageEnd, nap.getEnd()); damageEnd = Math.max(damageEnd,record.originalPos + record.originalSize); int [] r = clearNap(nap, document, damageStart, damageEnd); damageStart = r[0]; damageEnd = r[1]; } Element map = document.getDefaultRootElement(); if (changes.isInsert()) { damageStart = Math.min(damageStart, changes.getOffset()); damageEnd = Math.max(damageEnd, changes.getOffset() + changes.getLength()); int [] r = reassessIndentsAdd(damageStart, damageEnd); damageStart = r[0]; damageEnd = r[1]; } else if (changes.isRemove()) { damageStart = Math.min(damageStart, changes.getOffset()); int [] r = reassessIndentsRemove(damageStart, true); damageStart = r[0]; damageEnd = r[1]; } if (damageStart < damageEnd) { int line = map.getElementIndex(damageStart); int lastline = map.getElementIndex(damageEnd - 1); document.recalculateScopesForLinesInRange(line, lastline); } }
| Clear a node's cached indent information. If the node is an inner node this | also clears parent nodes as appropriate. | private int[] clearNap(NodeAndPosition<ParsedNode> nap, MoeSyntaxDocument document, int damageStart, int damageEnd) { if (nap.getNode().isInner()) { List<NodeAndPosition<ParsedNode>> list = new LinkedList<NodeAndPosition<ParsedNode>>(); NodeAndPosition<ParsedNode> top; top = new NodeAndPosition<ParsedNode>(document.getParsedNode(), 0, document.getLength()); while (top != null && top.getNode() != nap.getNode()){ if (top.getNode().isInner()) { list.clear(); } list.add(top); top = top.getNode().findNodeAt(nap.getEnd(), top.getPosition()); } for (NodeAndPosition<ParsedNode> cnap : list) { damageStart = Math.min(damageStart, cnap.getPosition()); damageEnd = Math.max(damageEnd, cnap.getEnd()); nodeIndents.remove(cnap.getNode()); } } return new int[] {damageStart, damageEnd }; } private void nodeRemoved(ParsedNode node) { nodeIndents.remove(node); } @OnThread(Tag.FXPlatform) public Node getParagraphicGraphic(int lineNumber) { if (lineNumber < 0) { return null; } lineNumber += 1; Label label = new Label("" + lineNumber); label.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); label.setEllipsisString("\u2026"); label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); JavaFXUtil.setPseudoclass("bj-odd", (lineNumber & 1) == 1, label); JavaFXUtil.addStyleClass(label, "moe-line-label"); Node stepMarkIcon = makeStepMarkIcon(); Node breakpointIcon = makeBreakpointIcon(); label.setGraphic(new StackPane(breakpointIcon, stepMarkIcon)); label.setOnContextMenuRequested(e -> { CheckMenuItem checkMenuItem = new CheckMenuItem(Config.getString("prefmgr.edit.displaylinenumbers")); checkMenuItem.setSelected(PrefMgr.getFlag(PrefMgr.LINENUMBERS)); checkMenuItem.setOnAction(ev -> { PrefMgr.setFlag(PrefMgr.LINENUMBERS, checkMenuItem.isSelected()); }); ContextMenu menu = new ContextMenu(checkMenuItem); menu.show(label, e.getScreenX(), e.getScreenY()); }); int lineNumberFinal = lineNumber; label.setOnMouseClicked(e -> { if (e.getClickCount() == 1 && e.getButton() == MouseButton.PRIMARY) { MoeEditor editor = editorPane.getEditor(); if (editor != null) { editor.toggleBreakpoint(editorPane.getDocument().getAbsolutePosition(lineNumberFinal - 1, 0)); } } e.consume(); }); EnumSet<ParagraphAttribute> attr = getParagraphAttributes(lineNumber); for (ParagraphAttribute possibleAttribute : ParagraphAttribute.values()) { JavaFXUtil.setPseudoclass(possibleAttribute.getPseudoclass(), attr.contains(possibleAttribute), label); } stepMarkIcon.setVisible(attr.contains(ParagraphAttribute.STEP_MARK)); breakpointIcon.setVisible(attr.contains(ParagraphAttribute.BREAKPOINT)); if (stepMarkIcon.isVisible() || breakpointIcon.isVisible() || (editorPane != null && !editorPane.isShowLineNumbers())) { label.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } else { label.setContentDisplay(ContentDisplay.TEXT_ONLY); } AnchorPane.setLeftAnchor(label, 0.0); AnchorPane.setRightAnchor(label, 3.0); AnchorPane.setTopAnchor(label, 0.0); AnchorPane.setBottomAnchor(label, 0.0); return new AnchorPane(label); } private static Node makeBreakpointIcon() { Node icon = Config.makeStopIcon(false); JavaFXUtil.addStyleClass(icon, "moe-breakpoint-icon"); return icon; } private Node makeStepMarkIcon() { Shape arrow = Config.makeArrowShape(false); JavaFXUtil.addStyleClass(arrow, "moe-step-mark-icon"); return arrow; }
| Sets attributes throughout the document. | | @param alterAttr Anything mapped to true will be added to all lines, anything mapped to false will be removed from all lines | @return The list of all line numbers where the attributes were changed | public Map> setParagraphAttributes(Map<ParagraphAttribute, Boolean> alterAttr) { Map<Integer, EnumSet<ParagraphAttribute>> changed = new HashMap<>(); for (int line = 1; line <= document.getDocument().getParagraphs().size(); line++) { changed.putAll(setParagraphAttributes(line, alterAttr)); } return changed; }
| Sets attributes for a particular line number. | | @param lineNumber the line number for which to change the attributes (first line is 1) | @param alterAttr the attributes to set the value for (other attributes will be unaffected) | @return The list of all line numbers where the attributes were changed | public Map> setParagraphAttributes(int lineNumber, Map<ParagraphAttribute, Boolean> alterAttr) { ScopeInfo paraStyle = editorPane.getParagraph(lineNumber - 1).getParagraphStyle(); if (paraStyle == null) { paraStyle = new ScopeInfo(EnumSet.noneOf(ParagraphAttribute.class)); } EnumSet<ParagraphAttribute> attr = EnumSet.copyOf(paraStyle.getAttributes()); boolean changed = false; for (Entry<ParagraphAttribute, Boolean> alter : alterAttr.entrySet()) { if (alter.getValue()) { changed = attr.add(alter.getKey()) || changed; } else { changed = attr.remove(alter.getKey()) || changed; } } if (changed) { editorPane.setParagraphStyle(lineNumber - 1, paraStyle.withAttributes(attr)); return Collections.singletonMap(lineNumber, EnumSet.copyOf(attr)); } else { return Collections.emptyMap(); } }
| Gets the paragraph attributes for a particular line. If none found, returns the empty set. | The set returned should not be modified directly. | | @param lineNumber The line number to retrieve attributes for (the first line is one rather than zero). | EnumSet<ParagraphAttribute> getParagraphAttributes(int lineNumber) { ScopeInfo scopeInfo = editorPane.getParagraph(lineNumber - 1).getParagraphStyle(); if (scopeInfo == null) { return EnumSet.noneOf(ParagraphAttribute.class); } else { return scopeInfo.getAttributes(); } }
| Sets up the colors based on the strength value | (from strongest (20) to white (0) | void resetColors() { BK = scopeColors.scopeBackgroundColorProperty().get(); C1 = getReducedColor(scopeColors.scopeClassOuterColorProperty()); C2 = getReducedColor(scopeColors.scopeClassColorProperty()); C3 = getReducedColor(scopeColors.scopeClassInnerColorProperty()); M1 = getReducedColor(scopeColors.scopeMethodOuterColorProperty()); M2 = getReducedColor(scopeColors.scopeMethodColorProperty()); S1 = getReducedColor(scopeColors.scopeSelectionOuterColorProperty()); S2 = getReducedColor(scopeColors.scopeSelectionColorProperty()); I1 = getReducedColor(scopeColors.scopeIterationOuterColorProperty()); I2 = getReducedColor(scopeColors.scopeIterationColorProperty()); } private Color getReducedColor(ObjectExpression<Color> c) { return scopeColors.getReducedColor(c, PrefMgr.getScopeHighlightStrength()).getValue(); } private static class Middle { private final Color bodyColor; private final int lhs; private final int rhs; private Color topColor; private Color bottomColor; public Middle(Color bodyColor, int lhs, int rhs) { this.bodyColor = bodyColor; this.lhs = Math.max(0, lhs); this.rhs = rhs; } public void drawTop(Color topColor) { this.topColor = topColor; } public void drawBottom(Color bottomColor) { this.bottomColor = bottomColor; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Middle middle = (Middle) o; if (lhs != middle.lhs) return false; if (rhs != middle.rhs) return false; if (!bodyColor.equals(middle.bodyColor)) return false; if (topColor != null ? !topColor.equals(middle.topColor) : middle.topColor != null) return false; return bottomColor != null ? bottomColor.equals(middle.bottomColor) : middle.bottomColor == null; } @Override public int hashCode() { int result = bodyColor.hashCode(); result = 31 * result + lhs; result = 31 * result + rhs; result = 31 * result + (topColor != null ? topColor.hashCode() : 0); result = 31 * result + (bottomColor != null ? bottomColor.hashCode() : 0); return result; } }
| This is one set of scopes for a single line in the document. A set of scopes | is a list of nested scope boxes applicable to that line. The first scope | is the outermost and last is innermost, so we render them in list order. | @OnThread(Tag.FX) public static class ScopeInfo { public static enum Special { NONE, BREAKPOINT, STEP; } private final List<SingleNestedScope> nestedScopes = new ArrayList<>(); private final EnumSet<ParagraphAttribute> attributes; private boolean incomplete = false; public ScopeInfo(EnumSet<ParagraphAttribute> attributes) { this.attributes = EnumSet.copyOf(attributes); } public EnumSet getAttributes() { return attributes; } public ScopeInfo withAttributes(EnumSet<ParagraphAttribute> attributes) { ScopeInfo scopeInfo = new ScopeInfo(attributes); scopeInfo.nestedScopes.addAll(nestedScopes); scopeInfo.incomplete = incomplete; return scopeInfo; } public boolean isIncomplete() { return incomplete; } private static class SingleNestedScope { private final LeftRight leftRight; private final Middle middle; private final int hashCode; public SingleNestedScope(LeftRight leftRight, Middle middle) { this.leftRight = leftRight; this.middle = middle; hashCode = leftRight.hashCode() * 31 + middle.hashCode(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SingleNestedScope that = (SingleNestedScope) o; if (hashCode != that.hashCode) return false; if (!leftRight.equals(that.leftRight)) return false; return middle.equals(that.middle); } @Override public int hashCode() { return hashCode; } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ScopeInfo scopeInfo = (ScopeInfo) o; if (incomplete != scopeInfo.incomplete) return false; if (!attributes.equals(scopeInfo.attributes)) return false; return nestedScopes.equals(scopeInfo.nestedScopes); } @Override public int hashCode() { int result = nestedScopes.hashCode(); result = 31 * result + attributes.hashCode(); result += isIncomplete() ? 1 : 0; return result; } @Override public String toString() { return "ScopeInfo{" + "nestedScopes=" + nestedScopes + ", attributes=" + attributes + ", incomplete=" + incomplete + '}'; } } private class LeftRight { private final int lhs; private final int rhs; private final boolean starts; private final boolean ends; private final Color fillColor; private final Color edgeColor; public LeftRight(int lhs, int rhs, boolean starts, boolean ends, Color fillColor, Color edgeColor) { this.lhs = Math.max(0, lhs); this.rhs = rhs; this.starts = starts; this.ends = ends; this.fillColor = fillColor; this.edgeColor = edgeColor; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LeftRight leftRight = (LeftRight) o; if (lhs != leftRight.lhs) return false; if (rhs != leftRight.rhs) return false; if (starts != leftRight.starts) return false; if (ends != leftRight.ends) return false; if (!fillColor.equals(leftRight.fillColor)) return false; return edgeColor.equals(leftRight.edgeColor); } @Override public int hashCode() { int result = lhs; result = 31 * result + rhs; result = 31 * result + (starts ? 1 : 0); result = 31 * result + (ends ? 1 : 0); result = 31 * result + fillColor.hashCode(); result = 31 * result + edgeColor.hashCode(); return result; } } }
top, use, map, class BlueJSyntaxView

.   getPseudoclass
.   BlueJSyntaxView
.   getEditorPane
.   setDuringUpdate
.   getTokenStylesFor
.   recalculateScopes
.   getImageFor
.   copy
.   drawImageFor
.   blend
.   fillRect
.   setEditorPane

top, use, map, class ThreeLines

.   recalcScopeMarkers

top, use, map, class DrawInfo

.   DrawInfo
.   drawScopes
.   getLeftEdge
.   drawNode
.   getBackgroundColor
.   colorsForNode
.   calculatedNestedScope
.   getScopeMiddle
.   getNodeRBound
.   nodeSkipsEnd
.   getNodeIndent
.   getNodeIndent
.   reassessIndentsAdd
.   reassessIndentsRemove
.   updateNodeIndent
.   getScopeStackAfter
.   findNonWhitespace
.   findNonWhitespaceComment
.   findNonWhitespaceBwards
.   updateDamage
.   clearNap
.   nodeRemoved
.   getParagraphicGraphic
.   makeBreakpointIcon
.   makeStepMarkIcon
.   setParagraphAttributes
.   setParagraphAttributes
.   resetColors
.   getReducedColor

top, use, map, class Middle

.   Middle
.   drawTop
.   drawBottom
.   equals
.   hashCode

top, use, map, class ScopeInfo

.   ScopeInfo
.   getAttributes
.   withAttributes
.   isIncomplete

top, use, map, class ScopeInfo . SingleNestedScope

.   SingleNestedScope
.   equals
.   hashCode
.   equals
.   hashCode
.   toString

top, use, map, class LeftRight

.   LeftRight
.   equals
.   hashCode




2377 neLoCode + 160 LoComm