blob: c5129333114bcb0f45113edec56309b1da1b2132 [file] [log] [blame]
/*=============================================================================#
# 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 static org.eclipse.statet.ltk.ui.LtkUI.BUNDLE_ID;
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, 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();
}
}