/*=============================================================================#
# Copyright (c) 2005-2016 Stephan Wahlbrink (WalWare.de) and others.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# Contributors:
# Stephan Wahlbrink - initial API and implementation
#=============================================================================*/
package de.walware.ecommons.ltk.ui.sourceediting.assist;
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.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6;
import org.eclipse.jface.text.contentassist.IContextInformation;
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.swt.graphics.Point;
import org.eclipse.ui.statushandlers.StatusManager;
import de.walware.ecommons.ICommonStatusConstants;
import de.walware.ecommons.text.ui.DefaultBrowserInformationInput;
import de.walware.ecommons.text.ui.PositionBasedCompletionProposal;
import de.walware.ecommons.ui.SharedUIResources;
import de.walware.ecommons.ltk.ui.sourceediting.ISourceEditor;
import de.walware.ecommons.ltk.ui.sourceediting.ITextEditToolSynchronizer;
import de.walware.ecommons.ltk.ui.templates.IWorkbenchTemplateContext;
/**
* Like default {@link org.eclipse.jface.text.templates.TemplateProposal}, but
* <ul>
* <li>supports {@link ITextEditToolSynchronizer}</li>
* </ul>
*/
public class TemplateProposal implements IAssistCompletionProposal,
ICompletionProposalExtension, ICompletionProposalExtension3, ICompletionProposalExtension4,
ICompletionProposalExtension5, ICompletionProposalExtension6 {
public static class TemplateComparator implements Comparator<TemplateProposal> {
private final Collator collator= Collator.getInstance();
@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());
}
}
private final Template template;
private final TemplateContext context;
private final int relevance;
private final Image image;
private IRegion region;
private IRegion selectionToSet; // initialized by apply()
private InclusivePositionUpdater updater;
public TemplateProposal(final Template template, final TemplateContext context,
final IRegion region, final Image image, final int relevance) {
assert (template != null);
assert (context != null);
assert (region != null);
this.template= template;
this.context= context;
this.image= image;
this.region= region;
this.relevance= relevance;
}
@Override
public void selected(final ITextViewer textViewer, final boolean smartToggle) {
}
@Override
public void unselected(final ITextViewer textViewer) {
}
@Override
public boolean validate(final IDocument document, final int offset, final DocumentEvent event) {
try {
final int replaceOffset= getReplaceOffset();
if (offset >= replaceOffset) {
final String content= document.get(replaceOffset, offset - replaceOffset);
return this.template.getName().regionMatches(true, 0, content, 0, content.length());
}
} catch (final BadLocationException e) {
// concurrent modification - ignore
}
return false;
}
protected TemplateContext getContext() {
return this.context;
}
protected Template getTemplate() {
return this.template;
}
@Override
public boolean isValidFor(final IDocument document, final int offset) {
// not called anymore
return false;
}
@Override
public char[] getTriggerCharacters() {
// no triggers
return new char[0];
}
@Override
public int getRelevance() {
return this.relevance;
}
@Override
public String getSortingString() {
return this.template.getName();
}
@Override
public boolean isAutoInsertable() {
return this.template.isAutoInsertable();
}
@Override
public String getDisplayString() {
return getStyledDisplayString().getString();
}
@Override
public StyledString getStyledDisplayString() {
final StyledString s= new StyledString(this.template.getName());
s.append(QUALIFIER_SEPARATOR, StyledString.QUALIFIER_STYLER);
s.append(this.template.getDescription(), StyledString.QUALIFIER_STYLER);
return s;
}
@Override
public Image getImage() {
return this.image;
}
@Override
public IInformationControlCreator getInformationControlCreator() {
return null;
}
@Override
public String getAdditionalProposalInfo() {
return null;
}
@Override
public Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
try {
final TemplateContext context= getContext();
context.setReadOnly(true);
if (context instanceof IWorkbenchTemplateContext) {
return new DefaultBrowserInformationInput(
null, getDisplayString(), ((IWorkbenchTemplateContext) context).evaluateInfo(getTemplate()),
DefaultBrowserInformationInput.FORMAT_SOURCE_INPUT);
}
final TemplateBuffer templateBuffer= context.evaluate(getTemplate());
if (templateBuffer != null) {
return new DefaultBrowserInformationInput(
null, getDisplayString(), templateBuffer.toString(),
DefaultBrowserInformationInput.FORMAT_SOURCE_INPUT);
}
}
catch (final TemplateException e) { }
catch (final BadLocationException e) { }
return null;
}
@Override
public void apply(final IDocument document) {
// not called anymore
}
@Override
public void apply(final IDocument document, final char trigger, final int offset) {
// not called anymore
}
@Override
public void apply(final ITextViewer viewer, final char trigger, final int stateMask, final int offset) {
final IDocument document= viewer.getDocument();
final Position regionPosition= new Position(this.region.getOffset(), this.region.getLength());
final Position offsetPosition= new Position(offset, 0);
try {
document.addPosition(regionPosition);
document.addPosition(offsetPosition);
this.context.setReadOnly(false);
TemplateBuffer templateBuffer;
try {
templateBuffer= this.context.evaluate(this.template);
}
catch (final TemplateException e1) {
this.selectionToSet= new Region(this.region.getOffset(), this.region.getLength());
return;
}
this.region= new Region(regionPosition.getOffset(), regionPosition.getLength());
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 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();
if (this.context instanceof IWorkbenchTemplateContext) {
final ISourceEditor editor= ((IWorkbenchTemplateContext) this.context).getEditor();
if (editor.getTextEditToolSynchronizer() != null) {
editor.getTextEditToolSynchronizer().install(model);
}
}
final LinkedModeUI ui= new LinkedModeUI(model, viewer);
ui.setExitPosition(viewer, getCaretOffset(templateBuffer) + start, 0, Integer.MAX_VALUE);
ui.enter();
this.selectionToSet= ui.getSelectedRegion();
} else {
ensurePositionCategoryRemoved(document);
this.selectionToSet= new Region(getCaretOffset(templateBuffer) + start, 0);
}
}
catch (final BadLocationException e) {
handleError(e);
}
catch (final BadPositionCategoryException e) {
handleError(e);
}
finally {
document.removePosition(regionPosition);
document.removePosition(offsetPosition);
}
}
private void handleError(final Exception e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.PLUGIN_ID,
ICommonStatusConstants.INTERNAL_TEMPLATE, "Template Evaluation Error", e));
this.selectionToSet= this.region;
}
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.context instanceof DocumentTemplateContext) {
final DocumentTemplateContext docContext= (DocumentTemplateContext) this.context;
start= docContext.getStart();
} else {
start= this.region.getOffset();
}
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.context instanceof DocumentTemplateContext) {
final DocumentTemplateContext docContext= (DocumentTemplateContext) this.context;
end= docContext.getEnd();
} else {
end= this.region.getOffset() + this.region.getLength();
}
return end;
}
@Override
public CharSequence getPrefixCompletionText(final IDocument document, final int completionOffset) {
return this.template.getName();
}
@Override
public int getPrefixCompletionStart(final IDocument document, final int completionOffset) {
return getReplaceOffset();
}
@Override
public Point getSelection(final IDocument document) {
if (this.selectionToSet != null) {
return new Point(this.selectionToSet.getOffset(), this.selectionToSet.getLength());
}
return null;
}
@Override
public int getContextInformationPosition() {
return this.region.getOffset();
}
@Override
public IContextInformation getContextInformation() {
return null;
}
}