From 3286205a29ade556a0ac3e19c68ac6ebb3c8a62d Mon Sep 17 00:00:00 2001 From: jsimomaa Date: Tue, 18 Jul 2017 15:45:03 +0300 Subject: [PATCH] Implementing Java camelCase breaking in SCL module editor This feature enables the behavior familiar from Eclipse's Java-editor where CTRL (SHIFT) + RIGHT_ARROW/LEFT_ARROW navigate text [EDIT] add missing MANIFEST.MF refs #7369 Change-Id: I7c9a37107ad9530bd421360fe35479960cce4d28 --- .../org.simantics.scl.ui/META-INF/MANIFEST.MF | 3 +- .../scl/ui/editor2/SCLModuleEditor2.java | 465 ++++++++++++++++++ .../iterator/DocumentCharacterIterator.java | 220 +++++++++ .../editor2/iterator/JavaBreakIterator.java | 431 ++++++++++++++++ .../ui/editor2/iterator/JavaWordIterator.java | 224 +++++++++ .../iterator/SequenceCharacterIterator.java | 158 ++++++ 6 files changed, 1500 insertions(+), 1 deletion(-) create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/DocumentCharacterIterator.java create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaBreakIterator.java create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaWordIterator.java create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/SequenceCharacterIterator.java diff --git a/bundles/org.simantics.scl.ui/META-INF/MANIFEST.MF b/bundles/org.simantics.scl.ui/META-INF/MANIFEST.MF index 931ae92cd..061dad852 100644 --- a/bundles/org.simantics.scl.ui/META-INF/MANIFEST.MF +++ b/bundles/org.simantics.scl.ui/META-INF/MANIFEST.MF @@ -13,7 +13,8 @@ Require-Bundle: org.eclipse.ui.editors;bundle-version="3.6.0", gnu.trove3;bundle-version="3.0.0", org.simantics.scl.osgi;bundle-version="1.0.0", org.simantics.scl.compiler;bundle-version="0.6.0", - org.junit;bundle-version="4.12.0";resolution:=optional + org.junit;bundle-version="4.12.0";resolution:=optional, + com.ibm.icu Export-Package: org.simantics.scl.ui.console, org.simantics.scl.ui.editor, org.simantics.scl.ui.editor2, diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/SCLModuleEditor2.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/SCLModuleEditor2.java index 100f49e25..05015bb0b 100644 --- a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/SCLModuleEditor2.java +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/SCLModuleEditor2.java @@ -1,17 +1,35 @@ package org.simantics.scl.ui.editor2; +import java.text.CharacterIterator; + +import org.eclipse.jface.action.IAction; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.resource.ResourceManager; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.link.LinkedModeModel; +import org.eclipse.jface.text.link.LinkedPosition; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ST; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.PartInitException; import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.editors.text.TextEditor; +import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; +import org.eclipse.ui.texteditor.IUpdate; +import org.eclipse.ui.texteditor.TextNavigationAction; import org.simantics.scl.ui.editor.SCLSourceViewerConfigurationNew; import org.simantics.scl.ui.editor.completion.SCLTextEditorEnvironment; +import org.simantics.scl.ui.editor2.iterator.DocumentCharacterIterator; +import org.simantics.scl.ui.editor2.iterator.JavaWordIterator; + +import com.ibm.icu.text.BreakIterator; public class SCLModuleEditor2 extends TextEditor { private boolean disposed = false; @@ -43,6 +61,34 @@ public class SCLModuleEditor2 extends TextEditor { updatePartName(); } + @Override + protected void createNavigationActions() { + super.createNavigationActions(); + + // Taken from org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.createNavigationActions() + final StyledText textWidget= getSourceViewer().getTextWidget(); + + IAction action = new NavigatePreviousSubWordAction(); + action.setActionDefinitionId(ITextEditorActionDefinitionIds.WORD_PREVIOUS); + setAction(ITextEditorActionDefinitionIds.WORD_PREVIOUS, action); + textWidget.setKeyBinding(SWT.CTRL | SWT.ARROW_LEFT, SWT.NULL); + + action = new NavigateNextSubWordAction(); + action.setActionDefinitionId(ITextEditorActionDefinitionIds.WORD_NEXT); + setAction(ITextEditorActionDefinitionIds.WORD_NEXT, action); + textWidget.setKeyBinding(SWT.CTRL | SWT.ARROW_RIGHT, SWT.NULL); + + action = new SelectPreviousSubWordAction(); + action.setActionDefinitionId(ITextEditorActionDefinitionIds.SELECT_WORD_PREVIOUS); + setAction(ITextEditorActionDefinitionIds.SELECT_WORD_PREVIOUS, action); + textWidget.setKeyBinding(SWT.CTRL | SWT.SHIFT | SWT.ARROW_LEFT, SWT.NULL); + + action = new SelectNextSubWordAction(); + action.setActionDefinitionId(ITextEditorActionDefinitionIds.SELECT_WORD_NEXT); + setAction(ITextEditorActionDefinitionIds.SELECT_WORD_NEXT, action); + textWidget.setKeyBinding(SWT.CTRL | SWT.SHIFT | SWT.ARROW_RIGHT, SWT.NULL); + } + protected void updatePartName() { setPartName(getEditorInput().getName()); } @@ -66,4 +112,423 @@ public class SCLModuleEditor2 extends TextEditor { public IDocument getDocument() { return getSourceViewer().getDocument(); } + + /** + * Text navigation action to navigate to the next sub-word. + * + * @since 3.0 + */ + protected abstract class NextSubWordAction extends TextNavigationAction { + + protected JavaWordIterator fIterator= new JavaWordIterator(); + + /** + * Creates a new next sub-word action. + * + * @param code Action code for the default operation. Must be an action code from @see org.eclipse.swt.custom.ST. + */ + protected NextSubWordAction(int code) { + super(getSourceViewer().getTextWidget(), code); + } + + /* + * @see org.eclipse.jface.action.IAction#run() + */ + @Override + public void run() { + final ISourceViewer viewer= getSourceViewer(); + final IDocument document= viewer.getDocument(); + try { + fIterator.setText((CharacterIterator)new DocumentCharacterIterator(document)); + int position= widgetOffset2ModelOffset(viewer, viewer.getTextWidget().getCaretOffset()); + if (position == -1) + return; + + int next= findNextPosition(position); + if (isBlockSelectionModeEnabled() && document.getLineOfOffset(next) != document.getLineOfOffset(position)) { + super.run(); // may navigate into virtual white space + } else if (next != BreakIterator.DONE) { + setCaretPosition(next); + getTextWidget().showSelection(); + fireSelectionChanged(); + } + } catch (BadLocationException x) { + // ignore + } + } + + /** + * Finds the next position after the given position. + * + * @param position the current position + * @return the next position + */ + protected int findNextPosition(int position) { + ISourceViewer viewer= getSourceViewer(); + int widget= -1; + int next= position; + while (next != BreakIterator.DONE && widget == -1) { // XXX: optimize + next= fIterator.following(next); + if (next != BreakIterator.DONE) + widget= modelOffset2WidgetOffset(viewer, next); + } + + IDocument document= viewer.getDocument(); + LinkedModeModel model= LinkedModeModel.getModel(document, position); + if (model != null && next != BreakIterator.DONE) { + LinkedPosition linkedPosition= model.findPosition(new LinkedPosition(document, position, 0)); + if (linkedPosition != null) { + int linkedPositionEnd= linkedPosition.getOffset() + linkedPosition.getLength(); + if (position != linkedPositionEnd && linkedPositionEnd < next) + next= linkedPositionEnd; + } else { + LinkedPosition nextLinkedPosition= model.findPosition(new LinkedPosition(document, next, 0)); + if (nextLinkedPosition != null) { + int nextLinkedPositionOffset= nextLinkedPosition.getOffset(); + if (position != nextLinkedPositionOffset && nextLinkedPositionOffset < next) + next= nextLinkedPositionOffset; + } + } + } + + return next; + } + + /** + * Sets the caret position to the sub-word boundary given with position. + * + * @param position Position where the action should move the caret + */ + protected abstract void setCaretPosition(int position); + } + + /** + * Text navigation action to navigate to the next sub-word. + * + * @since 3.0 + */ + protected class NavigateNextSubWordAction extends NextSubWordAction { + + /** + * Creates a new navigate next sub-word action. + */ + public NavigateNextSubWordAction() { + super(ST.WORD_NEXT); + } + + /* + * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.NextSubWordAction#setCaretPosition(int) + */ + @Override + protected void setCaretPosition(final int position) { + getTextWidget().setCaretOffset(modelOffset2WidgetOffset(getSourceViewer(), position)); + } + } + + /** + * Text operation action to delete the next sub-word. + * + * @since 3.0 + */ + protected class DeleteNextSubWordAction extends NextSubWordAction implements IUpdate { + + /** + * Creates a new delete next sub-word action. + */ + public DeleteNextSubWordAction() { + super(ST.DELETE_WORD_NEXT); + } + + /* + * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.NextSubWordAction#setCaretPosition(int) + */ + @Override + protected void setCaretPosition(final int position) { + if (!validateEditorInputState()) + return; + + final ISourceViewer viewer= getSourceViewer(); + StyledText text= viewer.getTextWidget(); + Point widgetSelection= text.getSelection(); + if (isBlockSelectionModeEnabled() && widgetSelection.y != widgetSelection.x) { + final int caret= text.getCaretOffset(); + final int offset= modelOffset2WidgetOffset(viewer, position); + + if (caret == widgetSelection.x) + text.setSelectionRange(widgetSelection.y, offset - widgetSelection.y); + else + text.setSelectionRange(widgetSelection.x, offset - widgetSelection.x); + text.invokeAction(ST.DELETE_NEXT); + } else { + Point selection= viewer.getSelectedRange(); + final int caret, length; + if (selection.y != 0) { + caret= selection.x; + length= selection.y; + } else { + caret= widgetOffset2ModelOffset(viewer, text.getCaretOffset()); + length= position - caret; + } + + try { + viewer.getDocument().replace(caret, length, ""); //$NON-NLS-1$ + } catch (BadLocationException exception) { + // Should not happen + } + } + } + + /* + * @see org.eclipse.ui.texteditor.IUpdate#update() + */ + public void update() { + setEnabled(isEditorInputModifiable()); + } + } + + /** + * Text operation action to select the next sub-word. + * + * @since 3.0 + */ + protected class SelectNextSubWordAction extends NextSubWordAction { + + /** + * Creates a new select next sub-word action. + */ + public SelectNextSubWordAction() { + super(ST.SELECT_WORD_NEXT); + } + + /* + * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.NextSubWordAction#setCaretPosition(int) + */ + @Override + protected void setCaretPosition(final int position) { + final ISourceViewer viewer= getSourceViewer(); + + final StyledText text= viewer.getTextWidget(); + if (text != null && !text.isDisposed()) { + + final Point selection= text.getSelection(); + final int caret= text.getCaretOffset(); + final int offset= modelOffset2WidgetOffset(viewer, position); + + if (caret == selection.x) + text.setSelectionRange(selection.y, offset - selection.y); + else + text.setSelectionRange(selection.x, offset - selection.x); + } + } + } + + /** + * Text navigation action to navigate to the previous sub-word. + * + * @since 3.0 + */ + protected abstract class PreviousSubWordAction extends TextNavigationAction { + + protected JavaWordIterator fIterator= new JavaWordIterator(); + + /** + * Creates a new previous sub-word action. + * + * @param code Action code for the default operation. Must be an action code from @see org.eclipse.swt.custom.ST. + */ + protected PreviousSubWordAction(final int code) { + super(getSourceViewer().getTextWidget(), code); + } + + /* + * @see org.eclipse.jface.action.IAction#run() + */ + @Override + public void run() { + final ISourceViewer viewer= getSourceViewer(); + final IDocument document= viewer.getDocument(); + try { + fIterator.setText((CharacterIterator)new DocumentCharacterIterator(document)); + int position= widgetOffset2ModelOffset(viewer, viewer.getTextWidget().getCaretOffset()); + if (position == -1) + return; + + int previous= findPreviousPosition(position); + if (isBlockSelectionModeEnabled() && document.getLineOfOffset(previous) != document.getLineOfOffset(position)) { + super.run(); // may navigate into virtual white space + } else if (previous != BreakIterator.DONE) { + setCaretPosition(previous); + getTextWidget().showSelection(); + fireSelectionChanged(); + } + } catch (BadLocationException x) { + // ignore - getLineOfOffset failed + } + + } + + /** + * Finds the previous position before the given position. + * + * @param position the current position + * @return the previous position + */ + protected int findPreviousPosition(int position) { + ISourceViewer viewer= getSourceViewer(); + int widget= -1; + int previous= position; + while (previous != BreakIterator.DONE && widget == -1) { // XXX: optimize + previous= fIterator.preceding(previous); + if (previous != BreakIterator.DONE) + widget= modelOffset2WidgetOffset(viewer, previous); + } + + IDocument document= viewer.getDocument(); + LinkedModeModel model= LinkedModeModel.getModel(document, position); + if (model != null && previous != BreakIterator.DONE) { + LinkedPosition linkedPosition= model.findPosition(new LinkedPosition(document, position, 0)); + if (linkedPosition != null) { + int linkedPositionOffset= linkedPosition.getOffset(); + if (position != linkedPositionOffset && previous < linkedPositionOffset) + previous= linkedPositionOffset; + } else { + LinkedPosition previousLinkedPosition= model.findPosition(new LinkedPosition(document, previous, 0)); + if (previousLinkedPosition != null) { + int previousLinkedPositionEnd= previousLinkedPosition.getOffset() + previousLinkedPosition.getLength(); + if (position != previousLinkedPositionEnd && previous < previousLinkedPositionEnd) + previous= previousLinkedPositionEnd; + } + } + } + + return previous; + } + + /** + * Sets the caret position to the sub-word boundary given with position. + * + * @param position Position where the action should move the caret + */ + protected abstract void setCaretPosition(int position); + } + + /** + * Text navigation action to navigate to the previous sub-word. + * + * @since 3.0 + */ + protected class NavigatePreviousSubWordAction extends PreviousSubWordAction { + + /** + * Creates a new navigate previous sub-word action. + */ + public NavigatePreviousSubWordAction() { + super(ST.WORD_PREVIOUS); + } + + /* + * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.PreviousSubWordAction#setCaretPosition(int) + */ + @Override + protected void setCaretPosition(final int position) { + getTextWidget().setCaretOffset(modelOffset2WidgetOffset(getSourceViewer(), position)); + } + } + + /** + * Text operation action to delete the previous sub-word. + * + * @since 3.0 + */ + protected class DeletePreviousSubWordAction extends PreviousSubWordAction implements IUpdate { + + /** + * Creates a new delete previous sub-word action. + */ + public DeletePreviousSubWordAction() { + super(ST.DELETE_WORD_PREVIOUS); + } + + /* + * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.PreviousSubWordAction#setCaretPosition(int) + */ + @Override + protected void setCaretPosition(int position) { + if (!validateEditorInputState()) + return; + + final int length; + final ISourceViewer viewer= getSourceViewer(); + StyledText text= viewer.getTextWidget(); + Point widgetSelection= text.getSelection(); + if (isBlockSelectionModeEnabled() && widgetSelection.y != widgetSelection.x) { + final int caret= text.getCaretOffset(); + final int offset= modelOffset2WidgetOffset(viewer, position); + + if (caret == widgetSelection.x) + text.setSelectionRange(widgetSelection.y, offset - widgetSelection.y); + else + text.setSelectionRange(widgetSelection.x, offset - widgetSelection.x); + text.invokeAction(ST.DELETE_PREVIOUS); + } else { + Point selection= viewer.getSelectedRange(); + if (selection.y != 0) { + position= selection.x; + length= selection.y; + } else { + length= widgetOffset2ModelOffset(viewer, text.getCaretOffset()) - position; + } + + try { + viewer.getDocument().replace(position, length, ""); //$NON-NLS-1$ + } catch (BadLocationException exception) { + // Should not happen + } + } + } + + /* + * @see org.eclipse.ui.texteditor.IUpdate#update() + */ + public void update() { + setEnabled(isEditorInputModifiable()); + } + } + + /** + * Text operation action to select the previous sub-word. + * + * @since 3.0 + */ + protected class SelectPreviousSubWordAction extends PreviousSubWordAction { + + /** + * Creates a new select previous sub-word action. + */ + public SelectPreviousSubWordAction() { + super(ST.SELECT_WORD_PREVIOUS); + } + + /* + * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.PreviousSubWordAction#setCaretPosition(int) + */ + @Override + protected void setCaretPosition(final int position) { + final ISourceViewer viewer= getSourceViewer(); + + final StyledText text= viewer.getTextWidget(); + if (text != null && !text.isDisposed()) { + + final Point selection= text.getSelection(); + final int caret= text.getCaretOffset(); + final int offset= modelOffset2WidgetOffset(viewer, position); + + if (caret == selection.x) + text.setSelectionRange(selection.y, offset - selection.y); + else + text.setSelectionRange(selection.x, offset - selection.x); + } + } + } + } diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/DocumentCharacterIterator.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/DocumentCharacterIterator.java new file mode 100644 index 000000000..de12b15f4 --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/DocumentCharacterIterator.java @@ -0,0 +1,220 @@ +package org.simantics.scl.ui.editor2.iterator; + +import java.text.CharacterIterator; + +import org.eclipse.core.runtime.Assert; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; + + +/** + * An IDocument based implementation of + * CharacterIterator and CharSequence. Note that + * the supplied document is not copied; if the document is modified during the + * lifetime of a DocumentCharacterIterator, the methods + * returning document content may not always return the same values. Also, if + * accessing the document fails with a {@link BadLocationException}, any of + * CharacterIterator methods as well as charAtmay + * return {@link CharacterIterator#DONE}. + * + * @since 3.0 + */ +public class DocumentCharacterIterator implements CharacterIterator, CharSequence { + + private int fIndex= -1; + private final IDocument fDocument; + private final int fFirst; + private final int fLast; + + private void invariant() { + Assert.isTrue(fIndex >= fFirst); + Assert.isTrue(fIndex <= fLast); + } + + /** + * Creates an iterator for the entire document. + * + * @param document the document backing this iterator + * @throws BadLocationException if the indices are out of bounds + */ + public DocumentCharacterIterator(IDocument document) throws BadLocationException { + this(document, 0); + } + + /** + * Creates an iterator, starting at offset first. + * + * @param document the document backing this iterator + * @param first the first character to consider + * @throws BadLocationException if the indices are out of bounds + */ + public DocumentCharacterIterator(IDocument document, int first) throws BadLocationException { + this(document, first, document.getLength()); + } + + /** + * Creates an iterator for the document contents from first (inclusive) to + * last (exclusive). + * + * @param document the document backing this iterator + * @param first the first character to consider + * @param last the last character index to consider + * @throws BadLocationException if the indices are out of bounds + */ + public DocumentCharacterIterator(IDocument document, int first, int last) throws BadLocationException { + if (document == null) + throw new NullPointerException(); + if (first < 0 || first > last) + throw new BadLocationException(); + if (last > document.getLength()) { + throw new BadLocationException(); + } + fDocument= document; + fFirst= first; + fLast= last; + fIndex= first; + invariant(); + } + + /* + * @see java.text.CharacterIterator#first() + */ + public char first() { + return setIndex(getBeginIndex()); + } + + /* + * @see java.text.CharacterIterator#last() + */ + public char last() { + if (fFirst == fLast) + return setIndex(getEndIndex()); + else + return setIndex(getEndIndex() - 1); + } + + /* + * @see java.text.CharacterIterator#current() + */ + public char current() { + if (fIndex >= fFirst && fIndex < fLast) + try { + return fDocument.getChar(fIndex); + } catch (BadLocationException e) { + // ignore + } + return DONE; + } + + /* + * @see java.text.CharacterIterator#next() + */ + public char next() { + return setIndex(Math.min(fIndex + 1, getEndIndex())); + } + + /* + * @see java.text.CharacterIterator#previous() + */ + public char previous() { + if (fIndex > getBeginIndex()) { + return setIndex(fIndex - 1); + } else { + return DONE; + } + } + + /* + * @see java.text.CharacterIterator#setIndex(int) + */ + public char setIndex(int position) { + if (position >= getBeginIndex() && position <= getEndIndex()) + fIndex= position; + else + throw new IllegalArgumentException(); + + invariant(); + return current(); + } + + /* + * @see java.text.CharacterIterator#getBeginIndex() + */ + public int getBeginIndex() { + return fFirst; + } + + /* + * @see java.text.CharacterIterator#getEndIndex() + */ + public int getEndIndex() { + return fLast; + } + + /* + * @see java.text.CharacterIterator#getIndex() + */ + public int getIndex() { + return fIndex; + } + + /* + * @see java.text.CharacterIterator#clone() + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + } + + /* + * @see java.lang.CharSequence#length() + */ + public int length() { + return getEndIndex() - getBeginIndex(); + } + + /** + * {@inheritDoc} + *

+ * Note that, if the document is modified concurrently, this method may + * return {@link CharacterIterator#DONE} if a {@link BadLocationException} + * was thrown when accessing the backing document. + *

+ * + * @param index {@inheritDoc} + * @return {@inheritDoc} + */ + public char charAt(int index) { + if (index >= 0 && index < length()) + try { + return fDocument.getChar(getBeginIndex() + index); + } catch (BadLocationException e) { + // ignore and return DONE + return DONE; + } + else + throw new IndexOutOfBoundsException(); + } + + /* + * @see java.lang.CharSequence#subSequence(int, int) + */ + public CharSequence subSequence(int start, int end) { + if (start < 0) + throw new IndexOutOfBoundsException(); + if (end < start) + throw new IndexOutOfBoundsException(); + if (end > length()) + throw new IndexOutOfBoundsException(); + try { + return new DocumentCharacterIterator(fDocument, getBeginIndex() + start, getBeginIndex() + end); + } catch (BadLocationException ex) { + throw new IndexOutOfBoundsException(); + } + } +} diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaBreakIterator.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaBreakIterator.java new file mode 100644 index 000000000..f02087896 --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaBreakIterator.java @@ -0,0 +1,431 @@ +package org.simantics.scl.ui.editor2.iterator; + +import java.text.CharacterIterator; + +import com.ibm.icu.text.BreakIterator; + +import org.eclipse.core.runtime.Assert; + + + +/** + * A java break iterator. It returns all breaks, including before and after + * whitespace, and it returns all camel case breaks. + *

+ * A line break may be any of "\n", "\r", "\r\n", "\n\r". + *

+ * + * @since 3.0 + */ +public class JavaBreakIterator extends BreakIterator { + + /** + * A run of common characters. + */ + protected static abstract class Run { + /** The length of this run. */ + protected int length; + + public Run() { + init(); + } + + /** + * Returns true if this run consumes ch, + * false otherwise. If true is returned, + * the length of the receiver is adjusted accordingly. + * + * @param ch the character to test + * @return true if ch was consumed + */ + protected boolean consume(char ch) { + if (isValid(ch)) { + length++; + return true; + } + return false; + } + + /** + * Whether this run accepts that character; does not update state. Called + * from the default implementation of consume. + * + * @param ch the character to test + * @return true if ch is accepted + */ + protected abstract boolean isValid(char ch); + + /** + * Resets this run to the initial state. + */ + protected void init() { + length= 0; + } + } + + static final class Whitespace extends Run { + @Override + protected boolean isValid(char ch) { + return Character.isWhitespace(ch) && ch != '\n' && ch != '\r'; + } + } + + static final class LineDelimiter extends Run { + /** State: INIT -> delimiter -> EXIT. */ + private char fState; + private static final char INIT= '\0'; + private static final char EXIT= '\1'; + + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#init() + */ + @Override + protected void init() { + super.init(); + fState= INIT; + } + + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#consume(char) + */ + @Override + protected boolean consume(char ch) { + if (!isValid(ch) || fState == EXIT) + return false; + + if (fState == INIT) { + fState= ch; + length++; + return true; + } else if (fState != ch) { + fState= EXIT; + length++; + return true; + } else { + return false; + } + } + + @Override + protected boolean isValid(char ch) { + return ch == '\n' || ch == '\r'; + } + } + + static final class Identifier extends Run { + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#isValid(char) + */ + @Override + protected boolean isValid(char ch) { + return Character.isJavaIdentifierPart(ch); + } + } + + static final class CamelCaseIdentifier extends Run { + /* states */ + private static final int S_INIT= 0; + private static final int S_LOWER= 1; + private static final int S_ONE_CAP= 2; + private static final int S_ALL_CAPS= 3; + private static final int S_EXIT= 4; + private static final int S_EXIT_MINUS_ONE= 5; + + /* character types */ + private static final int K_INVALID= 0; + private static final int K_LOWER= 1; + private static final int K_UPPER= 2; + private static final int K_OTHER= 3; + + private int fState; + + private final static int[][] MATRIX= new int[][] { + // K_INVALID, K_LOWER, K_UPPER, K_OTHER + { S_EXIT, S_LOWER, S_ONE_CAP, S_LOWER }, // S_INIT + { S_EXIT, S_LOWER, S_EXIT, S_LOWER }, // S_LOWER + { S_EXIT, S_LOWER, S_ALL_CAPS, S_LOWER }, // S_ONE_CAP + { S_EXIT, S_EXIT_MINUS_ONE, S_ALL_CAPS, S_LOWER }, // S_ALL_CAPS + }; + + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#init() + */ + @Override + protected void init() { + super.init(); + fState= S_INIT; + } + + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#consumes(char) + */ + @Override + protected boolean consume(char ch) { + int kind= getKind(ch); + fState= MATRIX[fState][kind]; + switch (fState) { + case S_LOWER: + case S_ONE_CAP: + case S_ALL_CAPS: + length++; + return true; + case S_EXIT: + return false; + case S_EXIT_MINUS_ONE: + length--; + return false; + default: + Assert.isTrue(false); + return false; + } + } + + /** + * Determines the kind of a character. + * + * @param ch the character to test + */ + private int getKind(char ch) { + if (Character.isUpperCase(ch)) + return K_UPPER; + if (Character.isLowerCase(ch)) + return K_LOWER; + if (Character.isJavaIdentifierPart(ch)) // _, digits... + return K_OTHER; + return K_INVALID; + } + + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#isValid(char) + */ + @Override + protected boolean isValid(char ch) { + return Character.isJavaIdentifierPart(ch); + } + } + + static final class Other extends Run { + /* + * @see org.eclipse.jdt.internal.ui.text.JavaBreakIterator.Run#isValid(char) + */ + @Override + protected boolean isValid(char ch) { + return !Character.isWhitespace(ch) && !Character.isJavaIdentifierPart(ch); + } + } + + private static final Run WHITESPACE= new Whitespace(); + private static final Run DELIMITER= new LineDelimiter(); + private static final Run CAMELCASE= new CamelCaseIdentifier(); // new Identifier(); + private static final Run OTHER= new Other(); + + /** The platform break iterator (word instance) used as a base. */ + protected final BreakIterator fIterator; + /** The text we operate on. */ + protected CharSequence fText; + /** our current position for the stateful methods. */ + private int fIndex; + + + /** + * Creates a new break iterator. + */ + public JavaBreakIterator() { + fIterator= BreakIterator.getWordInstance(); + fIndex= fIterator.current(); + } + + /* + * @see java.text.BreakIterator#current() + */ + @Override + public int current() { + return fIndex; + } + + /* + * @see java.text.BreakIterator#first() + */ + @Override + public int first() { + fIndex= fIterator.first(); + return fIndex; + } + + /* + * @see java.text.BreakIterator#following(int) + */ + @Override + public int following(int offset) { + // work around too eager IAEs in standard implementation + if (offset == getText().getEndIndex()) + return DONE; + + int next= fIterator.following(offset); + if (next == DONE) + return DONE; + + // TODO deal with complex script word boundaries + // Math.min(offset + run.length, next) does not work + // since BreakIterator.getWordInstance considers _ as boundaries + // seems to work fine, however + Run run= consumeRun(offset); + return offset + run.length; + + } + + /** + * Consumes a run of characters at the limits of which we introduce a break. + * @param offset the offset to start at + * @return the run that was consumed + */ + private Run consumeRun(int offset) { + // assert offset < length + + char ch= fText.charAt(offset); + int length= fText.length(); + Run run= getRun(ch); + while (run.consume(ch) && offset < length - 1) { + offset++; + ch= fText.charAt(offset); + } + + return run; + } + + /** + * Returns a run based on a character. + * + * @param ch the character to test + * @return the correct character given ch + */ + private Run getRun(char ch) { + Run run; + if (WHITESPACE.isValid(ch)) + run= WHITESPACE; + else if (DELIMITER.isValid(ch)) + run= DELIMITER; + else if (CAMELCASE.isValid(ch)) + run= CAMELCASE; + else if (OTHER.isValid(ch)) + run= OTHER; + else { + Assert.isTrue(false); + return null; + } + + run.init(); + return run; + } + + /* + * @see java.text.BreakIterator#getText() + */ + @Override + public CharacterIterator getText() { + return fIterator.getText(); + } + + /* + * @see java.text.BreakIterator#isBoundary(int) + */ + @Override + public boolean isBoundary(int offset) { + if (offset == getText().getBeginIndex()) + return true; + else + return following(offset - 1) == offset; + } + + /* + * @see java.text.BreakIterator#last() + */ + @Override + public int last() { + fIndex= fIterator.last(); + return fIndex; + } + + /* + * @see java.text.BreakIterator#next() + */ + @Override + public int next() { + fIndex= following(fIndex); + return fIndex; + } + + /* + * @see java.text.BreakIterator#next(int) + */ + @Override + public int next(int n) { + return fIterator.next(n); + } + + /* + * @see java.text.BreakIterator#preceding(int) + */ + @Override + public int preceding(int offset) { + if (offset == getText().getBeginIndex()) + return DONE; + + if (isBoundary(offset - 1)) + return offset - 1; + + int previous= offset - 1; + do { + previous= fIterator.preceding(previous); + } while (!isBoundary(previous)); + + int last= DONE; + while (previous < offset) { + last= previous; + previous= following(previous); + } + + return last; + } + + /* + * @see java.text.BreakIterator#previous() + */ + @Override + public int previous() { + fIndex= preceding(fIndex); + return fIndex; + } + + /* + * @see java.text.BreakIterator#setText(java.lang.String) + */ + @Override + public void setText(String newText) { + setText((CharSequence) newText); + } + + /** + * Creates a break iterator given a char sequence. + * @param newText the new text + */ + public void setText(CharSequence newText) { + fText= newText; + fIterator.setText(new SequenceCharacterIterator(newText)); + first(); + } + + /* + * @see java.text.BreakIterator#setText(java.text.CharacterIterator) + */ + @Override + public void setText(CharacterIterator newText) { + if (newText instanceof CharSequence) { + fText= (CharSequence) newText; + fIterator.setText(newText); + first(); + } else { + throw new UnsupportedOperationException("CharacterIterator not supported"); //$NON-NLS-1$ + } + } +} diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaWordIterator.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaWordIterator.java new file mode 100644 index 000000000..403cc9df3 --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaWordIterator.java @@ -0,0 +1,224 @@ +package org.simantics.scl.ui.editor2.iterator; + +import java.text.CharacterIterator; + +import com.ibm.icu.text.BreakIterator; + +import org.eclipse.core.runtime.Assert; + + + +/** + * Breaks java text into word starts, also stops at line start and end. No + * direction dependency. + * + * @since 3.0 + */ +public class JavaWordIterator extends BreakIterator { + + /** + * The underlying java break iterator. It returns all breaks, including + * before and after every whitespace. + */ + private JavaBreakIterator fIterator; + /** The current index for the stateful operations. */ + private int fIndex; + + /** + * Creates a new word iterator. + */ + public JavaWordIterator() { + fIterator= new JavaBreakIterator(); + first(); + } + + /* + * @see java.text.BreakIterator#first() + */ + @Override + public int first() { + fIndex= fIterator.first(); + return fIndex; + } + + /* + * @see java.text.BreakIterator#last() + */ + @Override + public int last() { + fIndex= fIterator.last(); + return fIndex; + } + + /* + * @see java.text.BreakIterator#next(int) + */ + @Override + public int next(int n) { + int next= 0; + while (--n > 0 && next != DONE) { + next= next(); + } + return next; + } + + /* + * @see java.text.BreakIterator#next() + */ + @Override + public int next() { + fIndex= following(fIndex); + return fIndex; + } + + /* + * @see java.text.BreakIterator#previous() + */ + @Override + public int previous() { + fIndex= preceding(fIndex); + return fIndex; + } + + + /* + * @see java.text.BreakIterator#preceding(int) + */ + @Override + public int preceding(int offset) { + int first= fIterator.preceding(offset); + if (isWhitespace(first, offset)) { + int second= fIterator.preceding(first); + if (second != DONE && !isDelimiter(second, first)) + return second; + } + return first; + } + + /* + * @see java.text.BreakIterator#following(int) + */ + @Override + public int following(int offset) { + int first= fIterator.following(offset); + if (eatFollowingWhitespace(offset, first)) { + int second= fIterator.following(first); + if (isWhitespace(first, second)) + return second; + } + return first; + } + + private boolean eatFollowingWhitespace(int offset, int exclusiveEnd) { + if (exclusiveEnd == DONE || offset == DONE) + return false; + + if (isWhitespace(offset, exclusiveEnd)) + return false; + if (isDelimiter(offset, exclusiveEnd)) + return false; + + return true; + } + + /** + * Returns true if the given sequence into the underlying text + * represents a delimiter, false otherwise. + * + * @param offset the offset + * @param exclusiveEnd the end offset + * @return true if the given range is a delimiter + */ + private boolean isDelimiter(int offset, int exclusiveEnd) { + if (exclusiveEnd == DONE || offset == DONE) + return false; + + Assert.isTrue(offset >= 0); + Assert.isTrue(exclusiveEnd <= getText().getEndIndex()); + Assert.isTrue(exclusiveEnd > offset); + + CharSequence seq= fIterator.fText; + + while (offset < exclusiveEnd) { + char ch= seq.charAt(offset); + if (ch != '\n' && ch != '\r') + return false; + offset++; + } + + return true; + } + + /** + * Returns true if the given sequence into the underlying text + * represents whitespace, but not a delimiter, false otherwise. + * + * @param offset the offset + * @param exclusiveEnd the end offset + * @return true if the given range is whitespace + */ + private boolean isWhitespace(int offset, int exclusiveEnd) { + if (exclusiveEnd == DONE || offset == DONE) + return false; + + Assert.isTrue(offset >= 0); + Assert.isTrue(exclusiveEnd <= getText().getEndIndex()); + Assert.isTrue(exclusiveEnd > offset); + + CharSequence seq= fIterator.fText; + + while (offset < exclusiveEnd) { + char ch= seq.charAt(offset); + if (!Character.isWhitespace(ch)) + return false; + if (ch == '\n' || ch == '\r') + return false; + offset++; + } + + return true; + } + + /* + * @see java.text.BreakIterator#current() + */ + @Override + public int current() { + return fIndex; + } + + /* + * @see java.text.BreakIterator#getText() + */ + @Override + public CharacterIterator getText() { + return fIterator.getText(); + } + + /** + * Sets the text as CharSequence. + * @param newText the new text + */ + public void setText(CharSequence newText) { + fIterator.setText(newText); + first(); + } + + /* + * @see java.text.BreakIterator#setText(java.text.CharacterIterator) + */ + @Override + public void setText(CharacterIterator newText) { + fIterator.setText(newText); + first(); + } + + /* + * @see java.text.BreakIterator#setText(java.lang.String) + */ + @Override + public void setText(String newText) { + setText((CharSequence) newText); + } + +} diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/SequenceCharacterIterator.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/SequenceCharacterIterator.java new file mode 100644 index 000000000..96520a315 --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/SequenceCharacterIterator.java @@ -0,0 +1,158 @@ +package org.simantics.scl.ui.editor2.iterator; + +import java.text.CharacterIterator; + +import org.eclipse.core.runtime.Assert; + + + +/** + * A CharSequence based implementation of CharacterIterator. + * + * @since 3.0 + */ +public class SequenceCharacterIterator implements CharacterIterator { + + private int fIndex= -1; + private final CharSequence fSequence; + private final int fFirst; + private final int fLast; + + private void invariant() { + Assert.isTrue(fIndex >= fFirst); + Assert.isTrue(fIndex <= fLast); + } + + /** + * Creates an iterator for the entire sequence. + * + * @param sequence the sequence backing this iterator + */ + public SequenceCharacterIterator(CharSequence sequence) { + this(sequence, 0); + } + + /** + * Creates an iterator. + * + * @param sequence the sequence backing this iterator + * @param first the first character to consider + * @throws IllegalArgumentException if the indices are out of bounds + */ + public SequenceCharacterIterator(CharSequence sequence, int first) throws IllegalArgumentException { + this(sequence, first, sequence.length()); + } + + /** + * Creates an iterator. + * + * @param sequence the sequence backing this iterator + * @param first the first character to consider + * @param last the last character index to consider + * @throws IllegalArgumentException if the indices are out of bounds + */ + public SequenceCharacterIterator(CharSequence sequence, int first, int last) throws IllegalArgumentException { + if (sequence == null) + throw new NullPointerException(); + if (first < 0 || first > last) + throw new IllegalArgumentException(); + if (last > sequence.length()) + throw new IllegalArgumentException(); + fSequence= sequence; + fFirst= first; + fLast= last; + fIndex= first; + invariant(); + } + + /* + * @see java.text.CharacterIterator#first() + */ + public char first() { + return setIndex(getBeginIndex()); + } + + /* + * @see java.text.CharacterIterator#last() + */ + public char last() { + if (fFirst == fLast) + return setIndex(getEndIndex()); + else + return setIndex(getEndIndex() - 1); + } + + /* + * @see java.text.CharacterIterator#current() + */ + public char current() { + if (fIndex >= fFirst && fIndex < fLast) + return fSequence.charAt(fIndex); + else + return DONE; + } + + /* + * @see java.text.CharacterIterator#next() + */ + public char next() { + return setIndex(Math.min(fIndex + 1, getEndIndex())); + } + + /* + * @see java.text.CharacterIterator#previous() + */ + public char previous() { + if (fIndex > getBeginIndex()) { + return setIndex(fIndex - 1); + } else { + return DONE; + } + } + + /* + * @see java.text.CharacterIterator#setIndex(int) + */ + public char setIndex(int position) { + if (position >= getBeginIndex() && position <= getEndIndex()) + fIndex= position; + else + throw new IllegalArgumentException(); + + invariant(); + return current(); + } + + /* + * @see java.text.CharacterIterator#getBeginIndex() + */ + public int getBeginIndex() { + return fFirst; + } + + /* + * @see java.text.CharacterIterator#getEndIndex() + */ + public int getEndIndex() { + return fLast; + } + + /* + * @see java.text.CharacterIterator#getIndex() + */ + public int getIndex() { + return fIndex; + } + + /* + * @see java.text.CharacterIterator#clone() + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + } +} -- 2.43.2