blob: e6faf0fa92ef7799f3daf6b062b8e2beb55a832b [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2016, 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.assist;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import static org.eclipse.statet.ltk.ui.LtkUI.BUNDLE_ID;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.contentassist.BoldStylerProvider;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension7;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.lang.ObjectUtils.ToStringBuilder;
import org.eclipse.statet.jcommons.text.core.BasicTextRegion;
import org.eclipse.statet.jcommons.text.core.SearchPattern;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.ecommons.ui.SharedUIResources;
import org.eclipse.statet.ecommons.ui.viewers.ViewerLabelUtils;
@NonNullByDefault
public abstract class SourceProposal<TContext extends AssistInvocationContext> implements AssistProposal,
ICompletionProposalExtension, ICompletionProposalExtension2, ICompletionProposalExtension3,
ICompletionProposalExtension4,
ICompletionProposalExtension6, ICompletionProposalExtension7 {
public static class ProposalParameters<TContext extends AssistInvocationContext> {
public final TContext context;
public final int replacementOffset;
public final SearchPattern namePattern;
public int baseRelevance;
public int matchRule;
public ProposalParameters(final TContext context, final int replacementOffset,
final SearchPattern namePattern, final int baseRelevance) {
this.context= nonNullAssert(context);
this.replacementOffset= replacementOffset;
this.namePattern= nonNullAssert(namePattern);
this.baseRelevance= baseRelevance;
}
public ProposalParameters(final TContext context, final int replacementOffset,
final SearchPattern namePattern) {
this(context, replacementOffset, namePattern, 0);
}
public ProposalParameters(final TContext context, final int replacementOffset,
final int baseRelevance) {
this.context= nonNullAssert(context);
this.replacementOffset= replacementOffset;
this.namePattern= null;
this.matchRule= SearchPattern.OTHER_MATCH;
this.baseRelevance= baseRelevance;
}
public boolean matchesNamePattern(final @Nullable String name) {
if (name == null) {
this.matchRule= 0;
return false;
}
return ((this.matchRule= this.namePattern.matches(name)) != 0);
}
}
protected static class ApplyData {
private @Nullable TextRegion selectionToSet;
private @Nullable IContextInformation contextInformation;
public ApplyData() {
}
public void setSelection(final TextRegion region) {
this.selectionToSet= region;
}
public void setSelection(final int offset) {
this.selectionToSet= new BasicTextRegion(offset, offset);
}
public void setSelection(final int offset, final int length) {
assert (length >= 0);
this.selectionToSet= new BasicTextRegion(offset, offset + length);
}
public void clearSelection() {
this.selectionToSet= null;
}
public @Nullable TextRegion getSelection() {
return this.selectionToSet;
}
public void setContextInformation(final IContextInformation info) {
this.contextInformation= info;
}
public @Nullable IContextInformation getContextInformation() {
return this.contextInformation;
}
}
private final TContext context;
private final int replacementOffset;
private final @Nullable SearchPattern namePattern;
private int matchOffset;
private String matchPattern;
private int matchRule;
private final int baseRelevance;
private @Nullable StyledString styledText;
private @Nullable Annotation rememberedOverwriteAnnotation;
private @Nullable ApplyData applyData;
public SourceProposal(final ProposalParameters<? extends TContext> parameters) {
this.context= parameters.context;
this.replacementOffset= parameters.replacementOffset;
this.namePattern= parameters.namePattern;
this.matchOffset= this.context.getInvocationOffset();
this.matchPattern= (this.namePattern != null) ? this.namePattern.getPattern() : null;
this.matchRule= parameters.matchRule;
this.baseRelevance= parameters.baseRelevance;
}
private boolean isInOverwriteMode(final boolean toggle) {
return toggle;
}
protected final TContext getInvocationContext() {
return this.context;
}
/**
* @param pattern the pattern or <code>null</code> for pattern of last match
* @return
*/
private final SearchPattern getNamePattern(final @Nullable String pattern) {
final SearchPattern namePattern= nonNullAssert(this.namePattern);
namePattern.setPattern((pattern != null) ? pattern : this.matchPattern);
return namePattern;
}
protected abstract String getName();
protected final int getReplacementOffset() {
return this.replacementOffset;
}
protected int computeReplacementLength(final int replacementOffset, final Point selection,
final int caretOffset, final boolean overwrite) throws BadLocationException {
final int end= Math.max(caretOffset, selection.x + selection.y);
return (end - replacementOffset);
}
protected final ApplyData getApplyData() {
ApplyData applyData= this.applyData;
if (applyData == null) {
applyData= createApplyData();
this.applyData= applyData;
}
return applyData;
}
protected ApplyData createApplyData() {
return new ApplyData();
}
//-- Validate --
@Override
@Deprecated
public boolean isValidFor(final IDocument document, final int offset) {
return false;
}
@Override
public boolean validate(final IDocument document, final int offset,
@Nullable final DocumentEvent event) {
return (doValidate(offset, event) != 0);
}
protected int doValidate(final int offset, @Nullable final DocumentEvent event) {
if (this.namePattern != null) {
try {
// System.out.println("doValidate " + getName() + " [" + getClass() + "]");
final String prefix= getValidationPrefix(offset);
if (prefix != null) {
return validatePattern(offset, prefix, getValidationName());
}
return 0;
}
catch (final BadLocationException e) {
return 0;
}
}
return (offset == getInvocationContext().getInvocationOffset()) ?
this.matchRule : 0;
}
protected @Nullable String getValidationPrefix(final int offset) throws BadLocationException {
final int startOffset= Math.max(getReplacementOffset(), 0);
if (offset >= startOffset) {
return getInvocationContext().getDocument().get(startOffset, offset - startOffset);
}
return null;
}
protected String getValidationName() {
return getName();
}
protected int validatePattern(final int offset, final String pattern, final String name) {
final SearchPattern namePattern= getNamePattern(pattern);
final int matchRule= namePattern.matches(name);
// System.out.println(" => " + pattern + " = " + matchRule);
if (matchRule != 0) {
this.matchOffset= offset;
this.matchPattern= pattern;
this.matchRule= matchRule;
}
return matchRule;
}
protected final int getMatchRule(final int offset) {
if (this.matchOffset == offset) {
return this.matchRule;
}
return doValidate(offset, null);
}
//-- Sorting --
@Override
public int getRelevance() {
int relevance= this.baseRelevance;
if ((this.matchRule & (SearchPattern.WORD_PREFIX_MATCH | SearchPattern.SUBSTRING_MATCH)) != 0) {
relevance-= 10;
}
return relevance;
}
@Override
public abstract String getSortingString();
//-- Item Label --
@Override
public String getDisplayString() {
return getName();
}
@Override
public final StyledString getStyledDisplayString() {
StyledString styledText= this.styledText;
if (styledText == null) {
styledText= computeStyledText();
this.styledText= styledText;
}
return styledText;
}
protected StyledString computeStyledText() {
return new StyledString(getDisplayString());
}
@Override
public StyledString getStyledDisplayString(final IDocument document, final int offset,
final BoldStylerProvider boldStylerProvider) {
final int matchRule;
// System.out.println("getString " + getName() + " " + offset + " = " + matchRule + " [" + getClass() + "]");
if (this.namePattern != null && (matchRule= getMatchRule(offset)) != 0) {
final StyledString styledText= new StyledString();
styledText.append(getStyledDisplayString());
final SearchPattern namePattern= getNamePattern(null);
final int[] matchingRegions= namePattern.getMatchingRegions(getValidationName(), matchRule);
if (matchingRegions != null) {
// System.out.println(" => " + Arrays.toString(matchingRegions));
styleMatchingRegions(styledText, matchRule, matchingRegions, boldStylerProvider);
}
return styledText;
}
return getStyledDisplayString();
}
protected void styleMatchingRegions(final StyledString styledText,
final int matchRule, final int[] matchingRegions,
final BoldStylerProvider boldStylerProvider) {
ViewerLabelUtils.setStyle(styledText, matchingRegions, boldStylerProvider.getBoldStyler() );
}
@Override
public Image getImage() {
return SharedUIResources.getImages().get(SharedUIResources.PLACEHOLDER_IMAGE_ID);
}
//-- Item Selection & Info --
@Override
public void selected(final ITextViewer viewer, final boolean smartToggle) {
if (isInOverwriteMode(smartToggle)) {
addOverwriteStyle();
}
else {
repairPresentation();
}
}
@Override
public void unselected(final ITextViewer viewer) {
repairPresentation();
}
@Override
public @Nullable String getAdditionalProposalInfo() {
return null;
}
@Override
public @Nullable IInformationControlCreator getInformationControlCreator() {
return null;
}
//-- Completion Trigger --
@Override
public boolean isAutoInsertable() {
return false;
}
@Override
public char @Nullable [] getTriggerCharacters() {
return null;
}
//-- Prefix Completion --
@Override
public int getPrefixCompletionStart(final IDocument document,
final int offset) {
return Math.max(getReplacementOffset(), 0);
}
@Override
public @Nullable CharSequence getPrefixCompletionText(final IDocument document,
final int offset) {
return getName();
}
//-- Apply --
/**
* Not supported, use {@link #apply(ITextViewer, char, int, int)}
*/
@Override
@Deprecated
public void apply(final IDocument document, final char trigger, final int offset) {
}
@Override
public void apply(final ITextViewer viewer, final char trigger, final int stateMask, final int offset) {
assert (getInvocationContext().getSourceViewer() == viewer);
final boolean smartToggle= (stateMask & SWT.CTRL) != 0;
try {
final int replacementOffset= Math.max(getReplacementOffset(), 0);
final int replacementLength= computeReplacementLength(replacementOffset, viewer.getSelectedRange(), offset, isInOverwriteMode(smartToggle));
if (validate(viewer.getDocument(), offset, null)) {
doApply(trigger, stateMask, offset, replacementOffset, replacementLength);
return;
}
}
catch (final BadLocationException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, BUNDLE_ID,
"Failed to apply completion proposal (" + getClass().getName() +").",
e ));
}
Display.getCurrent().beep();
}
protected void doApply(final char trigger, final int stateMask, final int caretOffset,
final int replacementOffset, final int replacementLength)
throws BadLocationException {
final AssistInvocationContext context= getInvocationContext();
final IDocument document= context.getDocument();
final ApplyData applyData= getApplyData();
final StringBuilder replacement= new StringBuilder(getName());
final int cursor= replacement.length();
document.replace(replacementOffset, replacementLength, replacement.toString());
applyData.setSelection(replacementOffset + cursor);
}
protected void reinvokeAssist() {
final ISourceViewer viewer= getInvocationContext().getSourceViewer();
if (viewer instanceof ITextOperationTarget) {
final ITextOperationTarget target= (ITextOperationTarget) viewer;
Display.getCurrent().asyncExec(new Runnable() {
@Override
public void run() {
if (target.canDoOperation(ISourceViewer.CONTENTASSIST_PROPOSALS)) {
target.doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
}
}
});
}
}
@Override
public @Nullable Point getSelection(final IDocument document) {
final ApplyData applyData= this.applyData;
if (applyData != null) {
final TextRegion selection= applyData.getSelection();
if (selection != null) {
return new Point(selection.getStartOffset(), selection.getLength());
}
}
return null;
}
@Override
public @Nullable IContextInformation getContextInformation() {
final ApplyData applyData= this.applyData;
if (applyData != null) {
return applyData.getContextInformation();
}
return null;
}
@Override
public int getContextInformationPosition() {
// return the default reference position, not the context position directly
return getInvocationContext().getSourceViewer().getSelectedRange().x;
}
private void addOverwriteStyle() {
final SourceViewer viewer= getInvocationContext().getSourceViewer();
final StyledText text= viewer.getTextWidget();
if (text == null || text.isDisposed()) {
return;
}
final int widgetCaret= viewer.getTextWidget().getCaretOffset();
final int modelCaret= viewer.widgetOffset2ModelOffset(widgetCaret);
final int replacementOffset= Math.max(getReplacementOffset(), 0);
int replacementLength;
try {
replacementLength= computeReplacementLength(replacementOffset, viewer.getSelectedRange(), modelCaret, true);
}
catch (final BadLocationException e) {
replacementLength= -1;
}
if (replacementLength < 0 || modelCaret >= replacementOffset + replacementLength) {
repairPresentation();
return;
}
final int offset= widgetCaret;
final int length= replacementOffset + replacementLength - modelCaret;
repairPresentation();
final Annotation annotation= new Annotation("org.eclipse.statet.ecommons.text.editorAnnotations.ContentAssistOverwrite", false, "");
viewer.getAnnotationModel().addAnnotation(annotation, new Position(offset, length));
this.rememberedOverwriteAnnotation= annotation;
}
private void repairPresentation() {
final Annotation rememberedOverwriteAnnotation= this.rememberedOverwriteAnnotation;
if (rememberedOverwriteAnnotation != null) {
final SourceViewer viewer= getInvocationContext().getSourceViewer();
this.rememberedOverwriteAnnotation= null;
viewer.getAnnotationModel().removeAnnotation(rememberedOverwriteAnnotation);
}
}
@Override
public String toString() {
final ToStringBuilder sb= new ToStringBuilder(SourceProposal.class, getClass());
sb.addProp("displayString", getDisplayString()); //$NON-NLS-1$
sb.addProp("relevance", getRelevance()); //$NON-NLS-1$
sb.addProp("sortingString", getSortingString()); //$NON-NLS-1$
return sb.toString();
}
}