Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
src/main/java/com/keenwrite/search/SearchModel.java
+/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.search;
+
+import com.keenwrite.util.CyclicIterator;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.IndexRange;
+import org.ahocorasick.trie.Emit;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+
+import static org.ahocorasick.trie.Trie.TrieBuilder;
+import static org.ahocorasick.trie.Trie.builder;
+
+/**
+ * Responsible for finding words in a text document.
+ */
+public class SearchModel {
+ private final TrieBuilder mBuilder = builder().ignoreCase().ignoreOverlaps();
+
+ private final ObjectProperty<IndexRange> mMatchProperty =
+ new SimpleObjectProperty<>();
+
+ private ListIterator<Emit> mMatches = CyclicIterator.of( List.of() );
+
+ /**
+ * The document to search.
+ */
+ private final String mHaystack;
+
+ /**
+ * Creates a new {@link SearchModel} that finds all text string in a
+ * document simultaneously.
+ *
+ * @param haystack The document to search for a text string.
+ */
+ public SearchModel( final String haystack ) {
+ mHaystack = haystack;
+ }
+
+ /**
+ * Observers can bind to this property to be informed when the current
+ * matched needle has been found in the haystack.
+ *
+ * @return The {@link IndexRange} property to observe, representing the
+ * most recently matched text offset into the document.
+ */
+ public ObjectProperty<IndexRange> matchProperty() {
+ return mMatchProperty;
+ }
+
+ /**
+ * Searches the document for text matching the given parameter value. This
+ * is the main entry point for kicking off text searches.
+ *
+ * @param needle The text string to find in the document, no regex allowed.
+ */
+ public void search( final String needle ) {
+ final var trie = mBuilder.addKeyword( needle ).build();
+ final var emits = trie.parseText( mHaystack );
+
+ mMatches = CyclicIterator.of( new ArrayList<>( emits ) );
+ }
+
+ /**
+ * Moves the search iterator to the next match, wrapping as needed.
+ */
+ public void advance() {
+ setCurrent( mMatches.next() );
+ }
+
+ /**
+ * Moves the search iterator to the previous match, wrapping as needed.
+ */
+ public void retreat() {
+ setCurrent( mMatches.previous() );
+ }
+
+ private void setCurrent( final Emit emit ) {
+ mMatchProperty.set( new IndexRange( emit.getStart(), emit.getEnd() ) );
+ }
+}
src/main/java/com/keenwrite/spelling/impl/SymSpellSpeller.java
public static SpellChecker forLexicon( final String filename ) {
try {
- final Collection<String> lexicon = readLexicon( filename );
+ final var lexicon = readLexicon( filename );
return SymSpellSpeller.forLexicon( lexicon );
} catch( final Exception ex ) {
assert lexiconWords != null && !lexiconWords.isEmpty();
- final SymSpellBuilder builder = new SymSpellBuilder()
+ final var builder = new SymSpellBuilder()
.setLexiconWords( lexiconWords );
src/main/java/com/keenwrite/ui/actions/ApplicationActions.java
import com.keenwrite.io.File;
import com.keenwrite.processors.ProcessorContext;
+import com.keenwrite.search.SearchModel;
import com.keenwrite.ui.controls.SearchBar;
import javafx.scene.control.Alert;
*/
private final MainPane mMainPane;
+
+ /**
+ * Tracks searching.
+ */
+ private final SearchModel mSearchModel;
public ApplicationActions( final MainPane mainPane ) {
mMainPane = mainPane;
+ mSearchModel = new SearchModel( getActiveTextEditor().getText() );
}
nodes.remove( searchBar );
getActiveTextEditor().getNode().requestFocus();
+ } );
+
+ searchBar.addInputListener( ( c, o, n ) -> {
+ if( n != null && !n.isEmpty() ) {
+ mSearchModel.search( n );
+ //mSearchModel.matchProperty().bind( );
+ }
} );
public void edit‿find_next() {
- System.out.println( "FIND THE NEXT THINGY!" );
+ mSearchModel.advance();
}
public void edit‿find_prev() {
- System.out.println( "FIND THE PREV THINGY!" );
+ mSearchModel.retreat();
}
src/main/java/com/keenwrite/ui/controls/SearchBar.java
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
public void requestFocus() {
mFind.requestFocus();
+ }
+
+ /**
+ * Adds a listener that triggers when the input text field changes.
+ *
+ * @param listener The listener to notify of change events.
+ */
+ public void addInputListener( final ChangeListener<String> listener ) {
+ mFind.textProperty().addListener( listener );
}
src/main/java/com/keenwrite/util/CyclicIterator.java
+/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.util;
+
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Responsible for iterating over a list either forwards or backwards. When
+ * the iterator reaches the last element in the list, the next element will
+ * be the first. When the iterator reaches the first element in the list,
+ * the previous element will be the last.
+ */
+public class CyclicIterator {
+ /**
+ * Returns an iterator that cycles indefinitely through the given list.
+ *
+ * @param list The list to cycle through indefinitely.
+ * @param <T> The type of list to be cycled.
+ * @return A list iterator that can travel forwards and backwards throughout
+ * time.
+ */
+ public static <T> ListIterator<T> of( final List<T> list ) {
+ return new ListIterator<>() {
+ // Assign an invalid index so that the first calls to either previous
+ // or next will return the zeroth or final element.
+ private int mIndex = -1;
+
+ /**
+ * @return {@code true}, always.
+ */
+ @Override
+ public boolean hasNext() {
+ return true;
+ }
+
+ /**
+ * @return {@code true}, always.
+ */
+ @Override
+ public boolean hasPrevious() {
+ return true;
+ }
+
+ @Override
+ public int nextIndex() {
+ return computeIndex( +1 );
+ }
+
+ @Override
+ public int previousIndex() {
+ return computeIndex( -1 );
+ }
+
+ @Override
+ public void remove() {
+ list.remove( mIndex );
+ }
+
+ @Override
+ public void set( final T t ) {
+ list.set( mIndex, t );
+ }
+
+ @Override
+ public void add( final T t ) {
+ list.add( mIndex, t );
+ }
+
+ /**
+ * Returns the next item in the list, which will cycle to the first
+ * item as necessary.
+ *
+ * @return The next item in the list, cycling to the start if needed.
+ */
+ @Override
+ public T next() {
+ return list.get( mIndex = computeIndex( +1 ) );
+ }
+
+ /**
+ * Returns the previous item in the list, which will cycle to the last
+ * item as necessary.
+ *
+ * @return The previous item in the list, cycling to the end if needed.
+ */
+ @Override
+ public T previous() {
+ return list.get( mIndex = computeIndex( -1 ) );
+ }
+
+ private int computeIndex( final int direction ) {
+ final var i = mIndex + direction;
+ final var result = i < 0 ? list.size() - 1 : (i % list.size());
+
+ // Ensure the invariant holds.
+ assert 0 <= result && result < list.size();
+
+ return result;
+ }
+ };
+ }
+}
src/test/java/com/keenwrite/util/CyclicIteratorTest.java
+/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.ListIterator;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests the {@link CyclicIterator} class.
+ */
+public class CyclicIteratorTest {
+ /**
+ * Test that the {@link CyclicIterator} can move forwards and backwards
+ * through a {@link List}.
+ */
+ @Test
+ public void test_Directions_NextPreviousCycles_Success() {
+ final var list = List.of( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 );
+ final var iterator = CyclicIterator.of( list );
+
+ // Test forwards through the iterator.
+ for( int i = 0; i < list.size(); i++ ) {
+ assertTrue( iterator.hasNext() );
+ assertEquals( i, iterator.next() );
+ }
+
+ // Loop to the first item.
+ iterator.next();
+
+ // Test backwards through the iterator.
+ for( int i = list.size() - 1; i >= 0; i-- ) {
+ assertTrue( iterator.hasPrevious() );
+ assertEquals( i, iterator.previous() );
+ }
+ }
+
+ /**
+ * Test that the {@link CyclicIterator} returns the last element when
+ * the very first API call is to {@link ListIterator#previous()}.
+ */
+ @Test
+ public void test_Direction_FirstPrevious_ReturnsLastElement() {
+ final var list = List.of( 1, 2, 3, 4, 5, 6, 7 );
+ final var iterator = CyclicIterator.of( list );
+
+ assertEquals( iterator.previous(), list.get( list.size() - 1 ) );
+ }
+}

Start search model for find functionality

Author DaveJarvis <email>
Date 2020-12-13 14:06:45 GMT-0800
Commit fcc94d989126778132d6baf0be25c66396115421
Parent d1380de
Delta 266 lines added, 4 lines removed, 262-line increase