Dave Jarvis' Repositories

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

Fix word at caret bug

Author DaveJarvis <email>
Date 2020-12-15 20:05:52 GMT-0800
Commit 14952c090af095940114582b779fbc2504b53927
Parent 69a550d
src/main/java/com/keenwrite/MainWindow.java
import javafx.scene.Node;
import javafx.scene.control.MenuBar;
-import javafx.scene.control.TextField;
-import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
-import org.controlsfx.control.StatusBar;
import static com.keenwrite.Messages.get;
import static com.keenwrite.ui.actions.ApplicationMenuBar.createMenu;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
/**
* Main window containing a tab pane in the center for file editors.
*
* @deprecated Use {@link MainPane}.
*/
@Deprecated
public class MainWindow {
- private final TextField mFindTextField;
private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane();
public MainWindow() {
- mFindTextField = createFindTextField();
- }
-
- /**
- * Called after the stage is shown.
- */
- public void init() {
- initFindInput();
- }
-
- /**
- * Initialize the find input text field to listen on F3, ENTER, and
- * ESCAPE key presses.
- */
- private void initFindInput() {
- final TextField input = getFindTextField();
-
- input.setOnKeyPressed( ( KeyEvent event ) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- editFindNext();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- ( focused, oldFocus, newFocus ) -> {
- if( !newFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
- }
-
- //---- Edit actions -------------------------------------------------------
-
- /**
- * Used to find text in the active file editor window.
- */
- private void editFind() {
- final TextField input = getFindTextField();
- getStatusBar().setGraphic( input );
- input.requestFocus();
- }
-
- public void editFindNext() {
- getActiveFileEditorTab().searchNext( getFindTextField().getText() );
}
public void editPreferences() {
getUserPreferencesView().show();
}
//---- Member creators ----------------------------------------------------
-
- private TextField createFindTextField() {
- return new TextField();
- }
private Node createMenuBar() {
- // Edit actions
- final Action editFindAction = Action
- .builder()
- .setText( "Main.menu.edit.find" )
- .setAccelerator( "Ctrl+F" )
- .setIcon( SEARCH )
- .setHandler( e -> editFind() )
- .build();
- final Action editFindNextAction = Action
- .builder()
- .setText( "Main.menu.edit.find_next" )
- .setAccelerator( "F3" )
- .setHandler( e -> editFindNext() )
- .build();
final Action editPreferencesAction = Action
.builder()
.setHandler( e -> getActiveEditorPane().insertImage() )
.build();
-
- final MenuAction SEPARATOR_ACTION = new SeparatorAction();
//---- MenuBar ----
// Edit Menu
final var editMenu = createMenu(
get( "Main.menu.edit" ),
- editFindAction,
- editFindNextAction,
- SEPARATOR_ACTION,
editPreferencesAction );
private FileEditorTabPane getFileEditorPane() {
return mFileEditorPane;
- }
-
- private StatusBar getStatusBar() {
- return StatusBarNotifier.getStatusBar();
- }
-
- private TextField getFindTextField() {
- return mFindTextField;
}
-
//---- Persistence accessors ----------------------------------------------
src/main/java/com/keenwrite/editors/TextEditor.java
/**
+ * Moves the caret to the given document offset.
+ *
+ * @param offset The absolute offset into the document, zero-based.
+ */
+ void moveTo( final int offset );
+
+ /**
* Returns an object that can be used to track the current caret position
* within the document.
*/
IndexRange getCaretWord();
+
+ /**
+ * Convenience method to get the word at the current caret position.
+ *
+ * @return This will return the empty string if the caret is out of bounds.
+ */
+ default String getCaretWordText() {
+ return getText( getCaretWord() );
+ }
/**
src/main/java/com/keenwrite/editors/base/PlainTextEditor.java
}
+ @Override
+ public void moveTo( final int offset ) {
+ super.moveTo( offset );
+ }
+
/**
* TODO: Merge functionality from {@link MarkdownEditor}.
src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
}
- /**
- * Delegate the focus request to the text area itself.
- */
- @Override
- public void requestFocus() {
- mTextArea.requestFocus();
- }
-
- @Override
- public void setText( final String text ) {
- mTextArea.clear();
- mTextArea.appendText( text );
- mTextArea.getUndoManager().mark();
- }
-
- @Override
- public String getText() {
- return mTextArea.getText();
- }
-
- @Override
- public Charset getEncoding() {
- return mEncoding;
- }
-
- @Override
- public File getFile() {
- return mFile;
- }
-
- @Override
- public void rename( final File file ) {
- mFile = file;
- }
-
- @Override
- public void undo() {
- final var manager = getUndoManager();
- xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
- }
-
- @Override
- public void redo() {
- final var manager = getUndoManager();
- xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
- }
-
- /**
- * Performs an undo or redo action, if possible, otherwise displays an error
- * message to the user.
- *
- * @param ready Answers whether the action can be executed.
- * @param action The action to execute.
- * @param key The informational message key having a value to display if
- * the {@link Supplier} is not ready.
- */
- private void xxdo(
- final Supplier<Boolean> ready, final Runnable action, final String key ) {
- if( ready.get() ) {
- action.run();
- }
- else {
- clue( key );
- }
- }
-
- @Override
- public void cut() {
- final var selected = mTextArea.getSelectedText();
-
- if( selected == null || selected.isEmpty() ) {
- mTextArea.selectLine();
- }
-
- mTextArea.cut();
- }
-
- @Override
- public void copy() {
- mTextArea.copy();
- }
-
- @Override
- public void paste() {
- mTextArea.paste();
- }
-
- @Override
- public void selectAll() {
- mTextArea.selectAll();
- }
-
- @Override
- public void bold() {
- enwrap( "**" );
- }
-
- @Override
- public void italic() {
- enwrap( "*" );
- }
-
- @Override
- public void superscript() {
- enwrap( "^" );
- }
-
- @Override
- public void subscript() {
- enwrap( "~" );
- }
-
- @Override
- public void strikethrough() {
- enwrap( "~~" );
- }
-
- @Override
- public void blockquote() {
- block( "> " );
- }
-
- @Override
- public void code() {
- enwrap( "`" );
- }
-
- @Override
- public void fencedCodeBlock() {
- final var key = "App.action.insert.fenced_code_block.prompt.text";
-
- // TODO: Introduce sample text if nothing is selected.
- //enwrap( "\n\n```\n", "\n```\n\n", get( key ) );
- }
-
- @Override
- public void heading( final int level ) {
- final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
- final var markup = format( "%s ", hashes );
-
- block( markup );
- }
-
- @Override
- public void unorderedList() {
- block( "* " );
- }
-
- @Override
- public void orderedList() {
- block( "1. " );
- }
-
- @Override
- public void horizontalRule() {
- block( format( "---%n%n" ) );
- }
-
- @Override
- public Node getNode() {
- return this;
- }
-
- @Override
- public ReadOnlyBooleanProperty modifiedProperty() {
- return mModified;
- }
-
- @Override
- public void clearModifiedProperty() {
- getUndoManager().mark();
- }
-
- @Override
- public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
- return mScrollPane;
- }
-
- @Override
- public Caret getCaret() {
- return mCaret;
- }
-
- private Caret createCaret( final StyleClassedTextArea editor ) {
- return Caret
- .builder()
- .with( Caret.Mutator::setEditor, editor )
- .build();
- }
-
- /**
- * This method adds listeners to editor events.
- *
- * @param <T> The event type.
- * @param <U> The consumer type for the given event type.
- * @param event The event of interest.
- * @param consumer The method to call when the event happens.
- */
- public <T extends Event, U extends T> void addEventListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- Nodes.addInputMap( mTextArea, consume( event, consumer ) );
- }
-
- @SuppressWarnings("unused")
- private void onEnterPressed( final KeyEvent event ) {
- final var currentLine = getCaretParagraph();
- final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
-
- // By default, insert a new line by itself.
- String newText = NEWLINE;
-
- // If the pattern was matched then determine what block type to continue.
- if( matcher.matches() ) {
- if( matcher.group( 2 ).isEmpty() ) {
- final var pos = mTextArea.getCaretPosition();
- mTextArea.selectRange( pos - currentLine.length(), pos );
- }
- else {
- // Indent the new line with the same whitespace characters and
- // list markers as current line. This ensures that the indentation
- // is propagated.
- newText = newText.concat( matcher.group( 1 ) );
- }
- }
-
- mTextArea.replaceSelection( newText );
- }
-
- private void cut( final KeyEvent event ) {
- cut();
- }
-
- private void tab( final KeyEvent event ) {
- final var range = mTextArea.selectionProperty().getValue();
- final var sb = new StringBuilder( 1024 );
-
- if( range.getLength() > 0 ) {
- final var selection = mTextArea.getSelectedText();
-
- selection.lines().forEach(
- ( l ) -> sb.append( "\t" ).append( l ).append( NEWLINE )
- );
- }
- else {
- sb.append( "\t" );
- }
-
- mTextArea.replaceSelection( sb.toString() );
- }
-
- private void untab( final KeyEvent event ) {
- final var range = mTextArea.selectionProperty().getValue();
-
- if( range.getLength() > 0 ) {
- final var selection = mTextArea.getSelectedText();
- final var sb = new StringBuilder( selection.length() );
-
- selection.lines().forEach(
- ( l ) -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
- .append( NEWLINE )
- );
-
- mTextArea.replaceSelection( sb.toString() );
- }
- else {
- final var p = getCaretParagraph();
-
- if( p.startsWith( "\t" ) ) {
- mTextArea.selectParagraph();
- mTextArea.replaceSelection( p.substring( 1 ) );
- }
- }
- }
-
- /**
- * Observers may listen for changes to the property returned from this method
- * to receive notifications when either the text or caret have changed.
- * This should not be used to track whether the text has been modified.
- */
- public void addDirtyListener( ChangeListener<Boolean> listener ) {
- mDirty.addListener( listener );
- }
-
- /**
- * Surrounds the selected text or word under the caret in Markdown markup.
- *
- * @param token The beginning and ending token for enclosing the text.
- */
- private void enwrap( final String token ) {
- enwrap( token, token );
- }
-
- /**
- * Surrounds the selected text or word under the caret in Markdown markup.
- *
- * @param began The beginning token for enclosing the text.
- * @param ended The ending token for enclosing the text.
- */
- private void enwrap( final String began, String ended ) {
- final var range = mTextArea.getSelection();
- String text = mTextArea.getText(
- range.getLength() == 0 ? getCaretWord() : range
- );
-
- int length = range.getLength();
- text = stripStart( text, null );
- final int beganIndex = range.getStart() + (length - text.length());
-
- length = text.length();
- text = stripEnd( text, null );
- final int endedIndex = range.getEnd() - (length - text.length());
-
- mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
- }
-
- /**
- * Inserts the given block-level markup at the current caret position
- * within the document. This will prepend two blank lines to ensure that
- * the block element begins at the start of a new line.
- *
- * @param markup The text to insert at the caret.
- */
- private void block( final String markup ) {
- final int pos = mTextArea.getCaretPosition();
- mTextArea.insertText( pos, format( "%n%n%s", markup ) );
- }
-
- /**
- * Returns the caret position within the current paragraph.
- *
- * @return A value from 0 to the length of the current paragraph.
- */
- private int getCaretColumn() {
- return mTextArea.getCaretColumn();
- }
-
- @Override
- public IndexRange getCaretWord() {
- final var paragraph = getCaretParagraph();
- final var length = paragraph.length();
- final var column = getCaretColumn();
-
- var began = column;
- var ended = column;
-
- while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
- began--;
- }
-
- while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
- ended++;
- }
-
- final var iterator = BreakIterator.getWordInstance();
- iterator.setText( paragraph );
-
- while( began < length && iterator.isBoundary( began + 1 ) ) {
- began++;
- }
-
- while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
- ended--;
- }
-
- final var offset = getCaretDocumentOffset( column );
-
- return IndexRange.normalize( began + offset, ended + offset );
- }
-
- private int getCaretDocumentOffset( final int column ) {
- return mTextArea.getCaretPosition() - column;
- }
-
- /**
- * Returns the index of the paragraph where the caret resides.
- *
- * @return A number greater than or equal to 0.
- */
- private int getCurrentParagraph() {
- return mTextArea.getCurrentParagraph();
- }
-
- /**
- * Returns the text for the paragraph that contains the caret.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCaretParagraph() {
- return getText( getCurrentParagraph() );
- }
-
- @Override
- public String getText( final int paragraph ) {
- return mTextArea.getText( paragraph );
- }
-
- @Override
- public String getText( final IndexRange indexes )
- throws IndexOutOfBoundsException {
- return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
- }
-
- @Override
- public void replaceText(final IndexRange indexes, final String s) {
+ @Override
+ public void moveTo( final int offset ) {
+ assert 0 <= offset && offset <= mTextArea.getLength();
+ mTextArea.moveTo( offset );
+ }
+
+ /**
+ * Delegate the focus request to the text area itself.
+ */
+ @Override
+ public void requestFocus() {
+ mTextArea.requestFocus();
+ }
+
+ @Override
+ public void setText( final String text ) {
+ mTextArea.clear();
+ mTextArea.appendText( text );
+ mTextArea.getUndoManager().mark();
+ }
+
+ @Override
+ public String getText() {
+ return mTextArea.getText();
+ }
+
+ @Override
+ public Charset getEncoding() {
+ return mEncoding;
+ }
+
+ @Override
+ public File getFile() {
+ return mFile;
+ }
+
+ @Override
+ public void rename( final File file ) {
+ mFile = file;
+ }
+
+ @Override
+ public void undo() {
+ final var manager = getUndoManager();
+ xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
+ }
+
+ @Override
+ public void redo() {
+ final var manager = getUndoManager();
+ xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
+ }
+
+ /**
+ * Performs an undo or redo action, if possible, otherwise displays an error
+ * message to the user.
+ *
+ * @param ready Answers whether the action can be executed.
+ * @param action The action to execute.
+ * @param key The informational message key having a value to display if
+ * the {@link Supplier} is not ready.
+ */
+ private void xxdo(
+ final Supplier<Boolean> ready, final Runnable action, final String key ) {
+ if( ready.get() ) {
+ action.run();
+ }
+ else {
+ clue( key );
+ }
+ }
+
+ @Override
+ public void cut() {
+ final var selected = mTextArea.getSelectedText();
+
+ if( selected == null || selected.isEmpty() ) {
+ mTextArea.selectLine();
+ }
+
+ mTextArea.cut();
+ }
+
+ @Override
+ public void copy() {
+ mTextArea.copy();
+ }
+
+ @Override
+ public void paste() {
+ mTextArea.paste();
+ }
+
+ @Override
+ public void selectAll() {
+ mTextArea.selectAll();
+ }
+
+ @Override
+ public void bold() {
+ enwrap( "**" );
+ }
+
+ @Override
+ public void italic() {
+ enwrap( "*" );
+ }
+
+ @Override
+ public void superscript() {
+ enwrap( "^" );
+ }
+
+ @Override
+ public void subscript() {
+ enwrap( "~" );
+ }
+
+ @Override
+ public void strikethrough() {
+ enwrap( "~~" );
+ }
+
+ @Override
+ public void blockquote() {
+ block( "> " );
+ }
+
+ @Override
+ public void code() {
+ enwrap( "`" );
+ }
+
+ @Override
+ public void fencedCodeBlock() {
+ final var key = "App.action.insert.fenced_code_block.prompt.text";
+
+ // TODO: Introduce sample text if nothing is selected.
+ //enwrap( "\n\n```\n", "\n```\n\n", get( key ) );
+ }
+
+ @Override
+ public void heading( final int level ) {
+ final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
+ final var markup = format( "%s ", hashes );
+
+ block( markup );
+ }
+
+ @Override
+ public void unorderedList() {
+ block( "* " );
+ }
+
+ @Override
+ public void orderedList() {
+ block( "1. " );
+ }
+
+ @Override
+ public void horizontalRule() {
+ block( format( "---%n%n" ) );
+ }
+
+ @Override
+ public Node getNode() {
+ return this;
+ }
+
+ @Override
+ public ReadOnlyBooleanProperty modifiedProperty() {
+ return mModified;
+ }
+
+ @Override
+ public void clearModifiedProperty() {
+ getUndoManager().mark();
+ }
+
+ @Override
+ public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
+ return mScrollPane;
+ }
+
+ @Override
+ public Caret getCaret() {
+ return mCaret;
+ }
+
+ private Caret createCaret( final StyleClassedTextArea editor ) {
+ return Caret
+ .builder()
+ .with( Caret.Mutator::setEditor, editor )
+ .build();
+ }
+
+ /**
+ * This method adds listeners to editor events.
+ *
+ * @param <T> The event type.
+ * @param <U> The consumer type for the given event type.
+ * @param event The event of interest.
+ * @param consumer The method to call when the event happens.
+ */
+ public <T extends Event, U extends T> void addEventListener(
+ final EventPattern<? super T, ? extends U> event,
+ final Consumer<? super U> consumer ) {
+ Nodes.addInputMap( mTextArea, consume( event, consumer ) );
+ }
+
+ @SuppressWarnings("unused")
+ private void onEnterPressed( final KeyEvent event ) {
+ final var currentLine = getCaretParagraph();
+ final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
+
+ // By default, insert a new line by itself.
+ String newText = NEWLINE;
+
+ // If the pattern was matched then determine what block type to continue.
+ if( matcher.matches() ) {
+ if( matcher.group( 2 ).isEmpty() ) {
+ final var pos = mTextArea.getCaretPosition();
+ mTextArea.selectRange( pos - currentLine.length(), pos );
+ }
+ else {
+ // Indent the new line with the same whitespace characters and
+ // list markers as current line. This ensures that the indentation
+ // is propagated.
+ newText = newText.concat( matcher.group( 1 ) );
+ }
+ }
+
+ mTextArea.replaceSelection( newText );
+ }
+
+ private void cut( final KeyEvent event ) {
+ cut();
+ }
+
+ private void tab( final KeyEvent event ) {
+ final var range = mTextArea.selectionProperty().getValue();
+ final var sb = new StringBuilder( 1024 );
+
+ if( range.getLength() > 0 ) {
+ final var selection = mTextArea.getSelectedText();
+
+ selection.lines().forEach(
+ ( l ) -> sb.append( "\t" ).append( l ).append( NEWLINE )
+ );
+ }
+ else {
+ sb.append( "\t" );
+ }
+
+ mTextArea.replaceSelection( sb.toString() );
+ }
+
+ private void untab( final KeyEvent event ) {
+ final var range = mTextArea.selectionProperty().getValue();
+
+ if( range.getLength() > 0 ) {
+ final var selection = mTextArea.getSelectedText();
+ final var sb = new StringBuilder( selection.length() );
+
+ selection.lines().forEach(
+ ( l ) -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
+ .append( NEWLINE )
+ );
+
+ mTextArea.replaceSelection( sb.toString() );
+ }
+ else {
+ final var p = getCaretParagraph();
+
+ if( p.startsWith( "\t" ) ) {
+ mTextArea.selectParagraph();
+ mTextArea.replaceSelection( p.substring( 1 ) );
+ }
+ }
+ }
+
+ /**
+ * Observers may listen for changes to the property returned from this method
+ * to receive notifications when either the text or caret have changed. This
+ * should not be used to track whether the text has been modified.
+ */
+ public void addDirtyListener( ChangeListener<Boolean> listener ) {
+ mDirty.addListener( listener );
+ }
+
+ /**
+ * Surrounds the selected text or word under the caret in Markdown markup.
+ *
+ * @param token The beginning and ending token for enclosing the text.
+ */
+ private void enwrap( final String token ) {
+ enwrap( token, token );
+ }
+
+ /**
+ * Surrounds the selected text or word under the caret in Markdown markup.
+ *
+ * @param began The beginning token for enclosing the text.
+ * @param ended The ending token for enclosing the text.
+ */
+ private void enwrap( final String began, String ended ) {
+ // Ensure selected text takes precedence over the word at caret position.
+ final var selected = mTextArea.selectionProperty().getValue();
+ final var range = selected.getLength() == 0
+ ? getCaretWord()
+ : selected;
+ String text = mTextArea.getText( range );
+
+ int length = range.getLength();
+ text = stripStart( text, null );
+ final int beganIndex = range.getStart() + (length - text.length());
+
+ length = text.length();
+ text = stripEnd( text, null );
+ final int endedIndex = range.getEnd() - (length - text.length());
+
+ mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
+ }
+
+ /**
+ * Inserts the given block-level markup at the current caret position
+ * within the document. This will prepend two blank lines to ensure that
+ * the block element begins at the start of a new line.
+ *
+ * @param markup The text to insert at the caret.
+ */
+ private void block( final String markup ) {
+ final int pos = mTextArea.getCaretPosition();
+ mTextArea.insertText( pos, format( "%n%n%s", markup ) );
+ }
+
+ /**
+ * Returns the caret position within the current paragraph.
+ *
+ * @return A value from 0 to the length of the current paragraph.
+ */
+ private int getCaretColumn() {
+ return mTextArea.getCaretColumn();
+ }
+
+ @Override
+ public IndexRange getCaretWord() {
+ final var paragraph = getCaretParagraph();
+ final var length = paragraph.length();
+ final var column = getCaretColumn();
+
+ var began = column;
+ var ended = column;
+
+ while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
+ began--;
+ }
+
+ while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
+ ended++;
+ }
+
+ final var iterator = BreakIterator.getWordInstance();
+ iterator.setText( paragraph );
+
+ while( began < length && iterator.isBoundary( began + 1 ) ) {
+ began++;
+ }
+
+ while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
+ ended--;
+ }
+
+ final var offset = getCaretDocumentOffset( column );
+
+ return IndexRange.normalize( began + offset, ended + offset );
+ }
+
+ private int getCaretDocumentOffset( final int column ) {
+ return mTextArea.getCaretPosition() - column;
+ }
+
+ /**
+ * Returns the index of the paragraph where the caret resides.
+ *
+ * @return A number greater than or equal to 0.
+ */
+ private int getCurrentParagraph() {
+ return mTextArea.getCurrentParagraph();
+ }
+
+ /**
+ * Returns the text for the paragraph that contains the caret.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getCaretParagraph() {
+ return getText( getCurrentParagraph() );
+ }
+
+ @Override
+ public String getText( final int paragraph ) {
+ return mTextArea.getText( paragraph );
+ }
+
+ @Override
+ public String getText( final IndexRange indexes )
+ throws IndexOutOfBoundsException {
+ return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
+ }
+
+ @Override
+ public void replaceText( final IndexRange indexes, final String s ) {
mTextArea.replaceText( indexes, s );
}
src/main/java/com/keenwrite/ui/actions/ApplicationActions.java
/**
- * Tracks searching.
+ * Tracks finding text in the active document.
*/
private final SearchModel mSearchModel;
alert.setGraphic( new ImageView( ICON_DIALOG ) );
alert.initOwner( getWindow() );
-
alert.showAndWait();
}
src/test/java/com/keenwrite/editors/markdown/MarkdownEditorTest.java
+package com.keenwrite.editors.markdown;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testfx.framework.junit5.ApplicationExtension;
+
+import java.util.regex.Pattern;
+
+import static java.util.regex.Pattern.compile;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(ApplicationExtension.class)
+public class MarkdownEditorTest {
+ private static final String[] WORDS = new String[]{
+ "Italicize",
+ "English's",
+ "foreign",
+ "words",
+ "based",
+ "on",
+ "popularity,",
+ "like",
+ "_bête_",
+ "_noire_",
+ "and",
+ "_Weltanschauung_",
+ "but",
+ "not",
+ "résumé.",
+ "Don't",
+ "omit",
+ "accented",
+ "characters!",
+ "Cœlacanthe",
+ "L'Haÿ-les-Roses",
+ "Mühlfeldstraße",
+ };
+
+ private static final String TEXT = String.join( " ", WORDS );
+
+ private static final Pattern REGEX = compile(
+ "[^\\p{IsAlphabetic}\\p{IsDigit}'-]" );
+
+ /**
+ * Test that the {@link MarkdownEditor} can retrieve a word at the caret
+ * position, regardless of whether the caret is at the beginning, middle, or
+ * end of the word.
+ */
+ @Test
+ public void test_CaretWord_GetISO88591Word_WordSelected() {
+ final var editor = createMarkdownEditor();
+
+ for( int i = 0; i < WORDS.length; i++ ) {
+ final var word = WORDS[ i ];
+ final var len = word.length();
+ final var expected = REGEX.matcher( word ).replaceAll( "" );
+
+ for( int j = 0; j < len; j++ ) {
+ editor.moveTo( offset( i ) + j );
+ final var actual = editor.getCaretWordText();
+ assertEquals( expected, actual );
+ }
+ }
+ }
+
+ /**
+ * Test that the {@link MarkdownEditor} can make a word bold.
+ */
+ @Test
+ public void test_CaretWord_SetWordBold_WordIsBold() {
+ final var index = 20;
+ final var editor = createMarkdownEditor();
+
+ editor.moveTo( offset( index ) );
+ editor.bold();
+ assertTrue( editor.getText().contains( "**" + WORDS[ index ] + "**" ) );
+ }
+
+ /**
+ * Returns the document offset for a string at the given index.
+ */
+ private static int offset( final int index ) {
+ assert 0 <= index && index < WORDS.length;
+ int offset = 0;
+
+ for( int i = 0; i < index; i++ ) {
+ offset += WORDS[ i ].length();
+ }
+
+ // Add the index to compensate for one space between words.
+ return offset + index;
+ }
+
+ /**
+ * Returns an instance of {@link MarkdownEditor} pre-populated with
+ * {@link #TEXT}.
+ *
+ * @return A new {@link MarkdownEditor} instance, ready for unit tests.
+ */
+ private MarkdownEditor createMarkdownEditor() {
+ final var editor = new MarkdownEditor();
+ editor.setText( TEXT );
+ return editor;
+ }
+}
Delta 543 lines added, 493 lines removed, 50-line increase