blob: f8ac22efa2a7e424fa56d7d49a41e355a50abb41 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2013, 2020 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;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.JFaceTextUtil;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.information.IInformationPresenter;
import org.eclipse.jface.text.source.IOverviewRuler;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.statet.ecommons.text.core.util.NonDeletingPositionUpdater;
public class SourceEditorViewer extends ProjectionViewer {
/**
* Text operation code for requesting the outline for the current input.
*/
public static final int SHOW_SOURCE_OUTLINE= 51;
/**
* Text operation code for requesting the outline for the element at the current position.
*/
public static final int SHOW_ELEMENT_OUTLINE= 52;
/**
* Text operation code for requesting the hierarchy for the current input.
*/
public static final int SHOW_ELEMENT_HIERARCHY= 53;
public static final int VARIABLE_LINE_HEIGHT= 0b0_0000_0000_0001_0000;
private static final int QUICK_PRESENTER_START= SHOW_SOURCE_OUTLINE;
private static final int QUICK_PRESENTER_END= SHOW_ELEMENT_HIERARCHY;
private final int flags;
private int lastSentSelectionOffset;
private int lastSentSelectionLength;
private IInformationPresenter sourceOutlinePresenter;
private IInformationPresenter elementOutlinePresenter;
private IInformationPresenter elementHierarchyPresenter;
public SourceEditorViewer(final Composite parent, final IVerticalRuler ruler,
final IOverviewRuler overviewRuler, final boolean showsAnnotationOverview, final int styles,
final int flags) {
super(parent, ruler, overviewRuler, showsAnnotationOverview, styles);
this.flags= flags;
addPostSelectionChangedListener(new ISelectionChangedListener() {
/**
* By default source viewers do not caret changes to selection change listeners, only
* to post selection change listeners. This sents these post selection changes after
* validation to the selection change listeners too.
*/
@Override
public void selectionChanged(final SelectionChangedEvent event) {
final ITextSelection selection= (ITextSelection) event.getSelection();
if (SourceEditorViewer.this.lastSentSelectionOffset != selection.getOffset()
|| SourceEditorViewer.this.lastSentSelectionLength != selection.getLength()) {
final Point currentSelection= getSelectedRange();
if (currentSelection.x == selection.getOffset() && currentSelection.y == selection.getLength()) {
fireSelectionChanged(currentSelection.x, currentSelection.y);
}
}
}
});
}
@Override
protected void fireSelectionChanged(final SelectionChangedEvent event) {
final ITextSelection selection= (ITextSelection) event.getSelection();
this.lastSentSelectionOffset= selection.getOffset();
this.lastSentSelectionLength= selection.getLength();
super.fireSelectionChanged(event);
}
private IInformationPresenter getPresenter(final int operation) {
switch (operation) {
case SHOW_SOURCE_OUTLINE:
return this.sourceOutlinePresenter;
case SHOW_ELEMENT_OUTLINE:
return this.elementOutlinePresenter;
case SHOW_ELEMENT_HIERARCHY:
return this.elementHierarchyPresenter;
default:
return null;
}
}
private void setPresenter(final int operation, final IInformationPresenter presenter) {
switch (operation) {
case SHOW_SOURCE_OUTLINE:
this.sourceOutlinePresenter= presenter;
return;
case SHOW_ELEMENT_OUTLINE:
this.elementOutlinePresenter= presenter;
return;
case SHOW_ELEMENT_HIERARCHY:
this.elementHierarchyPresenter= presenter;
return;
default:
if (presenter != null) {
presenter.uninstall();
}
return;
}
}
@Override
public boolean canDoOperation(final int operation) {
switch (operation) {
case SHOW_SOURCE_OUTLINE:
case SHOW_ELEMENT_OUTLINE:
case SHOW_ELEMENT_HIERARCHY:
return (getPresenter(operation) != null);
default:
return super.canDoOperation(operation);
}
}
@Override
public void doOperation(final int operation) {
switch (operation) {
case SHOW_SOURCE_OUTLINE:
case SHOW_ELEMENT_OUTLINE:
case SHOW_ELEMENT_HIERARCHY: {
final IInformationPresenter presenter= getPresenter(operation);
if (presenter != null) {
presenter.showInformation();
}
return; }
default:
super.doOperation(operation);
return;
}
}
@Override
public void configure(final SourceViewerConfiguration configuration) {
super.configure(configuration);
if (configuration instanceof SourceEditorViewerConfiguration) {
for (int operation= QUICK_PRESENTER_START; operation < QUICK_PRESENTER_END; operation++) {
final IInformationPresenter presenter= ((SourceEditorViewerConfiguration) configuration).getQuickPresenter(this, operation);
if (presenter != null) {
presenter.install(this);
}
setPresenter(operation, presenter);
}
}
}
@Override
public void unconfigure() {
for (int operation= QUICK_PRESENTER_START; operation < QUICK_PRESENTER_END; operation++) {
final IInformationPresenter presenter= getPresenter(operation);
if (presenter != null) {
presenter.uninstall();
setPresenter(operation, null);
}
}
super.unconfigure();
}
public String[] getDefaultPrefixes(final String contentType) {
return (this.fDefaultPrefixChars != null) ?
(String[]) this.fDefaultPrefixChars.get(contentType) :
null;
}
/*[ Workaround for E-Bug 480312 ]==============================================*/
private final class ViewerState {
/** The position tracking the selection. */
private Position selection;
/** The position tracking the visually stable line. */
private Position stableLine;
/** The pixel offset of the stable line measured from the client area. */
private int stablePixel;
/** The position updater for {@link #selection} and {@link #stableLine}. */
private IPositionUpdater updater;
/** The document that the position updater and the positions are registered with. */
private IDocument updaterDocument;
/** The position category used by {@link #updater}. */
private String updaterCategory;
private int topPixel;
/**
* Creates a new viewer state instance and connects it to the current document.
*/
public ViewerState() {
final IDocument document= getDocument();
if (document != null) {
connect(document);
}
}
public void updateSelection(final int offset, final int length) {
if (this.selection == null) {
this.selection= new Position(offset, length);
if (isConnected()) {
try {
this.updaterDocument.addPosition(this.updaterCategory, this.selection);
}
catch (final BadLocationException | BadPositionCategoryException e) {}
}
}
else {
updatePosition(this.selection, offset, length);
}
}
/**
* Updates the viewport, trying to keep the
* {@linkplain StyledText#getLinePixel(int) line pixel} of the caret line stable. If the
* selection has been updated while in redraw(false) mode, the new selection is revealed.
*/
private void updateViewport() {
final StyledText textWidget= getTextWidget();
if (this.selection != null) {
textWidget.setTopPixel(this.topPixel);
revealRange(this.selection.getOffset(), this.selection.getLength());
}
else if (this.stableLine != null) {
int stableLine;
try {
stableLine= this.updaterDocument.getLineOfOffset(this.stableLine.getOffset());
}
catch (final BadLocationException x) {
// ignore and return silently
textWidget.setTopPixel(this.topPixel);
return;
}
final int stableWidgetLine= getClosestWidgetLineForModelLine(stableLine);
if (stableWidgetLine == -1) {
textWidget.setTopPixel(this.topPixel);
return;
}
final int linePixel= textWidget.getLinePixel(stableWidgetLine);
final int delta= this.stablePixel - linePixel;
final int topPixel= textWidget.getTopPixel();
textWidget.setTopPixel(topPixel - delta);
}
}
/**
* Remembers the viewer state.
*
* @param document the document to remember the state of
*/
private void connect(final IDocument document) {
Assert.isLegal(document != null);
Assert.isLegal(!isConnected());
this.updaterDocument= document;
try {
final StyledText textWidget= getTextWidget();
this.updaterCategory= "ViewerState-" + hashCode();
this.updater= new NonDeletingPositionUpdater(this.updaterCategory);
this.updaterDocument.addPositionCategory(this.updaterCategory);
this.updaterDocument.addPositionUpdater(this.updater);
final int stableLine= getStableLine();
final int stableWidgetLine= modelLine2WidgetLine(stableLine);
this.stablePixel= textWidget.getLinePixel(stableWidgetLine);
final IRegion stableLineInfo= this.updaterDocument.getLineInformation(stableLine);
this.stableLine= new Position(stableLineInfo.getOffset(), stableLineInfo.getLength());
this.updaterDocument.addPosition(this.updaterCategory, this.stableLine);
this.topPixel= textWidget.getTopPixel();
}
catch (final BadPositionCategoryException e) {
// cannot happen
Assert.isTrue(false);
}
catch (final BadLocationException e) {
// should not happen except on concurrent modification
// ignore and disconnect
disconnect();
}
}
private void updatePosition(final Position position, final int offset, final int length) {
position.setOffset(offset);
position.setLength(length);
// http://bugs.eclipse.org/bugs/show_bug.cgi?id=32795
position.isDeleted= false;
}
/**
* Returns the document line to keep visually stable. If the caret line is (partially)
* visible, it is returned, otherwise the topmost (partially) visible line is returned.
*
* @return the visually stable line of this viewer state
*/
private int getStableLine() {
int stableLine; // the model line that we try to keep stable
final int caretLine= getTextWidget().getLineAtOffset(getTextWidget().getCaretOffset());
if (caretLine < JFaceTextUtil.getPartialTopIndex(getTextWidget()) || caretLine > JFaceTextUtil.getPartialBottomIndex(getTextWidget())) {
stableLine= JFaceTextUtil.getPartialTopIndex(SourceEditorViewer.this);
}
else {
stableLine= widgetLine2ModelLine(caretLine);
}
return stableLine;
}
/**
* Returns <code>true</code> if the viewer state is being tracked, <code>false</code>
* otherwise.
*
* @return the tracking state
*/
private boolean isConnected() {
return (this.updater != null);
}
/**
* Disconnects from the document.
*/
private void disconnect() {
if (isConnected()) {
try {
this.updaterDocument.removePosition(this.updaterCategory, this.stableLine);
this.updaterDocument.removePositionUpdater(this.updater);
this.updater= null;
this.updaterDocument.removePositionCategory(this.updaterCategory);
this.updaterCategory= null;
}
catch (final BadPositionCategoryException x) {
// cannot happen
Assert.isTrue(false);
}
}
}
}
private ViewerState viewerState;
@Override
protected void disableRedrawing() {
if ((this.flags & VARIABLE_LINE_HEIGHT) != 0) {
this.viewerState= new ViewerState();
}
super.disableRedrawing();
}
@Override
protected void enabledRedrawing(final int topIndex) {
super.enabledRedrawing(topIndex);
if (this.viewerState != null) {
this.viewerState.disconnect();
if (topIndex == -1) {
this.viewerState.updateViewport();
}
this.viewerState= null;
}
}
@Override
public void setSelectedRange(final int selectionOffset, final int selectionLength) {
if (this.viewerState != null && !redraws()) {
this.viewerState.updateSelection(selectionOffset, selectionLength);
return;
}
super.setSelectedRange(selectionOffset, selectionLength);
}
}