content: /*******************************************************************************
   * Copyright (c) 2000, 2008 IBM Corporation 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:
   *     IBM Corporation - initial API and implementation
   *******************************************************************************/
  package org.eclipse.wst.jsdt.internal.ui.text.template.contentassist;
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.core.runtime.IStatus;
  import org.eclipse.core.runtime.Status;
  import org.eclipse.jface.dialogs.MessageDialog;
  import org.eclipse.jface.text.BadLocationException;
  import org.eclipse.jface.text.BadPositionCategoryException;
  import org.eclipse.jface.text.Document;
  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.IRewriteTarget;
  import org.eclipse.jface.text.ITextViewer;
  import org.eclipse.jface.text.ITextViewerExtension;
  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.ICompletionProposalExtension2;
  import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
  import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4;
  import org.eclipse.jface.text.contentassist.IContextInformation;
  import org.eclipse.jface.text.link.ILinkedModeListener;
  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.source.LineRange;
  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.swt.SWT;
  import org.eclipse.swt.graphics.Image;
  import org.eclipse.swt.graphics.Point;
  import org.eclipse.swt.widgets.Shell;
  import org.eclipse.ui.IEditorPart;
  import org.eclipse.ui.part.IWorkbenchPartOrientation;
  import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
  import org.eclipse.wst.jsdt.internal.corext.template.java.CompilationUnitContext;
  import org.eclipse.wst.jsdt.internal.corext.template.java.JavaDocContext;
  import org.eclipse.wst.jsdt.internal.corext.util.Messages;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorHighlightingSynchronizer;
  import org.eclipse.wst.jsdt.internal.ui.javaeditor.IndentUtil;
  import org.eclipse.wst.jsdt.internal.ui.javaeditor.JavaEditor;
  import org.eclipse.wst.jsdt.internal.ui.util.ExceptionHandler;
  import org.eclipse.wst.jsdt.ui.text.java.IJavaCompletionProposal;
  
  /**
   * A template proposal.
   */
  public class TemplateProposal implements IJavaCompletionProposal, ICompletionProposalExtension2, ICompletionProposalExtension3, ICompletionProposalExtension4 {
  
  	private final Template fTemplate;
  	private final TemplateContext fContext;
  	private final Image fImage;
  	private IRegion fRegion;
  	private int fRelevance;
  
  	private IRegion fSelectedRegion; // initialized by apply()
  	private String fDisplayString;
  
  	/**
  	 * Creates a template proposal with a template and its context.
  	 *
  	 * @param template  the template
  	 * @param context   the context in which the template was requested
  	 * @param region 	the region this proposal applies to
  	 * @param image     the icon of the proposal
  	 */
  	public TemplateProposal(Template template, TemplateContext context, IRegion region, Image image) {
  		Assert.isNotNull(template);
  		Assert.isNotNull(context);
  		Assert.isNotNull(region);
  
  		fTemplate= template;
  		fContext= context;
  		fImage= image;
  		fRegion= region;
  
  		fDisplayString= null;
  
  		fRelevance= computeRelevance();
  	}
  
  	/**
  	 * Computes the relevance to match the relevance values generated by the
  	 * core content assistant.
  	 *
  	 * @return a sensible relevance value.
  	 */
  	private int computeRelevance() {
  		// see org.eclipse.wst.jsdt.internal.codeassist.RelevanceConstants
  		final int R_DEFAULT= 0;
  		final int R_INTERESTING= 5;
  		final int R_CASE= 10;
  		final int R_NON_RESTRICTED= 3;
  		final int R_EXACT_NAME = 4;
  		final int R_INLINE_TAG = 31;
  
  		int base= R_DEFAULT + R_INTERESTING + R_NON_RESTRICTED;
  
  		try {
  			if (fContext instanceof DocumentTemplateContext) {
  				DocumentTemplateContext templateContext= (DocumentTemplateContext) fContext;
  				IDocument document= templateContext.getDocument();
  
  				String content= document.get(fRegion.getOffset(), fRegion.getLength());
  				if (fTemplate.getName().startsWith(content))
  					base += R_CASE;
  				if (fTemplate.getName().equalsIgnoreCase(content))
  					base += R_EXACT_NAME;
  				if (fContext instanceof JavaDocContext)
  					base += R_INLINE_TAG;
  			}
  		} catch (BadLocationException e) {
  			// ignore - not a case sensitive match then
  		}
  
  		// see CompletionProposalCollector.computeRelevance
  		// just under keywords, but better than packages
  		final int TEMPLATE_RELEVANCE= 1;
  		return base * 16 + TEMPLATE_RELEVANCE;
  	}
  
  	/*
  	 * @see ICompletionProposal#apply(IDocument)
  	 */
  	public final void apply(IDocument document) {
  		// not called anymore
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#apply(org.eclipse.jface.text.ITextViewer, char, int, int)
  	 */
  	public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
  
  		try {
  
  			fContext.setReadOnly(false);
  			int start;
  			IDocument document;
  			TemplateBuffer templateBuffer;
  			try {
  				beginCompoundChange(viewer);
  
  				try {
  					// this may already modify the document (e.g. add imports)
  					templateBuffer= fContext.evaluate(fTemplate);
  				} catch (TemplateException e1) {
  					fSelectedRegion= fRegion;
  					return;
  				}
  
  				start= getReplaceOffset();
  				int end= getReplaceEndOffset();
  				end= Math.max(end, offset);
  
  				// insert template string
  				document= viewer.getDocument();
  				if (end > document.getLength())
  					end= offset;
  				String templateString= templateBuffer.getString();
  				document.replace(start, end - start, templateString);
  			} finally {
  				endCompoundChange(viewer);
  			}
  
  			// translate positions
  			LinkedModeModel model= new LinkedModeModel();
  			TemplateVariable[] variables= templateBuffer.getVariables();
  
  			MultiVariableGuess guess= fContext instanceof CompilationUnitContext ? ((CompilationUnitContext) fContext).getMultiVariableGuess() : null;
  
  			boolean hasPositions= false;
  			for (int i= 0; i != variables.length; i++) {
  				TemplateVariable variable= variables[i];
  
  				if (variable.isUnambiguous())
  					continue;
  
  				LinkedPositionGroup group= new LinkedPositionGroup();
  
  				int[] offsets= variable.getOffsets();
  				int length= variable.getLength();
  
  				LinkedPosition first;
  				if (guess != null && variable instanceof MultiVariable) {
  					first= new VariablePosition(document, offsets[0] + start, length, guess, (MultiVariable) variable);
  					guess.addSlave((VariablePosition) first);
  				} else {
  					String[] values= variable.getValues();
  					ICompletionProposal[] proposals= new ICompletionProposal[values.length];
  					for (int j= 0; j < values.length; j++) {
  						ensurePositionCategoryInstalled(document, model);
  						Position pos= new Position(offsets[0] + start, length);
  						document.addPosition(getCategory(), pos);
  						proposals[j]= new PositionBasedCompletionProposal(values[j], pos, length);
  					}
  
  					if (proposals.length > 1)
  						first= new ProposalPosition(document, offsets[0] + start, length, proposals);
  					else
  						first= new LinkedPosition(document, offsets[0] + start, length);
  				}
  
  				for (int j= 0; j != offsets.length; j++)
  					if (j == 0)
  						group.addPosition(first);
  					else
  						group.addPosition(new LinkedPosition(document, offsets[j] + start, length));
  
  				model.addGroup(group);
  				hasPositions= true;
  			}
  
  			if (hasPositions) {
  				model.forceInstall();
  				JavaEditor editor= getJavaEditor();
  				if (editor != null) {
  					model.addLinkingListener(new EditorHighlightingSynchronizer(editor));
  				}
  
  				LinkedModeUI ui= new EditorLinkedModeUI(model, viewer);
  				ui.setExitPosition(viewer, getCaretOffset(templateBuffer) + start, 0, Integer.MAX_VALUE);
  				ui.enter();
  
  				fSelectedRegion= ui.getSelectedRegion();
  			} else
  				fSelectedRegion= new Region(getCaretOffset(templateBuffer) + start, 0);
  
  		} catch (BadLocationException e) {
  			JavaScriptPlugin.log(e);
  			openErrorDialog(viewer.getTextWidget().getShell(), e);
  			fSelectedRegion= fRegion;
  		} catch (BadPositionCategoryException e) {
  			JavaScriptPlugin.log(e);
  			openErrorDialog(viewer.getTextWidget().getShell(), e);
  			fSelectedRegion= fRegion;
  		}
  
  	}
  
  	private void endCompoundChange(ITextViewer viewer) {
  		if (viewer instanceof ITextViewerExtension) {
  			ITextViewerExtension extension= (ITextViewerExtension) viewer;
  			IRewriteTarget target= extension.getRewriteTarget();
  			target.endCompoundChange();
  		}
  	}
  
  	private void beginCompoundChange(ITextViewer viewer) {
  		if (viewer instanceof ITextViewerExtension) {
  			ITextViewerExtension extension= (ITextViewerExtension) viewer;
  			IRewriteTarget target= extension.getRewriteTarget();
  			target.beginCompoundChange();
  		}
  	}
  
  	/**
  	 * Returns the currently active java editor, or <code>null</code> if it
  	 * cannot be determined.
  	 *
  	 * @return  the currently active java editor, or <code>null</code>
  	 */
  	private JavaEditor getJavaEditor() {
  		IEditorPart part= JavaScriptPlugin.getActivePage().getActiveEditor();
  		if (part instanceof JavaEditor)
  			return (JavaEditor) part;
  		else
  			return null;
  	}
  
  	/**
  	 * 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
  	 */
  	private int getReplaceOffset() {
  		int start;
  		if (fContext instanceof DocumentTemplateContext) {
  			DocumentTemplateContext docContext = (DocumentTemplateContext)fContext;
  			start= docContext.getStart();
  		} else {
  			start= fRegion.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
  	 */
  	private int getReplaceEndOffset() {
  		int end;
  		if (fContext instanceof DocumentTemplateContext) {
  			DocumentTemplateContext docContext = (DocumentTemplateContext)fContext;
  			end= docContext.getEnd();
  		} else {
  			end= fRegion.getOffset() + fRegion.getLength();
  		}
  		return end;
  	}
  
  	private void ensurePositionCategoryInstalled(final IDocument document, LinkedModeModel model) {
  		if (!document.containsPositionCategory(getCategory())) {
  			document.addPositionCategory(getCategory());
  			final InclusivePositionUpdater updater= new InclusivePositionUpdater(getCategory());
  			document.addPositionUpdater(updater);
  
  			model.addLinkingListener(new ILinkedModeListener() {
  
  				/*
  				 * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel, int)
  				 */
  				public void left(LinkedModeModel environment, int flags) {
  					try {
  						document.removePositionCategory(getCategory());
  					} catch (BadPositionCategoryException e) {
  						// ignore
  					}
  					document.removePositionUpdater(updater);
  				}
  
  				public void suspend(LinkedModeModel environment) {}
  				public void resume(LinkedModeModel environment, int flags) {}
  			});
  		}
  	}
  
  	private String getCategory() {
  		return "TemplateProposalCategory_" + toString(); //$NON-NLS-1$
  	}
  
  	private int getCaretOffset(TemplateBuffer buffer) {
  
  	    TemplateVariable[] variables= buffer.getVariables();
  		for (int i= 0; i != variables.length; i++) {
  			TemplateVariable variable= variables[i];
  			if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME))
  				return variable.getOffsets()[0];
  		}
  
  		return buffer.getString().length();
  	}
  
  	/*
  	 * @see ICompletionProposal#getSelection(IDocument)
  	 */
  	public Point getSelection(IDocument document) {
  		return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
  	}
  
  	/*
  	 * @see ICompletionProposal#getAdditionalProposalInfo()
  	 */
  	public String getAdditionalProposalInfo() {
  	    try {
  		    fContext.setReadOnly(true);
  			TemplateBuffer templateBuffer;
  			try {
  				templateBuffer= fContext.evaluate(fTemplate);
  			} catch (TemplateException e1) {
  				return null;
  			}
  
  			IDocument document= new Document(templateBuffer.getString());
  			IndentUtil.indentLines(document, new LineRange(0, document.getNumberOfLines()), null, null);
  			return document.get();
  
  	    } catch (BadLocationException e) {
  			handleException(JavaScriptPlugin.getActiveWorkbenchShell(), new CoreException(new Status(IStatus.ERROR, JavaScriptPlugin.getPluginId(), IStatus.OK, "", e))); //$NON-NLS-1$
  			return null;
  		}
  	}
  
  	/*
  	 * @see ICompletionProposal#getDisplayString()
  	 */
  	public String getDisplayString() {
  		if (fDisplayString == null) {
  			String[] arguments= new String[] { fTemplate.getName(), fTemplate.getDescription() };
  			fDisplayString= Messages.format(TemplateContentAssistMessages.TemplateProposal_displayString, arguments);
  		}
  		return fDisplayString;
  	}
  
  	public void setDisplayString(String displayString) {
  		fDisplayString= displayString;
  	}
  
  	/*
  	 * @see ICompletionProposal#getImage()
  	 */
  	public Image getImage() {
  		return fImage;
  	}
  
  	/*
  	 * @see ICompletionProposal#getContextInformation()
  	 */
  	public IContextInformation getContextInformation() {
  		return null;
  	}
  
  	private void openErrorDialog(Shell shell, Exception e) {
  		MessageDialog.openError(shell, TemplateContentAssistMessages.TemplateEvaluator_error_title, e.getMessage());
  	}
  
  	private void handleException(Shell shell, CoreException e) {
  		ExceptionHandler.handle(e, shell, TemplateContentAssistMessages.TemplateEvaluator_error_title, null);
  	}
  
  	/*
  	 * @see IJavaCompletionProposal#getRelevance()
  	 */
  	public int getRelevance() {
  		return fRelevance;
  	}
  
  	public void setRelevance(int relevance) {
  		fRelevance= relevance;
  	}
  
  	public Template getTemplate() {
  		return fTemplate;
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getInformationControlCreator()
  	 */
  	public IInformationControlCreator getInformationControlCreator() {
  		int orientation;
  		IEditorPart editor= getJavaEditor();
  		if (editor instanceof IWorkbenchPartOrientation)
  			orientation= ((IWorkbenchPartOrientation)editor).getOrientation();
  		else
  			orientation= SWT.LEFT_TO_RIGHT;
  		return new TemplateInformationControlCreator(orientation);
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#selected(org.eclipse.jface.text.ITextViewer, boolean)
  	 */
  	public void selected(ITextViewer viewer, boolean smartToggle) {
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#unselected(org.eclipse.jface.text.ITextViewer)
  	 */
  	public void unselected(ITextViewer viewer) {
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument, int, org.eclipse.jface.text.DocumentEvent)
  	 */
  	public boolean validate(IDocument document, int offset, DocumentEvent event) {
  		try {
  			int replaceOffset= getReplaceOffset();
  			if (offset >= replaceOffset) {
  				String content= document.get(replaceOffset, offset - replaceOffset);
  				String templateName= fTemplate.getName().toLowerCase();
  				boolean valid= templateName.startsWith(content.toLowerCase());
  				if (!valid && fContext instanceof JavaDocContext && templateName.startsWith("<")) { //$NON-NLS-1$
  					valid= templateName.startsWith(content.toLowerCase(), 1);
  				}
  				return valid;
  			}
  		} catch (BadLocationException e) {
  			// concurrent modification - ignore
  		}
  		return false;
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementString()
  	 */
  	public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
  		// bug 114360 - don't make selection templates prefix-completable
  		if (isSelectionTemplate())
  			return ""; //$NON-NLS-1$
  		return fTemplate.getName();
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementOffset()
  	 */
  	public int getPrefixCompletionStart(IDocument document, int completionOffset) {
  		return getReplaceOffset();
  	}
  
  	/*
  	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension4#isAutoInsertable()
  	 */
  	public boolean isAutoInsertable() {
  		if (isSelectionTemplate())
  			return false;
  		return fTemplate.isAutoInsertable();
  	}
  	
  	/**
  	 * Returns <code>true</code> if the proposal has a selection, e.g. will wrap some code.
  	 * 
  	 * @return <code>true</code> if the proposals completion length is non zero
  	 * 
  	 */
  	private boolean isSelectionTemplate() {
  		if (fContext instanceof DocumentTemplateContext) {
  			DocumentTemplateContext ctx= (DocumentTemplateContext) fContext;
  			if (ctx.getCompletionLength() > 0)
  				return true;
  		}
  		return false;
  	}
  }
  