| /*=============================================================================# |
| # Copyright (c) 2016, 2019 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 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.contentassist.IContextInformationExtension; |
| 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.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.StyledTextUtils; |
| |
| import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin; |
| |
| |
| @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; |
| private int contextInformationPosition= -1; |
| |
| |
| 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 <T extends IContextInformation & IContextInformationExtension> void setContextInformation(final T info) { |
| this.contextInformation= info; |
| this.contextInformationPosition= info.getContextInformationPosition(); |
| } |
| |
| public void setContextInformation(final IContextInformation info, final int position) { |
| this.contextInformation= info; |
| this.contextInformationPosition= position; |
| } |
| |
| public @Nullable IContextInformation getContextInformation() { |
| return this.contextInformation; |
| } |
| |
| public int getContextInformationPosition() { |
| return this.contextInformationPosition; |
| } |
| |
| } |
| |
| |
| 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) { |
| StyledTextUtils.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, LTKUIPlugin.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() { |
| final ApplyData applyData= this.applyData; |
| if (applyData != null) { |
| return applyData.getContextInformationPosition(); |
| } |
| return -1; |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| } |