| /******************************************************************************* |
| * Copyright (c) 2022 Dirk Steinkamp |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.ui.internal.texteditor.multiselection; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.widgets.Control; |
| |
| import org.eclipse.core.commands.AbstractHandler; |
| import org.eclipse.core.commands.ExecutionEvent; |
| import org.eclipse.core.commands.ExecutionException; |
| |
| import org.eclipse.core.runtime.Adapters; |
| |
| import org.eclipse.jface.viewers.ISelection; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IMultiTextSelection; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.MultiTextSelection; |
| import org.eclipse.jface.text.Region; |
| |
| import org.eclipse.ui.IEditorPart; |
| import org.eclipse.ui.handlers.HandlerUtil; |
| |
| import org.eclipse.ui.texteditor.ITextEditor; |
| import org.eclipse.ui.texteditor.ITextEditorExtension5; |
| |
| /** |
| * Common super class for Multi-Selection-actions, containing various helper |
| * methods. Subclasses need to overwrite {@link #execute()}, which is only |
| * invoked if the {@link #textEditor} and {@link #document} could be properly |
| * initialized. |
| * |
| * @see AddAllMatchesToMultiSelectionHandler |
| * @see MultiSelectionDownHandler |
| * @see MultiSelectionUpHandler |
| * @see StopMultiSelectionHandler |
| */ |
| abstract class AbstractMultiSelectionHandler extends AbstractHandler { |
| /** |
| * Each widget can have a different anchor selection, that is stored in the |
| * widget's data with this key. |
| */ |
| private static final String ANCHOR_REGION_KEY = "org.eclipse.ui.internal.texteditor.multiselection.AbstractMultiSelectionHandler.anchorRegion"; //$NON-NLS-1$ |
| private ExecutionEvent event; |
| private ITextEditor textEditor; |
| private IDocument document; |
| |
| /** |
| * This method needs to be overwritten from subclasses to handle the event. |
| * |
| * @throws ExecutionException an Exception the event handler might throw |
| */ |
| public abstract void execute() throws ExecutionException; |
| |
| @Override |
| public Object execute(ExecutionEvent event) throws ExecutionException { |
| if (initFrom(event)) { |
| execute(); |
| } |
| return null; |
| } |
| |
| public ExecutionEvent getEvent() { |
| return event; |
| } |
| |
| protected boolean isMultiSelectionActive() { |
| IRegion[] regions = getSelectedRegions(); |
| return regions != null && regions.length > 1; |
| } |
| |
| protected boolean nothingSelected() { |
| IRegion[] regions = getSelectedRegions(); |
| return regions == null || regions.length == 0 || (regions.length == 1 && regions[0].getLength() == 0); |
| } |
| |
| protected IRegion[] getSelectedRegions() { |
| ISelection selection = textEditor.getSelectionProvider().getSelection(); |
| |
| if (!(selection instanceof IMultiTextSelection)) { |
| return null; |
| } |
| |
| return ((IMultiTextSelection) selection).getRegions(); |
| } |
| |
| protected IRegion offsetAsCaretRegion(int offset) { |
| return createRegionIfValid(offset, 0); |
| } |
| |
| protected void selectRegion(IRegion region) { |
| selectRegions(new IRegion[] { region }); |
| } |
| |
| protected void selectRegions(IRegion[] regions) { |
| setBlockSelectionMode(false); |
| |
| ISelection newSelection = new MultiTextSelection(document, regions); |
| textEditor.getSelectionProvider().setSelection(newSelection); |
| } |
| |
| protected void selectIdentifierUnderCaret() { |
| int offset = getCaretOffset(); |
| |
| Region identifierRegion = getIdentifierUnderCaretRegion(offset); |
| if (identifierRegion != null) { |
| selectRegion(identifierRegion); |
| setAnchorRegion(identifierRegion); |
| } |
| } |
| |
| protected void selectCaretPosition() { |
| IRegion caretRegion = offsetAsCaretRegion(getCaretOffset()); |
| selectRegion(caretRegion); |
| setAnchorRegion(caretRegion); |
| } |
| |
| protected boolean allRegionsHaveSameText() { |
| return allRegionsHaveSameText(getSelectedRegions()); |
| } |
| |
| protected boolean allRegionsEmpty() { |
| IRegion[] selectedRegions = getSelectedRegions(); |
| if (selectedRegions == null) |
| return true; |
| return isEmpty(selectedRegions[0]) && allRegionsHaveSameText(selectedRegions); |
| } |
| |
| protected boolean isEmpty(IRegion region) { |
| return region == null || region.getLength() == 0; |
| } |
| |
| protected IRegion getAnchorRegion() { |
| return (IRegion) getWidget().getData(ANCHOR_REGION_KEY); |
| } |
| |
| protected void setAnchorRegion(IRegion selection) { |
| if (selection == null) { |
| getWidget().setData(ANCHOR_REGION_KEY, null); |
| } else { |
| getWidget().setData(ANCHOR_REGION_KEY, selection); |
| } |
| } |
| |
| private void initAnchorRegion() { |
| IRegion[] regions = getSelectedRegions(); |
| if ((regions != null && regions.length == 1) || !contains(regions, getAnchorRegion())) { |
| setAnchorRegion(regions[0]); |
| } |
| } |
| |
| private boolean contains(IRegion[] regions, IRegion region) { |
| return Arrays.asList(regions).contains(region); |
| } |
| |
| private boolean allRegionsHaveSameText(IRegion[] regions) { |
| if (regions == null || regions.length == 1) |
| return true; |
| |
| try { |
| return allRegionsHaveText(regions, regionAsString(regions[0])); |
| } catch (BadLocationException e) { |
| return false; |
| } |
| } |
| |
| private boolean allRegionsHaveText(IRegion[] regions, String text) throws BadLocationException { |
| for (IRegion iRegion : regions) { |
| if (!text.equals(regionAsString(iRegion))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| protected IRegion[] addRegion(IRegion[] regions, IRegion newRegion) { |
| if (newRegion != null) { |
| IRegion[] newRegions = Arrays.copyOf(regions, regions.length + 1); |
| newRegions[newRegions.length - 1] = newRegion; |
| return newRegions; |
| } else { |
| return regions; |
| } |
| } |
| |
| protected IRegion[] removeLastRegionButOne(IRegion[] regions) { |
| if (regions == null || regions.length == 0) |
| return null; |
| if (regions.length == 1) { |
| return regions; |
| } |
| |
| return Arrays.copyOf(regions, regions.length - 1); |
| } |
| |
| protected IRegion[] removeFirstRegionButOne(IRegion[] regions) { |
| if (regions == null || regions.length == 0) |
| return null; |
| if (regions.length == 1) { |
| return regions; |
| } |
| |
| return Arrays.copyOfRange(regions, 1, regions.length); |
| } |
| |
| protected int getCaretOffset() { |
| IRegion[] regions = getSelectedRegions(); |
| if (regions == null) { |
| return -1; |
| } |
| return regions[0].getOffset() + regions[0].getLength(); |
| } |
| |
| protected void setCaretOffset(int caretOffset) { |
| selectRegion(offsetAsCaretRegion(caretOffset)); |
| } |
| |
| protected IRegion findNextMatch(IRegion region) throws ExecutionException { |
| try { |
| if (region.getLength() == 0) { |
| return offsetAsCaretRegion(offsetInNextLine(region.getOffset())); |
| } else { |
| String searchString = getTextOfRegion(region); |
| |
| String fullText = getFullText(); |
| int matchPos = fullText.indexOf(searchString, offsetAfter(region)); |
| return createRegionIfValid(matchPos, region.getLength()); |
| } |
| } catch (BadLocationException e) { |
| throw new ExecutionException("Internal error in findNextMatch", e); |
| } |
| } |
| |
| protected IRegion findPreviousMatch(IRegion region) throws ExecutionException { |
| try { |
| if (region.getLength() == 0) { |
| return offsetAsCaretRegion(offsetInPreviousLine(region.getOffset())); |
| } else { |
| String searchString = getTextOfRegion(region); |
| |
| String fullText = getFullText(); |
| int matchPos = fullText.lastIndexOf(searchString, region.getOffset() - 1); |
| return createRegionIfValid(matchPos, region.getLength()); |
| } |
| } catch (BadLocationException e) { |
| throw new ExecutionException("Internal error in findPreviousMatch", e); |
| } |
| } |
| |
| private IRegion createRegionIfValid(int offset, int length) { |
| if ((offset < 0) || (offset > document.getLength())) |
| return null; |
| |
| return new Region(offset, Math.min(length, document.getLength() - offset)); |
| } |
| |
| protected IRegion[] findAllMatches(IRegion region) throws ExecutionException { |
| try { |
| String searchString = getTextOfRegion(region); |
| |
| String fullText = getFullText(); |
| List<IRegion> regions = findAllMatches(fullText, searchString); |
| return toArray(regions); |
| } catch (BadLocationException e) { |
| throw new ExecutionException("Internal error in findAllMatches", e); |
| } |
| } |
| |
| private List<IRegion> findAllMatches(String fullText, String searchString) { |
| List<IRegion> regions = new ArrayList<>(); |
| int length = searchString.length(); |
| int matchPos = 0; |
| while ((matchPos = fullText.indexOf(searchString, matchPos)) >= 0) { |
| regions.add(new Region(matchPos, length)); |
| matchPos += length; |
| } |
| return regions; |
| } |
| |
| private int offsetInNextLine(int offset) throws BadLocationException { |
| return moveOffsetByLines(offset, 1); |
| } |
| |
| private int offsetInPreviousLine(int offset) throws BadLocationException { |
| return moveOffsetByLines(offset, -1); |
| } |
| |
| private int moveOffsetByLines(int offset, int lineDelta) throws BadLocationException { |
| int lineNo = document.getLineOfOffset(offset); |
| int newLineNo = lineNo + lineDelta; |
| if ((newLineNo < 0) || (newLineNo >= document.getNumberOfLines())) |
| return -1; |
| |
| int newLineOffset = document.getLineOffset(newLineNo); |
| int delta = offset - document.getLineOffset(lineNo); |
| |
| return newLineOffset + delta; |
| } |
| |
| private boolean initFrom(ExecutionEvent event) { |
| this.event = event; |
| initTextEditor(); |
| if (textEditor == null) |
| return false; |
| document = getDocument(); |
| initAnchorRegion(); |
| return true; |
| } |
| |
| private void initTextEditor() { |
| IEditorPart editor = HandlerUtil.getActiveEditor(event); |
| textEditor = Adapters.adapt(editor, ITextEditor.class); |
| } |
| |
| private IDocument getDocument() { |
| return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); |
| } |
| |
| private IRegion[] toArray(List<IRegion> regions) { |
| return regions.toArray(new IRegion[regions.size()]); |
| } |
| |
| private int offsetAfter(IRegion region) { |
| return region.getOffset() + region.getLength(); |
| } |
| |
| private String getTextOfRegion(IRegion region) throws BadLocationException { |
| return document.get(region.getOffset(), region.getLength()); |
| } |
| |
| private String getFullText() { |
| return document.get(); |
| } |
| |
| private String regionAsString(IRegion region) throws BadLocationException { |
| return document.get(region.getOffset(), region.getLength()); |
| } |
| |
| private Region getIdentifierUnderCaretRegion(int offset) { |
| try { |
| int startOffset = findStartOfIdentifier(offset); |
| int endOffset = findEndOfIdentifier(startOffset); |
| Region identifierRegion = new Region(startOffset, endOffset - startOffset); |
| return identifierRegion; |
| } catch (BadLocationException e) { |
| return null; |
| } |
| } |
| |
| private int findStartOfIdentifier(int offset) throws BadLocationException { |
| for (int i = offset - 1; i >= 0; i--) { |
| if (!isJavaIdentifierCharAtPos(i)) { |
| return i + 1; |
| } |
| } |
| return 0; // start of document reached |
| } |
| |
| private int findEndOfIdentifier(int offset) throws BadLocationException { |
| for (int i = offset; i <= document.getLength(); i++) { |
| if (i == document.getLength() || !isJavaIdentifierCharAtPos(i)) { |
| return i; |
| } |
| } |
| return offset; |
| } |
| |
| private boolean isJavaIdentifierCharAtPos(int i) throws BadLocationException { |
| return Character.isJavaIdentifierStart(document.getChar(i)) |
| || Character.isJavaIdentifierPart(document.getChar(i)); |
| } |
| |
| private StyledText getWidget() { |
| return (StyledText) textEditor.getAdapter(Control.class); |
| } |
| |
| private void setBlockSelectionMode(boolean blockSelectionMode) { |
| if (!(textEditor instanceof ITextEditorExtension5)) { |
| return; |
| } |
| ITextEditorExtension5 ext = (ITextEditorExtension5) textEditor; |
| ext.setBlockSelectionMode(blockSelectionMode); |
| } |
| |
| protected boolean selectionIsAboveAnchorRegion() { |
| IRegion[] selectedRegions = getSelectedRegions(); |
| if (selectedRegions == null || selectedRegions.length == 1) |
| return false; |
| return isLastRegion(getAnchorRegion(), selectedRegions); |
| } |
| |
| protected boolean selectionIsBelowAnchorRegion() { |
| IRegion[] selectedRegions = getSelectedRegions(); |
| if (selectedRegions == null || selectedRegions.length == 1) |
| return false; |
| return isFirstRegion(getAnchorRegion(), selectedRegions); |
| } |
| |
| private boolean isLastRegion(IRegion region, IRegion[] regions) { |
| if (region == null || regions == null || regions.length == 0) |
| return false; |
| |
| return region.equals(regions[regions.length - 1]); |
| } |
| |
| private boolean isFirstRegion(IRegion region, IRegion[] regions) { |
| if (region == null || regions == null || regions.length == 0) |
| return false; |
| |
| return region.equals(regions[0]); |
| } |
| } |