Dave Jarvis' Repositories

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

Integrates find next and find previous functionality

AuthorDaveJarvis <email>
Date2020-12-16 00:26:34 GMT-0800
Commit0d6229b43f271925df49b661e360c4a18981ad64
Parent14952c0
src/main/java/com/keenwrite/FileEditorController.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * 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.keenwrite;
-
-import com.keenwrite.editors.markdown.MarkdownEditorPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.jetbrains.annotations.NotNull;
-
-import java.nio.file.Path;
-
-/**
- * Editor for a single file.
- */
-public final class FileEditorController {
-
- private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
-
- /**
- * File to load into the editor.
- */
- private Path mPath;
-
- /**
- * Searches from the caret position forward for the given string.
- *
- * @param needle The text string to match.
- */
- public void searchNext( final String needle ) {
- final var haystack = getEditorText();
- int index = haystack.indexOf( needle, getCaretTextOffset() );
-
- // Wrap around.
- if( index == -1 ) {
- index = haystack.indexOf( needle );
- }
-
- if( index >= 0 ) {
- setCaretTextOffset( 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.
- */
- private int getCaretTextOffset() {
- return getEditor().getCaretPosition();
- }
-
- /**
- * Moves the caret to a given offset.
- *
- * @param offset The new caret offset.
- */
- private void setCaretTextOffset( final int offset ) {
- getEditor().moveTo( offset );
- //getEditor().requestFollowCaret();
- }
-
- /**
- * Returns the text area associated with this tab.
- *
- * @return A text editor.
- */
- private StyleClassedTextArea getEditor() {
- return getEditorPane().getEditor();
- }
-
- /**
- * Returns the path to the file being edited in this tab.
- *
- * @return A non-null instance.
- */
- public Path getPath() {
- return mPath;
- }
-
- /**
- * Sets the path to a file for editing and then updates the tab with the
- * file contents.
- *
- * @param path A non-null instance.
- */
- public void setPath( final Path path ) {
- assert path != null;
- mPath = path;
- }
-
- /**
- * Forwards the request to the editor pane.
- *
- * @return The text to process.
- */
- public String getEditorText() {
- return getEditorPane().getText();
- }
-
- /**
- * Returns the editor pane, or creates one if it doesn't yet exist.
- *
- * @return The editor pane, never null.
- */
- @NotNull
- public MarkdownEditorPane getEditorPane() {
- return mEditorPane;
- }
-}
src/main/java/com/keenwrite/FileEditorTabPane.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * 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.keenwrite;
-
-import com.panemu.tiwulfx.control.dock.DetachableTabPane;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.ReadOnlyObjectWrapper;
-import javafx.beans.value.ChangeListener;
-import javafx.scene.control.Tab;
-
-/**
- * Tab pane for file editors.
- */
-public final class FileEditorTabPane extends DetachableTabPane {
-
- private final ReadOnlyObjectWrapper<FileEditorController> mActiveFileEditor =
- new ReadOnlyObjectWrapper<>();
-
- public FileEditorTabPane( ) {
- }
-
- /**
- * Allows observers to be notified when the current file editor tab changes.
- *
- * @param listener The listener to notify of tab change events.
- */
- public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
- // Observe the tab so that when a new tab is opened or selected,
- // a notification is kicked off.
- getSelectionModel().selectedItemProperty().addListener( listener );
- }
-
- /**
- * Returns the tab that has keyboard focus.
- *
- * @return A non-null instance.
- */
- public FileEditorController getActiveFileEditor() {
- return mActiveFileEditor.get();
- }
-
- /**
- * Returns the property corresponding to the tab that has focus.
- *
- * @return A non-null instance.
- */
- public ReadOnlyObjectProperty<FileEditorController> activeFileEditorProperty() {
- return mActiveFileEditor.getReadOnlyProperty();
- }
-
-}
src/main/java/com/keenwrite/MainWindow.java
package com.keenwrite;
-import com.keenwrite.editors.markdown.MarkdownEditorPane;
import com.keenwrite.preferences.UserPreferencesView;
import com.keenwrite.ui.actions.Action;
-import com.keenwrite.ui.actions.MenuAction;
-import com.keenwrite.ui.actions.SeparatorAction;
-import javafx.scene.Node;
-import javafx.scene.control.MenuBar;
-import javafx.scene.layout.VBox;
-import static com.keenwrite.Messages.get;
-import static com.keenwrite.ui.actions.ApplicationMenuBar.createMenu;
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
@Deprecated
public class MainWindow {
-
- private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane();
public MainWindow() {
- }
-
- public void editPreferences() {
- getUserPreferencesView().show();
- }
-
- //---- Member creators ----------------------------------------------------
-
- private Node createMenuBar() {
final Action editPreferencesAction = Action
.builder()
.setAccelerator( "Shortcut+L" )
.setIcon( LINK )
- .setHandler( e -> getActiveEditorPane().insertLink() )
+ //.setHandler( MarkdownEditorPane.insertLink() )
.build();
final Action insertImageAction = Action
.builder()
.setText( "Main.menu.insert.image" )
.setAccelerator( "Shortcut+G" )
.setIcon( PICTURE_ALT )
- .setHandler( e -> getActiveEditorPane().insertImage() )
+ //.setHandler( MarkdownEditorPane.insertImage() )
.build();
-
- //---- MenuBar ----
-
- // Edit Menu
- final var editMenu = createMenu(
- get( "Main.menu.edit" ),
- editPreferencesAction );
-
- // Insert Menu
- final var insertMenu = createMenu(
- get( "Main.menu.insert" ),
- insertLinkAction,
- insertImageAction
- );
-
- //---- MenuBar ----
- final var menuBar = new MenuBar(
- editMenu,
- insertMenu );
-
- return new VBox( menuBar );
- }
-
- //---- Convenience accessors ----------------------------------------------
-
- private MarkdownEditorPane getActiveEditorPane() {
- return getActiveFileEditorTab().getEditorPane();
- }
-
- private FileEditorController getActiveFileEditorTab() {
- return getFileEditorPane().getActiveFileEditor();
}
-
- //---- Member accessors ---------------------------------------------------
- private FileEditorTabPane getFileEditorPane() {
- return mFileEditorPane;
+ public void editPreferences() {
+ getUserPreferencesView().show();
}
src/main/java/com/keenwrite/editors/TextEditor.java
/**
+ * Requests that styling be added to the document between the given
+ * integer values.
+ *
+ * @param began Document offset where the style starts.
+ * @param ended Document offset where the style ends.
+ * @param style The style class to apply between the given offsets.
+ */
+ default void stylize( int began, int ended, String style ) {
+ }
+
+ /**
+ * Requests that styling be removed from the document between the given
+ * integer values.
+ *
+ * @param began Document offset where the style starts.
+ * @param ended Document offset where the style ends.
+ */
+ default void unstylize( int began, int ended ) {
+ }
+
+ /**
* Returns the complete text for the specified paragraph index.
*
src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
assert 0 <= offset && offset <= mTextArea.getLength();
mTextArea.moveTo( offset );
+ mTextArea.requestFollowCaret();
}
public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
return mScrollPane;
+ }
+
+ @Override
+ public void stylize( final int began, final int ended, final String style ) {
+ assert 0 <= began && began <= ended;
+ assert style != null;
+
+ mTextArea.setStyleClass( began, ended, style );
+ }
+
+ @Override
+ public void unstylize( final int began, final int ended ) {
+ assert 0 <= began && began <= ended;
+ mTextArea.clearStyle( began, ended );
}
src/main/java/com/keenwrite/search/SearchModel.java
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
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 =
+ private final ObjectProperty<IndexRange> mMatchOffset =
+ new SimpleObjectProperty<>();
+ private final ObjectProperty<Integer> mMatchCount =
+ new SimpleObjectProperty<>();
+ private final ObjectProperty<Integer> mMatchIndex =
new SimpleObjectProperty<>();
- private ListIterator<Emit> mMatches = CyclicIterator.of( List.of() );
+ private CyclicIterator<Emit> mMatches = new CyclicIterator<>( List.of() );
/**
public SearchModel( final String haystack ) {
mHaystack = haystack;
+ }
+
+ public ObjectProperty<Integer> matchCountProperty() {
+ return mMatchCount;
+ }
+
+ public ObjectProperty<Integer> matchIndexProperty() {
+ return mMatchIndex;
}
/**
- * Observers can bind to this property to be informed when the current
- * matched needle has been found in the haystack.
+ * Observers watch this property to be notified when a needle has been
+ * found in the haystack. Use {@link IndexRange#getStart()} to get the
+ * absolute offset into the text (zero-based).
*
* @return The {@link IndexRange} property to observe, representing the
* most recently matched text offset into the document.
*/
- public ObjectProperty<IndexRange> matchProperty() {
- return mMatchProperty;
+ public ObservableValue<IndexRange> matchOffsetProperty() {
+ return mMatchOffset;
}
/**
* 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 trie = builder()
+ .ignoreCase()
+ .ignoreOverlaps()
+ .addKeyword( needle )
+ .build();
final var emits = trie.parseText( mHaystack );
- mMatches = CyclicIterator.of( new ArrayList<>( emits ) );
+ mMatches = new CyclicIterator<>( new ArrayList<>( emits ) );
+ mMatchCount.set( emits.size() );
+ advance();
}
/**
* Moves the search iterator to the next match, wrapping as needed.
*/
public void advance() {
- setCurrent( mMatches.next() );
+ if( mMatches.hasNext() ) {
+ setCurrent( mMatches.next() );
+ }
}
/**
* Moves the search iterator to the previous match, wrapping as needed.
*/
public void retreat() {
- setCurrent( mMatches.previous() );
+ if( mMatches.hasPrevious() ) {
+ setCurrent( mMatches.previous() );
+ }
}
private void setCurrent( final Emit emit ) {
- mMatchProperty.set( new IndexRange( emit.getStart(), emit.getEnd() ) );
+ mMatchOffset.set( new IndexRange( emit.getStart(), emit.getEnd() ) );
+ mMatchIndex.set( mMatches.getIndex() + 1 );
}
}
src/main/java/com/keenwrite/ui/actions/ApplicationActions.java
mMainPane = mainPane;
mSearchModel = new SearchModel( getActiveTextEditor().getText() );
+ mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
+ final var editor = getActiveTextEditor();
+
+ // Clear highlighted areas before adding highlighting to a new region.
+ if( o != null ) {
+ editor.unstylize( o.getStart(), o.getEnd() + 1 );
+ }
+
+ if( n != null ) {
+ editor.moveTo( n.getStart() );
+ editor.stylize( n.getStart(), n.getEnd() + 1, "search" );
+ }
+ } );
}
if( nodes.isEmpty() ) {
final var searchBar = new SearchBar();
+
+ searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
+ searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
searchBar.setOnCancelAction( ( event ) -> {
+ final var editor = getActiveTextEditor();
+ final var indexes = mSearchModel.matchOffsetProperty().getValue();
nodes.remove( searchBar );
- getActiveTextEditor().getNode().requestFocus();
+ editor.getNode().requestFocus();
+ editor.unstylize( indexes.getStart(), indexes.getEnd() + 1 );
} );
searchBar.addInputListener( ( c, o, n ) -> {
if( n != null && !n.isEmpty() ) {
mSearchModel.search( n );
- //mSearchModel.matchProperty().bind( );
}
} );
src/main/java/com/keenwrite/ui/controls/SearchBar.java
private final TextField mFind = createTextField();
private final Text mMatches = new Text();
- private final IntegerProperty mMatchItem = new SimpleIntegerProperty();
+ private final IntegerProperty mMatchIndex = new SimpleIntegerProperty();
private final IntegerProperty mMatchCount = new SimpleIntegerProperty();
);
- mMatchItem.addListener( ( c, o, n ) -> updateMatchText() );
+ mMatchIndex.addListener( ( c, o, n ) -> updateMatchText() );
mMatchCount.addListener( ( c, o, n ) -> updateMatchText() );
updateMatchText();
* If the value is less than zero, the text will show zero.
*
- * @return The nth item number that matches the search string.
+ * @return The index of the latest search string match.
*/
- public IntegerProperty matchItemProperty() {
- return mMatchItem;
+ public IntegerProperty matchIndexProperty() {
+ return mMatchIndex;
}
*/
private void updateMatchText() {
- final var item = max( 0, mMatchItem.get() );
- final var total = max( 0, mMatchCount.get() );
+ final var index = max( 0, mMatchIndex.get() );
+ final var count = max( 0, mMatchCount.get() );
+ final var suffix = count == 0 ? "none" : "some";
- final var key = getMessageValue( "match", total == 0 ? "none" : "some" );
- mMatches.setText( get( key, item, total ) );
+ final var key = getMessageValue( "match", suffix );
+ mMatches.setText( get( key, index, count ) );
}
src/main/java/com/keenwrite/util/CyclicIterator.java
import java.util.List;
import java.util.ListIterator;
+import java.util.NoSuchElementException;
/**
* 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.
+ * <p>
+ * Due to the ability to move forwards and backwards through the list, rather
+ * than force client classes to track the list index independently, this
+ * iterator provides an accessor to the index. The index is zero-based.
+ * </p>
+ *
+ * @param <T> The type of list to be cycled.
*/
-public class CyclicIterator {
+public class CyclicIterator<T> implements ListIterator<T> {
+ private final List<T> mList;
+
/**
- * Returns an iterator that cycles indefinitely through the given list.
+ * Initialize to an invalid index so that the first calls to either
+ * {@link #previous()} or {@link #next()} will return the starting or ending
+ * element.
+ */
+ private int mIndex = -1;
+
+ /**
+ * Creates 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;
+ public CyclicIterator( final List<T> list ) {
+ mList = list;
+ }
- /**
- * @return {@code true}, always.
- */
- @Override
- public boolean hasNext() {
- return true;
- }
+ /**
+ * @return {@code true} if there is at least one element.
+ */
+ @Override
+ public boolean hasNext() {
+ return !mList.isEmpty();
+ }
- /**
- * @return {@code true}, always.
- */
- @Override
- public boolean hasPrevious() {
- return true;
- }
+ /**
+ * @return {@code true} if there is at least one element.
+ */
+ @Override
+ public boolean hasPrevious() {
+ return !mList.isEmpty();
+ }
- @Override
- public int nextIndex() {
- return computeIndex( +1 );
- }
+ @Override
+ public int nextIndex() {
+ return computeIndex( +1 );
+ }
- @Override
- public int previousIndex() {
- return computeIndex( -1 );
- }
+ @Override
+ public int previousIndex() {
+ return computeIndex( -1 );
+ }
- @Override
- public void remove() {
- list.remove( mIndex );
- }
+ @Override
+ public void remove() {
+ mList.remove( mIndex );
+ }
- @Override
- public void set( final T t ) {
- list.set( mIndex, t );
- }
+ @Override
+ public void set( final T t ) {
+ mList.set( mIndex, t );
+ }
- @Override
- public void add( final T t ) {
- list.add( mIndex, t );
- }
+ @Override
+ public void add( final T t ) {
+ mList.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 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 cycle( +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 ) );
- }
+ /**
+ * 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 cycle( -1 );
+ }
- private int computeIndex( final int direction ) {
- final var i = mIndex + direction;
- final var size = list.size();
- final var result = i < 0
- ? size - 1
- : size == 0 ? 0 : i % size;
+ /**
+ * Cycles to the next or previous element, depending on the direction value.
+ *
+ * @param direction Use -1 for previous, +1 for next.
+ * @return The next or previous item in the list.
+ */
+ private T cycle( final int direction ) {
+ try {
+ return mList.get( mIndex = computeIndex( direction ) );
+ } catch( final Exception ex ) {
+ throw new NoSuchElementException( ex );
+ }
+ }
- // Ensure the invariant holds.
- assert 0 <= result && result < size || size == 0 && result <= 0;
+ /**
+ * Returns the index of the value retrieved from the most recent call to
+ * either {@link #previous()} or {@link #next()}.
+ *
+ * @return The list item index or -1 if no calls have been made to retrieve
+ * an item from the list.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
- return result;
- }
- };
+ private int computeIndex( final int direction ) {
+ final var i = mIndex + direction;
+ final var size = mList.size();
+ final var result = i < 0
+ ? size - 1
+ : size == 0 ? 0 : i % size;
+
+ // Ensure the invariant holds.
+ assert 0 <= result && result < size || size == 0 && result <= 0;
+
+ return result;
}
}
src/main/resources/com/keenwrite/editor/markdown.css
}
+.markdown .search {
+ -rtfx-background-color: #ffe959;
+}
+
src/test/java/com/keenwrite/util/CyclicIteratorTest.java
import java.util.List;
import java.util.ListIterator;
+import java.util.NoSuchElementException;
import static org.junit.jupiter.api.Assertions.*;
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 );
+ final var iterator = createCyclicIterator( list );
// Test forwards through the iterator.
public void test_Direction_FirstPrevious_ReturnsLastElement() {
final var list = List.of( 1, 2, 3, 4, 5, 6, 7 );
- final var iterator = CyclicIterator.of( list );
+ final var iterator = createCyclicIterator( list );
assertEquals( iterator.previous(), list.get( list.size() - 1 ) );
}
@Test
public void test_Empty_Next_Exception() {
- final var list = List.of();
- final var iterator = CyclicIterator.of( list );
- assertThrows( ArrayIndexOutOfBoundsException.class, iterator::next );
+ final var iterator = createCyclicIterator( List.of() );
+ assertThrows( NoSuchElementException.class, iterator::next );
}
@Test
public void test_Empty_Previous_Exception() {
- final var list = List.of();
- final var iterator = CyclicIterator.of( list );
- assertThrows( ArrayIndexOutOfBoundsException.class, iterator::previous );
+ final var iterator = createCyclicIterator( List.of() );
+ assertThrows( NoSuchElementException.class, iterator::previous );
+ }
+
+ private <T> CyclicIterator<T> createCyclicIterator( final List<T> list ) {
+ return new CyclicIterator<>( list );
}
}
Delta231 lines added, 373 lines removed, 142-line decrease