Dave Jarvis' Repositories

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

Added search facility into scrollbar.

Authordjarvis <email>
Date2017-01-07 19:19:20 GMT-0800
Commita98036091b7cfff06b90ce15a6df1eacac7384a9
Parent7bbc62e
src/main/java/com/scrivenvar/FileEditorTab.java
/**
+ * Searches from the caret position forward for the given string.
+ *
+ * @param needle The text string to match.
+ */
+ public void searchNext( final String needle ) {
+ final String haystack = getEditorText();
+ final int index = haystack.indexOf( needle, getCaretPosition() );
+
+ if( index >= 0 ) {
+ setCaretPosition( index );
+ getEditor().selectRange( index, index + needle.length() );
+ }
+ }
+
+ /**
* Returns the index into the text where the caret blinks happily away.
*
* @return A number from 0 to the editor's document text length.
*/
public int getCaretPosition() {
return getEditor().getCaretPosition();
+ }
+
+ /**
+ * Moves the caret to a given offset.
+ *
+ * @param offset The new caret offset.
+ */
+ private void setCaretPosition( final int offset ) {
+ getEditor().moveTo( offset );
+ getEditor().requestFollowCaret();
}
/**
* Returns the caret's current row and column position.
- *
+ *
* @return The caret's offset into the document.
*/
src/main/java/com/scrivenvar/MainWindow.java
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.TreeView;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import static javafx.scene.input.KeyCode.ESCAPE;
-import javafx.scene.input.KeyEvent;
-import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-import org.controlsfx.control.StatusBar;
-import org.fxmisc.richtext.model.TwoDimensional.Position;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MainWindow implements Observer {
-
- private final Options options = Services.load( Options.class );
- private final Snitch snitch = Services.load( Snitch.class );
- private final Notifier notifier = Services.load( Notifier.class );
-
- private Scene scene;
- private MenuBar menuBar;
- private StatusBar statusBar;
- private Text lineNumberText;
-
- private DefinitionSource definitionSource;
- private DefinitionPane definitionPane;
- private FileEditorTabPane fileEditorPane;
- private HTMLPreviewPane previewPane;
-
- /**
- * Prevent re-instantiation processing classes.
- */
- private Map<FileEditorTab, Processor<String>> processors;
-
- public MainWindow() {
- initLayout();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- }
-
- /**
- * Listen for file editor tab pane to receive an open definition source event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- (ObservableValue<? extends Path> definitionFile,
- final Path oldPath, final Path newPath) -> {
- openDefinition( newPath );
-
- // Indirectly refresh the resolved map.
- setProcessors( null );
-
- updateDefinitionPane();
-
- try {
- getSnitch().ignore( oldPath );
- getSnitch().listen( newPath );
- } catch( final IOException ex ) {
- error( ex );
- }
-
- // Will create new processors and therefore a new resolved map.
- refreshSelectedTab( getActiveFileEditor() );
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new tab so
- * that the preview pane refreshes as necessary.
- */
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener(
- (final Change<? extends Tab> change) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- initTextChangeListener( tab );
- initCaretParagraphListener( tab );
- initVariableNameInjector( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous load.
- */
- private void initPreferences() {
- restoreDefinitionSource();
- getFileEditorPane().restorePreferences();
- updateDefinitionPane();
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- (ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab) -> {
-
- // If there was no old tab, then this is a first time load, which
- // can be ignored.
- if( oldTab != null ) {
- if( newTab == null ) {
- closeRemainingTab();
- }
- else {
- // Update the preview with the edited text.
- refreshSelectedTab( (FileEditorTab)newTab );
- }
- }
- }
- );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- (ObservableValue<? extends String> editor,
- final String oldValue, final String newValue) -> {
- refreshSelectedTab( tab );
- }
- );
- }
-
- private void initCaretParagraphListener( final FileEditorTab tab ) {
- tab.addCaretParagraphListener(
- (ObservableValue<? extends Integer> editor,
- final Integer oldValue, final Integer newValue) -> {
- refreshSelectedTab( tab );
- }
- );
- }
-
- private void initVariableNameInjector( final FileEditorTab tab ) {
- VariableNameInjector.listen( tab, getDefinitionPane() );
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited. When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- getSnitch().addObserver( this );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph changes,
- * or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void refreshSelectedTab( final FileEditorTab tab ) {
- if( tab.isFileOpen() ) {
- getPreviewPane().setPath( tab.getPath() );
-
- // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
- final Position p = tab.getCaretOffset();
- getLineNumberText().setText(
- get( STATUS_BAR_LINE,
- p.getMajor() + 1,
- p.getMinor() + 1,
- tab.getCaretPosition() + 1
- )
- );
-
- Processor<String> processor = getProcessors().get( tab );
-
- if( processor == null ) {
- processor = createProcessor( tab );
- getProcessors().put( tab, processor );
- }
-
- try {
- getNotifier().clear();
- processor.processChain( tab.getEditorText() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return getDefinitionSource().getResolvedMap();
- }
-
- /**
- * Returns the root node for the hierarchical definition source.
- *
- * @return Data to display in the definition pane.
- */
- private TreeView<String> getTreeView() {
- try {
- return getDefinitionSource().asTreeView();
- } catch( Exception e ) {
- error( e );
- }
-
- // Slightly redundant as getDefinitionSource() might have returned an
- // empty definition source.
- return (new EmptyDefinitionSource()).asTreeView();
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinition( final Path path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path.toString() );
- setDefinitionSource( ds );
- storeDefinitionSource();
- updateDefinitionPane();
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void updateDefinitionPane() {
- getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
- }
-
- private void restoreDefinitionSource() {
- final Preferences preferences = getPreferences();
- final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
-
- // If there's no definition source set, don't try to load it.
- if( source != null ) {
- setDefinitionSource( createDefinitionSource( source ) );
- }
- }
-
- private void storeDefinitionSource() {
- final Preferences preferences = getPreferences();
- final DefinitionSource ds = getDefinitionSource();
-
- preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
- }
-
- /**
- * Called when the last open tab is closed to clear the preview pane.
- */
- private void closeRemainingTab() {
- getPreviewPane().clear();
- }
-
- /**
- * Called when an exception occurs that warrants the user's attention.
- *
- * @param e The exception with a message that the user should know about.
- */
- private void error( final Exception e ) {
- getNotifier().notify( e );
- }
-
- //---- File actions -------------------------------------------------------
- /**
- * Called when an observable instance has changed. This is called by both the
- * snitch service and the notify service. The snitch service can be called for
- * different file types, including definition sources.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value != null ) {
- if( observable instanceof Snitch && value instanceof Path ) {
- final Path path = (Path)value;
- final FileTypePredicate predicate
- = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
-
- // Reload definitions.
- if( predicate.test( path.toFile() ) ) {
- updateDefinitionSource( path );
- }
-
- updateSelectedTab();
- }
- else if( observable instanceof Notifier && value instanceof String ) {
- updateStatusBar( (String)value );
- }
- }
- }
-
- /**
- * Updates the status bar to show the given message.
- *
- * @param s The message to show in the status bar.
- */
- private void updateStatusBar( final String s ) {
- Platform.runLater(
- () -> {
- final int index = s.indexOf( '\n' );
- final String message = s.substring( 0, index > 0 ? index : s.length() );
-
- getStatusBar().setText( message );
- }
- );
- }
-
- /**
- * Called when a file has been modified.
- *
- * @param file Path to the modified file.
- */
- private void updateSelectedTab() {
- Platform.runLater(
- () -> {
- // Brute-force XSLT file reload by re-instantiating all processors.
- resetProcessors();
- refreshSelectedTab( getActiveFileEditor() );
- }
- );
- }
-
- /**
- * Reloads the definition source from the given path.
- *
- * @param path The path containing new definition information.
- */
- private void updateDefinitionSource( final Path path ) {
- Platform.runLater(
- () -> {
- openDefinition( path );
- }
- );
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- File actions -------------------------------------------------------
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditor(), true );
- }
-
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditor() );
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( get( "Dialog.about.title" ) );
- alert.setHeaderText( get( "Dialog.about.header" ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Convenience accessors ----------------------------------------------
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- private Preferences getPreferences() {
- return getOptions().getState();
- }
-
- protected Scene getScene() {
- if( this.scene == null ) {
- this.scene = createScene();
- }
-
- return this.scene;
- }
-
- public Window getWindow() {
- return getScene().getWindow();
- }
-
- private MarkdownEditorPane getActiveEditor() {
- final EditorPane pane = getActiveFileEditor().getEditorPane();
-
- return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
- }
-
- private FileEditorTab getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- //---- Member accessors ---------------------------------------------------
- private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
- this.processors = map;
- }
-
- private Map<FileEditorTab, Processor<String>> getProcessors() {
- if( this.processors == null ) {
- setProcessors( new HashMap<>() );
- }
-
- return this.processors;
- }
-
- private FileEditorTabPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- private HTMLPreviewPane getPreviewPane() {
- if( this.previewPane == null ) {
- this.previewPane = createPreviewPane();
- }
-
- return this.previewPane;
- }
-
- private void setDefinitionSource( final DefinitionSource definitionSource ) {
- this.definitionSource = definitionSource;
- }
-
- private DefinitionSource getDefinitionSource() {
- if( this.definitionSource == null ) {
- this.definitionSource = new EmptyDefinitionSource();
- }
-
- return this.definitionSource;
- }
-
- private DefinitionPane getDefinitionPane() {
- if( this.definitionPane == null ) {
- this.definitionPane = createDefinitionPane();
- }
-
- return this.definitionPane;
- }
-
- private Options getOptions() {
- return this.options;
- }
-
- private Snitch getSnitch() {
- return this.snitch;
- }
-
- private Notifier getNotifier() {
- return this.notifier;
- }
-
- public void setMenuBar( final MenuBar menuBar ) {
- this.menuBar = menuBar;
- }
-
- public MenuBar getMenuBar() {
- return this.menuBar;
- }
-
- private Text getLineNumberText() {
- if( this.lineNumberText == null ) {
- this.lineNumberText = createLineNumberText();
- }
-
- return this.lineNumberText;
- }
-
- private synchronized StatusBar getStatusBar() {
- if( this.statusBar == null ) {
- this.statusBar = createStatusBar();
- }
-
- return this.statusBar;
- }
-
- //---- Member creators ----------------------------------------------------
- /**
- * Factory to create processors that are suited to different file types.
- *
- * @param tab The tab that is subjected to processing.
- *
- * @return A processor suited to the file type specified by the tab's path.
- */
- private Processor<String> createProcessor( final FileEditorTab tab ) {
- return createProcessorFactory().createProcessor( tab );
- }
-
- private ProcessorFactory createProcessorFactory() {
- return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
- }
-
- private DefinitionSource createDefinitionSource( final String path ) {
- final DefinitionSource ds
- = createDefinitionFactory().createDefinitionSource( path );
-
- if( ds instanceof FileDefinitionSource ) {
- try {
- getSnitch().listen( ((FileDefinitionSource)ds).getPath() );
- } catch( final IOException ex ) {
- error( ex );
- }
- }
-
- return ds;
- }
-
- /**
- * Create an editor pane to hold file editor tabs.
- *
- * @return A new instance, never null.
- */
- private FileEditorTabPane createFileEditorPane() {
- return new FileEditorTabPane();
- }
-
- private HTMLPreviewPane createPreviewPane() {
- return new HTMLPreviewPane();
- }
-
- private DefinitionPane createDefinitionPane() {
- return new DefinitionPane( getTreeView() );
- }
-
- private DefinitionFactory createDefinitionFactory() {
- return new DefinitionFactory();
- }
-
- private StatusBar createStatusBar() {
- return new StatusBar();
- }
-
- private Scene createScene() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane().getNode(),
- getFileEditorPane().getNode(),
- getPreviewPane().getNode() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
- getFloat( K_PANE_SPLIT_EDITOR, .45f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
-
- // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setBottom( getStatusBar() );
- borderPane.setCenter( splitPane );
-
- final VBox box = new VBox();
- box.setAlignment( Pos.BASELINE_CENTER );
- box.getChildren().add( getLineNumberText() );
- getStatusBar().getRightItems().add( box );
-
- return new Scene( borderPane );
- }
-
- private Text createLineNumberText() {
- return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
- }
-
- private Node createMenuBar() {
- final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
-
- // File actions
- Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
- Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
- Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
- Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
- Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
- createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
- Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
- Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
- Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
-
- // Edit actions
- Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
- e -> getActiveEditor().undo(),
- createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
- Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
- e -> getActiveEditor().redo(),
- createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
- Action editFindAction = new Action( Messages.get( "Main.menu.edit.find" ), "Shortcut+F", SEARCH,
- e -> getActiveEditor().find(),
- activeFileEditorIsNull );
- Action editReplaceAction = new Action( Messages.get( "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET,
- e -> getActiveEditor().replace(),
- activeFileEditorIsNull );
- Action editFindNextAction = new Action( Messages.get( "Main.menu.edit.find.next" ), "F3", null,
- e -> getActiveEditor().findNext(),
+import javafx.scene.control.TextField;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.TreeView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import static javafx.scene.input.KeyCode.ESCAPE;
+import javafx.scene.input.KeyEvent;
+import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+import org.controlsfx.control.StatusBar;
+import org.fxmisc.richtext.model.TwoDimensional.Position;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ *
+ * @author Karl Tauber and White Magic Software, Ltd.
+ */
+public class MainWindow implements Observer {
+
+ private final Options options = Services.load( Options.class );
+ private final Snitch snitch = Services.load( Snitch.class );
+ private final Notifier notifier = Services.load( Notifier.class );
+
+ private Scene scene;
+ private MenuBar menuBar;
+ private StatusBar statusBar;
+ private Text lineNumberText;
+
+ private DefinitionSource definitionSource;
+ private DefinitionPane definitionPane;
+ private FileEditorTabPane fileEditorPane;
+ private HTMLPreviewPane previewPane;
+
+ /**
+ * Prevent re-instantiation processing classes.
+ */
+ private Map<FileEditorTab, Processor<String>> processors;
+
+ public MainWindow() {
+ initLayout();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ }
+
+ /**
+ * Listen for file editor tab pane to receive an open definition source event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ (ObservableValue<? extends Path> definitionFile,
+ final Path oldPath, final Path newPath) -> {
+ openDefinition( newPath );
+
+ // Indirectly refresh the resolved map.
+ setProcessors( null );
+
+ updateDefinitionPane();
+
+ try {
+ getSnitch().ignore( oldPath );
+ getSnitch().listen( newPath );
+ } catch( final IOException ex ) {
+ error( ex );
+ }
+
+ // Will create new processors and therefore a new resolved map.
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new tab so
+ * that the preview pane refreshes as necessary.
+ */
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ // Update the preview pane on tab changes.
+ tabs.addListener(
+ (final Change<? extends Tab> change) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab)newTab;
+
+ initTextChangeListener( tab );
+ initCaretParagraphListener( tab );
+ initVariableNameInjector( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous load.
+ */
+ private void initPreferences() {
+ restoreDefinitionSource();
+ getFileEditorPane().restorePreferences();
+ updateDefinitionPane();
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ (ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab) -> {
+
+ // If there was no old tab, then this is a first time load, which
+ // can be ignored.
+ if( oldTab != null ) {
+ if( newTab == null ) {
+ closeRemainingTab();
+ }
+ else {
+ // Update the preview with the edited text.
+ refreshSelectedTab( (FileEditorTab)newTab );
+ }
+ }
+ }
+ );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ (ObservableValue<? extends String> editor,
+ final String oldValue, final String newValue) -> {
+ refreshSelectedTab( tab );
+ }
+ );
+ }
+
+ private void initCaretParagraphListener( final FileEditorTab tab ) {
+ tab.addCaretParagraphListener(
+ (ObservableValue<? extends Integer> editor,
+ final Integer oldValue, final Integer newValue) -> {
+ refreshSelectedTab( tab );
+ }
+ );
+ }
+
+ private void initVariableNameInjector( final FileEditorTab tab ) {
+ VariableNameInjector.listen( tab, getDefinitionPane() );
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited. When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ getSnitch().addObserver( this );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph changes,
+ * or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void refreshSelectedTab( final FileEditorTab tab ) {
+ if( tab.isFileOpen() ) {
+ getPreviewPane().setPath( tab.getPath() );
+
+ // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
+ final Position p = tab.getCaretOffset();
+ getLineNumberText().setText(
+ get( STATUS_BAR_LINE,
+ p.getMajor() + 1,
+ p.getMinor() + 1,
+ tab.getCaretPosition() + 1
+ )
+ );
+
+ Processor<String> processor = getProcessors().get( tab );
+
+ if( processor == null ) {
+ processor = createProcessor( tab );
+ getProcessors().put( tab, processor );
+ }
+
+ try {
+ getNotifier().clear();
+ processor.processChain( tab.getEditorText() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+ }
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void find() {
+ final TextField input = new TextField();
+
+ input.setOnKeyPressed( (KeyEvent event) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ getActiveFileEditor().searchNext( input.getText() );
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditor().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ (
+ final ObservableValue<? extends Boolean> focused,
+ final Boolean oFocus,
+ final Boolean nFocus) -> {
+ if( !nFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+
+ getStatusBar().setGraphic( input );
+
+ input.requestFocus();
+ }
+
+ public void findNext() {
+ System.out.println( "find next" );
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return getDefinitionSource().getResolvedMap();
+ }
+
+ /**
+ * Returns the root node for the hierarchical definition source.
+ *
+ * @return Data to display in the definition pane.
+ */
+ private TreeView<String> getTreeView() {
+ try {
+ return getDefinitionSource().asTreeView();
+ } catch( Exception e ) {
+ error( e );
+ }
+
+ // Slightly redundant as getDefinitionSource() might have returned an
+ // empty definition source.
+ return (new EmptyDefinitionSource()).asTreeView();
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinition( final Path path ) {
+ try {
+ final DefinitionSource ds = createDefinitionSource( path.toString() );
+ setDefinitionSource( ds );
+ storeDefinitionSource();
+ updateDefinitionPane();
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void updateDefinitionPane() {
+ getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
+ }
+
+ private void restoreDefinitionSource() {
+ final Preferences preferences = getPreferences();
+ final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
+
+ // If there's no definition source set, don't try to load it.
+ if( source != null ) {
+ setDefinitionSource( createDefinitionSource( source ) );
+ }
+ }
+
+ private void storeDefinitionSource() {
+ final Preferences preferences = getPreferences();
+ final DefinitionSource ds = getDefinitionSource();
+
+ preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
+ }
+
+ /**
+ * Called when the last open tab is closed to clear the preview pane.
+ */
+ private void closeRemainingTab() {
+ getPreviewPane().clear();
+ }
+
+ /**
+ * Called when an exception occurs that warrants the user's attention.
+ *
+ * @param e The exception with a message that the user should know about.
+ */
+ private void error( final Exception e ) {
+ getNotifier().notify( e );
+ }
+
+ //---- File actions -------------------------------------------------------
+ /**
+ * Called when an observable instance has changed. This is called by both the
+ * snitch service and the notify service. The snitch service can be called for
+ * different file types, including definition sources.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value != null ) {
+ if( observable instanceof Snitch && value instanceof Path ) {
+ final Path path = (Path)value;
+ final FileTypePredicate predicate
+ = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
+
+ // Reload definitions.
+ if( predicate.test( path.toFile() ) ) {
+ updateDefinitionSource( path );
+ }
+
+ updateSelectedTab();
+ }
+ else if( observable instanceof Notifier && value instanceof String ) {
+ updateStatusBar( (String)value );
+ }
+ }
+ }
+
+ /**
+ * Updates the status bar to show the given message.
+ *
+ * @param s The message to show in the status bar.
+ */
+ private void updateStatusBar( final String s ) {
+ Platform.runLater(
+ () -> {
+ final int index = s.indexOf( '\n' );
+ final String message = s.substring( 0, index > 0 ? index : s.length() );
+
+ getStatusBar().setText( message );
+ }
+ );
+ }
+
+ /**
+ * Called when a file has been modified.
+ *
+ * @param file Path to the modified file.
+ */
+ private void updateSelectedTab() {
+ Platform.runLater(
+ () -> {
+ // Brute-force XSLT file reload by re-instantiating all processors.
+ resetProcessors();
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ /**
+ * Reloads the definition source from the given path.
+ *
+ * @param path The path containing new definition information.
+ */
+ private void updateDefinitionSource( final Path path ) {
+ Platform.runLater(
+ () -> {
+ openDefinition( path );
+ }
+ );
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- File actions -------------------------------------------------------
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditor(), true );
+ }
+
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditor() );
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( get( "Dialog.about.title" ) );
+ alert.setHeaderText( get( "Dialog.about.header" ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Convenience accessors ----------------------------------------------
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ private Preferences getPreferences() {
+ return getOptions().getState();
+ }
+
+ protected Scene getScene() {
+ if( this.scene == null ) {
+ this.scene = createScene();
+ }
+
+ return this.scene;
+ }
+
+ public Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ final EditorPane pane = getActiveFileEditor().getEditorPane();
+
+ return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
+ }
+
+ private FileEditorTab getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ //---- Member accessors ---------------------------------------------------
+ private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
+ this.processors = map;
+ }
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ if( this.processors == null ) {
+ setProcessors( new HashMap<>() );
+ }
+
+ return this.processors;
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ if( this.previewPane == null ) {
+ this.previewPane = createPreviewPane();
+ }
+
+ return this.previewPane;
+ }
+
+ private void setDefinitionSource( final DefinitionSource definitionSource ) {
+ this.definitionSource = definitionSource;
+ }
+
+ private DefinitionSource getDefinitionSource() {
+ if( this.definitionSource == null ) {
+ this.definitionSource = new EmptyDefinitionSource();
+ }
+
+ return this.definitionSource;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ if( this.definitionPane == null ) {
+ this.definitionPane = createDefinitionPane();
+ }
+
+ return this.definitionPane;
+ }
+
+ private Options getOptions() {
+ return this.options;
+ }
+
+ private Snitch getSnitch() {
+ return this.snitch;
+ }
+
+ private Notifier getNotifier() {
+ return this.notifier;
+ }
+
+ public void setMenuBar( final MenuBar menuBar ) {
+ this.menuBar = menuBar;
+ }
+
+ public MenuBar getMenuBar() {
+ return this.menuBar;
+ }
+
+ private Text getLineNumberText() {
+ if( this.lineNumberText == null ) {
+ this.lineNumberText = createLineNumberText();
+ }
+
+ return this.lineNumberText;
+ }
+
+ private synchronized StatusBar getStatusBar() {
+ if( this.statusBar == null ) {
+ this.statusBar = createStatusBar();
+ }
+
+ return this.statusBar;
+ }
+
+ //---- Member creators ----------------------------------------------------
+ /**
+ * Factory to create processors that are suited to different file types.
+ *
+ * @param tab The tab that is subjected to processing.
+ *
+ * @return A processor suited to the file type specified by the tab's path.
+ */
+ private Processor<String> createProcessor( final FileEditorTab tab ) {
+ return createProcessorFactory().createProcessor( tab );
+ }
+
+ private ProcessorFactory createProcessorFactory() {
+ return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
+ }
+
+ private DefinitionSource createDefinitionSource( final String path ) {
+ final DefinitionSource ds
+ = createDefinitionFactory().createDefinitionSource( path );
+
+ if( ds instanceof FileDefinitionSource ) {
+ try {
+ getSnitch().listen( ((FileDefinitionSource)ds).getPath() );
+ } catch( final IOException ex ) {
+ error( ex );
+ }
+ }
+
+ return ds;
+ }
+
+ /**
+ * Create an editor pane to hold file editor tabs.
+ *
+ * @return A new instance, never null.
+ */
+ private FileEditorTabPane createFileEditorPane() {
+ return new FileEditorTabPane();
+ }
+
+ private HTMLPreviewPane createPreviewPane() {
+ return new HTMLPreviewPane();
+ }
+
+ private DefinitionPane createDefinitionPane() {
+ return new DefinitionPane( getTreeView() );
+ }
+
+ private DefinitionFactory createDefinitionFactory() {
+ return new DefinitionFactory();
+ }
+
+ private StatusBar createStatusBar() {
+ return new StatusBar();
+ }
+
+ private Scene createScene() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane().getNode(),
+ getFileEditorPane().getNode(),
+ getPreviewPane().getNode() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .45f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
+
+ // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setBottom( getStatusBar() );
+ borderPane.setCenter( splitPane );
+
+ final VBox box = new VBox();
+ box.setAlignment( Pos.BASELINE_CENTER );
+ box.getChildren().add( getLineNumberText() );
+ getStatusBar().getRightItems().add( box );
+
+ return new Scene( borderPane );
+ }
+
+ private Text createLineNumberText() {
+ return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
+ }
+
+ private Node createMenuBar() {
+ final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
+
+ // File actions
+ Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
+ Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
+ Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
+ Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
+ Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
+ createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
+ Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
+ Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
+ Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
+
+ // Edit actions
+ Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
+ e -> getActiveEditor().undo(),
+ createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
+ Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
+ e -> getActiveEditor().redo(),
+ createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
+ Action editFindAction = new Action( Messages.get( "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
+ e -> find(),
+ activeFileEditorIsNull );
+ Action editReplaceAction = new Action( Messages.get( "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET,
+ e -> getActiveEditor().replace(),
+ activeFileEditorIsNull );
+ Action editFindNextAction = new Action( Messages.get( "Main.menu.edit.find.next" ), "F3", null,
+ e -> findNext(),
activeFileEditorIsNull );
Action editFindPreviousAction = new Action( Messages.get( "Main.menu.edit.find.previous" ), "Shift+F3", null,
src/main/java/com/scrivenvar/definition/DefinitionPane.java
*/
public VariableTreeItem<String> findLeaf( final String value ) {
+ return findLeaf( value, false );
+ }
+
+ /**
+ * Returns the leaf that matches the given value. If the value is terminally
+ * punctuated, the punctuation is removed if no match was found.
+ *
+ * @param value The value to find, never null.
+ * @param contains Set to true to perform a substring match if starts with
+ * fails to match.
+ *
+ * @return The leaf that contains the given value, or null if neither the
+ * original value nor the terminally-trimmed value was found.
+ */
+ public VariableTreeItem<String> findLeaf(
+ final String value,
+ final boolean contains ) {
+
final VariableTreeItem<String> root = getTreeRoot();
- final VariableTreeItem<String> leaf = root.findLeaf( value );
+ final VariableTreeItem<String> leaf = root.findLeaf( value, contains );
return leaf == null
src/main/java/com/scrivenvar/definition/VariableTreeItem.java
private final static int DEFAULT_MAP_SIZE = 1000;
-
- private final static VariableDecorator VARIABLE_DECORATOR =
- new YamlVariableDecorator();
+
+ private final static VariableDecorator VARIABLE_DECORATOR
+ = new YamlVariableDecorator();
/**
*/
public VariableTreeItem<T> findLeaf( final String text ) {
+ return findLeaf( text, false );
+ }
+
+ /**
+ * 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.
+ * @param contains Set to true to perform a substring match if starts with
+ * fails.
+ *
+ * @return The leaf that has a value starting with the given text.
+ */
+ public VariableTreeItem<T> findLeaf(
+ final String text,
+ final boolean contains ) {
+
final Stack<VariableTreeItem<T>> stack = new Stack<>();
final VariableTreeItem<T> root = this;
node = stack.pop();
- if( node.valueStartsWith( text ) ) {
+ if( contains && node.valueContains( text ) ) {
found = true;
- } else {
+ }
+ else if( !contains && node.valueStartsWith( text ) ) {
+ found = true;
+ }
+ else {
for( final TreeItem<T> child : node.getChildren() ) {
stack.push( (VariableTreeItem<T>)child );
private boolean valueStartsWith( final String s ) {
return isLeaf() && getValue().toString().startsWith( s );
+ }
+
+ /**
+ * Returns true if this node is a leaf and its value contains the given text.
+ *
+ * @param s The text to compare against the node value.
+ *
+ * @return true Node is a leaf and its value contains the given value.
+ */
+ private boolean valueContains( final String s ) {
+ return isLeaf() && getValue().toString().contains( s );
}
map.put( key, value );
- } else {
+ }
+ else {
populate( child, map );
}
src/main/java/com/scrivenvar/editors/EditorPane.java
import org.fxmisc.wellbehaved.event.EventPattern;
import org.fxmisc.wellbehaved.event.InputMap;
-import org.fxmisc.wellbehaved.event.Nodes;
import static org.fxmisc.wellbehaved.event.InputMap.consume;
+import org.fxmisc.wellbehaved.event.Nodes;
/**
public void redo() {
getUndoManager().redo();
- }
-
- public void find() {
- System.out.println( "search" );
}
public void replace() {
System.out.println( "replace" );
}
- public void findNext() {
- System.out.println( "find next" );
- }
-
public void findPrevious() {
System.out.println( "find previous" );
src/main/java/com/scrivenvar/editors/VariableNameInjector.java
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() );
- decorateVariable();
- expand( leaf );
- }
- }
-
- /**
- * Called when autocomplete finishes on a valid leaf or when the user presses
- * Enter to finish manual autocomplete.
- */
- private void decorateVariable() {
- // 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 = getVariableDecorator().decorate( old );
-
- final int posEnded = getCurrentCaretPosition();
- final int posBegan = posEnded - old.length();
-
- getEditor().replaceText( posBegan, posEnded, newVariable );
- }
-
- /**
- * 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, including
- * 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 !isSpaceChar( c );
- }
-
- /**
- * 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 );
+ 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() );
+ decorateVariable();
+ expand( leaf );
+ }
+ }
+
+ /**
+ * Called when autocomplete finishes on a valid leaf or when the user presses
+ * Enter to finish manual autocomplete.
+ */
+ private void decorateVariable() {
+ // 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 = getVariableDecorator().decorate( old );
+
+ final int posEnded = getCurrentCaretPosition();
+ final int posBegan = posEnded - old.length();
+
+ getEditor().replaceText( posBegan, posEnded, newVariable );
+ }
+
+ /**
+ * 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, including
+ * 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 !isSpaceChar( c );
+ }
+
+ /**
+ * 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 );
}
Delta1192 lines added, 1058 lines removed, 134-line increase