| | |
| | /** |
| | - * 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() { |
| | - // Control and space are pressed. |
| | - 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: |
| | - // Don't decorate the variable upon exiting vMode. |
| | - vModeBackspace(); |
| | - break; |
| | - |
| | - case ESCAPE: |
| | - // Don't decorate the variable upon exiting vMode. |
| | - vModeStop(); |
| | - break; |
| | - |
| | - case ENTER: |
| | - case PERIOD: |
| | - case RIGHT: |
| | - case END: |
| | - // Stop at a leaf node, ENTER means accept. |
| | - if( vModeConditionalComplete() && keyCode == ENTER ) { |
| | - vModeStop(); |
| | - |
| | - // Decorate the variable upon exiting vMode. |
| | - decorate(); |
| | - } |
| | - 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 != null && !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( final KeyEvent e ) { |
| | - final String paragraph = getCaretParagraph(); |
| | - final int[] boundaries = getWordBoundaries( paragraph ); |
| | - final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] ); |
| | - |
| | - VariableTreeItem<String> leaf = findLeaf( word ); |
| | - |
| | - if( leaf == null ) { |
| | - // If a leaf doesn't match using "starts with", then try using "contains". |
| | - leaf = findLeaf( word, true ); |
| | - } |
| | - |
| | - if( leaf != null ) { |
| | - replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() ); |
| | - decorate(); |
| | - expand( leaf ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Called when autocomplete finishes on a valid leaf or when the user presses |
| | - * Enter to finish manual autocomplete. |
| | - */ |
| | - private void decorate() { |
| | - // A little bit of duplication... |
| | - final String paragraph = getCaretParagraph(); |
| | - final int[] boundaries = getWordBoundaries( paragraph ); |
| | - final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] ); |
| | - |
| | - final String newVariable = decorate( old ); |
| | - |
| | - final int posEnded = getCurrentCaretPosition(); |
| | - final int posBegan = posEnded - old.length(); |
| | - |
| | - getEditor().replaceText( posBegan, posEnded, newVariable ); |
| | - } |
| | - |
| | - /** |
| | - * Called when user double-clicks on a tree view item. |
| | - * |
| | - * @param variable The variable to decorate. |
| | - */ |
| | - private String decorate( final String variable ) { |
| | - return getVariableDecorator().decorate( variable ); |
| | - } |
| | - |
| | - /** |
| | - * Inserts the given string at the current caret position, or replaces |
| | - * selected text (if any). |
| | - * |
| | - * @param s The string to inject. |
| | - */ |
| | - private void replaceSelection( final String s ) { |
| | - getEditor().replaceSelection( s ); |
| | - } |
| | - |
| | - /** |
| | - * Updates the text at the given position within the current paragraph. |
| | - * |
| | - * @param posBegan The starting index in the paragraph text to replace. |
| | - * @param posEnded The ending index in 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 int p = getCurrentParagraph(); |
| | - |
| | - getEditor().replaceText( p, posBegan, p, posEnded, text ); |
| | - } |
| | - |
| | - /** |
| | - * Returns the caret's current paragraph position. |
| | - * |
| | - * @return A number greater than or equal to 0. |
| | - */ |
| | - private int getCurrentParagraph() { |
| | - return getEditor().getCurrentParagraph(); |
| | - } |
| | - |
| | - /** |
| | - * Returns current word boundary indexes into the current paragraph, excluding |
| | - * punctuation. |
| | - * |
| | - * @param p The paragraph wherein to hunt word boundaries. |
| | - * @param offset The offset into the paragraph to begin scanning left and |
| | - * right. |
| | - * |
| | - * @return The starting and ending index of the word closest to the caret. |
| | - */ |
| | - private int[] getWordBoundaries( final String p, final int offset ) { |
| | - // Remove dashes, but retain hyphens. Retain same number of characters |
| | - // to preserve relative indexes. |
| | - final String paragraph = p.replace( "---", " " ).replace( "--", " " ); |
| | - |
| | - return getWordAt( paragraph, offset ); |
| | - } |
| | - |
| | - /** |
| | - * Helper method to get the word boundaries for the current paragraph. |
| | - * |
| | - * @param paragraph |
| | - * |
| | - * @return |
| | - */ |
| | - private int[] getWordBoundaries( final String paragraph ) { |
| | - return getWordBoundaries( paragraph, getCurrentCaretColumn() ); |
| | - } |
| | - |
| | - /** |
| | - * 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 p 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 int[] getWordAt( final String p, final int offset ) { |
| | - return new int[]{ getWordBegan( p, offset ), getWordEnded( p, offset ) }; |
| | - } |
| | - |
| | - /** |
| | - * 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 !isWhitespace( c ) && !isPunctuation( c ); |
| | - } |
| | - |
| | - /** |
| | - * Returns true if the given character is part of the set of Latin (English) |
| | - * punctuation marks. |
| | - * |
| | - * @param c |
| | - * |
| | - * @return |
| | - */ |
| | - private static boolean isPunctuation( final char c ) { |
| | - return PUNCTUATION.indexOf( c ) != -1; |
| | - } |
| | - |
| | - /** |
| | - * Returns the text for the paragraph that contains the caret. |
| | - * |
| | - * @return A non-null string, possibly empty. |
| | - */ |
| | - private String getCaretParagraph() { |
| | - return getEditor().getText( 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 the variable name (or as much as has been typed so far). 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_CHAR ); |
| | - |
| | - while( i > 0 ) { |
| | - path = path.substring( i + 1 ); |
| | - i = path.indexOf( SEPARATOR_CHAR ); |
| | - } |
| | - |
| | - 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() ); |
| | - |
| | - try { |
| | - return textArea.getText( textBegan, textEnded ); |
| | - } |
| | - catch( final Exception e ) { |
| | - return textArea.getText(); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * 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, false ); |
| | - } |
| | - |
| | - /** |
| | - * Finds the first leaf having a value that starts with the given text, or |
| | - * contains the text if contains is true. |
| | - * |
| | - * @param text The text to find in the definition tree. |
| | - * @param contains Set true to perform a substring match after a starts with |
| | - * match. |
| | - * |
| | - * @return The leaf that starts with the given text, or null if not found. |
| | - */ |
| | - private VariableTreeItem<String> findLeaf( |
| | - final String text, |
| | - final boolean contains ) { |
| | - return getDefinitionPane().findLeaf( text, contains ); |
| | - } |
| | - |
| | - /** |
| | - * 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. |
| | - * Also asks the processing chain to modify the variable text into a |
| | - * machine-readable variable based on the format required by the file type. |
| | - * For example, a Markdown file (.md) will substitute a $VAR$ name while an R |
| | - * file (.Rmd, .Rxml) will use `r#xVAR`. |
| | - */ |
| | - private void vModeStop() { |
| | - removeEventListener( getKeyboardMap() ); |
| | - } |
| | - |
| | - /** |
| | - * Returns a variable decorator that corresponds to the given file type. |
| | - * |
| | - * @return |
| | - */ |
| | - private VariableDecorator getVariableDecorator() { |
| | - return VariableNameDecoratorFactory.newInstance( getFilename() ); |
| | - } |
| | - |
| | - private Path getFilename() { |
| | - return getFileEditorTab().getPath(); |
| | - } |
| | - |
| | - /** |
| | - * 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; |
| | - } |
| | - |
| | - private EditorPane getEditorPane() { |
| | - return getFileEditorTab().getEditorPane(); |
| | - } |
| | - |
| | - /** |
| | - * 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 ) { |
| | - getEditorPane().addEventListener( event, consumer ); |
| | + * Trap control+space and the @ key. |
| | + * |
| | + * @param tab The file editor that sends keyboard events for variable name |
| | + * injection. |
| | + */ |
| | + public void initKeyboardEventListeners( final FileEditorTab tab ) { |
| | + setFileEditorTab( tab ); |
| | + 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() { |
| | + // Control and space are pressed. |
| | + addKeyboardListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete ); |
| | + |
| | + // @ key in Linux? |
| | + addKeyboardListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode ); |
| | + // @ key in Windows. |
| | + addKeyboardListener( 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: |
| | + // Don't decorate the variable upon exiting vMode. |
| | + vModeBackspace(); |
| | + break; |
| | + |
| | + case ESCAPE: |
| | + // Don't decorate the variable upon exiting vMode. |
| | + vModeStop(); |
| | + break; |
| | + |
| | + case ENTER: |
| | + case PERIOD: |
| | + case RIGHT: |
| | + case END: |
| | + // Stop at a leaf node, ENTER means accept. |
| | + if( vModeConditionalComplete() && keyCode == ENTER ) { |
| | + vModeStop(); |
| | + |
| | + // Decorate the variable upon exiting vMode. |
| | + decorate(); |
| | + } |
| | + 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 != null && !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( final KeyEvent e ) { |
| | + final String paragraph = getCaretParagraph(); |
| | + final int[] boundaries = getWordBoundaries( paragraph ); |
| | + final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] ); |
| | + |
| | + VariableTreeItem<String> leaf = findLeaf( word ); |
| | + |
| | + if( leaf == null ) { |
| | + // If a leaf doesn't match using "starts with", then try using "contains". |
| | + leaf = findLeaf( word, true ); |
| | + } |
| | + |
| | + if( leaf != null ) { |
| | + replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() ); |
| | + decorate(); |
| | + expand( leaf ); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Called when autocomplete finishes on a valid leaf or when the user presses |
| | + * Enter to finish manual autocomplete. |
| | + */ |
| | + private void decorate() { |
| | + // A little bit of duplication... |
| | + final String paragraph = getCaretParagraph(); |
| | + final int[] boundaries = getWordBoundaries( paragraph ); |
| | + final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] ); |
| | + |
| | + final String newVariable = decorate( old ); |
| | + |
| | + final int posEnded = getCurrentCaretPosition(); |
| | + final int posBegan = posEnded - old.length(); |
| | + |
| | + getEditor().replaceText( posBegan, posEnded, newVariable ); |
| | + } |
| | + |
| | + /** |
| | + * Called when user double-clicks on a tree view item. |
| | + * |
| | + * @param variable The variable to decorate. |
| | + */ |
| | + private String decorate( final String variable ) { |
| | + return getVariableDecorator().decorate( variable ); |
| | + } |
| | + |
| | + /** |
| | + * Inserts the given string at the current caret position, or replaces |
| | + * selected text (if any). |
| | + * |
| | + * @param s The string to inject. |
| | + */ |
| | + private void replaceSelection( final String s ) { |
| | + getEditor().replaceSelection( s ); |
| | + } |
| | + |
| | + /** |
| | + * Updates the text at the given position within the current paragraph. |
| | + * |
| | + * @param posBegan The starting index in the paragraph text to replace. |
| | + * @param posEnded The ending index in 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 int p = getCurrentParagraph(); |
| | + |
| | + getEditor().replaceText( p, posBegan, p, posEnded, text ); |
| | + } |
| | + |
| | + /** |
| | + * Returns the caret's current paragraph position. |
| | + * |
| | + * @return A number greater than or equal to 0. |
| | + */ |
| | + private int getCurrentParagraph() { |
| | + return getEditor().getCurrentParagraph(); |
| | + } |
| | + |
| | + /** |
| | + * Returns current word boundary indexes into the current paragraph, excluding |
| | + * punctuation. |
| | + * |
| | + * @param p The paragraph wherein to hunt word boundaries. |
| | + * @param offset The offset into the paragraph to begin scanning left and |
| | + * right. |
| | + * |
| | + * @return The starting and ending index of the word closest to the caret. |
| | + */ |
| | + private int[] getWordBoundaries( final String p, final int offset ) { |
| | + // Remove dashes, but retain hyphens. Retain same number of characters |
| | + // to preserve relative indexes. |
| | + final String paragraph = p.replace( "---", " " ).replace( "--", " " ); |
| | + |
| | + return getWordAt( paragraph, offset ); |
| | + } |
| | + |
| | + /** |
| | + * Helper method to get the word boundaries for the current paragraph. |
| | + * |
| | + * @param paragraph |
| | + * |
| | + * @return |
| | + */ |
| | + private int[] getWordBoundaries( final String paragraph ) { |
| | + return getWordBoundaries( paragraph, getCurrentCaretColumn() ); |
| | + } |
| | + |
| | + /** |
| | + * 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 p 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 int[] getWordAt( final String p, final int offset ) { |
| | + return new int[]{ getWordBegan( p, offset ), getWordEnded( p, offset ) }; |
| | + } |
| | + |
| | + /** |
| | + * 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 !isWhitespace( c ) && !isPunctuation( c ); |
| | + } |
| | + |
| | + /** |
| | + * Returns true if the given character is part of the set of Latin (English) |
| | + * punctuation marks. |
| | + * |
| | + * @param c |
| | + * |
| | + * @return |
| | + */ |
| | + private static boolean isPunctuation( final char c ) { |
| | + return PUNCTUATION.indexOf( c ) != -1; |
| | + } |
| | + |
| | + /** |
| | + * Returns the text for the paragraph that contains the caret. |
| | + * |
| | + * @return A non-null string, possibly empty. |
| | + */ |
| | + private String getCaretParagraph() { |
| | + return getEditor().getText( 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 the variable name (or as much as has been typed so far). 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_CHAR ); |
| | + |
| | + while( i > 0 ) { |
| | + path = path.substring( i + 1 ); |
| | + i = path.indexOf( SEPARATOR_CHAR ); |
| | + } |
| | + |
| | + 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() ); |
| | + |
| | + try { |
| | + return textArea.getText( textBegan, textEnded ); |
| | + } |
| | + catch( final Exception e ) { |
| | + return textArea.getText(); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * 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, false ); |
| | + } |
| | + |
| | + /** |
| | + * Finds the first leaf having a value that starts with the given text, or |
| | + * contains the text if contains is true. |
| | + * |
| | + * @param text The text to find in the definition tree. |
| | + * @param contains Set true to perform a substring match after a starts with |
| | + * match. |
| | + * |
| | + * @return The leaf that starts with the given text, or null if not found. |
| | + */ |
| | + private VariableTreeItem<String> findLeaf( |
| | + final String text, |
| | + final boolean contains ) { |
| | + return getDefinitionPane().findLeaf( text, contains ); |
| | + } |
| | + |
| | + /** |
| | + * 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. |
| | + * Also asks the processing chain to modify the variable text into a |
| | + * machine-readable variable based on the format required by the file type. |
| | + * For example, a Markdown file (.md) will substitute a $VAR$ name while an R |
| | + * file (.Rmd, .Rxml) will use `r#xVAR`. |
| | + */ |
| | + private void vModeStop() { |
| | + removeEventListener( getKeyboardMap() ); |
| | + } |
| | + |
| | + /** |
| | + * Returns a variable decorator that corresponds to the given file type. |
| | + * |
| | + * @return |
| | + */ |
| | + private VariableDecorator getVariableDecorator() { |
| | + return VariableNameDecoratorFactory.newInstance( getFilename() ); |
| | + } |
| | + |
| | + private Path getFilename() { |
| | + return getFileEditorTab().getPath(); |
| | + } |
| | + |
| | + /** |
| | + * 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; |
| | + } |
| | + |
| | + private EditorPane getEditorPane() { |
| | + return getFileEditorTab().getEditorPane(); |
| | + } |
| | + |
| | + /** |
| | + * Delegates to the file editor pane, and, ultimately, to its text area. |
| | + */ |
| | + private <T extends Event, U extends T> void addKeyboardListener( |
| | + final EventPattern<? super T, ? extends U> event, |
| | + final Consumer<? super U> consumer ) { |
| | + getEditorPane().addKeyboardListener( event, consumer ); |
| | } |
| | |