Skip to content

Commit

Permalink
Completion for external declared entity
Browse files Browse the repository at this point in the history
Fixes eclipse-lemminx#660

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Apr 28, 2020
1 parent da98780 commit a6cee0b
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public String getInputEncoding() {
*/
@Override
public String getNotationName() {
throw new UnsupportedOperationException();
return getValue();
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
package org.eclipse.lemminx.extensions.contentmodel.model;

import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lsp4j.LocationLink;
import org.w3c.dom.Entity;

/**
* Content model document which abstracts element declaration from a given
Expand Down Expand Up @@ -72,4 +75,13 @@ public interface CMDocument {
* @return true if the content model document is dirty and false otherwise.
*/
boolean isDirty();

/**
* Returns list of declared entities.
*
* @return list of declared entities.
*/
default List<Entity> getEntities() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@

import org.apache.xerces.impl.dtd.DTDGrammar;
import org.apache.xerces.impl.dtd.XMLDTDLoader;
import org.apache.xerces.impl.dtd.XMLEntityDecl;
import org.apache.xerces.xni.Augmentations;
import org.apache.xerces.xni.XMLString;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.grammars.Grammar;
import org.apache.xerces.xni.parser.XMLInputSource;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.dom.DTDEntityDecl;
import org.eclipse.lemminx.extensions.contentmodel.model.CMAttributeDeclaration;
import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument;
import org.eclipse.lemminx.extensions.contentmodel.model.CMElementDeclaration;
import org.eclipse.lemminx.extensions.contentmodel.model.FilesChangedTracker;
import org.eclipse.lemminx.extensions.dtd.utils.DTDUtils;
import org.eclipse.lsp4j.LocationLink;
import org.w3c.dom.Entity;

/**
* DTD document.
Expand All @@ -46,7 +49,6 @@
*/
public class CMDTDDocument extends XMLDTDLoader implements CMDocument {


static class DTDNodeInfo {

private String comment;
Expand Down Expand Up @@ -100,6 +102,8 @@ public String getComment(String attrName) {
private Map<String, DTDNodeInfo> attributes;
private DTDNodeInfo nodeInfo;

private List<Entity> entities;

public CMDTDDocument() {
this(null);
}
Expand Down Expand Up @@ -213,8 +217,8 @@ public void attributeDecl(String elementName, String attributeName, String type,
nodeInfo.setComment(comment);
attributes.put(attributeName, nodeInfo);
}
super.attributeDecl(elementName, attributeName, type, enumeration, defaultType, defaultValue, nonNormalizedDefaultValue,
augs);
super.attributeDecl(elementName, attributeName, type, enumeration, defaultType, defaultValue,
nonNormalizedDefaultValue, augs);
}

@Override
Expand Down Expand Up @@ -298,4 +302,56 @@ public boolean isDirty() {
return tracker != null ? tracker.isDirty() : null;
}

@Override
public List<Entity> getEntities() {
if (entities == null) {
entities = computeEntities();
}
return entities;
}

private synchronized List<Entity> computeEntities() {
if (entities != null) {
return entities;
}
List<Entity> entities = new ArrayList<>();
fillEntities(fDTDGrammar, entities);
return entities;
}

/**
* Collect entities declared in the DTD grammar.
*
* @param grammar the DTD grammar.
* @param entities list to fill.
*/
private static void fillEntities(DTDGrammar grammar, List<Entity> entities) {
int index = 0;
XMLEntityDecl entityDecl = new XMLEntityDecl() {

@Override
public void setValues(String name, String publicId, String systemId, String baseSystemId, String notation,
String value, boolean isPE, boolean inExternal) {
if (inExternal && !isPE) {
// Only external entities (entities declared in the DTD) and not entity with %
// must be collected.
Entity entity = new DTDEntityDecl(0, 0) {
@Override
public String getNodeName() {
return name;
}

@Override
public String getNotationName() {
return value;
}
};
entities.add(entity);
}
};
};
while (grammar.getEntityDecl(index, entityDecl)) {
index++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
*/
package org.eclipse.lemminx.extensions.entities;

import java.util.List;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DTDEntityDecl;
import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.entities.EntitiesDocumentationUtils.PredefinedEntity;
import org.eclipse.lemminx.services.extensions.CompletionParticipantAdapter;
import org.eclipse.lemminx.services.extensions.ICompletionRequest;
Expand All @@ -26,6 +29,7 @@
import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.w3c.dom.Entity;
import org.w3c.dom.NamedNodeMap;

/**
Expand All @@ -40,34 +44,63 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons
if (entityRange == null) {
return;
}
boolean markdown = request.canSupportMarkupKind(MarkupKind.MARKDOWN);
// There is the '&' character before the offset where completion was triggered
collectCharacterEntityProposals(entityRange, markdown, request, response);
boolean markdown = request.canSupportMarkupKind(MarkupKind.MARKDOWN);
DOMDocument document = request.getXMLDocument();
collectInternalEntityProposals(document, entityRange, markdown, response);
collectExternalEntityProposals(document, entityRange, markdown, request, response);
collectPredefinedEntityProposals(entityRange, markdown, request, response);
}

/**
* Collect locally declare entities.
* Collect internal entities.
*
* @param document the DOM document.
* @param entityRange the entity range.
* @param markdown
* @param markdown true if the documentation can be formatted as markdown and
* false otherwise.
* @param request the completion request.
* @param response the completion response.
*
*/
private static void collectCharacterEntityProposals(Range entityRange, boolean markdown, ICompletionRequest request,
private static void collectInternalEntityProposals(DOMDocument document, Range entityRange, boolean markdown,
ICompletionResponse response) {
DOMDocument document = request.getXMLDocument();
DOMDocumentType docType = document.getDoctype();
if (docType == null) {
return;
}
NamedNodeMap entities = docType.getEntities();
for (int i = 0; i < entities.getLength(); i++) {
DTDEntityDecl entity = (DTDEntityDecl) entities.item(i);
if (entity.getName() != null) {
Entity entity = (Entity) entities.item(i);
if (entity.getNodeName() != null) {
// provide completion for the locally declared entity
fillCompletion(entity.getName(), entity.getValue(), false, entityRange, markdown, response);
fillCompletion(entity.getNodeName(), entity.getNotationName(), false, false, entityRange, markdown,
response);
}
}
}

/**
* Collect external entities.
*
* @param document the DOM document.
* @param entityRange the entity range.
* @param markdown true if the documentation can be formatted as markdown and
* false otherwise.
* @param request the completion request.
* @param response the completion response.
*/
private static void collectExternalEntityProposals(DOMDocument document, Range entityRange, boolean markdown,
ICompletionRequest request, ICompletionResponse response) {
ContentModelManager contentModelManager = request.getComponent(ContentModelManager.class);
CMDocument cmDocument = contentModelManager.findCMDocument(document, null);
if (cmDocument != null) {
List<Entity> entities = cmDocument.getEntities();
for (Entity entity : entities) {
if (entity.getNodeName() != null) {
// provide completion for the external declared entity
fillCompletion(entity.getNodeName(), entity.getNotationName(), true, false, entityRange, markdown,
response);
}
}
}
}
Expand All @@ -76,23 +109,23 @@ private static void collectCharacterEntityProposals(Range entityRange, boolean m
* Collect predefined entities.
*
* @param entityRange the entity range.
* @param markdown
* @param markdown true if the documentation can be formatted as markdown and
* false otherwise.
* @param request the completion request.
* @param response the completion response.
*
* @see https://www.w3.org/TR/xml/#sec-predefined-ent
*/
private void collectPredefinedEntityProposals(Range entityRange, boolean markdown, ICompletionRequest request,
ICompletionResponse response) {
// see https://www.w3.org/TR/xml/#sec-predefined-ent
PredefinedEntity[] entities = PredefinedEntity.values();
for (PredefinedEntity entity : entities) {
fillCompletion(entity.name(), entity.getValue(), true, entityRange, markdown, response);
fillCompletion(entity.name(), entity.getValue(), false, true, entityRange, markdown, response);
}
}

private static void fillCompletion(String name, String entityValue, boolean predefined, Range entityRange,
boolean markdown, ICompletionResponse response) {
private static void fillCompletion(String name, String entityValue, boolean external, boolean predefined,
Range entityRange, boolean markdown, ICompletionResponse response) {
String entityName = "&" + name + ";";
CompletionItem item = new CompletionItem();
item.setLabel(entityName);
Expand All @@ -101,7 +134,8 @@ private static void fillCompletion(String name, String entityValue, boolean pred
String insertText = entityName;
item.setFilterText(insertText);
item.setTextEdit(new TextEdit(entityRange, insertText));
item.setDocumentation(EntitiesDocumentationUtils.getDocumentation(name, entityValue, predefined, markdown));
item.setDocumentation(
EntitiesDocumentationUtils.getDocumentation(name, entityValue, external, predefined, markdown));
response.addCompletionItem(item);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,19 @@ public String getValue() {
private EntitiesDocumentationUtils() {
}

public static MarkupContent getDocumentation(String entityName, String entityValue, boolean predefined,
boolean markdown) {
/**
* Returns the entity documentation.
*
* @param entityName the entity name.
* @param entityValue the entity value.
* @param external true if it's an external entity and false otherwise.
* @param predefined true if it's an predefined entity and false otherwise.
* @param markdown true if the documentation can be formatted as markdown and
* false otherwise.
* @return the entity documentation.
*/
public static MarkupContent getDocumentation(String entityName, String entityValue, boolean external,
boolean predefined, boolean markdown) {
StringBuilder documentation = new StringBuilder();

// Title
Expand All @@ -62,6 +73,7 @@ public static MarkupContent getDocumentation(String entityName, String entityVal
if (entityValue != null && !entityValue.isEmpty()) {
addParameter("Value", entityValue, documentation, markdown);
}
addParameter("External", String.valueOf(external), documentation, markdown);
addParameter("Predefined", String.valueOf(predefined), documentation, markdown);

documentation.append(System.lineSeparator());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
*/
public class EntitiesCompletionExtensionsTest {

// Test for internal entities

@Test
public void afterAmp() throws BadLocationException {
// &|
Expand Down Expand Up @@ -93,7 +95,7 @@ public void insideWithAmp() throws BadLocationException {
}

@Test
public void nonep() throws BadLocationException {
public void none() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + //
"<!DOCTYPE article [\r\n" + //
" <!ENTITY mdash \"&#x2014;\">\r\n" + //
Expand All @@ -103,4 +105,22 @@ public void nonep() throws BadLocationException {
"</root>";
testCompletionFor(xml, 2 + 2 /* CDATA and Comments */);
}

// Test for external entities
@Test
public void external() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n" + //
"<!DOCTYPE root-element SYSTEM \"src/test/resources/dtd/base.dtd\" [\r\n" + //
" <!ENTITY mdash \"&#x2014;\">\r\n" + //
"]>\r\n" + //
"<root-element>\r\n" + //
"\r\n &|" + //
"</root-element>";
testCompletionFor(xml, null, "test.xml", 2 + //
2 /* CDATA and Comments */ + //
PredefinedEntity.values().length /* predefined entities */,
c("&mdash;", "&mdash;", r(6, 1, 6, 2), "&mdash;"), //
c("&foo;", "&foo;", r(6, 1, 6, 2), "&foo;"));
}

}
6 changes: 4 additions & 2 deletions org.eclipse.lemminx/src/test/resources/xml/base.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE root-element SYSTEM "../dtd/base.dtd" [

<!ENTITY mdash "&#x2014;">
]>
<root-element />
<root-element>

</root-element>

0 comments on commit a6cee0b

Please sign in to comment.