/*******************************************************************************
* Copyright (c) 2015-2016 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
* Angelo Zerr <angelo.zerr@gmail.com> - copied from org.eclipse.wst.xml.ui.internal.contentassist.AbstractXMLCompletionProposalComputer
* modified in order to process JSON Objects.
*******************************************************************************/
package org.eclipse.wst.json.ui.contentassist;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.wst.json.core.document.IJSONNode;
import org.eclipse.wst.json.core.document.IJSONPair;
import org.eclipse.wst.json.core.document.IJSONValue;
import org.eclipse.wst.json.core.regions.JSONRegionContexts;
import org.eclipse.wst.json.ui.internal.JSONUIMessages;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
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.ITextRegionContainer;
import org.eclipse.wst.sse.ui.contentassist.CompletionProposalInvocationContext;
import org.eclipse.wst.sse.ui.contentassist.ICompletionProposalComputer;
import org.eclipse.wst.sse.ui.internal.contentassist.ContentAssistUtils;
import org.w3c.dom.Node;
public abstract class AbstractJSONCompletionProposalComputer implements
ICompletionProposalComputer {
private static final String BLANK = " "; //$NON-NLS-1$
private static final String EMPTY = ""; //$NON-NLS-1$
private static final String COLON = ":"; //$NON-NLS-1$
protected static final String QUOTE = "\""; //$NON-NLS-1$
protected static final String TRUE = "true"; //$NON-NLS-1$
protected static final String FALSE = "false"; //$NON-NLS-1$
private String fErrorMessage;
private ITextViewer fTextViewer;
public AbstractJSONCompletionProposalComputer() {
fErrorMessage = null;
fTextViewer = null;
}
@Override
public List computeCompletionProposals(
CompletionProposalInvocationContext context,
IProgressMonitor monitor) {
ITextViewer textViewer = context.getViewer();
int documentPosition = context.getInvocationOffset();
setErrorMessage(null);
fTextViewer = textViewer;
IndexedRegion treeNode = ContentAssistUtils.getNodeAt(textViewer,
documentPosition <= 0 ? 0 : documentPosition - 1);
IJSONNode node = (IJSONNode) treeNode;
while ((node != null) && (node.getNodeType() == Node.TEXT_NODE)
&& (node.getParentNode() != null)) {
node = node.getParentNode();
}
ContentAssistRequest contentAssistRequest = null;
IStructuredDocumentRegion sdRegion = getStructuredDocumentRegion(documentPosition);
ITextRegion completionRegion = getCompletionRegion(documentPosition,
node);
// Fix completion region in case of JSON_OBJECT_CLOSE
if (completionRegion != null && completionRegion.getType() == JSONRegionContexts.JSON_OBJECT_CLOSE && documentPosition > 0) {
completionRegion = getCompletionRegion(documentPosition, node);
}
String matchString = EMPTY;
if (completionRegion != null) {
if (isPairValue(context, node)) {
try {
String nodeText = getNodeText(node);
int colonIndex = nodeText.indexOf(COLON);
int offset = documentPosition - node.getStartOffset();
if (colonIndex >= 0 && offset >= 0) {
String str = nodeText.substring(colonIndex+1, offset);
str = str.replaceAll(",", BLANK); //$NON-NLS-1$
matchString = str;
}
} catch (BadLocationException e) {
// ignore
}
} else {
matchString = getMatchString(sdRegion, completionRegion, documentPosition);
}
}
// compute normal proposals
contentAssistRequest = computeCompletionProposals(matchString,
completionRegion, (IJSONNode) treeNode, node != null ? node.getParentNode() : null, context);
if (contentAssistRequest == null) {
contentAssistRequest = new ContentAssistRequest(
(IJSONNode) treeNode, node != null ? node.getParentNode() : null, sdRegion,
completionRegion, documentPosition, 0, EMPTY);
setErrorMessage(JSONUIMessages.Content_Assist_not_availab_UI_);
}
/*
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=123892 Only set this
* error message if nothing else was already set
*/
if (contentAssistRequest.getProposals().size() == 0
&& getErrorMessage() == null) {
setErrorMessage(JSONUIMessages.Content_Assist_not_availab_UI_);
}
ICompletionProposal[] props = contentAssistRequest
.getCompletionProposals();
return (props != null) ? Arrays.asList(props) : new ArrayList(0);
}
/**
* @param errorMessage
* the reason why computeProposals was not able to find any
* completions.
*/
protected void setErrorMessage(String errorMessage) {
fErrorMessage = errorMessage;
}
/**
* <p>
* This does all the magic of figuring out where in the JSON type document
* the content assist was invoked and then calling the corresponding method
* to add the correct proposals
* </p>
*
* <p>
* <b>NOTE: </b>if overriding be sure to make super call back to this method
* otherwise you will loose all of the proposals generated by this method
* </p>
*
* @param matchString
* @param completionRegion
* @param treeNode
* @param xmlnode
* @param context
*
* @return {@link ContentAssistRequest} that now has all the proposals in it
*/
protected ContentAssistRequest computeCompletionProposals(
String matchString, ITextRegion completionRegion,
IJSONNode treeNode, IJSONNode xmlnode,
CompletionProposalInvocationContext context) {
int documentPosition = context.getInvocationOffset();
ContentAssistRequest contentAssistRequest = null;
String regionType = completionRegion!= null ? completionRegion.getType() : EMPTY;
IStructuredDocumentRegion sdRegion = getStructuredDocumentRegion(documentPosition);
// Handle the most common and best supported cases
if(xmlnode != null) {
if (xmlnode.getNodeType() == IJSONNode.DOCUMENT_NODE || xmlnode.getNodeType() == IJSONNode.OBJECT_NODE) {
if (treeNode.getNodeType() == IJSONNode.OBJECT_NODE) {
if (regionType == JSONRegionContexts.JSON_OBJECT_OPEN
|| regionType == JSONRegionContexts.JSON_OBJECT_CLOSE
|| regionType == JSONRegionContexts.JSON_OBJECT_KEY
|| regionType == JSONRegionContexts.JSON_COMMA
|| regionType == JSONRegionContexts.JSON_UNKNOWN) {
contentAssistRequest = computeObjectKeyProposals(matchString,
completionRegion, treeNode, xmlnode, context);
}
} else if ((treeNode.getNodeType() == IJSONNode.PAIR_NODE)) {
if (regionType == JSONRegionContexts.JSON_OBJECT_KEY
|| regionType == JSONRegionContexts.JSON_OBJECT_OPEN
|| regionType == JSONRegionContexts.JSON_OBJECT_CLOSE
|| regionType == JSONRegionContexts.JSON_ARRAY_OPEN
|| regionType == JSONRegionContexts.JSON_ARRAY_CLOSE
|| regionType == JSONRegionContexts.JSON_COMMA
|| regionType == JSONRegionContexts.JSON_VALUE_BOOLEAN
|| regionType == JSONRegionContexts.JSON_UNKNOWN
|| regionType == JSONRegionContexts.JSON_COLON
|| regionType == JSONRegionContexts.JSON_VALUE_STRING) {
contentAssistRequest = computeObjectKeyProposals(matchString,
completionRegion, treeNode, xmlnode, context);
}
}
}
}
return contentAssistRequest;
}
private ContentAssistRequest computeObjectKeyProposals(String matchString,
ITextRegion completionRegion, IJSONNode nodeAtOffset,
IJSONNode node, CompletionProposalInvocationContext context) {
int documentPosition = context.getInvocationOffset();
ContentAssistRequest contentAssistRequest = null;
IStructuredDocumentRegion sdRegion = getStructuredDocumentRegion(documentPosition);
int replaceLength = 0;
int begin = documentPosition;
if (completionRegion.getType() == JSONRegionContexts.JSON_OBJECT_KEY || completionRegion.getType() == JSONRegionContexts.JSON_UNKNOWN) {
replaceLength = completionRegion.getTextLength();
// if container region, be sure replace length is only the attribute
// value region not the entire container
if (completionRegion instanceof ITextRegionContainer) {
ITextRegion openRegion = ((ITextRegionContainer) completionRegion)
.getFirstRegion();
ITextRegion closeRegion = ((ITextRegionContainer) completionRegion)
.getLastRegion();
if (openRegion.getType() != closeRegion.getType()) {
replaceLength = openRegion.getTextLength();
}
}
begin = sdRegion.getStartOffset(completionRegion);
}
if (isPairValue(context, nodeAtOffset)) {
IJSONPair pair = (IJSONPair) nodeAtOffset;
IJSONValue value = pair.getValue();
if (value != null) {
try {
begin = value.getStartOffset();
String valueText = getNodeText(value);
valueText = valueText.trim();
replaceLength = valueText.length();
if (valueText.startsWith(QUOTE)) {
begin = begin + 1;
replaceLength = replaceLength - 1;
}
if (valueText.endsWith(QUOTE)) {
replaceLength = replaceLength - 1;
}
} catch (BadLocationException e) {
// ignore
}
}
} else if (nodeAtOffset instanceof IJSONPair) {
IJSONPair pair = (IJSONPair) nodeAtOffset;
try {
begin = pair.getStartOffset();
String text = getNodeText(pair);
text = text.trim();
replaceLength = pair.getName().length();
if (text.startsWith(QUOTE)) {
begin = begin + 1;
}
} catch (BadLocationException e) {
// ignore
}
}
contentAssistRequest = new ContentAssistRequest(nodeAtOffset,
node.getParentNode(), sdRegion, completionRegion, begin,
replaceLength, matchString);
addObjectKeyProposals(contentAssistRequest, context);
return contentAssistRequest;
}
protected abstract void addObjectKeyProposals(
ContentAssistRequest contentAssistRequest,
CompletionProposalInvocationContext context);
private ITextRegion getCompletionRegion(int offset,
IStructuredDocumentRegion sdRegion) {
ITextRegion region = sdRegion.getRegionAtCharacterOffset(offset);
if (region == null) {
return null;
}
if (sdRegion.getStartOffset(region) == offset) {
// The offset is at the beginning of the region
if ((sdRegion.getStartOffset(region) == sdRegion.getStartOffset())
&& (sdRegion.getPrevious() != null)
&& (!sdRegion.getPrevious().isEnded())) {
// Is the region also the start of the node? If so, the
// previous IStructuredDocumentRegion is
// where to look for a useful region.
region = sdRegion.getPrevious().getRegionAtCharacterOffset(
offset - 1);
} else {
// Is there no separating whitespace from the previous region?
// If not,
// then that region is the important one
ITextRegion previousRegion = sdRegion
.getRegionAtCharacterOffset(offset - 1);
if ((previousRegion != null)
&& (previousRegion != region)
&& (previousRegion.getTextLength() == previousRegion
.getLength())) {
region = previousRegion;
}
}
} else {
// The offset is NOT at the beginning of the region
if ((region.getType() != JSONRegionContexts.JSON_COLON)
&& (offset > sdRegion.getStartOffset(region)
+ region.getTextLength())) { // attached
// JSON_TAG_ATTRIBUTE_EQUALS
// filter due to
// #bug219992
// Is the offset within the whitespace after the text in this
// region?
// If so, use the next region
ITextRegion nextRegion = sdRegion
.getRegionAtCharacterOffset(sdRegion
.getStartOffset(region) + region.getLength());
if (nextRegion != null) {
region = nextRegion;
}
} else {
// Is the offset within the important text for this region?
// If so, then we've already got the right one.
}
}
// valid WHITE_SPACE region handler (#179924)
if ((region != null)
&& (region.getType() == JSONRegionContexts.WHITE_SPACE)) {
ITextRegion previousRegion = sdRegion
.getRegionAtCharacterOffset(sdRegion.getStartOffset(region) - 1);
if (previousRegion != null) {
region = previousRegion;
}
}
return region;
}
/**
* Return the region whose content's require completion. This is something
* of a misnomer as sometimes the user wants to be prompted for contents of
* a non-existant ITextRegion, such as for enumerated attribute values
* following an '=' sign.
*/
private ITextRegion getCompletionRegion(int documentPosition, IJSONNode node) {
if (node == null) {
return null;
}
ITextRegion region = null;
int offset = documentPosition;
IStructuredDocumentRegion flatNode = null;
if (node.getNodeType() == IJSONNode.DOCUMENT_NODE) {
if (node.getStructuredDocument().getLength() == 0) {
return null;
}
ITextRegion result = node.getStructuredDocument()
.getRegionAtCharacterOffset(offset)
.getRegionAtCharacterOffset(offset);
while (result == null) {
offset--;
result = node.getStructuredDocument()
.getRegionAtCharacterOffset(offset)
.getRegionAtCharacterOffset(offset);
}
return result;
}
IStructuredDocumentRegion startTag = node
.getStartStructuredDocumentRegion();
IStructuredDocumentRegion endTag = node
.getEndStructuredDocumentRegion();
// Determine if the offset is within the start
// IStructuredDocumentRegion, end IStructuredDocumentRegion, or
// somewhere within the Node's JSON content.
if ((startTag != null) && (startTag.getStartOffset() <= offset)
&& (offset < startTag.getStartOffset() + startTag.getLength())) {
flatNode = startTag;
} else if ((endTag != null) && (endTag.getStartOffset() <= offset)
&& (offset < endTag.getStartOffset() + endTag.getLength())) {
flatNode = endTag;
}
if (flatNode != null) {
// the offset is definitely within the start or end tag, continue
// on and find the region
region = getCompletionRegion(offset, flatNode);
} else {
// the docPosition is neither within the start nor the end, so it
// must be content
flatNode = node.getStructuredDocument().getRegionAtCharacterOffset(
offset);
// (pa) ITextRegion refactor
// if (flatNode.contains(documentPosition)) {
if ((flatNode.getStartOffset() <= documentPosition)
&& (flatNode.getEndOffset() >= documentPosition)) {
// we're interesting in completing/extending the previous
// IStructuredDocumentRegion if the current
// IStructuredDocumentRegion isn't plain content or if it's
// preceded by an orphan '<'
/*
* if ((offset == flatNode.getStartOffset()) &&
* (flatNode.getPrevious() != null) && (((flatNode
* .getRegionAtCharacterOffset(documentPosition) != null)) ||
* (flatNode.getPrevious().getLastRegion() .getType() ==
* JSONRegionContext.JSON_TAG_OPEN) || (flatNode
* .getPrevious().getLastRegion().getType() ==
* JSONRegionContext.JSON_END_TAG_OPEN))) {
*
* // Is the region also the start of the node? If so, the //
* previous IStructuredDocumentRegion is // where to look for a
* useful region. region =
* flatNode.getPrevious().getLastRegion(); } else if
* (flatNode.getEndOffset() == documentPosition) { region =
* flatNode.getLastRegion(); } else { region =
* flatNode.getFirstRegion(); }
*/
region = flatNode.getFirstRegion();
} else {
// catch end of document positions where the docPosition isn't
// in a IStructuredDocumentRegion
region = flatNode.getLastRegion();
}
}
return region;
}
private String getMatchString(IStructuredDocumentRegion parent,
ITextRegion aRegion, int offset) {
if (aRegion == null) {
return EMPTY;
}
String regionType = aRegion.getType();
if (regionType != JSONRegionContexts.JSON_OBJECT_KEY) {
return EMPTY;
}
if ((parent.getText(aRegion).length() > 0)
&& (parent.getStartOffset(aRegion) < offset)) {
return parent.getText(aRegion).substring(0,
offset - parent.getStartOffset(aRegion));
}
return EMPTY;
}
/**
* StructuredTextViewer must be set before using this.
*/
private IStructuredDocumentRegion getStructuredDocumentRegion(int pos) {
return ContentAssistUtils.getStructuredDocumentRegion(fTextViewer, pos);
}
/**
* <p>
* Returns information about possible contexts based on the specified
* location within the document that corresponds to the current cursor
* position within the text viewer.
* </p>
*
* @see org.eclipse.wst.sse.ui.contentassist.ICompletionProposalComputer#computeContextInformation(org.eclipse.wst.sse.ui.contentassist.CompletionProposalInvocationContext,
* org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public List computeContextInformation(
CompletionProposalInvocationContext context,
IProgressMonitor monitor) {
// no default context info
return Collections.EMPTY_LIST;
}
protected boolean isPairValue(CompletionProposalInvocationContext context, IJSONNode node) {
if ( !(node instanceof IJSONPair) ) {
return false;
}
int documentPosition = context.getInvocationOffset();
try {
String nodeText = getNodeText(node);
int colonIndex = nodeText.indexOf(COLON); //$NON-NLS-1$
if (colonIndex >= 0) {
return documentPosition > node.getStartOffset() + colonIndex;
}
} catch (BadLocationException e) {
// ignore
}
return false;
}
private String getNodeText(IJSONNode node) throws BadLocationException {
return node.getStructuredDocument().get(node.getStartOffset(), node.getEndOffset() - node.getStartOffset());
}
/**
* <p>
* helpful utility method for determining if one string starts with another
* one. This is case insensitive. If either are null then result is
* <code>true</code>
* </p>
*
* @param aString
* the string to check to see if it starts with the given prefix
* @param prefix
* check that the given string starts with this prefix
*
* @return <code>true</code> if the given string starts with the given
* prefix, <code>false</code> otherwise
*/
protected static boolean beginsWith(String aString, String prefix) {
if ((aString == null) || (prefix == null)) {
return true;
}
return aString.toLowerCase().startsWith(prefix.toLowerCase());
}
}