Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Updated remaining unchanged references to old application name. Added VariableTreeItem for common behaviours. Created test for speedy renaming of variable definitions is large texts.

Authordjarvis <email>
Date2016-11-25 21:43:21 GMT-0800
Commit2eff5eccbc8c0b85be5d4082e25195bc47603f6e
Parente11743b
README.md
------------
-[Download](https://github.com/DaveJarvis/scrivendor/releases/download/0.2/scrivendor.zip) the archive and extract it to any folder.
+[Download](https://github.com/DaveJarvis/scrivenvar/releases/download/0.4/scrivenvar.zip) the archive and extract it to any folder.
-Double-click `scrivendor.jar` to start the application.
+Double-click `scrivenvar.jar` to start the application.
If this does not work, try following command in a terminal window:
```
-java -jar scrivendor.jar
+java -jar scrivenvar.jar
```
[UndoFX]: https://github.com/TomasMikula/UndoFX
[FontAwesomeFX]: https://bitbucket.org/Jerady/fontawesomefx
+
Scrivenvar.jfdproj
<entry key="_i18nJavaSettingsEnabled" value="true" />
<entry key="_i18nSettingsEnabled" value="true" />
- <entry key="i18n.externalizeexcludes1" value="com.scrivendor.controls.EscapeTextField#escapeCharacters" />
- <entry key="i18n.externalizeexcludes2" value="com.scrivendor.controls.WebHyperlink#uri" />
+ <entry key="i18n.externalizeexcludes1" value="com.scrivenvar.controls.EscapeTextField#escapeCharacters" />
+ <entry key="i18n.externalizeexcludes2" value="com.scrivenvar.controls.WebHyperlink#uri" />
<entry key="i18n.javagetstringformat" value="Messages.get(${key})" />
</node>
build.gradle
apply plugin: 'application'
+sourceCompatibility = 1.8
+
mainClassName = 'com.scrivenvar.Main'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.4'
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.8.4'
+ compile group: 'junit', name: 'junit', version: '4.4'
}
-
-sourceCompatibility = 1.8
jar {
distributions {
main {
- baseName = 'scrivendor'
+ baseName = 'scrivenvar'
contents {
from { ['LICENSE', 'README.md'] }
src/main/java/com/scrivenvar/TestDefinitionPane.java
package com.scrivenvar;
-import static com.scrivenvar.Messages.get;
import com.scrivenvar.definition.DefinitionPane;
-import static com.scrivenvar.yaml.YamlTreeAdapter.adapt;
-import java.io.InputStream;
-import javafx.application.Application;
import static javafx.application.Application.launch;
-import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
-import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
-import org.fxmisc.flowless.VirtualizedScrollPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
/**
- * TestDefinitionPane application for debugging and head-banging.
+ * TestDefinitionPane application for debugging.
*/
-public final class TestDefinitionPane extends Application {
-
- private static Application app;
- private Scene scene;
-
- public static void main( String[] args ) {
- launch( args );
- }
-
+public final class TestDefinitionPane extends TestHarness {
/**
* Application entry point.
*
* @param stage The primary application stage.
*
* @throws Exception Could not read configuration file.
*/
@Override
public void start( final Stage stage ) throws Exception {
- initApplication();
- initScene();
- initStage( stage );
-
- TreeView<String> root = adapt(
- // TODO: Associate variable file with path to current file.
- asStream( "/com/scrivenvar/variables.yaml" ),
- get( "Pane.defintion.node.root.title" )
- );
+ super.start( stage );
- DefinitionPane pane = new DefinitionPane( root );
+ TreeView<String> root = createTreeView();
+ DefinitionPane pane = createDefinitionPane( root );
test( pane, "language.ai.", "article" );
System.out.println( "Path Node: " + node );
System.out.println( "Node Val : " + node.getValue() );
- }
-
- private void initApplication() {
- app = this;
- }
-
- private void initScene() {
- final StyleClassedTextArea editor = new StyleClassedTextArea( false );
- final VirtualizedScrollPane<StyleClassedTextArea> scrollPane = new VirtualizedScrollPane<>( editor );
-
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setCenter( scrollPane );
-
- setScene( new Scene( borderPane ) );
- }
-
- private void initStage( Stage stage ) {
- stage.setScene( getScene() );
- }
-
- private Scene getScene() {
- return this.scene;
- }
-
- private void setScene( Scene scene ) {
- this.scene = scene;
- }
-
- private static Application getApplication() {
- return app;
- }
-
- public static void showDocument( String uri ) {
- getApplication().getHostServices().showDocument( uri );
}
- private InputStream asStream( String resource ) {
- return getClass().getResourceAsStream( resource );
+ public static void main( String[] args ) {
+ launch( args );
}
}
src/main/java/com/scrivenvar/TestHarness.java
+/*
+ * Copyright 2016 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar;
+
+import static com.scrivenvar.Messages.get;
+import com.scrivenvar.definition.DefinitionPane;
+import static com.scrivenvar.yaml.YamlTreeAdapter.adapt;
+import java.io.IOException;
+import java.io.InputStream;
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.control.TreeView;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import org.fxmisc.flowless.VirtualizedScrollPane;
+import org.fxmisc.richtext.StyleClassedTextArea;
+
+/**
+ * TestDefinitionPane application for debugging and head-banging.
+ */
+public abstract class TestHarness extends Application {
+
+ private static Application app;
+ private Scene scene;
+
+ /**
+ * Application entry point.
+ *
+ * @param stage The primary application stage.
+ *
+ * @throws Exception Could not read configuration file.
+ */
+ @Override
+ public void start( final Stage stage ) throws Exception {
+ initApplication();
+ initScene();
+ initStage( stage );
+ }
+
+ protected TreeView<String> createTreeView() throws IOException {
+ return adapt(
+ asStream( "/com/scrivenvar/variables.yaml" ),
+ get( "Pane.defintion.node.root.title" )
+ );
+ }
+
+ protected DefinitionPane createDefinitionPane( TreeView<String> root ) {
+ return new DefinitionPane( root );
+ }
+
+ private void initApplication() {
+ app = this;
+ }
+
+ private void initScene() {
+ final StyleClassedTextArea editor = new StyleClassedTextArea( false );
+ final VirtualizedScrollPane<StyleClassedTextArea> scrollPane = new VirtualizedScrollPane<>( editor );
+
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setCenter( scrollPane );
+
+ setScene( new Scene( borderPane ) );
+ }
+
+ private void initStage( Stage stage ) {
+ stage.setScene( getScene() );
+ }
+
+ private Scene getScene() {
+ return this.scene;
+ }
+
+ private void setScene( Scene scene ) {
+ this.scene = scene;
+ }
+
+ private static Application getApplication() {
+ return app;
+ }
+
+ public static void showDocument( String uri ) {
+ getApplication().getHostServices().showDocument( uri );
+ }
+
+ protected InputStream asStream( String resource ) {
+ return getClass().getResourceAsStream( resource );
+ }
+}
src/main/java/com/scrivenvar/TestVariableNameProcessor.java
+/*
+ * Copyright 2016 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar;
+
+import com.scrivenvar.ui.VariableTreeItem;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import static java.util.concurrent.ThreadLocalRandom.current;
+import static javafx.application.Application.launch;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.stage.Stage;
+import static org.apache.commons.lang.RandomStringUtils.randomAscii;
+
+/**
+ * Tests substituting variable definitions with their values in a swath of text.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class TestVariableNameProcessor extends TestHarness {
+
+ private final static StringBuilder SOURCE
+ = new StringBuilder( randomAscii( 1000 ) );
+
+ public TestVariableNameProcessor() {
+ }
+
+ @Override
+ public void start( final Stage stage ) throws Exception {
+ super.start( stage );
+
+ final TreeView<String> root = createTreeView();
+ final LinkedHashMap<String, String> definitions = new LinkedHashMap<>();
+
+ populate( createTreeView().getRoot(), definitions );
+ injectVariables( definitions );
+
+ System.out.println( SOURCE );
+ System.exit( 0 );
+ }
+
+ private void injectVariables( final LinkedHashMap<String, String> definitions ) {
+ for( int i = 5; i > 0; i-- ) {
+ final int r = current().nextInt( 1, SOURCE.length() );
+ SOURCE.insert( r, randomKey( definitions ) );
+ }
+ }
+
+ private String randomKey( final LinkedHashMap<String, String> map ) {
+ final int r = current().nextInt( 1, map.size() - 1 );
+ return map.get( map.keySet().toArray()[ r ] );
+ }
+
+ private void populate( final TreeItem<String> parent, final Map<String, String> map ) {
+ for( final TreeItem<String> child : parent.getChildren() ) {
+ if( child.isLeaf() ) {
+ map.put(
+ asDefinition( ((VariableTreeItem<String>)child).toPath() ),
+ child.getValue() );
+ } else {
+ populate( child, map );
+ }
+ }
+ }
+
+ private String asDefinition( final String variable ) {
+ System.out.println( "VAR: " + variable );
+ return String.format( "$%s$", variable );
+ }
+
+ public static void main( String[] args ) {
+ launch( args );
+ }
+}
src/main/java/com/scrivenvar/definition/DefinitionPane.java
import static com.scrivenvar.definition.Lists.getFirst;
import com.scrivenvar.ui.AbstractPane;
+import com.scrivenvar.ui.VariableTreeItem;
import java.util.List;
-import java.util.Stack;
import javafx.collections.ObservableList;
import javafx.scene.Node;
* original value nor the terminally-trimmed value was found.
*/
- public TreeItem<String> findLeaf( final String value ) {
- final TreeItem<String> root = getTreeRoot();
- final TreeItem<String> leaf = findLeaf( root, value );
+ public VariableTreeItem<String> findLeaf( final String value ) {
+ final VariableTreeItem<String> root = getTreeRoot();
+ final VariableTreeItem<String> leaf = root.findLeaf( value );
return leaf == null
- ? findLeaf( root, rtrimTerminalPunctuation( value ) )
+ ? root.findLeaf( rtrimTerminalPunctuation( value ) )
: leaf;
- }
-
- /**
- * Finds a leaf starting at the current node with text that matches the given
- * value.
- *
- * @param root The node to search.
- * @param text The text to match against each leaf in the tree.
- *
- * @return The leaf that has a value starting with the given text.
- */
- public TreeItem<String> findLeaf(
- final TreeItem<String> root,
- final String text ) {
- final Stack<TreeItem<String>> stack = new Stack<>();
- boolean found = false;
- TreeItem<String> node = null;
-
- stack.push( root );
-
- while( !stack.isEmpty() && !found ) {
- node = stack.pop();
-
- if( node.isLeaf() && node.getValue().startsWith( text ) ) {
- found = true;
- } else {
- for( final TreeItem<String> child : node.getChildren() ) {
- stack.push( child );
- }
-
- // No match found, yet.
- node = null;
- }
- }
-
- return node;
}
* @return The first node added to the YAML definition tree.
*/
- private TreeItem<String> getTreeRoot() {
- return getTreeView().getRoot();
+ private VariableTreeItem<String> getTreeRoot() {
+ return (VariableTreeItem<String>)getTreeView().getRoot();
}
src/main/java/com/scrivenvar/editor/VariableNameInjector.java
import static com.scrivenvar.definition.Lists.getLast;
import com.scrivenvar.service.Settings;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import java.util.Stack;
-import java.util.function.Consumer;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.scene.control.IndexRange;
-import javafx.scene.control.TreeItem;
-import javafx.scene.input.InputEvent;
-import javafx.scene.input.KeyCode;
-import static javafx.scene.input.KeyCode.AT;
-import static javafx.scene.input.KeyCode.DIGIT2;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.MINUS;
-import static javafx.scene.input.KeyCode.SPACE;
-import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
-import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
-import javafx.scene.input.KeyEvent;
-import org.fxmisc.richtext.StyledTextArea;
-import org.fxmisc.wellbehaved.event.EventPattern;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import org.fxmisc.wellbehaved.event.InputMap;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static org.fxmisc.wellbehaved.event.InputMap.sequence;
-
-/**
- * Provides the logic for injecting variable names within the editor.
- *
- * @author White Magic Software, Ltd.
- */
-public class VariableNameInjector {
-
- private static final int NO_DIFFERENCE = -1;
-
- private final Settings settings = Services.load( Settings.class );
-
- /**
- * Used to capture keyboard events once the user presses @.
- */
- private InputMap<InputEvent> keyboardMap;
-
- private FileEditorPane fileEditorPane;
- private DefinitionPane definitionPane;
-
- /**
- * Position of the variable in the text when in variable mode.
- */
- private int initialCaretPosition = 0;
-
- public VariableNameInjector(
- final FileEditorPane editorPane,
- final DefinitionPane definitionPane ) {
- setFileEditorPane( editorPane );
- setDefinitionPane( definitionPane );
-
- initKeyboardEventListeners();
- }
-
- /**
- * Traps keys for performing various short-cut tasks, such as @-mode variable
- * insertion and control+space for variable autocomplete.
- *
- * @ key is pressed, a new keyboard map is inserted in place of the current
- * map -- this class goes into "variable edit mode" (a.k.a. vMode).
- *
- * @see createKeyboardMap()
- */
- private void initKeyboardEventListeners() {
- addEventListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete );
-
- // @ key in Linux?
- addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
- // @ key in Windows.
- addEventListener( keyPressed( AT ), this::vMode );
- }
-
- /**
- * The @ symbol is a short-cut to inserting a YAML variable reference.
- *
- * @param e Superfluous information about the key that was pressed.
- */
- private void vMode( KeyEvent e ) {
- setInitialCaretPosition();
- vModeStart();
- vModeAutocomplete();
- }
-
- /**
- * Receives key presses until the user completes the variable selection. This
- * allows the arrow keys to be used for selecting variables.
- *
- * @param e The key that was pressed.
- */
- private void vModeKeyPressed( KeyEvent e ) {
- final KeyCode keyCode = e.getCode();
-
- switch( keyCode ) {
- case BACK_SPACE:
- vModeBackspace();
- break;
-
- case ESCAPE:
- vModeStop();
- break;
-
- case ENTER:
- case PERIOD:
- case RIGHT:
- case END:
- // Stop at a leaf node, ENTER means accept.
- if( vModeConditionalComplete() && keyCode == ENTER ) {
- vModeStop();
- }
- break;
-
- case UP:
- cyclePathPrev();
- break;
-
- case DOWN:
- cyclePathNext();
- break;
-
- default:
- vModeFilterKeyPressed( e );
- break;
- }
-
- e.consume();
- }
-
- private void vModeBackspace() {
- deleteSelection();
-
- // Break out of variable mode by back spacing to the original position.
- if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
- vModeAutocomplete();
- } else {
- vModeStop();
- }
- }
-
- /**
- * Updates the text with the path selected (or typed) by the user.
- */
- private void vModeAutocomplete() {
- final TreeItem<String> node = getCurrentNode();
-
- if( !node.isLeaf() ) {
- final String word = getLastPathWord();
- final String label = node.getValue();
- final int delta = difference( label, word );
- final String remainder = delta == NO_DIFFERENCE
- ? label
- : label.substring( delta );
-
- final StyledTextArea textArea = getEditor();
- final int posBegan = getCurrentCaretPosition();
- final int posEnded = posBegan + remainder.length();
-
- textArea.replaceSelection( remainder );
-
- if( posEnded - posBegan > 0 ) {
- textArea.selectRange( posEnded, posBegan );
- }
-
- expand( node );
- }
- }
-
- /**
- * Only variable name keys can pass through the filter. This is called when
- * the user presses a key.
- *
- * @param e The key that was pressed.
- */
- private void vModeFilterKeyPressed( final KeyEvent e ) {
- if( isVariableNameKey( e ) ) {
- typed( e.getText() );
- }
- }
-
- /**
- * Performs an autocomplete depending on whether the user has finished typing
- * in a word. If there is a selected range, then this will complete the most
- * recent word and jump to the next child.
- *
- * @return true The auto-completed node was a terminal node.
- */
- private boolean vModeConditionalComplete() {
- acceptPath();
-
- final TreeItem<String> node = getCurrentNode();
- final boolean terminal = isTerminal( node );
-
- if( !terminal ) {
- typed( SEPARATOR );
- }
-
- return terminal;
- }
-
- /**
- * Pressing control+space will find a node that matches the current word and
- * substitute the YAML variable reference. This is called when the user is not
- * editing in vMode.
- *
- * @param e Ignored -- it can only be Ctrl+Space.
- */
- private void autocomplete( KeyEvent e ) {
- final int caretPos = getCurrentCaretColumn();
- final String paragraph = getCaretParagraph();
-
- final int[] boundaries = getWordBoundaries( paragraph, caretPos );
- final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
-
- final TreeItem<String> leaf = findLeaf( word );
-
- if( leaf != null ) {
- replaceText( boundaries[ 0 ], boundaries[ 1 ], toPath( leaf ) );
- expand( leaf );
- }
- }
-
- /**
- * Updates the text at the given position within the current paragraph.
- *
- * @param posBegan The starting index of the paragraph text to replace.
- * @param posEnded The ending index of the paragraph text to replace.
- * @param text Overwrite the paragraph substring with this text.
- */
- private void replaceText(
- final int posBegan, final int posEnded, final String text ) {
- final StyledTextArea textArea = getEditor();
- final int p = textArea.getCurrentParagraph();
- textArea.replaceText( p, posBegan, p, posEnded, text );
- }
-
- /**
- * Returns the path for a node, with nodes made distinct using the separator
- * character. This is the antithesis of the findExactNode method. This uses
- * two loops: one for pushing nodes onto a stack and one for popping them
- * off to create the path in desired order.
- *
- * @param node The tree item to path into a string, must not be null.
- *
- * @return A non-null string, possibly empty.
- */
- public String toPath( TreeItem<String> node ) {
- final Stack<TreeItem<String>> stack = new Stack<>();
-
- while( node.getParent() != null ) {
- stack.push( node );
- node = node.getParent();
- }
-
- final StringBuilder sb = new StringBuilder( getMaxVarLength() );
-
- while( !stack.isEmpty() ) {
- node = stack.pop();
-
- if( !node.isLeaf() ) {
- sb.append( node.getValue() );
-
- // This will add a superfluous separator, but instead of peeking at
- // the stack all the time, the last separator will be removed outside
- // the loop.
- sb.append( SEPARATOR );
- }
- }
-
- // Remove the trailing SEPARATOR.
- if( sb.length() > 0 ) {
- sb.setLength( sb.length() - 1 );
- }
-
- return sb.toString();
- }
-
- /**
- * Returns current word boundary indexes into the current paragraph, including
- * punctuation.
- *
- * @param paragraph The paragraph
- *
- * @return The starting and ending index of the word closest to the caret.
- */
- private int[] getWordBoundaries( String paragraph, final int offset ) {
- // Remove dashes, but retain hyphens. Retain same number of characters
- // to preserve relative indexes.
- paragraph = paragraph.replace( "---", " " ).replace( "--", " " );
-
- final int posBegan = getWordBegan( paragraph, offset );
- final int posEnded = getWordEnded( paragraph, offset );
-
- return new int[]{ posBegan, posEnded };
- }
-
- /**
- * Given an arbitrary offset into a string, this returns the word at that
- * index. The inputs and outputs include:
- *
- * <ul>
- * <li>surrounded by space: <code>hello | world!</code> ("");</li>
- * <li>end of word: <code>hello| world!</code> ("hello");</li>
- * <li>start of a word: <code>hello |world!</code> ("world!");</li>
- * <li>within a word: <code>hello wo|rld!</code> ("world!");</li>
- * <li>end of a paragraph: <code>hello world!|</code> ("world!");</li>
- * <li>start of a paragraph: <code>|hello world!</code> ("hello!"); or</li>
- * <li>after punctuation: <code>hello world!|</code> ("world!").</li>
- * </ul>
- *
- * @param s The string to scan for a word.
- * @param offset The offset within s to begin searching for the nearest word
- * boundary, must not be out of bounds of s.
- *
- * @return The word in s at the offset.
- *
- * @see getWordBegan( String, int )
- * @see getWordEnded( String, int )
- */
- private String getWordAt( final String s, final int offset ) {
- final int posBegan = getWordBegan( s, offset );
- final int posEnded = getWordEnded( s, offset );
-
- return s.substring( posBegan, posEnded );
- }
-
- /**
- * Returns the index into s where a word begins.
- *
- * @param s Never null.
- * @param offset Index into s to begin searching backwards for a word
- * boundary.
- *
- * @return The index where a word begins.
- */
- private int getWordBegan( final String s, int offset ) {
- while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
- offset--;
- }
-
- return offset;
- }
-
- /**
- * Returns the index into s where a word ends.
- *
- * @param s Never null.
- * @param offset Index into s to begin searching forwards for a word boundary.
- *
- * @return The index where a word ends.
- */
- private int getWordEnded( final String s, int offset ) {
- final int length = s.length();
-
- while( offset < length && isBoundary( s.charAt( offset ) ) ) {
- offset++;
- }
-
- return offset;
- }
-
- /**
- * Returns true if the given character can be reasonably expected to be part
- * of a word, including punctuation marks.
- *
- * @param c The character to compare.
- *
- * @return false The character is a space character.
- */
- private boolean isBoundary( final char c ) {
- return !isSpaceChar( c );
- }
-
- /**
- * Returns the text for the paragraph that contains the caret.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCaretParagraph() {
- final StyledTextArea textArea = getEditor();
- return textArea.getText( textArea.getCurrentParagraph() );
- }
-
- /**
- * Returns true if the node has children that can be selected (i.e., any
- * non-leaves).
- *
- * @param <T> The type that the TreeItem contains.
- * @param node The node to test for terminality.
- *
- * @return true The node has one branch and its a leaf.
- */
- private <T> boolean isTerminal( final TreeItem<T> node ) {
- final ObservableList<TreeItem<T>> branches = node.getChildren();
-
- return branches.size() == 1 && branches.get( 0 ).isLeaf();
- }
-
- /**
- * Inserts text that the user typed at the current caret position, then
- * performs an autocomplete for the variable name.
- *
- * @param text The text to insert, never null.
- */
- private void typed( final String text ) {
- getEditor().replaceSelection( text );
- vModeAutocomplete();
- }
-
- /**
- * Called when the user presses either End or Enter key.
- */
- private void acceptPath() {
- final IndexRange range = getSelectionRange();
-
- if( range != null ) {
- final int rangeEnd = range.getEnd();
- final StyledTextArea textArea = getEditor();
- textArea.deselect();
- textArea.moveTo( rangeEnd );
- }
- }
-
- /**
- * Replaces the entirety of the existing path (from the initial caret
- * position) with the given path.
- *
- * @param oldPath The path to replace.
- * @param newPath The replacement path.
- */
- private void replacePath( final String oldPath, final String newPath ) {
- final StyledTextArea textArea = getEditor();
- final int posBegan = getInitialCaretPosition();
- final int posEnded = posBegan + oldPath.length();
-
- textArea.deselect();
- textArea.replaceText( posBegan, posEnded, newPath );
- }
-
- /**
- * Called when the user presses the Backspace key.
- */
- private void deleteSelection() {
- final StyledTextArea textArea = getEditor();
- textArea.replaceSelection( "" );
- textArea.deletePreviousChar();
- }
-
- /**
- * Cycles the selected text through the nodes.
- *
- * @param direction true - next; false - previous
- */
- private void cycleSelection( final boolean direction ) {
- final TreeItem<String> node = getCurrentNode();
-
- // Find the sibling for the current selection and replace the current
- // selection with the sibling's value
- TreeItem< String> cycled = direction
- ? node.nextSibling()
- : node.previousSibling();
-
- // When cycling at the end (or beginning) of the list, jump to the first
- // (or last) sibling depending on the cycle direction.
- if( cycled == null ) {
- cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
- }
-
- final String path = getCurrentPath();
- final String cycledWord = cycled.getValue();
- final String word = getLastPathWord();
- final int index = path.indexOf( word );
- final String cycledPath = path.substring( 0, index ) + cycledWord;
-
- expand( cycled );
- replacePath( path, cycledPath );
- }
-
- /**
- * Cycles to the next sibling of the currently selected tree node.
- */
- private void cyclePathNext() {
- cycleSelection( true );
- }
-
- /**
- * Cycles to the previous sibling of the currently selected tree node.
- */
- private void cyclePathPrev() {
- cycleSelection( false );
- }
-
- /**
- * Returns all the characters from the initial caret column to the the first
- * whitespace character. This will return a path that contains zero or more
- * separators.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCurrentPath() {
- final String s = extractTextChunk();
- final int length = s.length();
-
- int i = 0;
-
- while( i < length && !isWhitespace( s.charAt( i ) ) ) {
- i++;
- }
-
- return s.substring( 0, i );
- }
-
- private <T> ObservableList<TreeItem<T>> getSiblings(
- final TreeItem<T> item ) {
- final TreeItem<T> parent = item.getParent();
- return parent == null ? item.getChildren() : parent.getChildren();
- }
-
- private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
- return getFirst( getSiblings( item ), item );
- }
-
- private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
- return getLast( getSiblings( item ), item );
- }
-
- /**
- * Returns the caret position as an offset into the text.
- *
- * @return A value from 0 to the length of the text (minus one).
- */
- private int getCurrentCaretPosition() {
- return getEditor().getCaretPosition();
- }
-
- /**
- * Returns the caret position within the current paragraph.
- *
- * @return A value from 0 to the length of the current paragraph.
- */
- private int getCurrentCaretColumn() {
- return getEditor().getCaretColumn();
- }
-
- /**
- * Returns the last word from the path.
- *
- * @return The last token.
- */
- private String getLastPathWord() {
- String path = getCurrentPath();
-
- int i = path.indexOf( SEPARATOR );
-
- while( i > 0 ) {
- path = path.substring( i + 1 );
- i = path.indexOf( SEPARATOR );
- }
-
- return path;
- }
-
- /**
- * Returns text from the initial caret position until some arbitrarily long
- * number of characters. The number of characters extracted will be
- * getMaxVarLength, or fewer, depending on how many characters remain to be
- * extracted. The result from this method is trimmed to the first whitespace
- * character.
- *
- * @return A chunk of text that includes all the words representing a path,
- * and then some.
- */
- private String extractTextChunk() {
- final StyledTextArea textArea = getEditor();
- final int textBegan = getInitialCaretPosition();
- final int remaining = textArea.getLength() - textBegan;
- final int textEnded = min( remaining, getMaxVarLength() );
-
- return textArea.getText( textBegan, textEnded );
- }
-
- /**
- * Returns the node for the current path.
- */
- private TreeItem<String> getCurrentNode() {
- return findNode( getCurrentPath() );
- }
-
- /**
- * Finds the node that most closely matches the given path.
- *
- * @param path The path that represents a node.
- *
- * @return The node for the path, or the root node if the path could not be
- * found, but never null.
- */
- private TreeItem<String> findNode( final String path ) {
- return getDefinitionPane().findNode( path );
- }
-
- /**
- * Finds the first leaf having a value that starts with the given text.
- *
- * @param text The text to find in the definition tree.
- *
- * @return The leaf that starts with the given text, or null if not found.
- */
- private TreeItem<String> findLeaf( final String text ) {
- return getDefinitionPane().findLeaf( text );
- }
-
- /**
- * Used to ignore typed keys in favour of trapping pressed keys.
- *
- * @param e The key that was typed.
- */
- private void vModeKeyTyped( KeyEvent e ) {
- e.consume();
- }
-
- /**
- * Used to lazily initialize the keyboard map.
- *
- * @return Mappings for keyTyped and keyPressed.
- */
- protected InputMap<InputEvent> createKeyboardMap() {
- return sequence(
- consume( keyTyped(), this::vModeKeyTyped ),
- consume( keyPressed(), this::vModeKeyPressed )
- );
- }
-
- private InputMap<InputEvent> getKeyboardMap() {
- if( this.keyboardMap == null ) {
- this.keyboardMap = createKeyboardMap();
- }
-
- return this.keyboardMap;
- }
-
- /**
- * Collapses the tree then expands and selects the given node.
- *
- * @param node The node to expand.
- */
- private void expand( final TreeItem<String> node ) {
- final DefinitionPane pane = getDefinitionPane();
- pane.collapse();
- pane.expand( node );
- pane.select( node );
- }
-
- /**
- * Returns true iff the key code the user typed can be used as part of a YAML
- * variable name.
- *
- * @param keyEvent Keyboard key press event information.
- *
- * @return true The key is a value that can be inserted into the text.
- */
- private boolean isVariableNameKey( final KeyEvent keyEvent ) {
- final KeyCode kc = keyEvent.getCode();
-
- return (kc.isLetterKey()
- || kc.isDigitKey()
- || (keyEvent.isShiftDown() && kc == MINUS))
- && !keyEvent.isControlDown();
- }
-
- /**
- * Starts to capture user input events.
- */
- private void vModeStart() {
- addEventListener( getKeyboardMap() );
- }
-
- /**
- * Restores capturing of user input events to the previous event listener.
- */
- private void vModeStop() {
- removeEventListener( getKeyboardMap() );
- }
-
- /**
- * Returns the index where the two strings diverge.
- *
- * @param s1 The string that could be a substring of s2, null allowed.
- * @param s2 The string that could be a substring of s1, null allowed.
- *
- * @return NO_DIFFERENCE if the strings are the same, otherwise the index
- * where they differ.
- */
- @SuppressWarnings( "StringEquality" )
- private int difference( final CharSequence s1, final CharSequence s2 ) {
- if( s1 == s2 ) {
- return NO_DIFFERENCE;
- }
-
- if( s1 == null || s2 == null ) {
- return 0;
- }
-
- int i = 0;
- final int limit = min( s1.length(), s2.length() );
-
- while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
- i++;
- }
-
- // If one string was shorter than the other, that's where they differ.
- return i;
- }
-
- /**
- * Delegates to the file editor pane, and, ultimately, to its text area.
- */
- private <T extends Event, U extends T> void addEventListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- getFileEditorPane().addEventListener( event, consumer );
- }
-
- /**
- * Delegates to the file editor pane, and, ultimately, to its text area.
- *
- * @param map The map of methods to events.
- */
- private void addEventListener( final InputMap<InputEvent> map ) {
- getFileEditorPane().addEventListener( map );
- }
-
- private void removeEventListener( final InputMap<InputEvent> map ) {
- getFileEditorPane().removeEventListener( map );
- }
-
- /**
- * Returns the position of the caret when variable mode editing was requested.
- *
- * @return The variable mode caret position.
- */
- private int getInitialCaretPosition() {
- return this.initialCaretPosition;
- }
-
- /**
- * Sets the position of the caret when variable mode editing was requested.
- * Stores the current position because only the text that comes afterwards is
- * a suitable variable reference.
- *
- * @return The variable mode caret position.
- */
- private void setInitialCaretPosition() {
- this.initialCaretPosition = getEditor().getCaretPosition();
- }
-
- private StyledTextArea getEditor() {
- return getFileEditorPane().getEditor();
- }
-
- public FileEditorPane getFileEditorPane() {
- return this.fileEditorPane;
- }
-
- private void setFileEditorPane( final FileEditorPane fileEditorPane ) {
- this.fileEditorPane = fileEditorPane;
- }
-
- private DefinitionPane getDefinitionPane() {
- return this.definitionPane;
- }
-
- private void setDefinitionPane( final DefinitionPane definitionPane ) {
- this.definitionPane = definitionPane;
- }
-
- private IndexRange getSelectionRange() {
- return getEditor().getSelection();
- }
-
- /**
- * Don't look ahead too far when trying to find the end of a node.
- *
- * @return 512 by default.
- */
- private int getMaxVarLength() {
- return getSettings().getSetting( "editor.variable.maxLength", 512 );
- }
-
+import com.scrivenvar.ui.VariableTreeItem;
+import static java.lang.Character.isSpaceChar;
+import static java.lang.Character.isWhitespace;
+import static java.lang.Math.min;
+import java.util.function.Consumer;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.scene.control.IndexRange;
+import javafx.scene.control.TreeItem;
+import javafx.scene.input.InputEvent;
+import javafx.scene.input.KeyCode;
+import static javafx.scene.input.KeyCode.AT;
+import static javafx.scene.input.KeyCode.DIGIT2;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.MINUS;
+import static javafx.scene.input.KeyCode.SPACE;
+import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
+import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
+import javafx.scene.input.KeyEvent;
+import org.fxmisc.richtext.StyledTextArea;
+import org.fxmisc.wellbehaved.event.EventPattern;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
+import org.fxmisc.wellbehaved.event.InputMap;
+import static org.fxmisc.wellbehaved.event.InputMap.consume;
+import static org.fxmisc.wellbehaved.event.InputMap.sequence;
+
+/**
+ * Provides the logic for injecting variable names within the editor.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class VariableNameInjector {
+
+ public static final int DEFAULT_MAX_VAR_LENGTH = 64;
+
+ private static final int NO_DIFFERENCE = -1;
+
+ private final Settings settings = Services.load( Settings.class );
+
+ /**
+ * Used to capture keyboard events once the user presses @.
+ */
+ private InputMap<InputEvent> keyboardMap;
+
+ private FileEditorPane fileEditorPane;
+ private DefinitionPane definitionPane;
+
+ /**
+ * Position of the variable in the text when in variable mode.
+ */
+ private int initialCaretPosition = 0;
+
+ public VariableNameInjector(
+ final FileEditorPane editorPane,
+ final DefinitionPane definitionPane ) {
+ setFileEditorPane( editorPane );
+ setDefinitionPane( definitionPane );
+
+ initKeyboardEventListeners();
+ }
+
+ /**
+ * Traps keys for performing various short-cut tasks, such as @-mode variable
+ * insertion and control+space for variable autocomplete.
+ *
+ * @ key is pressed, a new keyboard map is inserted in place of the current
+ * map -- this class goes into "variable edit mode" (a.k.a. vMode).
+ *
+ * @see createKeyboardMap()
+ */
+ private void initKeyboardEventListeners() {
+ addEventListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete );
+
+ // @ key in Linux?
+ addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
+ // @ key in Windows.
+ addEventListener( keyPressed( AT ), this::vMode );
+ }
+
+ /**
+ * The @ symbol is a short-cut to inserting a YAML variable reference.
+ *
+ * @param e Superfluous information about the key that was pressed.
+ */
+ private void vMode( KeyEvent e ) {
+ setInitialCaretPosition();
+ vModeStart();
+ vModeAutocomplete();
+ }
+
+ /**
+ * Receives key presses until the user completes the variable selection. This
+ * allows the arrow keys to be used for selecting variables.
+ *
+ * @param e The key that was pressed.
+ */
+ private void vModeKeyPressed( KeyEvent e ) {
+ final KeyCode keyCode = e.getCode();
+
+ switch( keyCode ) {
+ case BACK_SPACE:
+ vModeBackspace();
+ break;
+
+ case ESCAPE:
+ vModeStop();
+ break;
+
+ case ENTER:
+ case PERIOD:
+ case RIGHT:
+ case END:
+ // Stop at a leaf node, ENTER means accept.
+ if( vModeConditionalComplete() && keyCode == ENTER ) {
+ vModeStop();
+ }
+ break;
+
+ case UP:
+ cyclePathPrev();
+ break;
+
+ case DOWN:
+ cyclePathNext();
+ break;
+
+ default:
+ vModeFilterKeyPressed( e );
+ break;
+ }
+
+ e.consume();
+ }
+
+ private void vModeBackspace() {
+ deleteSelection();
+
+ // Break out of variable mode by back spacing to the original position.
+ if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
+ vModeAutocomplete();
+ } else {
+ vModeStop();
+ }
+ }
+
+ /**
+ * Updates the text with the path selected (or typed) by the user.
+ */
+ private void vModeAutocomplete() {
+ final TreeItem<String> node = getCurrentNode();
+
+ if( !node.isLeaf() ) {
+ final String word = getLastPathWord();
+ final String label = node.getValue();
+ final int delta = difference( label, word );
+ final String remainder = delta == NO_DIFFERENCE
+ ? label
+ : label.substring( delta );
+
+ final StyledTextArea textArea = getEditor();
+ final int posBegan = getCurrentCaretPosition();
+ final int posEnded = posBegan + remainder.length();
+
+ textArea.replaceSelection( remainder );
+
+ if( posEnded - posBegan > 0 ) {
+ textArea.selectRange( posEnded, posBegan );
+ }
+
+ expand( node );
+ }
+ }
+
+ /**
+ * Only variable name keys can pass through the filter. This is called when
+ * the user presses a key.
+ *
+ * @param e The key that was pressed.
+ */
+ private void vModeFilterKeyPressed( final KeyEvent e ) {
+ if( isVariableNameKey( e ) ) {
+ typed( e.getText() );
+ }
+ }
+
+ /**
+ * Performs an autocomplete depending on whether the user has finished typing
+ * in a word. If there is a selected range, then this will complete the most
+ * recent word and jump to the next child.
+ *
+ * @return true The auto-completed node was a terminal node.
+ */
+ private boolean vModeConditionalComplete() {
+ acceptPath();
+
+ final TreeItem<String> node = getCurrentNode();
+ final boolean terminal = isTerminal( node );
+
+ if( !terminal ) {
+ typed( SEPARATOR );
+ }
+
+ return terminal;
+ }
+
+ /**
+ * Pressing control+space will find a node that matches the current word and
+ * substitute the YAML variable reference. This is called when the user is not
+ * editing in vMode.
+ *
+ * @param e Ignored -- it can only be Ctrl+Space.
+ */
+ private void autocomplete( KeyEvent e ) {
+ final int caretPos = getCurrentCaretColumn();
+ final String paragraph = getCaretParagraph();
+
+ final int[] boundaries = getWordBoundaries( paragraph, caretPos );
+ final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
+
+ final VariableTreeItem<String> leaf = findLeaf( word );
+
+ if( leaf != null ) {
+ replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
+ expand( leaf );
+ }
+ }
+
+ /**
+ * Updates the text at the given position within the current paragraph.
+ *
+ * @param posBegan The starting index of the paragraph text to replace.
+ * @param posEnded The ending index of the paragraph text to replace.
+ * @param text Overwrite the paragraph substring with this text.
+ */
+ private void replaceText(
+ final int posBegan, final int posEnded, final String text ) {
+ final StyledTextArea textArea = getEditor();
+ final int p = textArea.getCurrentParagraph();
+ textArea.replaceText( p, posBegan, p, posEnded, text );
+ }
+
+ /**
+ * Returns current word boundary indexes into the current paragraph, including
+ * punctuation.
+ *
+ * @param paragraph The paragraph
+ *
+ * @return The starting and ending index of the word closest to the caret.
+ */
+ private int[] getWordBoundaries( String paragraph, final int offset ) {
+ // Remove dashes, but retain hyphens. Retain same number of characters
+ // to preserve relative indexes.
+ paragraph = paragraph.replace( "---", " " ).replace( "--", " " );
+
+ final int posBegan = getWordBegan( paragraph, offset );
+ final int posEnded = getWordEnded( paragraph, offset );
+
+ return new int[]{ posBegan, posEnded };
+ }
+
+ /**
+ * Given an arbitrary offset into a string, this returns the word at that
+ * index. The inputs and outputs include:
+ *
+ * <ul>
+ * <li>surrounded by space: <code>hello | world!</code> ("");</li>
+ * <li>end of word: <code>hello| world!</code> ("hello");</li>
+ * <li>start of a word: <code>hello |world!</code> ("world!");</li>
+ * <li>within a word: <code>hello wo|rld!</code> ("world!");</li>
+ * <li>end of a paragraph: <code>hello world!|</code> ("world!");</li>
+ * <li>start of a paragraph: <code>|hello world!</code> ("hello!"); or</li>
+ * <li>after punctuation: <code>hello world!|</code> ("world!").</li>
+ * </ul>
+ *
+ * @param s The string to scan for a word.
+ * @param offset The offset within s to begin searching for the nearest word
+ * boundary, must not be out of bounds of s.
+ *
+ * @return The word in s at the offset.
+ *
+ * @see getWordBegan( String, int )
+ * @see getWordEnded( String, int )
+ */
+ private String getWordAt( final String s, final int offset ) {
+ final int posBegan = getWordBegan( s, offset );
+ final int posEnded = getWordEnded( s, offset );
+
+ return s.substring( posBegan, posEnded );
+ }
+
+ /**
+ * Returns the index into s where a word begins.
+ *
+ * @param s Never null.
+ * @param offset Index into s to begin searching backwards for a word
+ * boundary.
+ *
+ * @return The index where a word begins.
+ */
+ private int getWordBegan( final String s, int offset ) {
+ while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
+ offset--;
+ }
+
+ return offset;
+ }
+
+ /**
+ * Returns the index into s where a word ends.
+ *
+ * @param s Never null.
+ * @param offset Index into s to begin searching forwards for a word boundary.
+ *
+ * @return The index where a word ends.
+ */
+ private int getWordEnded( final String s, int offset ) {
+ final int length = s.length();
+
+ while( offset < length && isBoundary( s.charAt( offset ) ) ) {
+ offset++;
+ }
+
+ return offset;
+ }
+
+ /**
+ * Returns true if the given character can be reasonably expected to be part
+ * of a word, including punctuation marks.
+ *
+ * @param c The character to compare.
+ *
+ * @return false The character is a space character.
+ */
+ private boolean isBoundary( final char c ) {
+ return !isSpaceChar( c );
+ }
+
+ /**
+ * Returns the text for the paragraph that contains the caret.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getCaretParagraph() {
+ final StyledTextArea textArea = getEditor();
+ return textArea.getText( textArea.getCurrentParagraph() );
+ }
+
+ /**
+ * Returns true if the node has children that can be selected (i.e., any
+ * non-leaves).
+ *
+ * @param <T> The type that the TreeItem contains.
+ * @param node The node to test for terminality.
+ *
+ * @return true The node has one branch and its a leaf.
+ */
+ private <T> boolean isTerminal( final TreeItem<T> node ) {
+ final ObservableList<TreeItem<T>> branches = node.getChildren();
+
+ return branches.size() == 1 && branches.get( 0 ).isLeaf();
+ }
+
+ /**
+ * Inserts text that the user typed at the current caret position, then
+ * performs an autocomplete for the variable name.
+ *
+ * @param text The text to insert, never null.
+ */
+ private void typed( final String text ) {
+ getEditor().replaceSelection( text );
+ vModeAutocomplete();
+ }
+
+ /**
+ * Called when the user presses either End or Enter key.
+ */
+ private void acceptPath() {
+ final IndexRange range = getSelectionRange();
+
+ if( range != null ) {
+ final int rangeEnd = range.getEnd();
+ final StyledTextArea textArea = getEditor();
+ textArea.deselect();
+ textArea.moveTo( rangeEnd );
+ }
+ }
+
+ /**
+ * Replaces the entirety of the existing path (from the initial caret
+ * position) with the given path.
+ *
+ * @param oldPath The path to replace.
+ * @param newPath The replacement path.
+ */
+ private void replacePath( final String oldPath, final String newPath ) {
+ final StyledTextArea textArea = getEditor();
+ final int posBegan = getInitialCaretPosition();
+ final int posEnded = posBegan + oldPath.length();
+
+ textArea.deselect();
+ textArea.replaceText( posBegan, posEnded, newPath );
+ }
+
+ /**
+ * Called when the user presses the Backspace key.
+ */
+ private void deleteSelection() {
+ final StyledTextArea textArea = getEditor();
+ textArea.replaceSelection( "" );
+ textArea.deletePreviousChar();
+ }
+
+ /**
+ * Cycles the selected text through the nodes.
+ *
+ * @param direction true - next; false - previous
+ */
+ private void cycleSelection( final boolean direction ) {
+ final TreeItem<String> node = getCurrentNode();
+
+ // Find the sibling for the current selection and replace the current
+ // selection with the sibling's value
+ TreeItem< String> cycled = direction
+ ? node.nextSibling()
+ : node.previousSibling();
+
+ // When cycling at the end (or beginning) of the list, jump to the first
+ // (or last) sibling depending on the cycle direction.
+ if( cycled == null ) {
+ cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
+ }
+
+ final String path = getCurrentPath();
+ final String cycledWord = cycled.getValue();
+ final String word = getLastPathWord();
+ final int index = path.indexOf( word );
+ final String cycledPath = path.substring( 0, index ) + cycledWord;
+
+ expand( cycled );
+ replacePath( path, cycledPath );
+ }
+
+ /**
+ * Cycles to the next sibling of the currently selected tree node.
+ */
+ private void cyclePathNext() {
+ cycleSelection( true );
+ }
+
+ /**
+ * Cycles to the previous sibling of the currently selected tree node.
+ */
+ private void cyclePathPrev() {
+ cycleSelection( false );
+ }
+
+ /**
+ * Returns all the characters from the initial caret column to the the first
+ * whitespace character. This will return a path that contains zero or more
+ * separators.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getCurrentPath() {
+ final String s = extractTextChunk();
+ final int length = s.length();
+
+ int i = 0;
+
+ while( i < length && !isWhitespace( s.charAt( i ) ) ) {
+ i++;
+ }
+
+ return s.substring( 0, i );
+ }
+
+ private <T> ObservableList<TreeItem<T>> getSiblings(
+ final TreeItem<T> item ) {
+ final TreeItem<T> parent = item.getParent();
+ return parent == null ? item.getChildren() : parent.getChildren();
+ }
+
+ private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
+ return getFirst( getSiblings( item ), item );
+ }
+
+ private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
+ return getLast( getSiblings( item ), item );
+ }
+
+ /**
+ * Returns the caret position as an offset into the text.
+ *
+ * @return A value from 0 to the length of the text (minus one).
+ */
+ private int getCurrentCaretPosition() {
+ return getEditor().getCaretPosition();
+ }
+
+ /**
+ * Returns the caret position within the current paragraph.
+ *
+ * @return A value from 0 to the length of the current paragraph.
+ */
+ private int getCurrentCaretColumn() {
+ return getEditor().getCaretColumn();
+ }
+
+ /**
+ * Returns the last word from the path.
+ *
+ * @return The last token.
+ */
+ private String getLastPathWord() {
+ String path = getCurrentPath();
+
+ int i = path.indexOf( SEPARATOR );
+
+ while( i > 0 ) {
+ path = path.substring( i + 1 );
+ i = path.indexOf( SEPARATOR );
+ }
+
+ return path;
+ }
+
+ /**
+ * Returns text from the initial caret position until some arbitrarily long
+ * number of characters. The number of characters extracted will be
+ * getMaxVarLength, or fewer, depending on how many characters remain to be
+ * extracted. The result from this method is trimmed to the first whitespace
+ * character.
+ *
+ * @return A chunk of text that includes all the words representing a path,
+ * and then some.
+ */
+ private String extractTextChunk() {
+ final StyledTextArea textArea = getEditor();
+ final int textBegan = getInitialCaretPosition();
+ final int remaining = textArea.getLength() - textBegan;
+ final int textEnded = min( remaining, getMaxVarLength() );
+
+ return textArea.getText( textBegan, textEnded );
+ }
+
+ /**
+ * Returns the node for the current path.
+ */
+ private TreeItem<String> getCurrentNode() {
+ return findNode( getCurrentPath() );
+ }
+
+ /**
+ * Finds the node that most closely matches the given path.
+ *
+ * @param path The path that represents a node.
+ *
+ * @return The node for the path, or the root node if the path could not be
+ * found, but never null.
+ */
+ private TreeItem<String> findNode( final String path ) {
+ return getDefinitionPane().findNode( path );
+ }
+
+ /**
+ * Finds the first leaf having a value that starts with the given text.
+ *
+ * @param text The text to find in the definition tree.
+ *
+ * @return The leaf that starts with the given text, or null if not found.
+ */
+ private VariableTreeItem<String> findLeaf( final String text ) {
+ return getDefinitionPane().findLeaf( text );
+ }
+
+ /**
+ * Used to ignore typed keys in favour of trapping pressed keys.
+ *
+ * @param e The key that was typed.
+ */
+ private void vModeKeyTyped( KeyEvent e ) {
+ e.consume();
+ }
+
+ /**
+ * Used to lazily initialize the keyboard map.
+ *
+ * @return Mappings for keyTyped and keyPressed.
+ */
+ protected InputMap<InputEvent> createKeyboardMap() {
+ return sequence(
+ consume( keyTyped(), this::vModeKeyTyped ),
+ consume( keyPressed(), this::vModeKeyPressed )
+ );
+ }
+
+ private InputMap<InputEvent> getKeyboardMap() {
+ if( this.keyboardMap == null ) {
+ this.keyboardMap = createKeyboardMap();
+ }
+
+ return this.keyboardMap;
+ }
+
+ /**
+ * Collapses the tree then expands and selects the given node.
+ *
+ * @param node The node to expand.
+ */
+ private void expand( final TreeItem<String> node ) {
+ final DefinitionPane pane = getDefinitionPane();
+ pane.collapse();
+ pane.expand( node );
+ pane.select( node );
+ }
+
+ /**
+ * Returns true iff the key code the user typed can be used as part of a YAML
+ * variable name.
+ *
+ * @param keyEvent Keyboard key press event information.
+ *
+ * @return true The key is a value that can be inserted into the text.
+ */
+ private boolean isVariableNameKey( final KeyEvent keyEvent ) {
+ final KeyCode kc = keyEvent.getCode();
+
+ return (kc.isLetterKey()
+ || kc.isDigitKey()
+ || (keyEvent.isShiftDown() && kc == MINUS))
+ && !keyEvent.isControlDown();
+ }
+
+ /**
+ * Starts to capture user input events.
+ */
+ private void vModeStart() {
+ addEventListener( getKeyboardMap() );
+ }
+
+ /**
+ * Restores capturing of user input events to the previous event listener.
+ */
+ private void vModeStop() {
+ removeEventListener( getKeyboardMap() );
+ }
+
+ /**
+ * Returns the index where the two strings diverge.
+ *
+ * @param s1 The string that could be a substring of s2, null allowed.
+ * @param s2 The string that could be a substring of s1, null allowed.
+ *
+ * @return NO_DIFFERENCE if the strings are the same, otherwise the index
+ * where they differ.
+ */
+ @SuppressWarnings( "StringEquality" )
+ private int difference( final CharSequence s1, final CharSequence s2 ) {
+ if( s1 == s2 ) {
+ return NO_DIFFERENCE;
+ }
+
+ if( s1 == null || s2 == null ) {
+ return 0;
+ }
+
+ int i = 0;
+ final int limit = min( s1.length(), s2.length() );
+
+ while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
+ i++;
+ }
+
+ // If one string was shorter than the other, that's where they differ.
+ return i;
+ }
+
+ /**
+ * Delegates to the file editor pane, and, ultimately, to its text area.
+ */
+ private <T extends Event, U extends T> void addEventListener(
+ final EventPattern<? super T, ? extends U> event,
+ final Consumer<? super U> consumer ) {
+ getFileEditorPane().addEventListener( event, consumer );
+ }
+
+ /**
+ * Delegates to the file editor pane, and, ultimately, to its text area.
+ *
+ * @param map The map of methods to events.
+ */
+ private void addEventListener( final InputMap<InputEvent> map ) {
+ getFileEditorPane().addEventListener( map );
+ }
+
+ private void removeEventListener( final InputMap<InputEvent> map ) {
+ getFileEditorPane().removeEventListener( map );
+ }
+
+ /**
+ * Returns the position of the caret when variable mode editing was requested.
+ *
+ * @return The variable mode caret position.
+ */
+ private int getInitialCaretPosition() {
+ return this.initialCaretPosition;
+ }
+
+ /**
+ * Sets the position of the caret when variable mode editing was requested.
+ * Stores the current position because only the text that comes afterwards is
+ * a suitable variable reference.
+ *
+ * @return The variable mode caret position.
+ */
+ private void setInitialCaretPosition() {
+ this.initialCaretPosition = getEditor().getCaretPosition();
+ }
+
+ private StyledTextArea getEditor() {
+ return getFileEditorPane().getEditor();
+ }
+
+ public FileEditorPane getFileEditorPane() {
+ return this.fileEditorPane;
+ }
+
+ private void setFileEditorPane( final FileEditorPane fileEditorPane ) {
+ this.fileEditorPane = fileEditorPane;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ return this.definitionPane;
+ }
+
+ private void setDefinitionPane( final DefinitionPane definitionPane ) {
+ this.definitionPane = definitionPane;
+ }
+
+ private IndexRange getSelectionRange() {
+ return getEditor().getSelection();
+ }
+
+ /**
+ * Don't look ahead too far when trying to find the end of a node.
+ *
+ * @return 512 by default.
+ */
+ private int getMaxVarLength() {
+ return getSettings().getSetting(
+ "editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
+ }
+
private Settings getSettings() {
return this.settings;
src/main/java/com/scrivenvar/processors/VariableNameProcessor.java
/*
- * The MIT License
+ * Copyright 2016 White Magic Software, Ltd.
*
- * Copyright 2016 .
+ * All rights reserved.
*
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
*
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
*
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.scrivenvar.processors;
src/main/java/com/scrivenvar/ui/VariableTreeItem.java
+/*
+ * Copyright 2016 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.ui;
+
+import static com.scrivenvar.definition.DefinitionPane.SEPARATOR;
+import static com.scrivenvar.editor.VariableNameInjector.DEFAULT_MAX_VAR_LENGTH;
+import java.util.Stack;
+import javafx.scene.control.TreeItem;
+
+/**
+ * Provides behaviour afforded to variable names and their corresponding value.
+ *
+ * @author White Magic Software, Ltd.
+ * @param <T> The type of TreeItem (usually String).
+ */
+public class VariableTreeItem<T> extends TreeItem<T> {
+
+ /**
+ * Constructs a new item with a default value.
+ *
+ * @param value Passed up to superclass.
+ */
+ public VariableTreeItem( final T value ) {
+ super( value );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value.
+ *
+ * @param text The text to match against each leaf in the tree.
+ *
+ * @return The leaf that has a value starting with the given text.
+ */
+ public VariableTreeItem<T> findLeaf( final String text ) {
+ final Stack<TreeItem<T>> stack = new Stack<>();
+ final TreeItem<T> root = this;
+
+ stack.push( root );
+
+ boolean found = false;
+ TreeItem<T> node = null;
+
+ while( !stack.isEmpty() && !found ) {
+ node = stack.pop();
+
+ if( node.isLeaf() && node.getValue().toString().startsWith( text ) ) {
+ found = true;
+ } else {
+ for( final TreeItem<T> child : node.getChildren() ) {
+ stack.push( child );
+ }
+
+ // No match found, yet.
+ node = null;
+ }
+ }
+
+ return (VariableTreeItem<T>)node;
+ }
+
+ /**
+ * Returns the path for this node, with nodes made distinct using the
+ * separator character. This uses two loops: one for pushing nodes onto a
+ * stack and one for popping them off to create the path in desired order.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ public String toPath() {
+ final Stack<TreeItem<T>> stack = new Stack<>();
+ TreeItem<T> node = this;
+
+ while( node.getParent() != null ) {
+ stack.push( node );
+ node = node.getParent();
+ }
+
+ final StringBuilder sb = new StringBuilder( DEFAULT_MAX_VAR_LENGTH );
+
+ while( !stack.isEmpty() ) {
+ node = stack.pop();
+
+ if( !node.isLeaf() ) {
+ sb.append( node.getValue() );
+
+ // This will add a superfluous separator, but instead of peeking at
+ // the stack all the time, the last separator will be removed outside
+ // the loop (one operation executed once).
+ sb.append( SEPARATOR );
+ }
+ }
+
+ // Remove the trailing SEPARATOR.
+ if( sb.length() > 0 ) {
+ sb.setLength( sb.length() - 1 );
+ }
+
+ return sb.toString();
+ }
+}
src/main/java/com/scrivenvar/yaml/YamlTreeAdapter.java
import com.fasterxml.jackson.databind.JsonNode;
+import com.scrivenvar.ui.VariableTreeItem;
import java.io.IOException;
import java.io.InputStream;
protected YamlTreeAdapter() {
}
-
+
/**
* Converts a YAML document to a TreeView based on the document keys. Only the
final YamlTreeAdapter adapter = new YamlTreeAdapter();
final JsonNode rootNode = YamlParser.parse( in );
- final TreeItem<String> rootItem = new TreeItem<>( name );
+ final TreeItem<String> rootItem = adapter.createTreeItem( name );
rootItem.setExpanded( true );
final JsonNode leafNode = rootNode.getValue();
final String key = rootNode.getKey();
- final TreeItem<String> leafItem = new TreeItem<>( key );
+ final TreeItem<String> leafItem = createTreeItem( key );
if( leafNode.isValueNode() ) {
- leafItem.getChildren().add( new TreeItem<>( rootNode.getValue().asText() ) );
+ leafItem.getChildren().add(
+ createTreeItem( rootNode.getValue().asText() )
+ );
}
rootItem.getChildren().add( leafItem );
if( leafNode.isObject() ) {
adapt( leafNode, leafItem );
}
+ }
+
+ /**
+ * Creates a new tree item that can be added to the tree view.
+ *
+ * @param value The node's value.
+ *
+ * @return A new tree item node, never null.
+ */
+ private TreeItem<String> createTreeItem( final String value ) {
+ return new VariableTreeItem<>( value );
}
}
Delta1155 lines added, 929 lines removed, 226-line increase