package bluej.stride.framedjava.slots;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import bluej.stride.framedjava.slots.StructuredSlot.SplitInfo;
import bluej.stride.generic.Frame.View;
import bluej.stride.generic.InteractionManager;
import bluej.utility.Debug;
import bluej.utility.Utility;
import bluej.utility.javafx.FXConsumer;
import bluej.utility.javafx.FXFunction;
import bluej.utility.javafx.FXPlatformConsumer;
import bluej.utility.javafx.FXPlatformFunction;
import bluej.utility.javafx.FXPlatformRunnable;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.SharedTransition;
import bluej.utility.javafx.TextFieldDelegate;
import bluej.utility.javafx.binding.DeepListBinding;
import threadchecker.OnThread;
import threadchecker.Tag;
| This class is the major part of the display and logic for expressions. Here's how the architecture works:
|
| <h3>The Expression Tree</h3>
|
| This gets turned into a rich tree of expressions. The expressions nest as follows.
|
| - There is InfixStructured (this class), which holds a sequence of operands (see the "fields" field)
|* and in the middle of each of these is optionally an operator (Operator class, "operators" field).
* An operand (which implements StructuredSlotComponent) can be:
* - a fairly standard text field (StructuredSlotField),
* - a string literal (StringLiteralExpressionSlot) or
* - a bracketed expression (BracketedStructured).
|
| - A BracketedStructured is really a thin wrapper around InfixStructured. So an expression tree is
| really made up primarily of InfixStructured, with each one (excluding the top-level) wrapped in
| a BracketedStructured.
|
| Given an expression like:
|
| 1 + 2 * getX() + convert(5 + 7) - (3 * (15 + 6))
|
| We have a top-level InfixStructured. This has an array of eleven operands:
|
| [0]: StructuredSlotField, content: "1"
|* [1]: StructuredSlotField, content: "2"
* [2]: StructuredSlotField, content: "getX"
* [3]: BracketedStructured, with an InfixExpressionSlot containing one [empty] operand (and no operators):
* [0]: StructuredSlotField, content: ""
* [4]: StructuredSlotField, content: ""
* [5]: StructuredSlotField, content: "convert"
* [6]: BracketedStructured, with an InfixExpressionSlot with two operands:
* [0]: StructuredSlotField, content: "5"
* [1]: StructuredSlotField, content: "7"
* And one operator:
* [0]: Operator, content: "+"
* [7]: StructuredSlotField, content: ""
* [8]: StructuredSlotField, content: ""
* [9]: BracketedStructured, with an InfixExpressionSlot with three operands:
* [0]: StructuredSlotField, content "3"
* [1]: BracketedStructured, with two operands:
* [0]: StructuredSlotField, content: "15"
* [1]: StructuredSlotField, content: "6"
* and one operator:
* [0]: Operator, content: "+"
* [2]: StructuredSlotField, content ""
* And two operators:
* [0]: Operator, content "*"
* [1]: null
* [10]:StructuredSlotField, content ""
*
* And an array of ten operators:
* [0]: Operator, content: "+"
* [1]: Operator, content: "*"
* [2]: null
* [3]: null
* [4]: Operator, content: "+"
* [5]: null
* [6]: null
* [7]: Operator, content: "-"
* [8]: null
* [9]: null
*
* There are a few rules to these slots:
*
* - Operator N is between Operand N and N+1. So operator 0 is between operand 0 and operand 1, etc
* - The size of the operators array is always exactly one less than the size of the operands array
| (although many operator entries may be null).
| - Each BracketedStructured is surrounded by two null operators. See explanation below.
| - Operators are only null when they are adjacent to a BracketedStructured.
| - A BracketedStructured never appears in the very first or very last entry in the operands array. (See below.)
|
| The non-obvious rules relate to BracketedStructured. To start with, consider the expression "1>=2". In
|* a standard text editor, this a string of length 4, and there are 5 possible caret positions (before each
* character, and after the last one). If we turn this into an InfixStructured, we get two operands and an operator:
| field 0: StructuredSlotField, content "1"
|* operator 0: Operator, content ">="
* field 1: StructuredSlotField, content "2"
*
* In our version, we have four valid caret positions: before the '1', after the '1', before the '2', after the '2'.
* We lose only the caret position in the middle of the operator, but you can always alter it by going before/after it
| and using delete/backspace, which will chip off one character at a time from the oprator.
|
| Now consider the same expression with some brackets: "1>=(2)". In a standard text editor, it's a string
|* of length 6, and you have 7 caret positions. My first attempt at turning this into an InfixStructured
* [NOTE: THIS IS NOT HOW IT NOW WORKS!] was:
* field 0: StructuredSlotField, content "1"
* operator 0: Operator, content ">="
* field 1: BracketedStructured, containing:
* field 0: StructuredSlotField, content "2"
*
* This seems logical, but we would only have 4 caret positions -- again: before the '1', after the '1', before the '2', after the '2'.
* If you want to add content after the closing bracket, there is no way to do so. If you want to delete
| the '=' (to turn it into strictly greater than), there is no caret position from which to do so, and nor
| can you add any content between the operator and the bracket. So while this scheme correctly contains all
| the current text, it *does not allow all possible edits to take place*. The fix for this is as follows:
|
| Each BracketedStructured must always have an StructuredSlotField before and after it, with no operator
| inbetween the brackets and field. These fields correspond to the space just outside the brackets, to
| always allow editing in those positions. Often, these extra fields will be empty (e.g. in the case of
| "1*(2-3)/5", these extra fields will be empty. However, in the case of method calls such as "move(5)",
|* the "move" occupies the field beforehand, and in the case of casts such as "(int)x", the "x" occupies
* the field afterwards.
*
* This rule is what leads to several of our rules above. Since BracketedStructured always has these fields
* before and after with no operators, we store null in the corresponding operator fields, and BracketedStructured
| can never be first or last in the InfixStructured.
|
| <h3>Floating-Point Literals</h3>
|
| One special case in expressions is floating point literals. Given an expression like "-x.y", we would usually
|* turn this into an InfixStructured with a minus operator and dot operator. However, for expressions like
* "-9.3", this is misleading; the dot is not really an operator here (though the minus is perhaps less clear;
* we could think of it as a unary minus). So we have a special case that if a dot appears in a place
| where it looks like part of a numeric literal (roughly: if it is preceded by a series of digits, but it gets
| more complicated with floating hex literals, and we also must take care around "a415.y" etc) then we do
|* not insert an operator, but rather allow the dot in the slot itself. The insertion and deletion logic
* has to work hard to handle this -- not only must it handle inserting and deleting the dot itself, but
| for example deleting the "g" in "g325.7" changes the content from a syntax error into a valid numeric literal.
|*
* <h3>Deleting/Inserting Operators</h3>
*
* Adding text content to an existing text field is simple -- you just change the content of that field.
| However, adding or deleting an operator or bracket may require merging or splitting existing slots,
| especially so if selection is involved. For example, in the field "12345", selecting the "234" and hitting
|* "(" should split the slot into two, the first half containing "1", the second half containing "5", and
* inbetween those there will be a BracketedStructured inserted which contains "234".
*
* There is complicated logic to the insertion, which is covered in the comments for the insertChar method, below.
*
* <h3>Selection</h3>
*
* InfixStructured items support selection. The logic for selection in expressions is similar to that for frames:
| a bracketed expression is a whole unit (like a frame is), and thus you either select the whole bracket,
| or none of it. Other than brackets, you can select across any parts of an expression. If you consider
| the expression "1+2+(3*4)+5", then the valid selections you can make from just before the 2 are
|* (using curly brackets for selection):
* - "1{}}2+(3*4)+5" [heading backwards]
* - "{}+}2+(3*4)+5" [heading backwards]
* - "1+{}}+(3*4)+5" [this, and rest, heading forwards]
* - "1+{}+}(3*4)+5"
* - "1+{}+(3*4)}+5"
* - "1+{}+(3*4)+}5"
* - "1+{}+(3*4)+5}"
*
* The valid selections from just after the 3 are:
* - "1+2+({}}*4)+5" [heading backwards]
* - "1+2+(3{}}4)+5" [heading forwards]
* - "1+2+(3{}4})+5" [heading forwards]
*
* That is, your selection cannot leave a bracketed expression that you start inside of, because
* allowing the selection to leave the bracket would mean you were selecting part of a bracketed expression.
|
| <h3>Methods with Underscore Names</h3>
|
| In some cases, there are methods that we want to test, where we need to pass different parameters
| to the usual public methods. To facilitate this, some methods (like insert), have a package-visible
| version with an underscore ("insert_") which is used directly by the tests, and a public version
|* without the underscore ("insert") that should be used in all normal, non-test code
*
* <h3>Caret Position</h3>
*
* Because we have a rich structure in the expression, the notion of caret position has changed. In a normal text slot,
| it would just be an integer, indicating the position along the string. In our expression slots, it is
| a list of integers (singly linked list, in CaretPos). The meaning of each integer depends on the component it is applied to.
|
| At the outer-most level, we have an InfixStructured. Here, the first integer is an index into the "fields" array,
|* indicating which slot we are in. You then strip off the first integer, and use the rest of the list to find
* the position in the subfield. If it's an StructuredSlotField, the index is simply a standard caret position.
| If it's a BracketedStructured, it applies to the contained InfixStructured.
|
| So ultimately, the CaretPos is a list of integers where the last one applies to an StructuredSlotField, and the preceding
| ones are indexes into InfixStructured's "fields" arrays.
|*/
public abstract class InfixStructured<SLOT extends StructuredSlot<?, INFIX, ?>, INFIX extends InfixStructured<SLOT, INFIX>>
implements TextFieldDelegate<StructuredSlotField>{
|
|// Regex matching JLS "Digits"; underscores can appear within
private final static String DIGITS_REGEX = "\\d([0-9_]*\\d)?";
private final static String HEX_DIGITS_REGEX = "[0-9A-Fa-f]([0-9A-Fa-f_]*[0-9A-Fa-f])?";
private static final String DEFAULT_RANGE_START = lang.stride.Utility.class.getName() + "(";
// fields is always 1 longer than operators. Always an StructuredSlotField in first and last position (which may be same, when size 1).
protected final ProtectedList<StructuredSlotComponent> fields = new ProtectedList<>();
|
|// Operator 0 is between field 0 and field 1. Operator N trails field N.
|
|// Can be null when the operator is effectively a bracket.
|
|protected final ProtectedList<Operator> operators = new ProtectedList<>();
|
|//private final FlowPane components = new FlowPane();
|
|//private final HBox components = new HBox();
|
|private final ObservableList<Node> components = FXCollections.observableArrayList();
|
|protected final BracketedStructured<INFIX, SLOT> parent; // null if top-level
|
|// The characters which cause us to go up a level (e.g. a closing bracket) or
|
|// move to the next slot
|
|private final Set<Character> closingChars = new HashSet<>();
|
|private final InteractionManager editor;
|
|protected final SLOT slot; // Can be null when testing, but otherwise non-null
|
|private final StringProperty textProperty = new SimpleStringProperty();
|
|private final BooleanProperty previewingJavaRange = new SimpleBooleanProperty(false);
|
|private final StringProperty startRangeText = new SimpleStringProperty(DEFAULT_RANGE_START);
|
|private final StringProperty endRangeText = new SimpleStringProperty(")");
/**
* The caret position for the start of the selection (null if and only if no selection)
*/
private CaretPos anchorPos;
/**
* Create top-level InfixStructured, just inside the StructuredSlot
public InfixStructured(InteractionManager editor, SLOT slot, StructuredSlot.ModificationToken token)
{
this(editor, slot, "", null, token);
}
| Create InfixStructured just inside the given BracketedStructured.
|
| @param initialContent The initial content. This should be suitable for putting into a single
| StructuredSlotField; it should not contain any operators or brackets, etc. If you need
| to add rich content, pass "" for this parameter and insert the rich content afterwards.
|*/
public InfixStructured(InteractionManager editor, SLOT slot, String initialContent, BracketedStructured wrapper,
StructuredSlot.ModificationToken token, Character... closingChars)
|
|{
|
|this.editor = editor;
|
|this.parent = wrapper;
|
|this.closingChars.addAll(Arrays.asList(closingChars));
|
|this.slot = slot;
|
|this.textProperty.set(initialContent);
|
|// When starting, add just one empty field:
|
|fields.add(makeNewField(initialContent, false), token);
|
|final ObservableList<Node> extraPrefix = FXCollections.observableArrayList();
|
|final ObservableList<Node> extraSuffix = FXCollections.observableArrayList();
|
|JavaFXUtil.addChangeListener(previewingJavaRange, previewing -> {
|
|if (previewing)
|
|{
|
|Label start = new Label();
|
|start.textProperty().bind(startRangeText);
|
|extraPrefix.setAll(start);
|
|Label end = new Label();
|
|end.textProperty().bind(endRangeText);
|
|extraSuffix.setAll(end);
|
|}
|
|else
|
|{
|
|extraPrefix.clear();
|
|extraSuffix.clear();
|
|}
|
|});
|
|new DeepListBinding<Node>(components) {
|
|@Override
|
|protected Stream<ObservableList<?>> getListenTargets()
|
|{
|
|return Stream.concat(Stream.of(fields.observable(), operators.observable(), extraPrefix, extraSuffix), fields.stream().map(StructuredSlotComponent::getComponents));
|
|}
|
|@Override
|
|protected Stream<Node> calculateValues()
|
|{
|
|// Important that we flatMap after interleaving, to preserve ordering:
|
|return Utility.concat(
|
|extraPrefix.stream(),
|
|Utility.interleave(
|
|fields.stream().map(c -> c.getComponents().stream()),
|
|operators.stream().map(o -> o == null ? Stream.<Node>empty() : Stream.of(o.getNode())))
|
|.flatMap(x -> x),
|
|extraSuffix.stream()
|
|);
|
|}
|
|}.startListening();
|
|fields.observable().addListener((ListChangeListener)c -> {
|
|// If we are not a single field, remove prompts on all fields:
|
|if (fields.size() != 1)
|
|{
|
|fields.forEach(comp -> {
|
|if (comp instanceof StructuredSlotField)
|
|{
|
|((StructuredSlotField)comp).setPromptText("");
}
});
}
});
// Now we calculate precedence as follows:
// - We find the lowest precedence operator (preferring leftmost).
// - We recursively traverse the LHS and RHS of this operator with same algorithm.
|
|// - On return, if lowest operator in LHS or RHS is identical to outer, we take
|
|components.addListener((ListChangeListener)c ->
|
|calculatePrecedences(operators.stream().collect(Collectors.toList()), fields.stream().map(StructuredSlotComponent::isFieldAndEmpty).limit(operators.size()).collect(Collectors.toList()))
|
|);
|
|updateBreaks();
|
|JavaFXUtil.addChangeListener(textProperty, value -> updateBreaks());
|
|}
|
|public Stream<? extends Node> makeDisplayClone(InteractionManager editor)
|
|{
|
|// Important that we flatMap after interleaving, to preserve ordering:
|
|return Utility.interleave(
|
|fields.stream().map(c -> c.makeDisplayClone(editor)),
|
|operators.stream().map(o -> o == null ? Stream.<Node>empty() : Stream.of(o.makeDisplayClone(editor))))
|
|.flatMap(x -> x);
|
|}
|
|private void updateBreaks()
|
|{
|
|// Update possible breaks.
|
|// Breaks are not allowed between a method name and the opening bracket:
|
|// Spot the method calls and inform the brackets:
|
|// Start at second item because first can't be a bracket:
|
|for (int i = 1; i < fields.size(); i++)
|
|{
|
|if (fields.get(i) instanceof BracketedStructured)
|
|{
|
|// A method call is a bracketed expression with non-empty
|
|// field directly before it:
|
|((BracketedStructured)fields.get(i)).notifyIsMethodParams(
|
|fields.get(i-1) instanceof StructuredSlotField
|
|&& !fields.get(i-1).isFieldAndEmpty());
|
|}
|
|}
|
|}
|
|// package-visible and static for testing
|
|static OpPrec calculatePrecedences(List<Operator> ops, List<Boolean> isUnary)
|
|{
|
|int lowestPrec = Integer.MAX_VALUE;
|
|int lowestIndex = -1;
|
|for (int i = 0; i < ops.size(); i++)
|
|{
|
|if (ops.get(i) == null)
|
|continue;
|
|if (ops.get(i).get().equals("."))
{
// always DOT priority; skip it:
ops.get(i).setPrecedence(Operator.Precedence.DOT);
continue;
}
else if (ops.get(i).get().equals(","))
{
// always COMMA priority; skip it:
ops.get(i).setPrecedence(Operator.Precedence.COMMA);
continue;
}
|
|else if (ops.get(i).get().equals("new "))
{
// always COMMA priority; skip it:
ops.get(i).setPrecedence(Operator.Precedence.NEW);
continue;
}
|
|int prec = Operator.getOperatorPrecedence(ops.get(i).get(), isUnary.get(i).booleanValue());
|
|// Prefer left-hand op, needs to be strictly lower
|
|if (prec < lowestPrec)
|
|{
|
|lowestPrec = prec;
|
|lowestIndex = i;
|
|}
|
|}
|
|if (lowestIndex != -1)
|
|{
|
|// Split the list into left and right:
|
|List<Operator> lhs = ops.subList(0, lowestIndex);
|
|List<Boolean> lhsUnary = isUnary.subList(0, lowestIndex);
|
|List<Operator> rhs = ops.subList(lowestIndex + 1, ops.size());
|
|List<Boolean> rhsUnary = isUnary.subList(lowestIndex + 1, ops.size());
|
|OpPrec lhsPrec = calculatePrecedences(lhs, lhsUnary);
|
|OpPrec rhsPrec = calculatePrecedences(rhs, rhsUnary);
|
|int ourLevel;
|
|if (lhsPrec.prec == lowestPrec || rhsPrec.prec == lowestPrec ||
|
|(lhsPrec.prec == -1 && rhsPrec.prec == -1))
|
|{
|
|// Same precendence
|
|ourLevel = Math.max(lhsPrec.levels, rhsPrec.levels);
|
|}
|
|else
|
|{
|
|// One higher precedence:
|
|ourLevel = 1 + Math.max(lhsPrec.levels, rhsPrec.levels);
|
|}
|
|ops.get(lowestIndex).setPrecedence(Operator.getPrecForLevel(ourLevel));
|
|return new OpPrec(lowestPrec, ourLevel);
|
|}
|
|else
|
|{
|
|// No operators (none, or all are null)
|
|return new OpPrec(-1, 0);
|
|}
|
|}
|
|private static boolean precedesDotInFloatingPointLiteral(String before) {
|
|return before.matches("\\A\\s*[+-]?" + DIGITS_REGEX + "\\z")
|| before.matches("\\A\\s*0x" + HEX_DIGITS_REGEX + "\\z");
}
protected StructuredSlotField makeNewField(String content, boolean stringLiteral)
{
StructuredSlotField f = new StructuredSlotField(this, content, stringLiteral);
if (editor != null) // Can be null during testing
|
|editor.setupFocusableSlotComponent(slot, f.getNodeForPos(null), true, () -> slot.getExtensions(), slot.getHints());
|
|f.onKeyPressedProperty().set(event -> {
|
|if (event.isShiftDown() && event.isControlDown() && event.getText().length() > 0 && event.getCode() != KeyCode.CONTROL && event.getCode() != KeyCode.SHIFT)
|
|{
|
|slot.notifyModifiedPress(event.getCode());
|
|event.consume();
|
|return;
|
|}
|
|//Which key?
|
|switch (event.getCode())
|
|{
|
|case ENTER:
|
|slot.enter();
|
|event.consume();
|
|break;
|
|case UP:
|
|slot.up();
|
|event.consume();
|
|break;
|
|case DOWN:
|
|slot.down();
|
|event.consume();
|
|break;
|
|case SPACE:
|
|if (event.isControlDown())
|
|{
|
|slot.showSuggestionDisplay(f, f.getCurrentPos().index, stringLiteral);
|
|event.consume();
|
|}
|
|break;
|
|default:
|
|if (slot.checkFilePreviewShortcut(event.getCode()))
|
|event.consume();
|
|break;
|
|}
|
|});
|
|// Put the handlers for links on the field:
|
|f.addEventHandler(MouseEvent.MOUSE_MOVED, e -> {
|
|CaretPos relNearest = getNearest(e.getSceneX(), e.getSceneY(), false, Optional.empty()).getPos();
|
|CaretPos absNearest = absolutePos(relNearest);
|
|f.setPseudoclass("bj-hyperlink", e.isShortcutDown() && slot.getOverlay().hoverAtPos(slot.getTopLevel().caretPosToStringPos(absNearest, false)) != null);
});
f.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getClickCount() > 1) // Double and triple clicks will be handled by the text field
|
|return;
|
|if (!(e.isShortcutDown() || e.getButton() == MouseButton.MIDDLE))
|
|return; // Either they must be holding shortcut button, or middle clicking
|
|// check for click on underlined region
|
|CaretPos relNearest = getNearest(e.getSceneX(), e.getSceneY(), false, Optional.empty()).getPos();
|
|CaretPos absNearest = absolutePos(relNearest);
|
|// First we check existing underlines:
|
|FXPlatformRunnable linkRunnable = slot.getOverlay().hoverAtPos(slot.getTopLevel().caretPosToStringPos(absNearest, false));
|
|if (linkRunnable != null)
|
|{
|
|// We've found an existing underline; run the action:
|
|linkRunnable.run();
|
|}
|
|else
|
|{
|
|// No existing underline; this may be because:
|
|// - there isn't anything of interest in this position
|
|// - the mouse click has changed things (e.g. in var frames, clicking the type slot focuses it and changes the frame content)
|
|// - the user middle-clicked, so we didn't scan in advance
|
|// Thus to cover the latter two options, we scan again to be sure:
|
|slot.withLinksAtPos(absNearest, optLink ->
|
|optLink.ifPresent(link -> {
|
|FXPlatformRunnable onClick = link.getOnClick();
|
|if (onClick != null)
|
|onClick.run();
|
|})
|
|);
|
|}
|
|});
|
|return f;
|
|}
|
|/**
| Position and focus the caret at the given position. Returns the Node that will receive
| focus.
|
p.public Node positionCaret(CaretPos pos)
{
if (pos == null)
return null;
pos = pos.normalise();
if (pos.index == -1)
{
return parent.positionParentPos(pos.subPos);
}
else
{
StructuredSlotComponent foc = fields.get(pos.index);
return foc.focusAtPos(pos.subPos);
}
}
| Draws the selection from anchorPos (if null, draw no selection) to the
| position given as a parameter. It does not matter whether anchorPos
| is before or after cur. If they are equal, no selection is drawn.
|
public void drawSelection(CaretPos cur)
{
if (anchorPos == null || anchorPos.equals(cur))
{
slot.clearSelection(false);
}
else
{
CaretPos start, end;
if (anchorPos.before(cur)) {
start = anchorPos;
end = cur;
}
else {
start = cur;
end = anchorPos;
}
slot.drawSelection(getAllStartEndPositionsBetween(start, end).collect(Collectors.toList()));
}
}
| Finds all the TextOverlayPosition items at the beginning and ends of all StructuredSlotField
| and Operator items (no matter how deeply nested) between the two caret positions, book-ended by the given caret
| positions themselves.
|
| We need this to calculate where to draw the selection boxes and error underlines for
| multi-field selections/errors.
|
| @param start The start position, or null to mean from the start of this entire expression
| @param end The end position, or null to mean to the end of this entire expression
|
Stream<TextOverlayPosition> getAllStartEndPositionsBetween(CaretPos start, CaretPos end)
{
if (start == null) {
start = new CaretPos(0, getFirstField().getStartPos());
}
boolean useVeryEnd = end == null;
if (end == null) {
end = new CaretPos(fields.size() - 1, getLastField().getEndPos());
}
int startIndex = start.index;
int endIndex = end.index;
if (startIndex == endIndex) {
Stream<TextOverlayPosition> s = fields.get(startIndex).getAllStartEndPositionsBetween(start.subPos, end.subPos);
if (useVeryEnd)
s = Stream.concat(s, Stream.of(((StructuredSlotField) fields.get(fields.size() - 1)).calculateOverlayEnd()));
return s;
}
Stream<TextOverlayPosition> s = fields.get(startIndex).getAllStartEndPositionsBetween(start.subPos, null);
for (int i = startIndex + 1; i < endIndex; i++)
{
if (operators.get(i - 1) != null) {
s = Stream.concat(s, operators.get(i - 1).getStartEndPositions(this));
}
s = Stream.concat(s, fields.get(i).getAllStartEndPositionsBetween(null, null));
}
if (operators.get(endIndex - 1) != null)
s = Stream.concat(s, operators.get(endIndex - 1).getStartEndPositions(this));
s = Stream.concat(s, fields.get(endIndex).getAllStartEndPositionsBetween(null, end.subPos));
if (useVeryEnd)
s = Stream.concat(s, Stream.of(((StructuredSlotField) fields.get(fields.size() - 1)).calculateOverlayEnd()));
return s;
}
public TextOverlayPosition calculateOverlayPos(CaretPos p)
{
StructuredSlotComponent f = fields.get(p.index);
return f.calculateOverlayPos(p.subPos);
}
public double sceneToOverlayX(double sceneX)
{
return slot.sceneToOverlayX(sceneX);
}
public double sceneToOverlayY(double sceneY)
{
return slot.sceneToOverlayY(sceneY);
}
public void deselect()
{
anchorPos = null;
drawSelection(null);
}
@Override
public void backwardAtStart(StructuredSlotField f)
{
backwardAtStart((StructuredSlotComponent) f);
}
| Handle moving backwards (left) when at the start of the given field.
|
| This will either move to the end of the appropriate preceding field, or if the passed
| parameter is the first field, will take the appropriate action via its surrounding
| BracketedStructured/StructuredSlot
|
public void backwardAtStart(StructuredSlotComponent f)
{
int i = findField(f);
if (i == -1) throw new IllegalStateException();
if (i > 0) {
fields.get(i - 1).focusAtEnd();
}
else
{
if (parent != null) {
parent.focusBefore();
}
else {
slot.getSlotParent().focusLeft(slot);
}
}
}
@Override
public void forwardAtEnd(StructuredSlotField f)
{
forwardAtEnd((StructuredSlotComponent)f);
}
| Handle moving forwards (right) when at the start of the given field.
|
| This will either move to the beginning of the appropriate following field, or if the passed
| parameter is the last field, will take the appropriate action via its surrounding
| BracketedStructured/StructuredSlot
|
public void forwardAtEnd(StructuredSlotComponent f)
{
int i = findField(f);
if (i == -1) throw new IllegalStateException();
if (i < fields.size() - 1) {
fields.get(i + 1).focusAtStart();
}
else
{
if (parent != null)
parent.focusAfter();
else{
slot.getSlotParent().focusRight(slot);
}
}
}
| Handle Home being pressed in the given field
|
@Override
public boolean home(StructuredSlotField f)
{
getFirstField().focusAtStart();
return true;
}
| Handle End being pressed in the given field
|
@Override
public boolean end(StructuredSlotField f, boolean asPartOfNextWordCommand)
{
if (asPartOfNextWordCommand)
f.focusAtEnd();
else{ end();
}
return true;
}
| Handle Shift-Home being pressed in the given field
|
@Override
public boolean selectHome(StructuredSlotField id, int caretPos)
{
int i = findField(id);
setAnchorIfUnset(new CaretPos(i, new CaretPos(caretPos, null)));
int dest = fields.get(i) instanceof StringLiteralExpression ? i : 0;
fields.get(dest).focusAtStart();
drawSelection(new CaretPos(dest, new CaretPos(0, null)));
return true;
}
| Handle Shift-End being pressed in the given field
|
@Override
public boolean selectEnd(StructuredSlotField id, int caretPos)
{
int i = findField(id);
setAnchorIfUnset(new CaretPos(i, new CaretPos(caretPos, null)));
int dest = fields.get(i) instanceof StringLiteralExpression ? i : fields.size() - 1;
fields.get(dest).focusAtEnd();
drawSelection(new CaretPos(dest, fields.get(dest).getEndPos()));
return true;
}
StructuredSlotField getFirstField()
{
return (StructuredSlotField)fields.get(0);
}
private StructuredSlotField getLastField()
{
return (StructuredSlotField)fields.get(fields.size() - 1);
}
@Override
public boolean previousWord(StructuredSlotField f, boolean atStart)
{
if (atStart) {
backwardAtStart(f);
return true;
}
return false;
}
@Override
public boolean nextWord(StructuredSlotField f, boolean atEnd)
{
if (atEnd) {
forwardAtEnd(f);
return true;
}
return false;
}
@Override
public boolean endOfNextWord(StructuredSlotField f, boolean atEnd)
{
return nextWord(f, atEnd);
}
@Override
public boolean selectAll(StructuredSlotField f)
{
home(null);
selectEnd(getFirstField(), 0);
return true;
}
@Override
@OnThread(Tag.FXPlatform)
public boolean selectNextWord(StructuredSlotField f)
{
setAnchorIfUnset(getCurrentPos());
if (f.getCurrentPos().equals(f.getEndPos()))
{
int i = findField(f);
if (fields.get(i) instanceof StringLiteralExpression)
return false;
else{ selectForward(f, f.getCurrentPos().index, true);
}
}
else
{
CaretPos anch = this.anchorPos;
f.nextWord();
this.anchorPos = anch;
drawSelection(new CaretPos(findField(f), f.getCurrentPos()));
}
return true;
}
@Override
@OnThread(Tag.FXPlatform)
public boolean selectPreviousWord(StructuredSlotField f)
{
setAnchorIfUnset(getCurrentPos());
if (f.getCurrentPos().equals(f.getStartPos()))
{
int i = findField(f);
if (fields.get(i) instanceof StringLiteralExpression)
return false;
else{ selectBackward(f, 0);
}
}
else
{
f.previousWord();
drawSelection(new CaretPos(findField(f), f.getCurrentPos()));
}
return true;
}
@Override
@OnThread(Tag.FXPlatform)
public boolean cut()
{
copy();
deleteSelection();
return true;
}
@Override
public void moveTo(double sceneX, double sceneY, boolean setAnchor)
{
CaretPos pos = getNearest(sceneX, sceneY, true, Optional.empty()).getPos();
positionCaret(pos);
if (setAnchor) {
anchorPos = pos;
}
}
p.public PosAndDist getNearest(double sceneX, double sceneY, boolean canDescend, Optional<Integer> restrictTo)
{
PosAndDist nearest = new PosAndDist();
for (int i = 0; i < fields.size();i++) {
final int index = i;
if (restrictTo.isPresent() && restrictTo.get() != i)
continue;
nearest = PosAndDist.nearest(nearest, fields.get(i).getNearest(sceneX, sceneY, canDescend, anchorPos != null && anchorPos.index == i).copyAdjustPos(p -> new CaretPos(index, p)));
}
return nearest;
}
@Override
public void selectTo(double sceneX, double sceneY)
{
CaretPos pos = getNearest(sceneX, sceneY, false, anchorPos != null && fields.get(anchorPos.index) instanceof StringLiteralExpression ? Optional.of(anchorPos.index) : Optional.empty()).getPos();
positionCaret(pos);
drawSelection(pos);
}
@Override
public void selected()
{
if (anchorPos == null || anchorPos.equals(getCurrentPos()))
{
anchorPos = null;
drawSelection(null);
}
}
private void setAnchorIfUnset(CaretPos caretPos)
{
if (anchorPos == null)
anchorPos = caretPos;
}
@Override
public boolean selectBackward(StructuredSlotField f, int posInSlot)
{
int start = findField(f);
if (posInSlot > 0) {
setAnchorIfUnset(new CaretPos(start, new CaretPos(posInSlot, null)));
CaretPos newPos = new CaretPos(start, new CaretPos(posInSlot - 1, null));
drawSelection(newPos);
positionCaret(newPos);
return true;
}
if (fields.get(start) instanceof StringLiteralExpression)
return false;
for (int i = start - 1; i >= 0; i--) {
CaretPos pos = fields.get(i).getSelectIntoPos(true);
if (pos != null) {
pos = new CaretPos(i, pos);
setAnchorIfUnset(new CaretPos(start, new CaretPos(posInSlot, null)));
positionCaret(pos);
drawSelection(pos);
return true;
}
}
return false;
}
@Override
public boolean selectForward(StructuredSlotField f, int posInSlot, boolean atEnd)
{
int start = findField(f);
if (!atEnd) {
setAnchorIfUnset(new CaretPos(start, new CaretPos(posInSlot, null)));
CaretPos newPos = new CaretPos(start, new CaretPos(posInSlot + 1, null));
drawSelection(newPos);
positionCaret(newPos);
return true;
}
if (fields.get(start) instanceof StringLiteralExpression)
return false;
for (int i = start + 1; i < fields.size(); i++) {
CaretPos pos = fields.get(i).getSelectIntoPos(false);
if (pos != null) {
pos = new CaretPos(i, pos);
setAnchorIfUnset(new CaretPos(start, new CaretPos(posInSlot, null)));
positionCaret(pos);
drawSelection(pos);
return true;
}
}
return false;
}
public void delete(StructuredSlotField f, int start, int end)
{
modification(token -> f.setText(f.getText().substring(0, start) + f.getText().substring(end), token));
}
@Override
public boolean copy()
{
if (anchorPos == null)
return true;
CaretPos cur = getCurrentPos();
CaretPos start, end;
if (anchorPos.before(cur)) {
start = anchorPos;
end = cur;
}
else {
start = cur;
end = anchorPos;
}
String s = getCopyText(start, end);
Clipboard.getSystemClipboard().setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, s));
return true;
}
public String getCopyText(CaretPos start, CaretPos end)
{
if (start == null)
start = new CaretPos(0, new CaretPos(0, null));
if (end == null)
end = new CaretPos(fields.size() - 1, getLastField().getEndPos());
if (start.index == end.index) {
return fields.get(start.index).getCopyText(start.subPos, end.subPos);
}
StringBuilder b = new StringBuilder();
b.append(fields.get(start.index).getCopyText(start.subPos, null));
if (operators.get(start.index) != null)
b.append(operators.get(start.index).getCopyText());
for (int i = start.index + 1; i < end.index; i++) {
b.append(fields.get(i).getCopyText(null, null));
if (operators.get(i) != null)
b.append(operators.get(i).getCopyText());
}
b.append(fields.get(end.index).getCopyText(null, end.subPos));
return b.toString();
}
private String getJavaCodeForFields(int start, int end)
{
StringBuilder b = new StringBuilder();
for (int i = start; i < end; i++)
{
b.append(fields.get(i).getJavaCode());
if (i < operators.size() && operators.get(i) != null && i < end - 1)
b.append(operators.get(i).getJavaCode());
}
return b.toString();
}
public String getJavaCode()
{
StringBuilder b = new StringBuilder();
int closing = 0;
int last = 0;
for (int i = 0; i < operators.size(); i++)
{
if (operators.get(i) != null)
{
String op = operators.get(i).get();
if (op.equals(".."))
{
b.append(lang.stride.Utility.class.getName() + ".makeRange(");
b.append(getJavaCodeForFields(last, i + 1));
b.append(", ");
last = i + 1;
closing += 1;
}
else if (op.equals(","))
b.append(getJavaCodeForFields(last, i + 1));
for (; closing > 0; closing--)
b.append(")");
b.append(", ");
last = i + 1;
}
}
}
b.append(getJavaCodeForFields(last, fields.size()));
for (; closing > 0; closing--)
b.append(")");
return b.toString();
}
@Override
@OnThread(Tag.FXPlatform)
public boolean deleteSelection()
{
return modificationReturnPlatform(token -> deleteSelectionImpl(getCurrentPos(), token) != null);
}
@OnThread(Tag.FXPlatform)
private CaretPos deleteSelectionImpl(CaretPos cur, StructuredSlot.ModificationToken token)
{
if (anchorPos == null || anchorPos.equals(cur))
{
anchorPos = null;
return null;
}
CaretPos start, end;
if (anchorPos.before(cur))
{
start = anchorPos;
end = cur;
}
else
{
start = cur;
end = anchorPos;
}
anchorPos = null;
if (start.index == end.index)
{
if (fields.get(start.index) instanceof BracketedStructured)
{
InfixStructured nested = ((BracketedStructured)fields.get(start.index)).getContent();
nested.setAnchorIfUnset(start.subPos);
return new CaretPos(start.index, nested.deleteSelectionImpl(end.subPos, token));
}
else if (fields.get(start.index) instanceof StringLiteralExpression)
{
StringLiteralExpression s = (StringLiteralExpression) fields.get(start.index);
StructuredSlotField f = s.getField();
f.setText(f.getText().substring(0, start.subPos.index) + f.getText().substring(end.subPos.index), token);
return start;
}
}
StructuredSlotField startField = (StructuredSlotField)fields.get(start.index);
StructuredSlotField endField = (StructuredSlotField)fields.get(end.index);
startField.setText(startField.getText().substring(0, start.subPos.index) + endField.getText().substring(end.subPos.index), token);
for (int i = start.index + 1; i <= end.index;i++)
{
operators.remove(start.index, token);
fields.remove(start.index + 1, token);
}
CaretPos pos = checkFieldChange(start.index, start, token);
positionCaret(pos);
if (slot != null)
{
slot.clearSelection(true);
}
return pos;
}
| Deletes the character before the given position in the text field.
| The atStart parameter is a bit redundant (check if posInField == 0), but mirrors that of
| deleteNext.
|
@OnThread(Tag.FXPlatform)
public boolean deletePrevious(StructuredSlotField f, int posInField, boolean atStart)
{
modificationPlatform(token -> positionCaret(deletePrevious_(f, posInField, atStart, token)));
return true;
}
@OnThread(Tag.FXPlatform)
public CaretPos deletePreviousAtPos(CaretPos p, StructuredSlot.ModificationToken token)
{
StructuredSlotComponent c = fields.get(p.index);
if (c instanceof StructuredSlotField)
{
return deletePrevious_((StructuredSlotField)c, p.subPos.index, p.subPos.index == 0, token);
}
p.public else if(c instanceof StringLiteralExpression)
{
return deletePrevious_(((StringLiteralExpression)c).getField(), p.subPos.index, p.subPos.index == 0, token);
}
p.public else if(c instanceof BracketedStructured)
{
return new CaretPos(p.index, ((BracketedStructured)c).getContent().deletePreviousAtPos(p.subPos, token));
}
throw new IllegalStateException();
}
@OnThread(Tag.FXPlatform)
p.public CaretPos deletePrevious_(StructuredSlotField f, int posInField, boolean atStart, StructuredSlot.ModificationToken token)
{
int index = findField(f);
if (atStart)
{
if (index > 0)
{
Operator prev = operators.get(index - 1);
if (prev == null)
{
boolean inString = fields.get(index) instanceof StringLiteralExpression;
return flattenCompound(inString ? index : index - 1, !inString, token);
}
else
{
String op = prev.get();
if (op.length() > 1 && !op.equals("new "))
{
prev.set(op.substring(0, op.length() - 1));
return checkFieldChange(index - 1, new CaretPos(index, new CaretPos(0, null)), token);
}
else
{
operators.remove(index - 1, token);
StructuredSlotField prevField = (StructuredSlotField)fields.get(index - 1);
String opRemaining = "";
if (op.equals("new "))
{
opRemaining = "new";
}
int newPos = prevField.getText().length() + opRemaining.length();
prevField.setText(prevField.getText() + opRemaining + f.getText(), token);
fields.remove(index, token);
return checkFieldChange(index - 1, new CaretPos(index - 1, new CaretPos(newPos, null)), token);
}
}
}
else
{
if (parent != null) {
return new CaretPos(-1, parent.flatten(false, token));
}
if (slot != null) {
if (slot.backspaceAtStart()) {
return null;
}
return new CaretPos(0, new CaretPos(0, null));
}
else {
if (editor != null) {
throw new IllegalStateException("No parent nor slot");
}
return new CaretPos(0, new CaretPos(0, null));
}
}
}
else {
String s = f.getText();
f.setText(s.substring(0, posInField - 1) + s.substring(posInField), token);
CaretPos p = checkFieldChange(index, new CaretPos(index, new CaretPos(posInField - 1, null)), token);
return p;
}
}
@OnThread(Tag.FXPlatform)
public CaretPos flattenCompound(StructuredSlotComponent item, boolean caretAtEnd,
StructuredSlot.ModificationToken token)
{
return flattenCompound(fields.indexOf(item), caretAtEnd, token);
}
| Given the index of a compound field (e.g. BracketedStructured, StringLiteralExpressionField),
| takes its content and flattens it, i.e. unwraps it from the BracketedStructured and inserts it
| into the "index" position in this InfixStructured. Used when deleting brackets but you want
|* to retain the content.
*/
@OnThread(Tag.FXPlatform)
private CaretPos flattenCompound(int index, boolean atEnd, StructuredSlot.ModificationToken token)
|
|{
|
|StructuredSlotField fieldBefore = (StructuredSlotField) fields.get(index - 1);
|
|String after = fields.get(index+1).getCopyText(null, null);
|
|String content = fields.get(index).getCopyText(fields.get(index).getStartPos(), fields.get(index).getEndPos());
|
|// Must remove second field first:
|
|fields.remove(index+1, token);
|
|if (operators.remove(index, token) != null) throw new IllegalStateException();
|
|fields.remove(index, token);
|
|if (operators.remove(index-1, token) != null) throw new IllegalStateException();
|
|int len = fieldBefore.getText().length();
|
|CaretPos mid = insertImpl(fieldBefore, len, content, false, token);
|
|// TODO stop using testing method here:
|
|testingInsert(mid, after);
|
|if (atEnd)
|
|return mid;
|
|return new CaretPos(index - 1, new CaretPos(len, null));
|
|}
|
|/**
| Deletes the next character after the given position. atEnd is a convenience
| variable indicating if the deletion is requested at the last caret position
| in the given field.
|
@OnThread(Tag.FXPlatform)
public boolean deleteNext(StructuredSlotField f, int posInField, boolean atEnd)
{
modificationPlatform(token -> positionCaret(deleteNextImpl(f, posInField, atEnd, token)));
return true;
}
@OnThread(Tag.FXPlatform)
private CaretPos deleteNextImpl(StructuredSlotField f, int posInField, boolean atEnd,
StructuredSlot.ModificationToken token)
{
int index = findField(f);
if (atEnd)
{
if (index < fields.size() - 1)
{
Operator next = operators.get(index);
if (next == null)
{
boolean inString = fields.get(index) instanceof StringLiteralExpression;
return flattenCompound(inString ? index : index + 1, inString, token);
}
else
{
String op = next.get();
if (op.length() > 1 && isOperator(op.substring(1)))
{
next.set(op.substring(1));
return checkFieldChange(index, new CaretPos(index, new CaretPos(posInField, null)), token);
}
else
{
String opRemaining = "";
if (op.equals("new "))
{
opRemaining = "ew";
}
operators.remove(index, token);
int newPos = f.getText().length();
f.setText(f.getText() + opRemaining + ((StructuredSlotField)fields.get(index + 1)).getText(), token);
fields.remove(index + 1, token);
return checkFieldChange(index, new CaretPos(index, new CaretPos(newPos, null)), token);
}
}
}
else
{
if (parent != null)
return new CaretPos(-1, parent.flatten(true, token));
else
{
if (slot != null)
{
if (slot.deleteAtEnd())
return null;
}
return new CaretPos(index, new CaretPos(posInField, null));
}
}
}
else
{
String s = f.getText();
f.setText(s.substring(0, posInField) + s.substring(posInField+1), token);
return checkFieldChange(index, new CaretPos(index, new CaretPos(posInField, null)), token);
}
}
public CaretPos getCurrentPos()
{
for (int i = 0; i < fields.size(); i++)
{
CaretPos pos = fields.get(i).getCurrentPos();
if (pos != null)
return new CaretPos(i, pos);
}
return null;
}
private int findField(StructuredSlotComponent f)
{
for (int i = 0; i < fields.size(); i++)
{
if (fields.get(i) == f)
{
return i;
}
else if (fields.get(i) instanceof StringLiteralExpression)
{
if (f == ((StringLiteralExpression)fields.get(i)).getField())
{
return i;
}
}
}
return -1;
}
| Handles inserting the given text at the given caret position in the given slot-field.
|
@OnThread(Tag.FXPlatform)
public void insert(StructuredSlotField f, int posInField, String text)
{
modificationPlatform(token -> insertImpl(f, posInField, text, true, token));
}
| Implementation of insert method, used directly by test classes.
|
@OnThread(Tag.FXPlatform)
public CaretPos insertImpl(StructuredSlotField f, int posInField, String text, boolean user,
StructuredSlot.ModificationToken token)
{
if ( parent == null && !text.isEmpty() && text.length() > 0 && closingChars.contains(text.charAt(0)) ) {
slot.focusNext();
return null;
}
int index = findField(f);
if (index == -1)
{
return null;
}
CaretPos pos = new CaretPos(index, new CaretPos(posInField, null));
if (text.length() > 0 && !isOpeningBracket(text.charAt(0)) && text.charAt(0) != '\"' && text.charAt(0) != '\'')
{
CaretPos postDeletion = deleteSelectionImpl(pos, token);
if (postDeletion != null)
pos = postDeletion;
}
for (int i = 0; i < text.length(); i++)
{
pos = insertChar(pos, text.charAt(i), user, token);
anchorPos = null;
if (pos.index == Integer.MAX_VALUE)
{
if (parent == null)
throw new IllegalStateException();
else
{
parent.insertAfter(text.substring(i+1));
return null;
}
}
}
positionCaret(pos);
if (slot != null)
{
slot.clearSelection(true);
}
return pos;
}
@OnThread(Tag.FXPlatform)
p.public CaretPos insertAtPos(CaretPos p, String after, StructuredSlot.ModificationToken token)
{
StructuredSlotComponent f = fields.get(p.index);
if (f instanceof StructuredSlotField) {
return insertImpl((StructuredSlotField)fields.get(p.index), p.subPos.index, after, false, token);
}
if (f instanceof StringLiteralExpression) {
return insertImpl(((StringLiteralExpression)fields.get(p.index)).getField(), p.subPos.index, after, false, token);
}
return new CaretPos(p.index, ((BracketedStructured)fields.get(p.index)).testingContent().insertAtPos(p.subPos, after, token));
}
@OnThread(Tag.FXPlatform)
protected CaretPos insertChar(CaretPos pos, char c, boolean user, StructuredSlot.ModificationToken token)
{
final StructuredSlotComponent slot = fields.get(pos.index);
if (slot instanceof StructuredSlotField)
{
final StructuredSlotField f = (StructuredSlotField)slot;
final Operator prev = pos.index == 0 ? null : operators.get(pos.index - 1);
final Operator next = pos.index >= operators.size() ? null : operators.get(pos.index);
int posInField = pos.subPos.index;
if (isDisallowed(c))
return pos;
if (Character.isWhitespace(c) && !f.getText().substring(0, posInField).equals("new"))
{
return pos;
}
if (posInField == 0 && prev != null &&
isOperator(prev.get() + c))
{
prev.set(prev.get() + c);
return pos;
}
else if (posInField == f.getText().length() && next != null &&
isOperator("" + c + next.get()))
{
next.set("" + c + next.get());
p.public return pos;
}
else if (c == ',' && posInField == f.getText().length() && next != null &&
next.get().equals(",") && pos.index + 1 < fields.size() &&
fields.get(pos.index + 1).getCopyText(null, null).isEmpty())
{
return new CaretPos(pos.index + 1, new CaretPos(0, null));
}
else if (beginsOperator(c) && c != '.' && c != '+' && c != '-')
String before = f.getText().substring(0, posInField);
String following = f.getText().substring(posInField);
f.setText(before, token);
operators.add(pos.index, new Operator("" + c, this), token);
fields.add(pos.index + 1, makeNewField(following, false), token);
return new CaretPos(pos.index + 1, new CaretPos(0, null));
}
else if (anchorPos != null && (isOpeningBracket(c) || c == '\"' || c == '\''))
{
String content = anchorPos.before(pos) ? getCopyText(anchorPos, pos) : getCopyText(pos, anchorPos);
pos = deleteSelectionImpl(pos, token);
pos = insertChar(pos, c, false, token);
insertAtPos(pos, content, token);
return new CaretPos(pos.index + 1, new CaretPos(0, null));
}
else if (isOpeningBracket(c))
{
if (posInField == f.getText().length() &&
pos.index + 1 < fields.size() && fields.get(pos.index + 1) instanceof BracketedStructured)
{
BracketedStructured following = (BracketedStructured) fields.get(pos.index + 1);
if (following.getOpening() == c)
{
return new CaretPos(pos.index + 1, new CaretPos(0, new CaretPos(0, null)));
}
}
String following = f.getText().substring(posInField);
f.setText(f.getText().substring(0, posInField), token);
operators.add(pos.index, null, token);
fields.add(pos.index + 1, new BracketedStructured(editor, this, this.slot, c, "", token), token);
if (pos.index + 1 >= operators.size() || operators.get(pos.index + 1) != null
|| !(fields.get(pos.index + 2) instanceof StructuredSlotField))
{
operators.add(pos.index + 1, null, token);
fields.add(pos.index + 2, makeNewField(following, false), token);
}
else
{
StructuredSlotField follow = (StructuredSlotField)fields.get(pos.index+2);
follow.setText(following + follow.getText(), token);
}
return new CaretPos(pos.index + 1, new CaretPos(0, new CaretPos(0, null)));
}
else if (isClosingBracket(c))
{
if (closingChars.contains(c) && posInField == f.getText().length())
{
return new CaretPos(Integer.MAX_VALUE, null);
}
else
{
return pos;
}
}
else if (c == '\"' || c == '\'')
{
String following = f.getText().substring(posInField);
f.setText(f.getText().substring(0, posInField), token);
operators.add(pos.index, null, token);
fields.add(pos.index + 1, new StringLiteralExpression(c, makeNewField("", true), this), token);
if (pos.index + 1 >= operators.size() || operators.get(pos.index + 1) != null || fields.get(pos.index + 2) instanceof StringLiteralExpression || fields.get(pos.index + 2) instanceof BracketedStructured)
{
operators.add(pos.index + 1, null, token);
fields.add(pos.index + 2, makeNewField(following, false), token);
}
else
{
StructuredSlotField follow = (StructuredSlotField)fields.get(pos.index+2);
follow.setText(following + follow.getText(), token);
}
return new CaretPos(pos.index + 1, new CaretPos(0, new CaretPos(0, null)));
}
else
{
if (f.getText().substring(0, posInField).equals("new")
&& Character.isWhitespace(c))
{
String following = f.getText().substring(posInField);
f.setText("", token);
operators.add(pos.index, new Operator("new ", this), token);
fields.add(pos.index + 1, makeNewField(following, false), token);
return new CaretPos(pos.index + 1, new CaretPos(0, null));
}
else
{
f.setText(f.getText().substring(0, posInField) + c + f.getText().substring(posInField), token);
CaretPos overridePos = checkFieldChange(pos.index, new CaretPos(pos.index, new CaretPos(posInField+1, null)), c == '.', user, token);
return overridePos;
}
}
}
p.public else if(slot instanceof BracketedStructured)
{
CaretPos newSubPos = ((BracketedStructured)slot).getContent().insertChar(pos.subPos, c, false, token);
if (newSubPos.index == Integer.MAX_VALUE)
{
return new CaretPos(pos.index + 1, new CaretPos(0, null));
}
else
{
return new CaretPos(pos.index, newSubPos);
}
}
p.public else if(slot instanceof StringLiteralExpression)
{
StringLiteralExpression lit = (StringLiteralExpression)slot;
final StructuredSlotField f = lit.getField();
final int posInField = pos.subPos.index;
if ((c == '\"' || c == '\'') && ("" + c).equals(lit.getQuote()) && getEscapeStatus(f.getText().substring(0, posInField)) == EscapeStatus.NORMAL)
{
if (posInField == f.getText().length())
{
return new CaretPos(pos.index + 1, new CaretPos(0, null));
}
else
{
return pos;
}
}
f.setText(f.getText().substring(0, posInField) + c + f.getText().substring(posInField), token);
return new CaretPos(pos.index, new CaretPos(posInField+1, null));
}
return null;
}
abstract protected boolean isDisallowed(char c);
abstract protected boolean isOpeningBracket(char c);
abstract protected boolean isClosingBracket(char c);
public void setEditable(boolean editable)
{
fields.forEach(c -> c.setEditable(editable));
}
public boolean isNumericLiteral()
{
return fields.stream().allMatch(StructuredSlotComponent::isNumericLiteral);
}
| Gets the EscapeStatus for the given String prefix. See above comment for EscapeStatus
|
private EscapeStatus getEscapeStatus(String text)
{
EscapeStatus status = EscapeStatus.NORMAL;
for (char c : text.toCharArray())
{
if (status == EscapeStatus.NORMAL)
{
if (c == '\\')
{
status = EscapeStatus.AFTER_BACKSLASH;
}
}
else if (status == EscapeStatus.AFTER_BACKSLASH)
{
status = EscapeStatus.NORMAL;
}
}
return status;
}
@OnThread(Tag.FXPlatform)
private CaretPos checkFieldChange(int index, CaretPos pos, StructuredSlot.ModificationToken token)
{
return checkFieldChange(index, pos, false, false, token);
}
| Checks the field at the given index, and performs any rearrangements necessary that
| relate to floating point literals. This may involve turning a dot in a field into an
| operator, or turning an operator back into field content (for dots, pluses and minuses).
|
| Returns the new position (adjustment of the passed pos)
|
@OnThread(Tag.FXPlatform)
private CaretPos checkFieldChange(int index, CaretPos pos, boolean addedDot, boolean user, StructuredSlot.ModificationToken token)
{
if (fields.get(index) instanceof StringLiteralExpression)
return pos;
StructuredSlotField f = (StructuredSlotField)fields.get(index);
String prevOp = (index > 0 && operators.get(index - 1) != null)
? operators.get(index - 1).get() : "";
String nextOp = (index < operators.size() && operators.get(index) != null)
? operators.get(index).get() : "";
StructuredSlotField prevField = index > 0 && fields.get(index - 1) instanceof StructuredSlotField
? (StructuredSlotField)fields.get(index - 1) : null;
boolean precedingBracket = index > 0 && operators.get(index - 1) == null;
boolean bracketBeforePrevField = index > 1 && prevField != null && operators.get(index - 2) == null;
int dotIndex = -1;
while ((dotIndex = f.getText().indexOf('.', dotIndex + 1)) != -1)
{
String beforeDot = f.getText().substring(0, dotIndex);
String afterDot = f.getText().substring(dotIndex + 1);
boolean isDoubleDot = isOperator("..") && afterDot.startsWith(".");
if (!supportsFloatLiterals() || !precedesDotInFloatingPointLiteral(beforeDot) || isDoubleDot
| two dots is operator
|
{
f.setText(beforeDot, token);
if (isDoubleDot)
{
operators.add(index, new Operator("..", this), token);
fields.add(index + 1, makeNewField(afterDot.substring(1), false), token);
}
else
{
boolean wasShowingSuggestions = slot != null && slot.isShowingSuggestions();
operators.add(index, new Operator(".", this), token);
fields.add(index + 1, makeNewField(afterDot, false), token);
if (wasShowingSuggestions && user && addedDot)
JavaFXUtil.runPlatformLater(() -> slot.showSuggestionDisplay((StructuredSlotField)fields.get(index+1), 0, false));
}
if (pos.index > index)
pos = new CaretPos(pos.index + (isDoubleDot ? 2 : 1), pos.subPos);
else if (pos.index == index)
{
if (pos.subPos.index <= beforeDot.length())
{
}
else{ pos = new CaretPos(index + 1, new CaretPos(pos.subPos.index - (beforeDot.length() + (isDoubleDot ? 2 : 1)), null));
}
}
pos = checkFieldChange(index, pos, token);
return checkFieldChange(index + 1, pos, token);
}
}
if (supportsFloatLiterals() && precedesDotInFloatingPointLiteral(f.getText()) && nextOp.equals("."))
{
int prevLen = f.getText().length();
f.setText(f.getText() + nextOp + ((StructuredSlotField)fields.get(index + 1)).getText(), token);
operators.remove(index, token);
fields.remove(index+1, token);
if (pos.index > index + 1)
{
pos = new CaretPos(pos.index - 1, pos.subPos);
}
else if (pos.index == index + 1)
{
pos = new CaretPos(index, new CaretPos(pos.subPos.index + prevLen + 1, null));
}
nextOp = (index < operators.size() && operators.get(index) != null)
? operators.get(index).get() : "";
}
Function<Integer, Integer> findPlusMinus = prev -> {
int plusIndex = f.getText().indexOf('+', prev + 1);
int minusIndex = f.getText().indexOf('-', prev + 1);
if (plusIndex == -1)
return minusIndex;
else if (minusIndex == -1)
return plusIndex;
else{ return Math.min(plusIndex, minusIndex);
}
};
int plusMinusIndex = -1;
while ((plusMinusIndex = findPlusMinus.apply(plusMinusIndex)) != -1)
{
String before = f.getText().substring(0, plusMinusIndex);
String after = f.getText().substring(plusMinusIndex + 1);
boolean atBeginningAndUnary = before.equals("") && !precedingBracket && (!prevOp.equals("") || prevField == null || prevField.getText().equals("")) && succeedsOpeningPlusMinusInFloatingPointLiteral(after);
boolean midwayAfterEorP = precedesPlusMinusInFloatingPointLiteral(before);
if (!atBeginningAndUnary && !midwayAfterEorP)
{
operators.add(index, new Operator(f.getText().substring(plusMinusIndex, plusMinusIndex+1), this), token);
f.setText(before, token);
fields.add(index + 1, makeNewField(after, false), token);
if (pos.index > index)
return new CaretPos(pos.index + 1, pos.subPos);
else if (pos.index == index)
{
if (pos.subPos.index <= before.length())
return pos;
else{ return new CaretPos(index + 1, new CaretPos(pos.subPos.index - (before.length() + 1), null));
}
}
else{
return pos;
}
}
}
if (precedesPlusMinusInFloatingPointLiteral(f.getText()) && (nextOp.equals("+") || nextOp.equals("-")))
{
int prevLen = f.getText().length();
f.setText(f.getText() + nextOp + ((StructuredSlotField)fields.get(index + 1)).getText(), token);
operators.remove(index, token);
fields.remove(index+1, token);
if (pos.index > index + 1)
{
pos = new CaretPos(pos.index - 1, pos.subPos);
}
else if (pos.index == index + 1)
{
pos = new CaretPos(index, new CaretPos(pos.subPos.index + prevLen + 1, null));
}
}
if ((prevOp.equals("+") || prevOp.equals("-")) && succeedsOpeningPlusMinusInFloatingPointLiteral(f.getText())
&& prevField != null && prevField.getText().equals("") && !bracketBeforePrevField)
{
operators.remove(index - 1, token);
fields.remove(index - 1, token);
f.setText(prevOp + f.getText(), token);
pos = new CaretPos(pos.index - 1, new CaretPos(pos.subPos.index + 1, null));
}
return pos;
}
| Does this structured slot support floating point literals? True for expression slots, false for type slots
|
protected abstract boolean supportsFloatLiterals();
private boolean precedesPlusMinusInFloatingPointLiteral(String before)
{
return before.matches("\\A\\s*0x" + HEX_DIGITS_REGEX + "(\\.(" + HEX_DIGITS_REGEX +")?)?[pP]\\z") ||
before.matches("\\A\\s*[+-]?" + DIGITS_REGEX + "(\\.(" + DIGITS_REGEX + ")?)?[eE]\\z");
}
private boolean succeedsOpeningPlusMinusInFloatingPointLiteral(String after)
{
return after.matches("\\A\\d.*");
}
public void focusAtStart()
{
getFirstField().focusAtStart();
}
public void focusAtEnd()
{
getLastField().focusAtEnd();
}
public CaretPos getStartPos()
{
return new CaretPos(0, getFirstField().getStartPos());
}
public CaretPos getEndPos()
{
return new CaretPos(fields.size() - 1, getLastField().getEndPos());
}
public void end()
{
getLastField().focusAtEnd();
}
public ObservableList getComponents()
{
return components;
}
public boolean isEmpty()
{
return fields.size() == 1 && getFirstField().isEmpty();
}
public void requestFocus()
{
getFirstField().requestFocus();
}
void withContent(BiConsumer<ProtectedList<StructuredSlotComponent>, ProtectedList<Operator>> setPrompts)
{
setPrompts.accept(fields, operators);
}
p.public Region getNodeForPos(CaretPos pos)
{
StructuredSlotComponent f = fields.get(pos.index);
return f.getNodeForPos(pos.subPos);
}
| Creates mappings between a caret position and string position. See CaretPosMap for more info.
|
| If javaString is true, maps to generated Java code string. If false,
| maps to copy-string/frame source. Difference is that if you want to write
| "6 - -5", then in copy-string this must be "6--5" (no spaces supported) and in
|* Java code it must be "6- -5"; to prevent becoming joined -- operator. I.e. in Java
* code, a space is generated for each empty slot.
*/
//package-visible
List<CaretPosMap> mapCaretPosStringPos(IntCounter cur, boolean javaString)
|
|{
|
|List<CaretPosMap> r = new ArrayList<>();
|
|BiConsumer<Integer, Integer> addForRange = (startIncl, endExcl) -> {
|
|for (int i = startIncl; i < endExcl; i++)
|
|{
|
|for (CaretPosMap cpm : fields.get(i).mapCaretPosStringPos(cur, javaString))
|
|{
|
|r.add(cpm.wrap(i));
|
|}
|
|if (i < operators.size() && i < endExcl - 1)
|
|{
|
|Operator op = operators.get(i);
|
|if (op != null)
|
|{
|
|cur.counter += javaString ? op.getJavaCode().length() : op.get().length();
|
|}
|
|}
|
|}
|
|};
|
|if (!javaString)
|
|{
|
|addForRange.accept(0, fields.size());
|
|}
|
|else
|
|{
|
|int closing = 0;
|
|int last = 0;
|
|for (int i = 0; i < fields.size(); i++)
|
|{
|
|if (i < operators.size() && operators.get(i) != null)
|
|{
|
|String op = operators.get(i).get();
|
|int commaLength = ", ".length();
// See getJavaCode() for logic
if (op.equals(".."))
{
cur.counter += (lang.stride.Utility.class.getName() + ".makeRange(").length();
addForRange.accept(last, i + 1);
cur.counter += commaLength;
last = i + 1;
closing += 1;
|
|}
|
|else if (op.equals(","))
{
addForRange.accept(last, i+1);
cur.counter += closing + commaLength;
closing = 0;
|
|last = i + 1;
|
|}
|
|}
|
|}
|
|addForRange.accept(last, fields.size());
|
|cur.counter += closing;
|
|}
|
|return r;
|
|}
|
|/**
| Maps a given string position into a CaretPos.
| Only really applicable when called on the top-level InfixStructured.
| Should probably be moved to StructuredSlot.
|
| If javaString is true, maps to generated Java code string. If false,
| maps to copy-string/frame source. Difference is that if you want to write
| "6 - -5", then in copy-string this must be "6--5" (no spaces supported) and in
|* Java code it must be "6- -5"; to prevent becoming joined -- operator. I.e. in Java
* code, a space is generated for each empty slot.
*/
//package-visible
public defVis CaretPos stringPosToCaretPos(int pos, boolean javaString)
|
|{
|
|List<CaretPosMap> mapping = mapCaretPosStringPos(new IntCounter(), javaString);
|
|for (CaretPosMap cpm : mapping) {
|
|if (pos <= cpm.endIndex) {
|
|if (pos >= cpm.startIndex || javaString /*For javaString, we want to find nearest pos
return cpm.posOuter.append(new CaretPos(Math.max(0, pos - cpm.startIndex), null));
else{ return null;
}
}
}
Debug.message("Could not find position for: " + pos);
return null;
}
| Maps a given CaretPos into a position in the full string for the expression.
| Only really applicable when called on the top-level InfixStructured.
| Should probably be moved to StructuredSlot.
|
| If javaString is true, maps to generated Java code string. If false,
| maps to copy-string/frame source. Difference is that if you want to write
| "6 - -5", then in copy-string this must be "6--5" (no spaces supported) and in
|* Java code it must be "6- -5"; to prevent becoming joined -- operator. I.e. in Java
* code, a space is generated for each empty slot.
*/
//package-visible
public defVis int caretPosToStringPos(CaretPos pos, boolean javaString)
|
|{
|
|List<CaretPosMap> mapping = mapCaretPosStringPos(new IntCounter(), javaString);
|
|for (CaretPosMap cpm : mapping) {
|
|Optional<Integer> i = pos.getFollowing(cpm.posOuter);
|
|if (i.isPresent()) {
|
|return i.get() + cpm.startIndex;
|
|}
|
|}
|
|throw new IllegalStateException();
|
|}
|
|//package-visible
|
|double getBaseline()
|
|{
|
|// 3 is a hack/guess at the baseline
|
|TextField field = (TextField)components.get(0);
|
|double height = field.getHeight() - 3 - field.getPadding().getBottom();
|
|if (field.getBorder() != null && field.getBorder().getInsets() != null)
|
|height -= field.getBorder().getInsets().getBottom();
|
|return height;
|
|}
|
|public void blank(StructuredSlot.ModificationToken token)
|
|{
|
|token.check();
|
|operators.clear(token);
|
|fields.clear(token);
|
|fields.add(makeNewField("", false), token);
anchorPos = null;
}
public boolean isFocused()
{
return fields.stream().anyMatch(StructuredSlotComponent::isFocused);
}
public boolean isCollapsible(StructuredSlotField f)
|
|{
|
|// A field is collapsible if:
|
|// - It occurs to the left-hand side of an operator that is, or can be, unary
|
|// - It occurs between a bracket (null operator) and an operator, or
|
|// a bracket and the end, or two brackets (could be cast on lhs)
|
|// - This includes single slots, but there is exception for only slot at top-level, unless we are parameters to constructor
|
|int index = findField(f);
|
|if (index == -1) {
|
|// This can occur, e.g. while field is being removed and has lost focus:
|
|return false;
|
|}
|
|boolean opBefore = index == 0 || operators.get(index - 1) != null;
|
|boolean opAfter = index == operators.size() || operators.get(index) != null;
|
|//boolean unaryBefore = index != 0 && canBeUnary(operators.get(index - 1));
|
|boolean unaryAfter = index < operators.size() && canBeUnary(Utility.orNull(operators.get(index), Operator::get));
|
|if (fields.size() == 1 && parent == null)
|
|{
|
|if (slot != null && slot.canCollapse())
|
|return true; // Can collapse in this case
|
|else{ return false;
|
|} // Only field at top level, not constructor params
|
|}
|
|else if (fields.size() == 1 && parent != null)
|
|return true; // Only field in brackets
|
|else if (opBefore && opAfter)
|
|return unaryAfter;
|
|else{ return true;
|
|}
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public void insertNext(BracketedStructured bracketedExpression, String text)
|
|{
|
|int index = fields.indexOf(bracketedExpression);
|
|insert((StructuredSlotField) fields.get(index + 1), 0, text);
|
|}
|
|// For testing purposes, package visible
|
|public defVis String testingGetState(CaretPos caret)
|
|{
|
|caret = Utility.orNull(caret, CaretPos::normalise);
|
|StringBuilder r = new StringBuilder();
|
|for (int i = 0; i < fields.size(); i++) {
|
|r.append(fields.get(i).testingGetState((caret != null && i == caret.index) ? caret.subPos : null));
|
|if (i < operators.size()) {
|
|if (operators.get(i) == null) {
|
|r.append("_");
}
else {
r.append(operators.get(i).get());
}
}
}
return r.toString();
}
|
|// If rememberPos is \0, give position after inserting whole string
|
|// If rememberPos is any other char, remember pos at first occurrence of that char in the string
|
|// e.g. "a+$b", '$' will give the caret position just before the b. The instance of rememberPos
// will be ignored (it will not be inserted).
@OnThread(Tag.FXPlatform)
public CaretPos testingInsert(String text, char rememberPos)
|
|{
|
|return modificationReturnPlatform(token -> {
|
|int index = text.indexOf(rememberPos);
|
|if (rememberPos != '\0' && index != -1)
|
|{
|
|String before = text.substring(0, index);
|
|String after = text.substring(index + 1);
|
|CaretPos p = insertImpl((StructuredSlotField)fields.get(0), 0, before, false, token);
|
|testingInsert(p, after);
|
|return p;
|
|}
|
|return insertImpl((StructuredSlotField)fields.get(0), 0, text, false, token);
|
|});
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public defVis CaretPos testingInsert(CaretPos p, String after)
|
|{
|
|return modificationReturnPlatform(token -> insertAtPos(p, after, token));
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public defVis CaretPos testingBackspace(CaretPos p)
|
|{
|
|return modificationReturnPlatform(token -> {
|
|StructuredSlotComponent f = fields.get(p.index);
|
|if (f instanceof StructuredSlotField)
|
|return deletePrevious_((StructuredSlotField)fields.get(p.index), p.subPos.index, p.subPos.index == 0, token);
|
|public defVis else if (f instanceof StringLiteralExpression)
|
|return deletePrevious_(((StringLiteralExpression)fields.get(p.index)).getField(), p.subPos.index, p.subPos.index == 0, token);
|
|else{ return new CaretPos(p.index, ((BracketedStructured)fields.get(p.index)).testingContent().testingBackspace(p.subPos));
|
|}
|
|});
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public defVis CaretPos testingDelete(CaretPos p)
|
|{
|
|StructuredSlotComponent s = fields.get(p.index);
|
|StructuredSlotField f;
|
|if (s instanceof StructuredSlotField)
|
|f = (StructuredSlotField)fields.get(p.index);
|
|public defVis else if (s instanceof StringLiteralExpression)
|
|f = ((StringLiteralExpression)fields.get(p.index)).getField();
|
|else{ return new CaretPos(p.index, ((BracketedStructured)fields.get(p.index)).testingContent().testingDelete(p.subPos));
|
|}
|
|return modificationReturnPlatform(token ->
|
|deleteNextImpl(f, p.subPos.index, p.subPos.index == f.getText().length(), token));
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public defVis CaretPos testingDeleteSelection(CaretPos start, CaretPos end)
|
|{
|
|anchorPos = start;
|
|return modificationReturnPlatform(token -> deleteSelectionImpl(end, token));
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public defVis CaretPos testingInsertWithSelection(CaretPos start, CaretPos end, char c)
|
|{
|
|anchorPos = start;
|
|return modificationReturnPlatform(token -> insertAtPos(end, "" + c, token));
}
@OnThread(Tag.FXPlatform)
public void insertSuggestion(CaretPos p, String name, char opening, List<String> params, StructuredSlot.ModificationToken token)
{
StructuredSlotComponent f = fields.get(p.index);
|
|if (f instanceof StructuredSlotField)
|
|{
|
|// Blank the field (easier than working out prefixes etc)
|
|((StructuredSlotField) f).setText("", token);
// And insert the new name. It may have a dot in it, so caret may move to another field:
p = insertImpl((StructuredSlotField)f, 0, name, false, token);
if (params != null)
|
|{
|
|StringBuilder commas = new StringBuilder();
|
|for (int i = 0; i < params.size() - 1; i++)
|
|commas.append(',');
|
|if (p.index + 1 < fields.size() && fields.get(p.index + 1) instanceof BracketedStructured && ((BracketedStructured)fields.get(p.index + 1)).getOpening() == opening)
|
|{
|
|BracketedStructured b = ((BracketedStructured)fields.get(p.index + 1));
|
|if (b.getContent().isEmpty())
|
|{
|
|b.getContent().insertAtPos(new CaretPos(0, new CaretPos(0, null)), commas.toString(), token);
|
|}
|
|// else if there are brackets with content, leave them alone.
|
|// We use a runLater as we need to request focus after the suggestion window has been hidden:
|
|JavaFXUtil.runAfterCurrent(() ->
|
|b.focusAtStart()
|
|);
|
|}
|
|else
|
|{
|
|insertAtPos(new CaretPos(p.index, new CaretPos(p.subPos.index, null)), "(" + commas.toString() + ")", token);
// If no parameters, focus after the brackets. Otherwise, focus first parameter
StructuredSlotComponent focusField = fields.get(params.isEmpty() ? p.index + 2 : p.index + 1);
|
|// We use a runLater as we need to request focus after the suggestion window has been hidden:
|
|JavaFXUtil.runAfterCurrent(() ->
|
|focusField.focusAtStart()
|
|);
|
|}
|
|}
|
|}
|
|else
|
|{
|
|f.insertSuggestion(p.subPos, name, opening, params, token);
|
|}
|
|}
|
|@OnThread(Tag.FXPlatform)
|
|public abstract void calculateTooltipFor(StructuredSlotField expressionSlotField, FXConsumer<String> handler);
|
|protected CaretPos absolutePos(CaretPos p)
|
|{
|
|if (parent == null)
|
|return p;
|
|else{ return parent.absolutePos(p);
|
|}
|
|}
|
|public CaretPos absolutePos(BracketedStructured bracketedExpression, CaretPos p)
|
|{
|
|return absolutePos(new CaretPos(fields.indexOf(bracketedExpression), p));
|
|}
|
|//package-visible
|
|InteractionManager getEditor()
|
|{
|
|return editor;
|
|}
|
|// List is as long as there are parameters, but contains null if the parameter is non-simple,
|
|// i.e. does not consist of a single field
|
|public List<StructuredSlotField> getSimpleParameters()
|
|{
|
|List<StructuredSlotField> r = new ArrayList<>();
|
|int lastComma = -1; // Just before first parameter, in effect
|
|for (int i = 0; i < fields.size(); i++)
|
|{
|
|StructuredSlotField f = fields.get(i) instanceof StructuredSlotField ? (StructuredSlotField)fields.get(i) : null;
|
|// Look at operator afterwards:
|
|if (i == fields.size() - 1 || (operators.get(i) != null && operators.get(i).getCopyText().equals(",")))
{
if (lastComma == i - 1 && f != null)
{
r.add(f);
}
else
{
r.add(null);
|
|}
|
|lastComma = i;
|
|}
|
|}
|
|return r;
|
|}
|
|@Override
|
|public void clicked()
|
|{
|
|slot.hideSuggestionDisplay();
|
|}
|
|@Override
|
|public void caretMoved()
|
|{
|
|if (slot != null) // Can be null during testing{
|
|JavaFXUtil.ifOnPlatform(() -> slot.caretMoved());
|
|}
|
|}
|
|@Override
|
|@OnThread(Tag.FXPlatform)
|
|public void escape()
|
|{
|
|slot.escape();
|
|}
|
|public void addClosingChar(char closingChar)
|
|{
|
|this.closingChars.add(closingChar);
|
|}
|
|public Stream<InfixStructured<?, ?>> getAllExpressions()
|
|{
|
|return Stream.concat(Stream.of(this), fields.stream().flatMap(StructuredSlotComponent::getAllExpressions));
|
|}
|
|public StringExpression textProperty()
|
|{
|
|return textProperty;
|
|}
|
|public TextOverlayPosition calculateOverlayEnd()
|
|{
|
|return getLastField().calculateOverlayEnd();
|
|}
|
|/**
| Makes no change directly. Always returns non-null, but list may be empty
|
List<StructuredSlot.PlainVarReference> findPlainVarUse(String name)
{
List<StructuredSlot.PlainVarReference> refs = new ArrayList<>();
for (int i = 0; i < fields.size(); i++)
{
if (fields.get(i) instanceof StructuredSlotField)
{
StructuredSlotField f = (StructuredSlotField)fields.get(i);
if (f.getText().equals(name)
&& (i == 0 || operators.get(i - 1) == null || !operators.get(i - 1).get().equals("."))
&& (i == fields.size() - 1 || !(fields.get(i) instanceof BracketedStructured) || ((BracketedStructured)fields.get(i)).getOpening() != '('))
{
refs.add(new StructuredSlot.PlainVarReference(text -> modification(token -> f.setText(text, token)), f.getNodeForPos(null)));
}
}
else if (fields.get(i) instanceof BracketedStructured)
{
refs.addAll(((BracketedStructured)fields.get(i)).getContent().findPlainVarUse(name));
}
}
return refs;
}
| Returns true iff the infix expression is of the form {},2,3},
| or that in some number of round brackets.
|
public boolean isCurlyLiteral()
{
if (fields.size() != 3)
return false;
if (operators.get(0) != null || operators.get(1) != null)
return false;
if (!fields.get(0).isFieldAndEmpty() || !fields.get(2).isFieldAndEmpty())
return false;
if (! (fields.get(1) instanceof BracketedStructured))
return false;
BracketedStructured e = (BracketedStructured) fields.get(1);
if (e.getOpening() == '{')
return true;
else{ return false;
}
}
p.public void showHighlightedBrackets(BracketedStructured wrapper, CaretPos pos)
{
if (wrapper != null && pos != null && pos.index == 0 && fields.get(0).getStartPos().equals(pos.subPos))
{
wrapper.highlightBrackets(true);
}
else if (wrapper != null && pos != null && pos.index == fields.size() - 1 && fields.get(fields.size() - 1).getEndPos().equals(pos.subPos))
{
wrapper.highlightBrackets(true);
}
for (int i = 0; i < fields.size(); i++)
{
StructuredSlotComponent f = fields.get(i);
if (f instanceof BracketedStructured)
{
boolean cursorBefore =
i > 0 &&
pos != null && pos.index == i - 1 &&
fields.get(i - 1) instanceof StructuredSlotField &&
fields.get(i - 1).getEndPos().equals(pos.subPos);
boolean cursorAfter =
i < fields.size() - 1 &&
pos != null && pos.index == i + 1 &&
fields.get(i + 1) instanceof StructuredSlotField &&
fields.get(i + 1).getStartPos().equals(pos.subPos);
BracketedStructured e = (BracketedStructured)f;
e.highlightBrackets(cursorBefore || cursorAfter);
e.getContent().showHighlightedBrackets(e, pos != null && pos.index == i ? pos.subPos : null);
}
}
}
public void setView(View oldView, View newView, SharedTransition animate, Optional<String> forLoopVarName)
{
fields.forEach(f -> f.setView(oldView, newView, animate));
operators.forEach(o -> { if (o != null) o.setView(newView, animate);
});
if (newView == View.NORMAL)
{
previewingJavaRange.set(false);
}
else
{
switch (checkRangeExpression())
{
case RANGE_CONSTANT:
if (forLoopVarName.isPresent())
{
Operator rangeOp = operators.stream().filter(op -> op != null && op.get().equals("..")).findFirst().get();
previewingJavaRange.set(true);
startRangeText.set("");
endRangeText.set("; " + forLoopVarName.get() + "++");
rangeOp.setJavaPreviewRangeOverride("; " + forLoopVarName.get() + " <=");
break;
}
case RANGE_NON_CONSTANT:
previewingJavaRange.set(true);
startRangeText.set(lang.stride.Utility.class.getName() + "(");
endRangeText.set(")");
break;
default:
previewingJavaRange.set(false);
break;
}
}
}
RangeType checkRangeExpression()
{
if (operators.stream().anyMatch(op -> op != null && op.get().equals(",")))
return RangeType.NOT_RANGE;
Optional<Operator> rangeOp = operators.stream().filter(op -> op != null && op.get().equals("..")).findFirst();
if (!rangeOp.isPresent())
return RangeType.NOT_RANGE;
if (fields.stream().allMatch(StructuredSlotComponent::isNumericLiteral))
return RangeType.RANGE_CONSTANT;
else{ return RangeType.RANGE_NON_CONSTANT;
}
}
@OnThread(Tag.FXPlatform)
public void paste()
{
StructuredSlotField focused = fields.stream()
.filter(f -> f instanceof StructuredSlotField && f.isFocused())
.map(f -> (StructuredSlotField)f)
.findFirst()
.orElse(null);
if (focused != null)
{
focused.paste();
}
}
StructuredSlot<?, ?, ?> getSlot()
{
return slot;
}
@OnThread(Tag.FXPlatform)
public boolean suggestingFor(StructuredSlotField f)
{
if (slot == null)
return false;
int index = findField(f);
CaretPos fieldPos = absolutePos(new CaretPos(index, null));
return slot.suggestingFor(fieldPos);
}
public SplitInfo trySplitOn(String target)
{
for (int i = 0; i < operators.size(); i++)
{
Operator op = operators.get(i);
if (op != null && op.get().equals(target))
{
return new SplitInfo(getCopyText(null, new CaretPos(i, fields.get(i).getEndPos())), getCopyText(new CaretPos(i + 1, fields.get(i + 1).getStartPos()), null));
}
}
return null;
}
public boolean isAlmostBlank()
{
return fields.stream().allMatch(StructuredSlotComponent::isAlmostBlank);
}
public void notifyLostFocus(StructuredSlotField except)
{
fields.forEach(f -> { if (f != except) f.notifyLostFocus(except);
});
}
boolean isInSelection()
{
return anchorPos != null || (parent != null && parent.isInSelection());
}
public int calculateEffort()
{
return fields.stream().filter(f -> f != null).mapToInt(StructuredSlotComponent::calculateEffort).sum() + operators.stream().filter(op -> op != null).mapToInt(op -> op.get().length()).sum();
}
| The escape status in a String literal. For example, in the string literal:
| "Hi!\n C:\\Program Files"
|* we can pass various substrings to find the escape status (using curly brackets to
* surround the string, to avoid confusion):
*
* {}i!} : NORMAL
* {}i!\}: AFTER_BACKSLASH
| {}i!\n}: NORMAL
| {}i!\n C:\}: AFTER_BACKSLASH
| {}i!\n C:\\}: NORMAL
|
private static enum EscapeStatus { NORMAL, AFTER_BACKSLASH
}
static enum RangeType { RANGE_CONSTANT, RANGE_NON_CONSTANT, NOT_RANGE
}
| Stores a mapping between a CaretPos structure and a traditional
| integer position into a string. This is used to map between the two, for example
| when dealing with error messages that came from a string but need to be mapped back
| into a location in the InfixStructured
|
| The posOuter field holds the CaretPos that points to a given text field, but without a final location
| in the field. So if you have an InfixStructured like "abc", posOuter will actually just be null,
|* startIndex will be 0 and endIndex will be 2. But if you have "get(abc)", then posOuter would be
* new CaretPos(1, null) [because the brackets are index 1 in the InfixStructured] and startIndex would be
* 3 and endIndex would be 5.
*/
// Package-visible
static class CaretPosMap
|
|{
|
|private final CaretPos posOuter; // Has null at inner point where you should put within-field caret index
|
|private final int startIndex; // The corresponding index within the entire String
|
|private final int endIndex; // The corresponding index within the entire String
|
|// Package-visible
|
|CaretPosMap(CaretPos posOuter, int startIndex, int endIndex)
|
|{
|
|this.posOuter = posOuter;
|
|this.startIndex = startIndex;
|
|this.endIndex = endIndex;
|
|}
|
|/**
| Makes a copy of this CaretPosMap, with the given index on the beginning of the posOuter field.
|
public CaretPosMap wrap(int index)
{
return new CaretPosMap(new CaretPos(index, posOuter), startIndex, endIndex);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CaretPosMap other = (CaretPosMap) obj;
if (endIndex != other.endIndex)
return false;
if (posOuter == null) {
if (other.posOuter != null)
return false;
}
else if (!posOuter.equals(other.posOuter))
return false;
if (startIndex != other.startIndex)
return false;
return true;
}
@Override
public int hashCode()
{
return startIndex % 31;
}
@Override
public String toString()
{
return "CaretPosMap [posOuter=" + posOuter + ", startIndex="
+ startIndex + ", endIndex=" + endIndex + "]";
}
}
p.public abstract boolean isOperator(String s);
p.public abstract boolean beginsOperator(char c);
p.public abstract boolean canBeUnary(String s);
p.public abstract INFIX newInfix(InteractionManager editor, SLOT slot, String initialContent, BracketedStructured<?, SLOT> wrapper, StructuredSlot.ModificationToken token, Character... closingChars);
private <T> T modificationReturn(FXFunction<StructuredSlot.ModificationToken, T> modificationAction)
{
if (slot != null)
return slot.modificationReturn(modificationAction);
else{ return StructuredSlot.testingModification(modificationAction);
}
}
protected <T> void modification(FXConsumer<StructuredSlot.ModificationToken> modificationAction)
{
if (slot != null)
slot.modificationReturn(t -> {modificationAction.accept(t);return 0;
});
else{ StructuredSlot.testingModification(t -> {modificationAction.accept(t);
} return 0;
});
}
@OnThread(Tag.FXPlatform)
protected <T> void modificationPlatform(FXPlatformConsumer<StructuredSlot.ModificationToken> modificationAction)
{
modification(modificationAction::accept);
}
@OnThread(Tag.FXPlatform)
private <T> T modificationReturnPlatform(FXPlatformFunction<StructuredSlot.ModificationToken, T> modificationAction)
{
return modificationReturn(modificationAction::apply);
}
String calculateText()
{
return Utility.interleave(
fields.stream().map(f -> f.getText()),
operators.stream().map(o -> o == null ? null : o.get()))
.filter(x -> x != null).collect(Collectors.joining());
}
| A simple class to allow a mutable integer to be passed around while building a CaretPosMap
|
static class IntCounter
{
public int counter = 0;
}
private static class OpPrec
{
final int prec;
final int levels;
OpPrec(int prec, int levels)
{
this.prec = prec;
this.levels = levels;
}
}
}
. InfixStructured
. positionCaret
. drawSelection
. calculateOverlayPos
. sceneToOverlayX
. sceneToOverlayY
. deselect
. backwardAtStart
. backwardAtStart
. forwardAtEnd
. forwardAtEnd
. home
. end
. selectHome
. selectEnd
. getLastField
. previousWord
. nextWord
. endOfNextWord
. selectAll
. selectNextWord
. selectPreviousWord
. cut
. moveTo
. getNearest
. selectTo
. selected
. setAnchorIfUnset
. selectBackward
. selectForward
. delete
. copy
. getCopyText
. getJavaCodeForFields
. getJavaCode
. deleteSelection
. deleteSelectionImpl
. deletePrevious
. deletePreviousAtPos
. if
. if
. deletePrevious_
. flattenCompound
. deleteNext
. deleteNextImpl
. getCurrentPos
. findField
. insert
. insertImpl
. insertAtPos
. insertChar
. if
. if
. isDisallowed
. isOpeningBracket
. isClosingBracket
. setEditable
. isNumericLiteral
. getEscapeStatus
. checkFieldChange
. checkFieldChange
. supportsFloatLiterals
. precedesPlusMinusInFloatingPointLiteral
. succeedsOpeningPlusMinusInFloatingPointLiteral
. focusAtStart
. focusAtEnd
. getStartPos
. getEndPos
. end
. getComponents
. isEmpty
. requestFocus
. withContent
. getNodeForPos
. isCurlyLiteral
. showHighlightedBrackets
. setView
. paste
. suggestingFor
. trySplitOn
. isAlmostBlank
. notifyLostFocus
. calculateEffort
. wrap
. equals
. hashCode
. toString
. isOperator
. beginsOperator
. canBeUnary
. newInfix
. modification
. modificationPlatform
top,
use,
map,
class IntCounter
top,
use,
map,
class OpPrec
2366 neLoCode
+ 670 LoComm