blob: aabbf88eb6e7edb5dc4211e6b67251bbe825db66 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2011, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ltk.ui.sourceediting.actions;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Event;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.statet.ecommons.text.DocumentCodepointIterator;
import org.eclipse.statet.ecommons.text.ICodepointIterator;
import org.eclipse.statet.ecommons.text.TextUtil;
import org.eclipse.statet.ecommons.ui.util.DNDUtils;
import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor;
public abstract class SourceEditorTextHandler extends AbstractHandler {
// TODO Add CamelCase support
private static int W_INIT= -1;
private static int W_WORD= 0;
private static int W_SEP= 1;
private static int W_WS= 2;
protected static class ExecData {
private final ISourceEditor editor;
private final SourceViewer viewer;
private final StyledText widget;
private final AbstractDocument document;
private final int caretWidgetOffset;
private final int caretDocOffset;
private int caretDocLine= Integer.MIN_VALUE;
private IRegion caretDocLineInfo;
private LinkedModeModel linkedModel;
public ExecData(final ISourceEditor editor) throws BadLocationException {
this.editor= editor;
this.viewer= editor.getViewer();
this.widget= getViewer().getTextWidget();
this.document= (AbstractDocument) getViewer().getDocument();
this.caretWidgetOffset= getWidget().getCaretOffset();
this.caretDocOffset= getViewer().widgetOffset2ModelOffset(getCaretWidgetOffset());
if (this.caretDocOffset < 0) {
throw new BadLocationException();
}
}
public boolean isSmartHomeBeginEndEnabled() {
final IPreferenceStore store= EditorsUI.getPreferenceStore();
return (store != null
&& store.getBoolean(AbstractTextEditor.PREFERENCE_NAVIGATION_SMART_HOME_END) );
}
public ISourceEditor getEditor() {
return this.editor;
}
public SourceViewer getViewer() {
return this.viewer;
}
public StyledText getWidget() {
return this.widget;
}
public AbstractDocument getDocument() {
return this.document;
}
public int toWidgetOffset(final int docOffset) {
return this.viewer.modelOffset2WidgetOffset(docOffset);
}
public int toDocOffset(final int widgetOffset) {
return this.viewer.widgetOffset2ModelOffset(widgetOffset);
}
public int getCaretWidgetOffset() {
return this.caretWidgetOffset;
}
public int getCaretDocOffset() {
return this.caretDocOffset;
}
public int getCaretDocLine() throws BadLocationException {
if (this.caretDocLine == Integer.MIN_VALUE) {
this.caretDocLine= getDocument().getLineOfOffset(getCaretDocOffset());
}
return this.caretDocLine;
}
public IRegion getCaretDocLineInformation() throws BadLocationException {
if (this.caretDocLineInfo == null) {
this.caretDocLineInfo= getDocument().getLineInformation(getCaretDocLine());
}
return this.caretDocLineInfo;
}
public int getCaretDocLineStartOffset() throws BadLocationException {
return getCaretDocLineInformation().getOffset();
}
public int getCaretDocLineEndOffset() throws BadLocationException {
final IRegion region= getCaretDocLineInformation();
return region.getOffset() + region.getLength();
}
public int getCaretColumn() throws BadLocationException {
return getCaretDocOffset() - getCaretDocLineStartOffset();
}
public LinkedModeModel getLinkedModel() {
if (this.linkedModel == null) {
this.linkedModel= LinkedModeModel.getModel(getDocument(), getCaretDocOffset());
}
return this.linkedModel;
}
}
private final ISourceEditor editor;
public SourceEditorTextHandler(final ISourceEditor editor) {
this.editor= editor;
}
private ISourceEditor getEditor(final Object context) {
return this.editor;
}
protected int getTextActionId() {
return 0;
}
protected boolean isEditAction() {
switch (getTextActionId()) {
case ST.DELETE_PREVIOUS:
case ST.DELETE_NEXT:
case ST.DELETE_WORD_PREVIOUS:
case ST.DELETE_WORD_NEXT:
return true;
default:
return false;
}
}
@Override
public void setEnabled(final Object evaluationContext) {
final ISourceEditor editor= getEditor(evaluationContext);
setBaseEnabled(editor != null
&& (!isEditAction() || editor.isEditable(false)) );
}
@Override
public Object execute(final ExecutionEvent event) throws ExecutionException {
final ISourceEditor editor= getEditor(event.getApplicationContext());
if (editor == null) {
return null;
}
if (isEditAction() && !editor.isEditable(true)) {
return null;
}
try {
final ExecData data= new ExecData(editor);
final Point oldSelection= data.getWidget().getSelection();
try {
exec(data);
}
finally {
data.getWidget().showSelection();
final Point newSelection= data.getWidget().getSelection();
if (!newSelection.equals(oldSelection)) {
fireSelectionChanged(data, newSelection);
}
}
}
catch (final BadLocationException e) {
throw new ExecutionException("An error occurred when executing the text viewer command.", e);
}
return null;
}
private void fireSelectionChanged(final ExecData data, final Point newSelection) {
final Event event= new Event();
event.x= newSelection.x;
event.y= newSelection.y;
data.getWidget().notifyListeners(SWT.Selection, event);
}
protected void exec(final ExecData data) throws BadLocationException {
final int textActionId= getTextActionId();
if (textActionId != 0) {
data.getWidget().invokeAction(textActionId);
}
}
protected int findPreviousWordOffset(final ExecData data, final int offset,
final boolean sameLine) throws BadLocationException {
int bound= 0;
if (data.getLinkedModel() != null) {
final LinkedPosition linkedPosition= data.getLinkedModel().findPosition(
new LinkedPosition(data.getDocument(), offset, 0) );
if (linkedPosition != null) {
final int begin= linkedPosition.getOffset();
if (begin < offset) {
bound= begin;
}
}
}
int previousOffset= offset;
if (offset == data.getCaretDocLineStartOffset()) {
if (!sameLine && data.getCaretDocLine() > 0) {
final IRegion nextLine= data.getDocument().getLineInformation(data.getCaretDocLine()-1);
previousOffset= nextLine.getOffset() + nextLine.getLength();
}
else {
previousOffset= offset;
}
}
else {
if (bound < data.getCaretDocLineStartOffset()) {
bound= data.getCaretDocLineStartOffset();
}
if (offset <= bound) {
return offset;
}
final ICodepointIterator iterator= DocumentCodepointIterator.create(data.getDocument(),
bound, offset );
iterator.setIndex(offset, ICodepointIterator.PREPARE_BACKWARD);
int mode= W_INIT;
int cp= iterator.previous();
while (cp != -1) {
final int newMode= getMode(cp);
if (mode != W_INIT && mode != W_WS && newMode != mode) {
break;
}
mode= newMode;
previousOffset= iterator.getCurrentIndex();
cp= iterator.previous();
}
}
if (previousOffset < bound) {
previousOffset= bound;
}
return previousOffset;
}
protected int findNextWordOffset(final ExecData data, final int offset,
final boolean sameLine) throws BadLocationException {
int bound= data.getDocument().getLength();
if (data.getLinkedModel() != null) {
final LinkedPosition linkedPosition= data.getLinkedModel().findPosition(
new LinkedPosition(data.getDocument(), offset, 0) );
if (linkedPosition != null) {
final int end= linkedPosition.getOffset() + linkedPosition.getLength();
if (end > offset) {
bound= end;
}
}
}
int nextOffset= offset;
if (data.getCaretDocLineEndOffset() <= offset) {
if (!sameLine) {
nextOffset= data.getCaretDocLineStartOffset() + data.getDocument().getLineLength(data.getCaretDocLine());
}
else {
nextOffset= offset;
}
}
else {
if (bound > data.getCaretDocLineEndOffset()) {
bound= data.getCaretDocLineEndOffset();
}
final ICodepointIterator iterator= DocumentCodepointIterator.create(data.getDocument(),
offset, bound );
iterator.setIndex(offset, ICodepointIterator.PREPARE_FORWARD);
int mode= W_INIT;
int cp= iterator.current();
while (cp != -1) {
final int newMode= getMode(cp);
if (mode != W_INIT && newMode != W_WS && newMode != mode) {
break;
}
mode= newMode;
nextOffset= iterator.getCurrentIndex() + iterator.getCurrentLength();
cp= iterator.next();
}
}
if (nextOffset > bound) {
nextOffset= bound;
}
return nextOffset;
}
private int getMode(final int cp) {
if (Character.isLetterOrDigit(cp)) {
return W_WORD;
}
else if (cp == ' ' || cp == '\t') {
return W_WS;
}
else {
return W_SEP;
}
}
public int getCaretSmartLineStartOffset(final ExecData data) throws BadLocationException {
if (data.isSmartHomeBeginEndEnabled()) {
final LinkedModeModel linkedModel= data.getLinkedModel();
if (linkedModel != null) {
final LinkedPosition position= linkedModel.findPosition(
new LinkedPosition(data.getDocument(), data.getCaretDocOffset(), 0) );
if (position != null) {
if (data.getCaretDocOffset() > position.getOffset()) {
return position.getOffset();
}
}
}
}
return data.getCaretDocLineStartOffset();
}
public int getCaretSmartLineEndOffset(final ExecData data) throws BadLocationException {
if (data.isSmartHomeBeginEndEnabled()) {
final LinkedModeModel linkedModel= data.getLinkedModel();
if (linkedModel != null) {
final LinkedPosition position= linkedModel.findPosition(
new LinkedPosition(data.getDocument(), data.getCaretDocOffset(), 0) );
if (position != null) {
if (data.getCaretDocOffset() < position.getOffset() + position.getLength()) {
return position.getOffset() + position.getLength();
}
}
}
}
return data.getCaretDocLineEndOffset();
}
protected IRegion getWholeLinesRegion(final ExecData data) throws BadLocationException {
final Point selectedRange= data.getViewer().getSelectedRange();
return TextUtil.getBlock(data.getDocument(), selectedRange.x, selectedRange.y);
}
protected IRegion getToLineBeginRegion(final ExecData data) throws BadLocationException {
final int startOffset= getCaretSmartLineStartOffset(data);
return new Region(startOffset, data.getCaretDocOffset() - startOffset);
}
protected IRegion getToLineEndRegion(final ExecData data) throws BadLocationException {
final int endOffset= getCaretSmartLineEndOffset(data);
return new Region(data.getCaretDocOffset(), endOffset - data.getCaretDocOffset());
}
protected void expandBlockSelection(final ExecData data, final int newWidgetOffset) {
if (newWidgetOffset > data.getCaretWidgetOffset()) {
while (data.getWidget().getCaretOffset() < newWidgetOffset) {
data.getWidget().invokeAction(ST.SELECT_COLUMN_NEXT);
}
}
else {
while (data.getWidget().getCaretOffset() > newWidgetOffset) {
data.getWidget().invokeAction(ST.SELECT_COLUMN_PREVIOUS);
}
}
}
protected void expandDocSelection(final ExecData data, final int newDocOffset) {
int otherDocOffset;
{ final Point widgetSelection= data.getWidget().getSelection();
otherDocOffset= data.toDocOffset((data.getCaretWidgetOffset() == widgetSelection.x) ?
widgetSelection.y : widgetSelection.x);
}
if (data.toWidgetOffset(newDocOffset) < 0 && data.getViewer() instanceof ProjectionViewer) {
((ProjectionViewer) data.getViewer()).exposeModelRange(new Region(newDocOffset, 0));
}
selectAndReveal(data, otherDocOffset, newDocOffset - otherDocOffset);
}
protected void copyToClipboard(final ExecData data, final IRegion docRegion)
throws BadLocationException {
final String text= data.getDocument().get(docRegion.getOffset(), docRegion.getLength());
final Clipboard clipboard= new Clipboard(data.getWidget().getDisplay());
try {
DNDUtils.setContent(clipboard,
new String[] { text },
new Transfer[] { TextTransfer.getInstance() } );
}
finally {
clipboard.dispose();
}
}
protected void delete(final ExecData data, final IRegion docRegion)
throws BadLocationException {
if (data.getViewer() instanceof ProjectionViewer) {
((ProjectionViewer) data.getViewer()).exposeModelRange(docRegion);
}
data.getViewer().setSelectedRange(docRegion.getOffset(), 0);
if (docRegion.getLength() > 0) {
data.getDocument().replace(docRegion.getOffset(), docRegion.getLength(), ""); //$NON-NLS-1$
}
data.getViewer().revealRange(data.getViewer().getSelectedRange().x, 0);
}
protected void selectAndReveal(final ExecData data, final int docOffset, final int length) {
data.getViewer().setSelectedRange(docOffset, length);
data.getViewer().revealRange(docOffset, length);
}
}