Dave Jarvis' Repositories

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

Refactored adding event listeners to the markdown editor pane so that the at symbol can trigger actions related to YAML documents.

Authordjarvis <email>
Date2016-10-22 21:06:42 GMT-0700
Commit4ae090bd9e0d79ee4a489709e755a3b4564e1c36
Parent26ae098
Delta425 lines added, 405 lines removed, 20-line increase
src/main/java/com/scrivendor/editor/MarkdownEditorPane.java
/*
- * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivendor.editor;
-
-import static com.scrivendor.Constants.STYLESHEET_EDITOR;
-import com.scrivendor.dialogs.ImageDialog;
-import com.scrivendor.dialogs.LinkDialog;
-import com.scrivendor.ui.AbstractPane;
-import com.scrivendor.util.Utils;
-import java.nio.file.Path;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javafx.application.Platform;
-import javafx.beans.InvalidationListener;
-import javafx.beans.WeakInvalidationListener;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyDoubleProperty;
-import javafx.beans.property.ReadOnlyDoubleWrapper;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.ReadOnlyObjectWrapper;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.scene.Node;
-import javafx.scene.control.IndexRange;
-import static javafx.scene.input.KeyCode.D;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCombination.SHORTCUT_DOWN;
-import javafx.scene.input.KeyEvent;
-import org.fxmisc.flowless.VirtualizedScrollPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.fxmisc.undo.UndoManager;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static org.fxmisc.wellbehaved.event.InputMap.sequence;
-import org.fxmisc.wellbehaved.event.Nodes;
-import org.pegdown.PegDownProcessor;
-import org.pegdown.ast.RootNode;
-
-/**
- * Markdown editor pane.
- *
- * Uses pegdown (https://github.com/sirthias/pegdown) for styling the markdown
- * content within a text area.
- *
- * @author Karl Tauber
- */
-public class MarkdownEditorPane extends AbstractPane {
-
- private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
- "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
-
- private PegDownProcessor pegDownProcessor;
- private StyleClassedTextArea editor;
- private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
- private String lineSeparator = getLineSeparator();
-
- public MarkdownEditorPane() {
- initEditor();
- initScrollPane();
- initScrollEventListener();
- initOptionEventListener();
- }
-
- private void initEditor() {
- final StyleClassedTextArea textArea = getEditor();
-
- textArea.setWrapText( true );
- textArea.getStyleClass().add( "markdown-editor" );
- textArea.getStylesheets().add( STYLESHEET_EDITOR );
-
- textArea.textProperty().addListener( (observable, oldText, newText) -> {
- textChanged( newText );
- } );
-
- Nodes.addInputMap( textArea, sequence(
- consume( keyPressed( ENTER ), this::enterPressed ),
- consume( keyPressed( D, SHORTCUT_DOWN ), this::deleteLine ) )
- );
- }
-
- private void initScrollPane() {
- this.scrollPane = new VirtualizedScrollPane<>( getEditor() );
- }
-
- /**
- * Add a listener to update the scrollY property.
- */
- private void initScrollEventListener() {
- final StyleClassedTextArea textArea = getEditor();
-
- ChangeListener<Double> scrollYListener = (observable, oldValue, newValue) -> {
- double value = textArea.estimatedScrollYProperty().getValue();
- double maxValue = textArea.totalHeightEstimateProperty().getOrElse( 0. ) - textArea.getHeight();
- scrollY.set( (maxValue > 0) ? Math.min( Math.max( value / maxValue, 0 ), 1 ) : 0 );
- };
-
- textArea.estimatedScrollYProperty().addListener( scrollYListener );
- textArea.totalHeightEstimateProperty().addListener( scrollYListener );
- }
-
- /**
- * Listen to option changes.
- */
- private void initOptionEventListener() {
- StyleClassedTextArea textArea = getEditor();
-
- InvalidationListener listener = e -> {
- if( textArea.getScene() == null ) {
- // Editor closed but not yet garbage collected.
- return;
- }
-
- // Re-process markdown if markdown extensions option changes.
- if( e == getOptions().markdownExtensionsProperty() ) {
- pegDownProcessor = null;
- textChanged( textArea.getText() );
- }
- };
-
- WeakInvalidationListener weakOptionsListener = new WeakInvalidationListener( listener );
- getOptions().markdownExtensionsProperty().addListener( weakOptionsListener );
- }
-
- protected StyleClassedTextArea createTextArea() {
- return new StyleClassedTextArea( false );
- }
-
- private void setEditor( StyleClassedTextArea textArea ) {
- this.editor = textArea;
- }
-
- private synchronized StyleClassedTextArea getEditor() {
- if( this.editor == null ) {
- setEditor( createTextArea() );
- }
-
- return this.editor;
- }
-
- public Node getNode() {
- return scrollPane;
- }
-
- public UndoManager getUndoManager() {
- return editor.getUndoManager();
- }
-
- @Override
- public void requestFocus() {
- Platform.runLater( () -> editor.requestFocus() );
- }
-
- private String getLineSeparator() {
- final String separator = getOptions().getLineSeparator();
- return (separator != null) ? separator : System.getProperty( "line.separator", "\n" );
- }
-
- private String determineLineSeparator( String str ) {
- int strLength = str.length();
- for( int i = 0; i < strLength; i++ ) {
- char ch = str.charAt( i );
- if( ch == '\n' ) {
- return (i > 0 && str.charAt( i - 1 ) == '\r') ? "\r\n" : "\n";
- }
- }
- return getLineSeparator();
- }
-
- // 'markdown' property
- public String getMarkdown() {
- String markdown = editor.getText();
- if( !lineSeparator.equals( "\n" ) ) {
- markdown = markdown.replace( "\n", lineSeparator );
- }
- return markdown;
- }
-
- public void setMarkdown( String markdown ) {
- lineSeparator = determineLineSeparator( markdown );
- editor.replaceText( markdown );
- editor.selectRange( 0, 0 );
- }
-
- public ObservableValue<String> markdownProperty() {
- return editor.textProperty();
- }
-
- // 'markdownAST' property
- private final ReadOnlyObjectWrapper<RootNode> markdownAST = new ReadOnlyObjectWrapper<>();
-
- public RootNode getMarkdownAST() {
- return markdownAST.get();
- }
-
- public ReadOnlyObjectProperty<RootNode> markdownASTProperty() {
- return markdownAST.getReadOnlyProperty();
- }
-
- // 'scrollY' property
- private final ReadOnlyDoubleWrapper scrollY = new ReadOnlyDoubleWrapper();
-
- public double getScrollY() {
- return scrollY.get();
- }
-
- public ReadOnlyDoubleProperty scrollYProperty() {
- return scrollY.getReadOnlyProperty();
- }
-
- // 'path' property
- private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
-
- public Path getPath() {
- return path.get();
- }
-
- public void setPath( Path path ) {
- this.path.set( path );
- }
-
- public ObjectProperty<Path> pathProperty() {
- return path;
- }
-
- private Path getParentPath() {
- Path parentPath = getPath();
- return (parentPath != null) ? parentPath.getParent() : null;
- }
-
- private void textChanged( String newText ) {
- RootNode astRoot = parseMarkdown( newText );
- applyHighlighting( astRoot );
- markdownAST.set( astRoot );
- }
-
- /**
- * TODO: Change to interface so that other processors can be pipelined.
- *
- * @return
- */
- private synchronized PegDownProcessor getPegDownProcessor() {
- if( this.pegDownProcessor == null ) {
- this.pegDownProcessor = createPegDownProcessor();
- }
-
- return this.pegDownProcessor;
- }
-
- protected PegDownProcessor createPegDownProcessor() {
- return new PegDownProcessor( getOptions().getMarkdownExtensions() );
- }
-
- private RootNode parseMarkdown( String text ) {
- return getPegDownProcessor().parseMarkdown( text.toCharArray() );
- }
-
- private void applyHighlighting( RootNode astRoot ) {
- MarkdownSyntaxHighlighter.highlight( editor, astRoot );
- }
-
- private void enterPressed( KeyEvent e ) {
- String currentLine = editor.getText( editor.getCurrentParagraph() );
-
- String newText = "\n";
- Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
- if( matcher.matches() ) {
- if( !matcher.group( 2 ).isEmpty() ) {
- // indent new line with same whitespace characters and list markers as current line
- newText = newText.concat( matcher.group( 1 ) );
- } else {
- // current line contains only whitespace characters and list markers
- // --> empty current line
- int caretPosition = editor.getCaretPosition();
- editor.selectRange( caretPosition - currentLine.length(), caretPosition );
- }
- }
- editor.replaceSelection( newText );
- }
-
- private void deleteLine( KeyEvent e ) {
- int start = editor.getCaretPosition() - editor.getCaretColumn();
- int end = start + editor.getParagraph( editor.getCurrentParagraph() ).length() + 1;
- editor.deleteText( start, end );
- }
-
- public void undo() {
- editor.getUndoManager().undo();
- }
-
- public void redo() {
- editor.getUndoManager().redo();
- }
-
- public void surroundSelection( String leading, String trailing ) {
- surroundSelection( leading, trailing, null );
- }
-
- public void surroundSelection( String leading, String trailing, String hint ) {
- // Note: not using editor.insertText() to insert leading and trailing
- // because this would add two changes to undo history
-
- IndexRange selection = editor.getSelection();
- int start = selection.getStart();
- int end = selection.getEnd();
-
- String selectedText = editor.getSelectedText();
-
- // remove leading and trailing whitespaces from selected text
- String trimmedSelectedText = selectedText.trim();
- if( trimmedSelectedText.length() < selectedText.length() ) {
- start += selectedText.indexOf( trimmedSelectedText );
- end = start + trimmedSelectedText.length();
- }
-
- // remove leading whitespaces from leading text if selection starts at zero
- if( start == 0 ) {
- leading = Utils.ltrim( leading );
- }
-
- // remove trailing whitespaces from trailing text if selection ends at text end
- if( end == editor.getLength() ) {
- trailing = Utils.rtrim( trailing );
- }
-
- // remove leading line separators from leading text
- // if there are line separators before the selected text
- if( leading.startsWith( "\n" ) ) {
- for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
- if( !"\n".equals( editor.getText( i, i + 1 ) ) ) {
- break;
- }
- leading = leading.substring( 1 );
- }
- }
-
- // remove trailing line separators from trailing or leading text
- // if there are line separators after the selected text
- boolean trailingIsEmpty = trailing.isEmpty();
- String str = trailingIsEmpty ? leading : trailing;
- if( str.endsWith( "\n" ) ) {
- for( int i = end; i < editor.getLength() && str.endsWith( "\n" ); i++ ) {
- if( !"\n".equals( editor.getText( i, i + 1 ) ) ) {
- break;
- }
- str = str.substring( 0, str.length() - 1 );
- }
- if( trailingIsEmpty ) {
- leading = str;
- } else {
- trailing = str;
- }
- }
-
- int selStart = start + leading.length();
- int selEnd = end + leading.length();
-
- // insert hint text if selection is empty
- if( hint != null && trimmedSelectedText.isEmpty() ) {
- trimmedSelectedText = hint;
- selEnd = selStart + hint.length();
- }
-
- // prevent undo merging with previous text entered by user
- editor.getUndoManager().preventMerge();
-
- // replace text and update selection
- editor.replaceText( start, end, leading + trimmedSelectedText + trailing );
- editor.selectRange( selStart, selEnd );
- }
-
- public void insertLink() {
- LinkDialog dialog = new LinkDialog( getNode().getScene().getWindow(), getParentPath() );
- dialog.showAndWait().ifPresent( result -> {
- editor.replaceSelection( result );
- } );
- }
-
- public void insertImage() {
- ImageDialog dialog = new ImageDialog( getNode().getScene().getWindow(), getParentPath() );
- dialog.showAndWait().ifPresent( result -> {
- editor.replaceSelection( result );
+ * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivendor.editor;
+
+import static com.scrivendor.Constants.STYLESHEET_EDITOR;
+import com.scrivendor.dialogs.ImageDialog;
+import com.scrivendor.dialogs.LinkDialog;
+import com.scrivendor.ui.AbstractPane;
+import com.scrivendor.util.Utils;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.WeakInvalidationListener;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.Event;
+import javafx.scene.Node;
+import javafx.scene.control.IndexRange;
+import static javafx.scene.input.KeyCode.AT;
+import static javafx.scene.input.KeyCode.D;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.X;
+import static javafx.scene.input.KeyCode.Y;
+import static javafx.scene.input.KeyCombination.SHORTCUT_DOWN;
+import javafx.scene.input.KeyEvent;
+import org.fxmisc.flowless.VirtualizedScrollPane;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.fxmisc.undo.UndoManager;
+import org.fxmisc.wellbehaved.event.EventPattern;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+import org.fxmisc.wellbehaved.event.InputMap;
+import static org.fxmisc.wellbehaved.event.InputMap.consume;
+import static org.fxmisc.wellbehaved.event.InputMap.sequence;
+import org.fxmisc.wellbehaved.event.Nodes;
+import org.pegdown.PegDownProcessor;
+import org.pegdown.ast.RootNode;
+
+/**
+ * Markdown editor pane.
+ *
+ * Uses pegdown (https://github.com/sirthias/pegdown) for styling the markdown
+ * content within a text area.
+ *
+ * @author Karl Tauber,
+ */
+public class MarkdownEditorPane extends AbstractPane {
+
+ private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
+ "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
+
+ private PegDownProcessor pegDownProcessor;
+ private StyleClassedTextArea editor;
+ private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
+ private String lineSeparator = getLineSeparator();
+
+ private final ReadOnlyObjectWrapper<RootNode> markdownAST = new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyDoubleWrapper scrollY = new ReadOnlyDoubleWrapper();
+ private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
+
+ public MarkdownEditorPane() {
+ initEditor();
+ initScrollPane();
+ initScrollEventListener();
+ initOptionEventListener();
+ }
+
+ private void initEditor() {
+ final StyleClassedTextArea textArea = getEditor();
+
+ textArea.setWrapText( true );
+ textArea.getStyleClass().add( "markdown-editor" );
+ textArea.getStylesheets().add( STYLESHEET_EDITOR );
+
+ textArea.textProperty().addListener( (observable, oldText, newText) -> {
+ textChanged( newText );
+ } );
+
+ addEventListener( keyPressed( ENTER ), this::enterPressed );
+ addEventListener( keyPressed( X, SHORTCUT_DOWN ), this::cutLine );
+ addEventListener( keyPressed( Y, SHORTCUT_DOWN ), this::deleteLine );
+ }
+
+ public <T extends Event, U extends T> void addEventListener(
+ EventPattern<? super T, ? extends U> event, Consumer<? super U> consumer ) {
+ Nodes.addInputMap( getEditor(), sequence( consume( event, consumer ) ) );
+ }
+
+ private void initScrollPane() {
+ this.scrollPane = new VirtualizedScrollPane<>( getEditor() );
+ }
+
+ /**
+ * Add a listener to update the scrollY property.
+ */
+ private void initScrollEventListener() {
+ final StyleClassedTextArea textArea = getEditor();
+
+ ChangeListener<Double> scrollYListener = (observable, oldValue, newValue) -> {
+ double value = textArea.estimatedScrollYProperty().getValue();
+ double maxValue = textArea.totalHeightEstimateProperty().getOrElse( 0. ) - textArea.getHeight();
+ scrollY.set( (maxValue > 0) ? Math.min( Math.max( value / maxValue, 0 ), 1 ) : 0 );
+ };
+
+ textArea.estimatedScrollYProperty().addListener( scrollYListener );
+ textArea.totalHeightEstimateProperty().addListener( scrollYListener );
+ }
+
+ /**
+ * Listen to option changes.
+ */
+ private void initOptionEventListener() {
+ StyleClassedTextArea textArea = getEditor();
+
+ InvalidationListener listener = e -> {
+ if( textArea.getScene() == null ) {
+ // Editor closed but not yet garbage collected.
+ return;
+ }
+
+ // Re-process markdown if markdown extensions option changes.
+ if( e == getOptions().markdownExtensionsProperty() ) {
+ pegDownProcessor = null;
+ textChanged( textArea.getText() );
+ }
+ };
+
+ WeakInvalidationListener weakOptionsListener = new WeakInvalidationListener( listener );
+ getOptions().markdownExtensionsProperty().addListener( weakOptionsListener );
+ }
+
+ protected StyleClassedTextArea createTextArea() {
+ return new StyleClassedTextArea( false );
+ }
+
+ private void setEditor( StyleClassedTextArea textArea ) {
+ this.editor = textArea;
+ }
+
+ private synchronized StyleClassedTextArea getEditor() {
+ if( this.editor == null ) {
+ setEditor( createTextArea() );
+ }
+
+ return this.editor;
+ }
+
+ public Node getNode() {
+ return scrollPane;
+ }
+
+ public UndoManager getUndoManager() {
+ return getEditor().getUndoManager();
+ }
+
+ @Override
+ public void requestFocus() {
+ Platform.runLater( () -> getEditor().requestFocus() );
+ }
+
+ private String getLineSeparator() {
+ final String separator = getOptions().getLineSeparator();
+ return (separator != null) ? separator : System.getProperty( "line.separator", "\n" );
+ }
+
+ private String determineLineSeparator( String str ) {
+ int strLength = str.length();
+ for( int i = 0; i < strLength; i++ ) {
+ char ch = str.charAt( i );
+ if( ch == '\n' ) {
+ return (i > 0 && str.charAt( i - 1 ) == '\r') ? "\r\n" : "\n";
+ }
+ }
+ return getLineSeparator();
+ }
+
+ public String getMarkdown() {
+ String markdown = getEditor().getText();
+ if( !lineSeparator.equals( "\n" ) ) {
+ markdown = markdown.replace( "\n", lineSeparator );
+ }
+ return markdown;
+ }
+
+ public void setMarkdown( String markdown ) {
+ lineSeparator = determineLineSeparator( markdown );
+ getEditor().replaceText( markdown );
+ getEditor().selectRange( 0, 0 );
+ }
+
+ public ObservableValue<String> markdownProperty() {
+ return getEditor().textProperty();
+ }
+
+ public RootNode getMarkdownAST() {
+ return markdownAST.get();
+ }
+
+ public ReadOnlyObjectProperty<RootNode> markdownASTProperty() {
+ return markdownAST.getReadOnlyProperty();
+ }
+
+ public double getScrollY() {
+ return scrollY.get();
+ }
+
+ public ReadOnlyDoubleProperty scrollYProperty() {
+ return scrollY.getReadOnlyProperty();
+ }
+
+ public Path getPath() {
+ return path.get();
+ }
+
+ public void setPath( Path path ) {
+ this.path.set( path );
+ }
+
+ public ObjectProperty<Path> pathProperty() {
+ return path;
+ }
+
+ private Path getParentPath() {
+ Path parentPath = getPath();
+ return (parentPath != null) ? parentPath.getParent() : null;
+ }
+
+ private void textChanged( String newText ) {
+ RootNode astRoot = parseMarkdown( newText );
+ applyHighlighting( astRoot );
+ markdownAST.set( astRoot );
+ }
+
+ /**
+ * TODO: Change to interface so that other processors can be pipelined.
+ *
+ * @return
+ */
+ private synchronized PegDownProcessor getPegDownProcessor() {
+ if( this.pegDownProcessor == null ) {
+ this.pegDownProcessor = createPegDownProcessor();
+ }
+
+ return this.pegDownProcessor;
+ }
+
+ protected PegDownProcessor createPegDownProcessor() {
+ return new PegDownProcessor( getOptions().getMarkdownExtensions() );
+ }
+
+ private RootNode parseMarkdown( String text ) {
+ return getPegDownProcessor().parseMarkdown( text.toCharArray() );
+ }
+
+ private void applyHighlighting( RootNode astRoot ) {
+ MarkdownSyntaxHighlighter.highlight( editor, astRoot );
+ }
+
+ private void atPressed( KeyEvent e ) {
+
+ }
+
+ private void enterPressed( KeyEvent e ) {
+ final String currentLine = getEditor().getText( getEditor().getCurrentParagraph() );
+ final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
+
+ String newText = "\n";
+
+ if( matcher.matches() ) {
+ if( !matcher.group( 2 ).isEmpty() ) {
+ // indent new line with same whitespace characters and list markers as current line
+ newText = newText.concat( matcher.group( 1 ) );
+ } else {
+ // current line contains only whitespace characters and list markers
+ // --> empty current line
+ int caretPosition = getEditor().getCaretPosition();
+ getEditor().selectRange( caretPosition - currentLine.length(), caretPosition );
+ }
+ }
+
+ getEditor().replaceSelection( newText );
+ }
+
+ /**
+ * TODO: Copy the line into the paste buffer before deleting.
+ *
+ * @param e
+ */
+ private void cutLine( KeyEvent e ) {
+ deleteLine( e );
+ }
+
+ private void deleteLine( KeyEvent e ) {
+ int start = getEditor().getCaretPosition() - getEditor().getCaretColumn();
+ int end = start + getEditor().getParagraph( getEditor().getCurrentParagraph() ).length() + 1;
+ getEditor().deleteText( start, end );
+ }
+
+ public void undo() {
+ getEditor().getUndoManager().undo();
+ }
+
+ public void redo() {
+ getEditor().getUndoManager().redo();
+ }
+
+ public void surroundSelection( String leading, String trailing ) {
+ surroundSelection( leading, trailing, null );
+ }
+
+ public void surroundSelection( String leading, String trailing, String hint ) {
+ // Note: not using getEditor().insertText() to insert leading and trailing
+ // because this would add two changes to undo history
+
+ IndexRange selection = getEditor().getSelection();
+ int start = selection.getStart();
+ int end = selection.getEnd();
+
+ String selectedText = getEditor().getSelectedText();
+
+ // remove leading and trailing whitespaces from selected text
+ String trimmedSelectedText = selectedText.trim();
+ if( trimmedSelectedText.length() < selectedText.length() ) {
+ start += selectedText.indexOf( trimmedSelectedText );
+ end = start + trimmedSelectedText.length();
+ }
+
+ // remove leading whitespaces from leading text if selection starts at zero
+ if( start == 0 ) {
+ leading = Utils.ltrim( leading );
+ }
+
+ // remove trailing whitespaces from trailing text if selection ends at text end
+ if( end == getEditor().getLength() ) {
+ trailing = Utils.rtrim( trailing );
+ }
+
+ // remove leading line separators from leading text
+ // if there are line separators before the selected text
+ if( leading.startsWith( "\n" ) ) {
+ for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
+ if( !"\n".equals( getEditor().getText( i, i + 1 ) ) ) {
+ break;
+ }
+ leading = leading.substring( 1 );
+ }
+ }
+
+ // remove trailing line separators from trailing or leading text
+ // if there are line separators after the selected text
+ boolean trailingIsEmpty = trailing.isEmpty();
+ String str = trailingIsEmpty ? leading : trailing;
+ if( str.endsWith( "\n" ) ) {
+ for( int i = end; i < getEditor().getLength() && str.endsWith( "\n" ); i++ ) {
+ if( !"\n".equals( getEditor().getText( i, i + 1 ) ) ) {
+ break;
+ }
+ str = str.substring( 0, str.length() - 1 );
+ }
+ if( trailingIsEmpty ) {
+ leading = str;
+ } else {
+ trailing = str;
+ }
+ }
+
+ int selStart = start + leading.length();
+ int selEnd = end + leading.length();
+
+ // insert hint text if selection is empty
+ if( hint != null && trimmedSelectedText.isEmpty() ) {
+ trimmedSelectedText = hint;
+ selEnd = selStart + hint.length();
+ }
+
+ // prevent undo merging with previous text entered by user
+ getEditor().getUndoManager().preventMerge();
+
+ // replace text and update selection
+ getEditor().replaceText( start, end, leading + trimmedSelectedText + trailing );
+ getEditor().selectRange( selStart, selEnd );
+ }
+
+ public void insertLink() {
+ LinkDialog dialog = new LinkDialog( getNode().getScene().getWindow(), getParentPath() );
+ dialog.showAndWait().ifPresent( result -> {
+ getEditor().replaceSelection( result );
+ } );
+ }
+
+ public void insertImage() {
+ ImageDialog dialog = new ImageDialog( getNode().getScene().getWindow(), getParentPath() );
+ dialog.showAndWait().ifPresent( result -> {
+ getEditor().replaceSelection( result );
} );
}