Implementing Java camelCase breaking in SCL module editor 24/724/3
authorjsimomaa <jani.simomaa@gmail.com>
Tue, 18 Jul 2017 12:45:03 +0000 (15:45 +0300)
committerJani Simomaa <jani.simomaa@semantum.fi>
Thu, 20 Jul 2017 06:03:32 +0000 (09:03 +0300)
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

bundles/org.simantics.scl.ui/META-INF/MANIFEST.MF
bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/SCLModuleEditor2.java
bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/DocumentCharacterIterator.java [new file with mode: 0644]
bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaBreakIterator.java [new file with mode: 0644]
bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/JavaWordIterator.java [new file with mode: 0644]
bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/editor2/iterator/SequenceCharacterIterator.java [new file with mode: 0644]

index 931ae92cdc710f1b675f7eb343a21389103142b8..061dad8522a0f7e7758588ab8fd3192238b4a835 100644 (file)
@@ -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,
index 100f49e252f68fe6cc8e42b795b12acc0c9bf3b3..05015bb0b36dfeba8cee5afa2b44d3e11bd8d527 100644 (file)
@@ -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 <code>position</code>.
+         *
+         * @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 <code>position</code>.
+         *
+         * @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 (file)
index 0000000..de12b15
--- /dev/null
@@ -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 <code>IDocument</code> based implementation of
+ * <code>CharacterIterator</code> and <code>CharSequence</code>. Note that
+ * the supplied document is not copied; if the document is modified during the
+ * lifetime of a <code>DocumentCharacterIterator</code>, the methods
+ * returning document content may not always return the same values. Also, if
+ * accessing the document fails with a {@link BadLocationException}, any of
+ * <code>CharacterIterator</code> methods as well as <code>charAt</code>may
+ * 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 <code>first</code>.
+        * 
+        * @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 <code>first</code> (inclusive) to
+        * <code>last</code> (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}
+        * <p>
+        * 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.
+        * </p>
+        *
+        * @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 (file)
index 0000000..f020878
--- /dev/null
@@ -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.
+ * <p>
+ * A line break may be any of "\n", "\r", "\r\n", "\n\r".
+ * </p>
+ *
+ * @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 <code>true</code> if this run consumes <code>ch</code>,
+                * <code>false</code> otherwise. If <code>true</code> is returned,
+                * the length of the receiver is adjusted accordingly.
+                *
+                * @param ch the character to test
+                * @return <code>true</code> if <code>ch</code> 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 <code>consume</code>.
+                *
+                * @param ch the character to test
+                * @return <code>true</code> if <code>ch</code> 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 <code>ch</code>
+        */
+       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 (file)
index 0000000..403cc9d
--- /dev/null
@@ -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 <code>true</code> if the given sequence into the underlying text
+        * represents a delimiter, <code>false</code> otherwise.
+        *
+        * @param offset the offset
+        * @param exclusiveEnd the end offset
+        * @return <code>true</code> 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 <code>true</code> if the given sequence into the underlying text
+        * represents whitespace, but not a delimiter, <code>false</code> otherwise.
+        *
+        * @param offset the offset
+        * @param exclusiveEnd the end offset
+        * @return <code>true</code> 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 <code>CharSequence</code>.
+        * @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 (file)
index 0000000..96520a3
--- /dev/null
@@ -0,0 +1,158 @@
+package org.simantics.scl.ui.editor2.iterator;
+
+import java.text.CharacterIterator;
+
+import org.eclipse.core.runtime.Assert;
+
+
+
+/**
+ * A <code>CharSequence</code> based implementation of <code>CharacterIterator</code>.
+ *
+ * @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();
+               }
+       }
+}