| +/* 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() ) ); | ||
| + } | ||
| +} | ||
| 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 ); | ||
| 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(); | ||
| } | ||
| 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 ); | ||
| } | ||
| +/* 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; | ||
| + } | ||
| + }; | ||
| + } | ||
| +} | ||
| +/* 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 ) ); | ||
| + } | ||
| +} | ||
| 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 |