| /*=============================================================================# |
| # Copyright (c) 2005, 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 java.util.Comparator; |
| |
| import com.ibm.icu.text.Collator; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.BadPositionCategoryException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.contentassist.ICompletionProposal; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5; |
| import org.eclipse.jface.text.link.ILinkedModeListener; |
| import org.eclipse.jface.text.link.InclusivePositionUpdater; |
| import org.eclipse.jface.text.link.LinkedModeModel; |
| import org.eclipse.jface.text.link.LinkedModeUI; |
| import org.eclipse.jface.text.link.LinkedPosition; |
| import org.eclipse.jface.text.link.LinkedPositionGroup; |
| import org.eclipse.jface.text.link.ProposalPosition; |
| import org.eclipse.jface.text.templates.DocumentTemplateContext; |
| import org.eclipse.jface.text.templates.GlobalTemplateVariables; |
| import org.eclipse.jface.text.templates.Template; |
| import org.eclipse.jface.text.templates.TemplateBuffer; |
| import org.eclipse.jface.text.templates.TemplateContext; |
| import org.eclipse.jface.text.templates.TemplateException; |
| import org.eclipse.jface.text.templates.TemplateVariable; |
| import org.eclipse.jface.viewers.StyledString; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| |
| import org.eclipse.statet.jcommons.lang.NonNull; |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| import org.eclipse.statet.jcommons.text.core.SearchPattern; |
| import org.eclipse.statet.jcommons.text.core.TextRegion; |
| |
| import org.eclipse.statet.ecommons.text.ui.DefaultBrowserInformationInput; |
| import org.eclipse.statet.ecommons.text.ui.PositionBasedCompletionProposal; |
| |
| import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin; |
| import org.eclipse.statet.ltk.ui.LTKUI; |
| import org.eclipse.statet.ltk.ui.sourceediting.TextEditToolSynchronizer; |
| import org.eclipse.statet.ltk.ui.templates.IWorkbenchTemplateContext; |
| import org.eclipse.statet.ltk.ui.util.LTKSelectionUtils; |
| |
| |
| /** |
| * Like default {@link org.eclipse.jface.text.templates.TemplateProposal}, but |
| * <ul> |
| * <li>supports substring matching |
| * <li>supports {@link TextEditToolSynchronizer}</li> |
| * </ul> |
| */ |
| @NonNullByDefault |
| public class TemplateProposal extends SourceProposal implements ICompletionProposalExtension5 { |
| |
| public static class TemplateComparator implements Comparator<TemplateProposal> { |
| |
| |
| private final Collator collator= Collator.getInstance(); |
| |
| |
| public TemplateComparator() { |
| } |
| |
| |
| @Override |
| public int compare(final TemplateProposal arg0, final TemplateProposal arg1) { |
| final int result= this.collator.compare(arg0.getTemplate().getName(), arg1.getTemplate().getName()); |
| if (result != 0) { |
| return result; |
| } |
| return this.collator.compare(arg0.getDisplayString(), arg1.getDisplayString()); |
| } |
| |
| } |
| |
| public static class TemplateProposalParameters<TContext extends AssistInvocationContext> |
| extends ProposalParameters<TContext> { |
| |
| |
| public final DocumentTemplateContext templateContext; |
| |
| public final TextRegion region; |
| |
| public Template template; |
| |
| |
| @SuppressWarnings("null") |
| public TemplateProposalParameters( |
| final TContext context, final TextRegion region, |
| final SearchPattern namePattern, |
| final DocumentTemplateContext templateContext) { |
| super(context, region.getStartOffset(), namePattern); |
| |
| this.templateContext= templateContext; |
| this.region= region; |
| } |
| |
| public TemplateProposalParameters( |
| final TContext context, final TextRegion region, |
| final DocumentTemplateContext templateContext, final Template template) { |
| super(context, region.getStartOffset(), 0); |
| |
| this.templateContext= templateContext; |
| this.region= region; |
| |
| this.template= template; |
| } |
| |
| } |
| |
| |
| private final Template template; |
| private final DocumentTemplateContext templateContext; |
| |
| private final Image image; |
| |
| private TextRegion region; |
| |
| private @Nullable InclusivePositionUpdater updater; |
| |
| |
| public TemplateProposal(final TemplateProposalParameters<?> parameters, |
| final Image image) { |
| super(parameters); |
| |
| this.template= nonNullAssert(parameters.template); |
| this.templateContext= nonNullAssert(parameters.templateContext); |
| this.image= nonNullAssert(image); |
| this.region= nonNullAssert(parameters.region); |
| } |
| |
| public TemplateProposal(final TemplateProposalParameters<?> parameters) { |
| this(parameters, |
| LTKUIPlugin.getInstance().getImageRegistry().get(LTKUI.OBJ_TEXT_TEMPLATE_IMAGE_ID) ); |
| } |
| |
| |
| protected TemplateContext getContext() { |
| return this.templateContext; |
| } |
| |
| protected Template getTemplate() { |
| return this.template; |
| } |
| |
| protected boolean isSelectionTemplate() { |
| if (this.templateContext instanceof DocumentTemplateContext) { |
| final DocumentTemplateContext docContext= this.templateContext; |
| if (docContext.getCompletionLength() > 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| @Override |
| protected String getName() { |
| return this.template.getName(); |
| } |
| |
| |
| @Override |
| public String getSortingString() { |
| return this.template.getName(); |
| } |
| |
| |
| @Override |
| public String getDisplayString() { |
| return getStyledDisplayString().getString(); |
| } |
| |
| @Override |
| public StyledString computeStyledText() { |
| final StyledString styledText= new StyledString(this.template.getName()); |
| if (!this.template.getDescription().isEmpty()) { |
| styledText.append(QUALIFIER_SEPARATOR, StyledString.QUALIFIER_STYLER); |
| styledText.append(this.template.getDescription(), StyledString.QUALIFIER_STYLER); |
| } |
| return styledText; |
| } |
| |
| @Override |
| public Image getImage() { |
| return this.image; |
| } |
| |
| |
| @Override |
| public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) { |
| try { |
| final TemplateContext context= getContext(); |
| context.setReadOnly(true); |
| final String preview; |
| if (context instanceof IWorkbenchTemplateContext) { |
| preview= ((IWorkbenchTemplateContext)context).evaluateInfo(getTemplate()); |
| } |
| else { |
| final TemplateBuffer templateBuffer= context.evaluate(getTemplate()); |
| preview= (templateBuffer != null) ? templateBuffer.toString() : null; |
| } |
| |
| if (preview != null) { |
| return new DefaultBrowserInformationInput(getDisplayString(), |
| preview, DefaultBrowserInformationInput.FORMAT_SOURCE_INPUT, |
| getInvocationContext().getTabSize() ); |
| } |
| } |
| catch (final TemplateException | BadLocationException e) {} |
| return null; |
| } |
| |
| |
| @Override |
| public boolean isAutoInsertable() { |
| if (isSelectionTemplate()) { |
| return false; |
| } |
| return this.template.isAutoInsertable(); |
| } |
| |
| |
| @Override |
| public @Nullable CharSequence getPrefixCompletionText(final IDocument document, final int offset) { |
| if (isSelectionTemplate()) { |
| return null; |
| } |
| return super.getPrefixCompletionText(document, offset); |
| } |
| |
| |
| @Override |
| public void apply(final ITextViewer viewer, final char trigger, final int stateMask, final int offset) { |
| final IDocument document= viewer.getDocument(); |
| final ApplyData applyData= getApplyData(); |
| |
| final Position regionPosition= new Position(this.region.getStartOffset(), this.region.getLength()); |
| final Position offsetPosition= new Position(offset, 0); |
| try { |
| document.addPosition(regionPosition); |
| document.addPosition(offsetPosition); |
| this.templateContext.setReadOnly(false); |
| TemplateBuffer templateBuffer; |
| try { |
| templateBuffer= nonNullAssert(this.templateContext.evaluate(this.template)); |
| } |
| catch (final TemplateException | NullPointerException e) { |
| applyData.setSelection(this.region); |
| return; |
| } |
| |
| this.region= LTKSelectionUtils.toTextRegion(regionPosition); |
| final int start= getReplaceOffset(); |
| final int end= Math.max(getReplaceEndOffset(), offsetPosition.getOffset()); |
| |
| // insert template string |
| final String templateString= templateBuffer.getString(); |
| document.replace(start, end - start, templateString); |
| |
| // translate positions |
| final LinkedModeModel model= new LinkedModeModel(); |
| final TemplateVariable[] variables= templateBuffer.getVariables(); |
| boolean hasPositions= false; |
| for (int i= 0; i != variables.length; i++) { |
| final TemplateVariable variable= variables[i]; |
| |
| if (variable.isUnambiguous()) { |
| continue; |
| } |
| |
| final LinkedPositionGroup group= new LinkedPositionGroup(); |
| |
| final int[] offsets= variable.getOffsets(); |
| final int length= variable.getLength(); |
| |
| final String[] values= variable.getValues(); |
| final ICompletionProposal[] proposals= new @NonNull ICompletionProposal[values.length]; |
| for (int j= 0; j < values.length; j++) { |
| ensurePositionCategoryInstalled(document, model); |
| final Position pos= new Position(offsets[0] + start, length); |
| document.addPosition(getCategory(), pos); |
| proposals[j]= new PositionBasedCompletionProposal(values[j], pos, length); |
| } |
| |
| for (int j= 0; j < offsets.length; j++) { |
| if (j == 0 && proposals.length > 1) { |
| group.addPosition(new ProposalPosition(document, offsets[j] + start, length, proposals)); |
| } |
| else { |
| group.addPosition(new LinkedPosition(document, offsets[j] + start, length)); |
| } |
| } |
| |
| model.addGroup(group); |
| hasPositions= true; |
| } |
| |
| if (hasPositions) { |
| model.forceInstall(); |
| |
| final TextEditToolSynchronizer toolSynchronizer= getInvocationContext().getEditor() |
| .getTextEditToolSynchronizer(); |
| if (toolSynchronizer != null) { |
| toolSynchronizer.install(model); |
| } |
| |
| final LinkedModeUI ui= new LinkedModeUI(model, viewer); |
| ui.setExitPosition(viewer, getCaretOffset(templateBuffer) + start, 0, Integer.MAX_VALUE); |
| ui.enter(); |
| |
| applyData.setSelection(LTKSelectionUtils.toTextRegion(ui.getSelectedRegion())); |
| } |
| else { |
| ensurePositionCategoryRemoved(document); |
| applyData.setSelection(getCaretOffset(templateBuffer) + start); |
| } |
| } |
| catch (final BadLocationException | BadPositionCategoryException e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, 0, |
| "Template Evaluation Error", |
| e )); |
| applyData.clearSelection(); |
| } |
| finally { |
| document.removePosition(regionPosition); |
| document.removePosition(offsetPosition); |
| } |
| } |
| |
| |
| private String getCategory() { |
| return "TemplateProposalCategory_" + toString(); //$NON-NLS-1$ |
| } |
| |
| private void ensurePositionCategoryInstalled(final IDocument document, final LinkedModeModel model) { |
| if (!document.containsPositionCategory(getCategory())) { |
| document.addPositionCategory(getCategory()); |
| this.updater= new InclusivePositionUpdater(getCategory()); |
| document.addPositionUpdater(this.updater); |
| |
| model.addLinkingListener(new ILinkedModeListener() { |
| |
| @Override |
| public void left(final LinkedModeModel environment, final int flags) { |
| ensurePositionCategoryRemoved(document); |
| } |
| |
| @Override |
| public void suspend(final LinkedModeModel environment) {} |
| @Override |
| public void resume(final LinkedModeModel environment, final int flags) {} |
| }); |
| } |
| } |
| |
| private void ensurePositionCategoryRemoved(final IDocument document) { |
| if (document.containsPositionCategory(getCategory())) { |
| try { |
| document.removePositionCategory(getCategory()); |
| } catch (final BadPositionCategoryException e) { |
| // ignore |
| } |
| document.removePositionUpdater(this.updater); |
| } |
| } |
| |
| private int getCaretOffset(final TemplateBuffer buffer) { |
| final TemplateVariable[] variables= buffer.getVariables(); |
| for (int i= 0; i != variables.length; i++) { |
| final TemplateVariable variable= variables[i]; |
| if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME)) { |
| return variable.getOffsets()[0]; |
| } |
| } |
| return buffer.getString().length(); |
| } |
| |
| /** |
| * Returns the offset of the range in the document that will be replaced by |
| * applying this template. |
| * |
| * @return the offset of the range in the document that will be replaced by |
| * applying this template |
| */ |
| protected final int getReplaceOffset() { |
| int start; |
| if (this.templateContext instanceof DocumentTemplateContext) { |
| final DocumentTemplateContext docContext= this.templateContext; |
| start= docContext.getStart(); |
| } |
| else { |
| start= this.region.getStartOffset(); |
| } |
| return start; |
| } |
| |
| /** |
| * Returns the end offset of the range in the document that will be replaced |
| * by applying this template. |
| * |
| * @return the end offset of the range in the document that will be replaced |
| * by applying this template |
| */ |
| protected final int getReplaceEndOffset() { |
| int end; |
| if (this.templateContext instanceof DocumentTemplateContext) { |
| final DocumentTemplateContext docContext= this.templateContext; |
| end= docContext.getEnd(); |
| } |
| else { |
| end= this.region.getEndOffset(); |
| } |
| return end; |
| } |
| |
| @Override |
| public int getContextInformationPosition() { |
| return this.region.getStartOffset(); |
| } |
| |
| } |