| /******************************************************************************* |
| * Copyright (c) 2004, 2007 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.html.ui.internal.contentassist; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.contentassist.ICompletionProposal; |
| import org.eclipse.jface.text.contentassist.IContentAssistProcessor; |
| import org.eclipse.jface.util.IPropertyChangeListener; |
| import org.eclipse.jface.util.PropertyChangeEvent; |
| import org.eclipse.wst.css.ui.internal.contentassist.CSSContentAssistProcessor; |
| import org.eclipse.wst.html.core.internal.contentmodel.HTMLCMDocument; |
| import org.eclipse.wst.html.core.internal.provisional.HTML40Namespace; |
| import org.eclipse.wst.html.core.internal.provisional.HTMLCMProperties; |
| import org.eclipse.wst.html.core.text.IHTMLPartitions; |
| import org.eclipse.wst.html.ui.StructuredTextViewerConfigurationHTML; |
| import org.eclipse.wst.html.ui.internal.HTMLUIPlugin; |
| import org.eclipse.wst.html.ui.internal.editor.HTMLEditorPluginImageHelper; |
| import org.eclipse.wst.html.ui.internal.editor.HTMLEditorPluginImages; |
| import org.eclipse.wst.html.ui.internal.preferences.HTMLUIPreferenceNames; |
| import org.eclipse.wst.html.ui.internal.templates.TemplateContextTypeIdsHTML; |
| import org.eclipse.wst.sse.core.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.INodeAdapterFactory; |
| import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; |
| import org.eclipse.wst.sse.ui.internal.IReleasable; |
| import org.eclipse.wst.sse.ui.internal.StructuredTextViewer; |
| import org.eclipse.wst.sse.ui.internal.contentassist.ContentAssistUtils; |
| import org.eclipse.wst.sse.ui.internal.contentassist.CustomCompletionProposal; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMDocument; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration; |
| import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery; |
| import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; |
| import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; |
| import org.eclipse.wst.xml.core.internal.ssemodelquery.ModelQueryAdapter; |
| import org.eclipse.wst.xml.ui.internal.contentassist.AbstractContentAssistProcessor; |
| import org.eclipse.wst.xml.ui.internal.contentassist.ContentAssistRequest; |
| import org.eclipse.wst.xml.ui.internal.contentassist.XMLContentModelGenerator; |
| import org.eclipse.wst.xml.ui.internal.contentassist.XMLRelevanceConstants; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.DocumentType; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| public class HTMLContentAssistProcessor extends AbstractContentAssistProcessor implements IPropertyChangeListener { |
| private INodeAdapterFactory factoryForCSS = null; |
| protected IPreferenceStore fPreferenceStore = null; |
| protected boolean isXHTML = false; |
| private HTMLTemplateCompletionProcessor fTemplateProcessor = null; |
| private IContentAssistProcessor fJSContentAssistProcessor = null; |
| private List fTemplateContexts = new ArrayList(); |
| |
| public HTMLContentAssistProcessor() { |
| |
| super(); |
| } |
| |
| protected void addAttributeNameProposals(ContentAssistRequest contentAssistRequest) { |
| addTemplates(contentAssistRequest, TemplateContextTypeIdsHTML.ATTRIBUTE); |
| super.addAttributeNameProposals(contentAssistRequest); |
| } |
| |
| protected void addAttributeValueProposals(ContentAssistRequest contentAssistRequest) { |
| addTemplates(contentAssistRequest, TemplateContextTypeIdsHTML.ATTRIBUTE_VALUE); |
| super.addAttributeValueProposals(contentAssistRequest); |
| } |
| |
| /** |
| * Add the proposals for a completely empty document |
| */ |
| protected void addEmptyDocumentProposals(ContentAssistRequest contentAssistRequest) { |
| addTemplates(contentAssistRequest, TemplateContextTypeIdsHTML.NEW); |
| } |
| |
| protected void addPCDATAProposal(String nodeName, ContentAssistRequest contentAssistRequest) { |
| if (isXHTML) |
| super.addPCDATAProposal(nodeName, contentAssistRequest); |
| } |
| |
| protected void addStartDocumentProposals(ContentAssistRequest contentAssistRequest) { |
| if (isXHTML) |
| addEmptyDocumentProposals(contentAssistRequest); |
| } |
| |
| protected void addTagInsertionProposals(ContentAssistRequest contentAssistRequest, int childPosition) { |
| addTemplates(contentAssistRequest, TemplateContextTypeIdsHTML.TAG); |
| super.addTagInsertionProposals(contentAssistRequest, childPosition); |
| } |
| |
| /** |
| * Adds templates to the list of proposals |
| * |
| * @param contentAssistRequest |
| * @param context |
| */ |
| private void addTemplates(ContentAssistRequest contentAssistRequest, String context) { |
| addTemplates(contentAssistRequest, context, contentAssistRequest.getReplacementBeginPosition()); |
| } |
| |
| /** |
| * Adds templates to the list of proposals |
| * |
| * @param contentAssistRequest |
| * @param context |
| * @param startOffset |
| */ |
| private void addTemplates(ContentAssistRequest contentAssistRequest, String context, int startOffset) { |
| if (contentAssistRequest == null) |
| return; |
| |
| // if already adding template proposals for a certain context type, do |
| // not add again |
| if (!fTemplateContexts.contains(context)) { |
| fTemplateContexts.add(context); |
| boolean useProposalList = !contentAssistRequest.shouldSeparate(); |
| |
| if (getTemplateCompletionProcessor() != null) { |
| getTemplateCompletionProcessor().setContextType(context); |
| ICompletionProposal[] proposals = getTemplateCompletionProcessor().computeCompletionProposals(fTextViewer, startOffset); |
| for (int i = 0; i < proposals.length; ++i) { |
| if (useProposalList) |
| contentAssistRequest.addProposal(proposals[i]); |
| else |
| contentAssistRequest.addMacro(proposals[i]); |
| } |
| } |
| } |
| } |
| |
| protected boolean beginsWith(String aString, String prefix) { |
| if (aString == null || prefix == null || prefix.length() == 0) |
| return true; |
| int minimumLength = Math.min(prefix.length(), aString.length()); |
| String beginning = aString.substring(0, minimumLength); |
| return beginning.equalsIgnoreCase(prefix); |
| } |
| |
| protected ContentAssistRequest computeCompletionProposals(int documentPosition, String matchString, ITextRegion completionRegion, IDOMNode treeNode, IDOMNode xmlnode) { |
| ContentAssistRequest request = super.computeCompletionProposals(documentPosition, matchString, completionRegion, treeNode, xmlnode); |
| // bug115927 use original document position for all/any region templates |
| addTemplates(request, TemplateContextTypeIdsHTML.ALL, documentPosition); |
| return request; |
| } |
| |
| /** |
| * Return a list of proposed code completions based on the specified |
| * location within the document that corresponds to the current cursor |
| * position within the text-editor control. |
| * |
| * @param documentPosition |
| * a location within the document |
| * @return an array of code-assist items |
| */ |
| public ICompletionProposal[] computeCompletionProposals(ITextViewer textViewer, int documentPosition) { |
| fTemplateContexts.clear(); |
| |
| IndexedRegion treeNode = ContentAssistUtils.getNodeAt(textViewer, documentPosition); |
| IDOMNode node = (IDOMNode) treeNode; |
| setErrorMessage(null); |
| |
| // check if it's in a comment node |
| IStructuredDocument structuredDocument = (IStructuredDocument) textViewer.getDocument(); |
| IStructuredDocumentRegion fn = structuredDocument.getRegionAtCharacterOffset(documentPosition); |
| if (fn != null && fn.getType() == DOMRegionContext.XML_COMMENT_TEXT && documentPosition != fn.getStartOffset()) { |
| return new ICompletionProposal[0]; |
| } |
| |
| // CMVC 242695 |
| // if it's a </script> tag, bounce back to JS ca processor... |
| if (fn != null && fn.getType() == DOMRegionContext.XML_TAG_NAME && documentPosition == fn.getStartOffset()) { |
| ITextRegionList v = fn.getRegions(); |
| if (v.size() > 1) { |
| // determine that it's a close tag |
| if ((v.get(0)).getType() == DOMRegionContext.XML_END_TAG_OPEN) { |
| Iterator it = v.iterator(); |
| ITextRegion region = null; |
| // search for script tag name |
| while (it.hasNext()) { |
| region = (ITextRegion) it.next(); |
| if (fn.getText(region).equalsIgnoreCase("script")) { //$NON-NLS-1$ |
| IContentAssistProcessor jsProcessor = getJSContentAssistProcessor(); |
| if (jsProcessor != null) { |
| return jsProcessor.computeCompletionProposals(textViewer, documentPosition); |
| } |
| return new ICompletionProposal[0]; |
| } |
| } |
| } |
| } |
| } |
| |
| isXHTML = getXHTML(node); |
| |
| fGenerator = null; // force reload of content generator |
| |
| // handle blank HTML document case |
| if (treeNode == null || isViewerEmpty(textViewer)) { |
| // cursor is at the EOF |
| ICompletionProposal htmlTagProposal = getHTMLTagProposal((StructuredTextViewer) textViewer, documentPosition); |
| ICompletionProposal[] superResults = super.computeCompletionProposals(textViewer, documentPosition); |
| if (superResults != null && superResults.length > 0 && htmlTagProposal != null) { |
| ICompletionProposal[] blankHTMLDocResults = new ICompletionProposal[superResults.length + 1]; |
| blankHTMLDocResults[0] = htmlTagProposal; |
| System.arraycopy(superResults, 0, blankHTMLDocResults, 1, superResults.length); |
| return blankHTMLDocResults; |
| } |
| } |
| |
| if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { |
| |
| // check embedded CSS proposals at the beginning of the STYLE end |
| // tag |
| Element element = (Element) node; |
| String tagName = element.getTagName(); |
| if (tagName != null && tagName.equalsIgnoreCase(HTML40Namespace.ATTR_NAME_STYLE)) {//$NON-NLS-1$ |
| IStructuredDocumentRegion endStructuredDocumentRegion = node.getEndStructuredDocumentRegion(); |
| if (endStructuredDocumentRegion != null && endStructuredDocumentRegion.getStartOffset() == documentPosition) { |
| IStructuredDocumentRegion startStructuredDocumentRegion = node.getStartStructuredDocumentRegion(); |
| if (startStructuredDocumentRegion != null) { |
| int offset = startStructuredDocumentRegion.getEndOffset(); |
| int pos = documentPosition - offset; |
| ICompletionProposal[] proposals = getCSSProposals(textViewer, pos, node, offset, (char) 0); |
| if (proposals != null) |
| return proposals; |
| } |
| } |
| } |
| |
| // check inline CSS proposals |
| // need to find attr region from sd region |
| IStructuredDocumentRegion sdRegion = ContentAssistUtils.getStructuredDocumentRegion(textViewer, documentPosition); |
| Iterator regions = sdRegion.getRegions().iterator(); |
| ITextRegion styleNameRegion = null; |
| ITextRegion styleValueRegion = null; |
| while (regions.hasNext()) { |
| styleNameRegion = (ITextRegion) regions.next(); |
| if (styleNameRegion.getType().equals(DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) && sdRegion.getText(styleNameRegion).equalsIgnoreCase(HTML40Namespace.ATTR_NAME_STYLE)) { //$NON-NLS-1$ |
| // the next region should be "=" |
| if (regions.hasNext()) { |
| regions.next(); // skip the "=" |
| // next region should be attr value region |
| if (regions.hasNext()) { |
| styleValueRegion = (ITextRegion) regions.next(); |
| break; |
| } |
| } |
| } |
| } |
| |
| if (styleValueRegion != null) { |
| int offset = sdRegion.getStartOffset(styleValueRegion); |
| int end = sdRegion.getTextEndOffset(styleValueRegion); |
| if (documentPosition >= offset && documentPosition <= end) { |
| boolean askCSS = true; |
| char quote = (char) 0; |
| String text = sdRegion.getText(styleValueRegion); |
| int length = (text != null ? text.length() : 0); |
| if (length > 0) { |
| char firstChar = text.charAt(0); |
| if (firstChar == '"' || firstChar == '\'') { |
| if (documentPosition == offset) { |
| // before quote |
| askCSS = false; |
| } |
| else { |
| offset++; |
| quote = firstChar; |
| } |
| } |
| if (documentPosition == end) { |
| if (length > 1 && text.charAt(length - 1) == quote) { |
| // after quote |
| askCSS = false; |
| } |
| } |
| } |
| if (askCSS) { |
| int pos = documentPosition - offset; |
| ICompletionProposal[] proposals = getCSSProposals(textViewer, pos, node, offset, quote); |
| if (proposals != null) |
| return proposals; |
| } |
| } |
| } |
| } |
| |
| return super.computeCompletionProposals(textViewer, documentPosition); |
| } |
| |
| /** |
| * Returns true if there is no text or it's all white space, otherwise |
| * returns false |
| * |
| * @param treeNode |
| * @param textViewer |
| * @return boolean |
| */ |
| private boolean isViewerEmpty(ITextViewer textViewer) { |
| boolean isEmpty = false; |
| String text = textViewer.getTextWidget().getText(); |
| if (text == null || (text != null && text.trim().equals(""))) //$NON-NLS-1$ |
| isEmpty = true; |
| return isEmpty; |
| } |
| |
| /** |
| * @return ICompletionProposal |
| */ |
| private ICompletionProposal getHTMLTagProposal(StructuredTextViewer viewer, int documentPosition) { |
| IModelManager mm = StructuredModelManager.getModelManager(); |
| IStructuredModel model = null; |
| ICompletionProposal result = null; |
| try { |
| if (mm != null) { |
| model = mm.getExistingModelForRead(viewer.getDocument()); |
| |
| if (model != null) { |
| IDOMDocument doc = ((IDOMModel) model).getDocument(); |
| |
| ModelQuery mq = ModelQueryUtil.getModelQuery(doc); |
| if (mq != null) { |
| |
| // XHTML requires lowercase tagname for lookup |
| CMDocument correspondingCMDocument = mq.getCorrespondingCMDocument(doc); |
| if (correspondingCMDocument != null) { |
| CMElementDeclaration htmlDecl = (CMElementDeclaration) correspondingCMDocument.getElements().getNamedItem(HTML40Namespace.ElementName.HTML.toLowerCase()); |
| if (htmlDecl != null) { |
| StringBuffer proposedTextBuffer = new StringBuffer(); |
| getContentGenerator().generateTag(doc, htmlDecl, proposedTextBuffer); |
| |
| String proposedText = proposedTextBuffer.toString(); |
| String requiredName = getContentGenerator().getRequiredName(doc, htmlDecl); |
| |
| CustomCompletionProposal proposal = new CustomCompletionProposal(proposedText, documentPosition, |
| /* start pos */ |
| 0, /* replace length */ |
| requiredName.length() + 2, /* |
| * cursor position |
| * after |
| * (relavtive to |
| * start) |
| */ |
| HTMLEditorPluginImageHelper.getInstance().getImage(HTMLEditorPluginImages.IMG_OBJ_TAG_GENERIC), requiredName, null, null, XMLRelevanceConstants.R_TAG_NAME); |
| result = proposal; |
| } |
| } |
| } |
| } |
| } |
| } |
| finally { |
| if (model != null) |
| model.releaseFromRead(); |
| } |
| return result; |
| } |
| |
| /** |
| * @see AbstractContentAssistProcessor#getContentGenerator() |
| */ |
| public XMLContentModelGenerator getContentGenerator() { |
| if (fGenerator == null) { |
| if (isXHTML) |
| fGenerator = XHTMLMinimalContentModelGenerator.getInstance(); |
| else |
| fGenerator = HTMLMinimalContentModelGenerator.getInstance(); |
| } |
| return fGenerator; |
| } |
| |
| protected ICompletionProposal[] getCSSProposals(ITextViewer viewer, int pos, IDOMNode element, int offset, char quote) { |
| |
| CSSContentAssistProcessor cssProcessor = new CSSContentAssistProcessor(); |
| cssProcessor.setDocumentOffset(offset); |
| cssProcessor.setQuoteCharOfStyleAttribute(quote); |
| |
| return cssProcessor.computeCompletionProposals(viewer, pos); |
| } |
| |
| protected String getEmptyTagCloseString() { |
| if (isXHTML) |
| return " />"; //$NON-NLS-1$ |
| return ">"; //$NON-NLS-1$ |
| } |
| |
| private IContentAssistProcessor getJSContentAssistProcessor() { |
| if (fJSContentAssistProcessor == null) { |
| fJSContentAssistProcessor = new StructuredTextViewerConfigurationHTML().getContentAssistant(null).getContentAssistProcessor(IHTMLPartitions.SCRIPT); |
| } |
| return fJSContentAssistProcessor; |
| } |
| |
| private HTMLTemplateCompletionProcessor getTemplateCompletionProcessor() { |
| if (fTemplateProcessor == null) { |
| fTemplateProcessor = new HTMLTemplateCompletionProcessor(); |
| } |
| return fTemplateProcessor; |
| } |
| |
| /** |
| * Determine if this Document is an XHTML Document. Oprates solely off of |
| * the Document Type declaration |
| */ |
| protected boolean getXHTML(Node node) { |
| if (node == null) |
| return false; |
| |
| Document doc = null; |
| if (node.getNodeType() != Node.DOCUMENT_NODE) |
| doc = node.getOwnerDocument(); |
| else |
| doc = ((Document) node); |
| |
| if (doc instanceof IDOMDocument) |
| return ((IDOMDocument) doc).isXMLType(); |
| |
| |
| if (doc instanceof INodeNotifier) { |
| ModelQueryAdapter adapter = (ModelQueryAdapter) ((INodeNotifier) doc).getAdapterFor(ModelQueryAdapter.class); |
| CMDocument cmdoc = null; |
| if (adapter != null && adapter.getModelQuery() != null) |
| cmdoc = adapter.getModelQuery().getCorrespondingCMDocument(doc); |
| if (cmdoc != null) { |
| // treat as XHTML unless we've got the in-code HTML content |
| // model |
| if (cmdoc instanceof HTMLCMDocument) |
| return false; |
| if (cmdoc.supports(HTMLCMProperties.IS_XHTML)) |
| return Boolean.TRUE.equals(cmdoc.getProperty(HTMLCMProperties.IS_XHTML)); |
| } |
| } |
| // this should never be reached |
| DocumentType docType = doc.getDoctype(); |
| return docType != null && docType.getPublicId() != null && docType.getPublicId().indexOf("-//W3C//DTD XHTML ") == 0; //$NON-NLS-1$ |
| } |
| |
| protected void init() { |
| getPreferenceStore().addPropertyChangeListener(this); |
| reinit(); |
| } |
| |
| protected void reinit() { |
| String key = HTMLUIPreferenceNames.AUTO_PROPOSE; |
| boolean doAuto = getPreferenceStore().getBoolean(key); |
| if (doAuto) { |
| key = HTMLUIPreferenceNames.AUTO_PROPOSE_CODE; |
| completionProposalAutoActivationCharacters = getPreferenceStore().getString(key).toCharArray(); |
| } |
| else { |
| completionProposalAutoActivationCharacters = null; |
| } |
| } |
| |
| public void release() { |
| if (factoryForCSS != null) { |
| factoryForCSS.release(); |
| } |
| if (fJSContentAssistProcessor instanceof IReleasable) { |
| ((IReleasable)fJSContentAssistProcessor).release(); |
| } |
| getPreferenceStore().removePropertyChangeListener(this); |
| super.release(); |
| } |
| |
| protected boolean stringsEqual(String a, String b) { |
| return a.equalsIgnoreCase(b); |
| } |
| |
| public void propertyChange(PropertyChangeEvent event) { |
| String property = event.getProperty(); |
| |
| if (property.compareTo(HTMLUIPreferenceNames.AUTO_PROPOSE) == 0 || property.compareTo(HTMLUIPreferenceNames.AUTO_PROPOSE_CODE) == 0) { |
| reinit(); |
| } |
| } |
| |
| protected IPreferenceStore getPreferenceStore() { |
| if (fPreferenceStore == null) |
| fPreferenceStore = HTMLUIPlugin.getDefault().getPreferenceStore(); |
| |
| return fPreferenceStore; |
| } |
| |
| public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int documentPosition, IndexedRegion indexedNode, ITextRegion region) { |
| return computeCompletionProposals(viewer, documentPosition); |
| } |
| } |