Skip to content

Commit

Permalink
completionItem/resolve support
Browse files Browse the repository at this point in the history
Collect model documentation for completion items in the resolve step

Fixes eclipse#616, redhat-developer/vscode-xml#679

Signed-off-by: David Thompson <davthomp@redhat.com>
  • Loading branch information
datho7561 committed Jun 22, 2022
1 parent 4cd0a54 commit 8d8db70
Show file tree
Hide file tree
Showing 38 changed files with 702 additions and 264 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
//Use consistent indentation
"editor.detectIndentation": false,
"editor.tabSize": 4,
"editor.insertSpaces": false
"editor.insertSpaces": false,
"cSpell.words": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
});
}

@Override
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) {
return computeDOMAsync(unresolved.getData(), (xmlDocument, cancelChecker) -> {
return getXMLLanguageService().resolveCompletionItem(unresolved, xmlDocument, sharedSettings, cancelChecker);
});
}

@Override
public CompletableFuture<Hover> hover(HoverParams params) {
return computeDOMAsync(params.getTextDocument(), (xmlDocument, cancelChecker) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings;
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.extensions.ICompletionParticipant;
import org.eclipse.lemminx.services.extensions.IDocumentLinkParticipant;
import org.eclipse.lemminx.services.extensions.IHoverParticipant;
import org.eclipse.lemminx.services.extensions.ITypeDefinitionParticipant;
Expand All @@ -45,6 +44,7 @@
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant;
import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService;
import org.eclipse.lemminx.services.extensions.completion.ICompletionParticipant;
import org.eclipse.lemminx.services.extensions.diagnostics.IDiagnosticsParticipant;
import org.eclipse.lemminx.services.extensions.save.ISaveContext;
import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager;
Expand Down Expand Up @@ -151,7 +151,7 @@ private void updateSettings(ContentModelSettings settings, ISaveContext context)
if (useCache != null) {
contentModelManager.setUseCache(useCache);
}

// Download external resources
XMLDownloadExternalResourcesSettings downloadExternalResources = settings.getDownloadExternalResources();
boolean downloadExternalResourcesEnabled = downloadExternalResources == null
Expand Down Expand Up @@ -212,7 +212,7 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) {
registry.registerDocumentLifecycleParticipant(documentTelemetryParticipant);
formatterParticipant = new ContentModelFormatterParticipant(contentModelManager);
registry.registerFormatterParticipant(formatterParticipant);

// Register custom commands to re-validate XML files
IXMLCommandService commandService = registry.getCommandService();
if (commandService != null) {
Expand Down Expand Up @@ -242,7 +242,7 @@ public void stop(XMLExtensionsRegistry registry) {
registry.unregisterCodeLensParticipant(codeLensParticipant);
registry.unregisterDocumentLifecycleParticipant(documentTelemetryParticipant);
registry.unregisterFormatterParticipant(formatterParticipant);

// Un-register custom commands to re-validate XML files
IXMLCommandService commandService = registry.getCommandService();
if (commandService != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

Expand All @@ -25,11 +26,14 @@
import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument;
import org.eclipse.lemminx.extensions.contentmodel.model.CMElementDeclaration;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.participants.completion.AttributeValueCompletionResolver;
import org.eclipse.lemminx.extensions.contentmodel.utils.XMLGenerator;
import org.eclipse.lemminx.services.AttributeCompletionItem;
import org.eclipse.lemminx.services.extensions.CompletionParticipantAdapter;
import org.eclipse.lemminx.services.data.DataEntryField;
import org.eclipse.lemminx.services.extensions.ICompletionRequest;
import org.eclipse.lemminx.services.extensions.ICompletionResponse;
import org.eclipse.lemminx.services.extensions.completion.CompletionParticipantAdapter;
import org.eclipse.lemminx.services.extensions.completion.ICompletionItemResolveParticipant;
import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse;
import org.eclipse.lemminx.uriresolver.CacheResourceDownloadingException;
import org.eclipse.lemminx.utils.StringUtils;
import org.eclipse.lsp4j.CompletionItem;
Expand All @@ -45,14 +49,25 @@
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.gson.JsonObject;

/**
* Extension to support XML completion based on content model (XML Schema
* completion, etc)
*/
public class ContentModelCompletionParticipant extends CompletionParticipantAdapter {

private final HashMap<String, ICompletionItemResolveParticipant> completionResolvers;

public ContentModelCompletionParticipant() {
completionResolvers = new HashMap<>();
completionResolvers.put(AttributeValueCompletionResolver.PARTICIPANT_ID,
new AttributeValueCompletionResolver());
}

@Override
public void onTagOpen(ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker) throws Exception {
public void onTagOpen(ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker)
throws Exception {
try {
DOMDocument document = request.getXMLDocument();
ContentModelManager contentModelManager = request.getComponent(ContentModelManager.class);
Expand Down Expand Up @@ -122,7 +137,7 @@ private boolean hasNamespace(String namespaceURI, Collection<CMDocument> cmRootD

/**
* Fill with possible element declarations.
*
*
* @param parentElement the parent DOM element
* @param cmElement the content model element declaration
* @param defaultPrefix
Expand Down Expand Up @@ -150,7 +165,7 @@ private static void fillWithPossibleElementDeclaration(DOMElement parentElement,

/**
* Fill with children element declarations
*
*
* @param element
* @param cmDocument
* @param cmElements
Expand Down Expand Up @@ -201,7 +216,7 @@ private static void fillCompletionItem(Collection<CMElementDeclaration> elements

/**
* Add completion item with all tag names of the node list.
*
*
* @param list
* @param tags
* @param request
Expand Down Expand Up @@ -264,7 +279,8 @@ private static boolean isGenerateEndTag(DOMNode node, int offset, String tagName
}

@Override
public void onAttributeName(boolean generateValue, ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker)
public void onAttributeName(boolean generateValue, ICompletionRequest request, ICompletionResponse response,
CancelChecker cancelChecker)
throws Exception {
// otherwise, manage completion based on XML Schema, DTD.
DOMElement parentElement = request.getNode().isElement() ? (DOMElement) request.getNode() : null;
Expand Down Expand Up @@ -301,20 +317,24 @@ private void fillAttributesWithCMAttributeDeclarations(DOMElement parentElement,
return;
}
for (CMAttributeDeclaration attributeDeclaration : attributes) {
String prefix = (parentElement != null ? parentElement.getPrefix(attributeDeclaration.getNamespace()) : null);
String prefix = (parentElement != null ? parentElement.getPrefix(attributeDeclaration.getNamespace())
: null);
String attrName = attributeDeclaration.getName(prefix);
if (!parentElement.hasAttribute(attrName)) {
CompletionItem item = new AttributeCompletionItem(attrName, canSupportSnippet, fullRange, generateValue,
attributeDeclaration.getDefaultValue(), attributeDeclaration.getEnumerationValues(), request.getSharedSettings());
MarkupContent documentation = XMLGenerator.createMarkupContent(attributeDeclaration, elementDeclaration, request);
attributeDeclaration.getDefaultValue(), attributeDeclaration.getEnumerationValues(),
request.getSharedSettings());
MarkupContent documentation = XMLGenerator.createMarkupContent(attributeDeclaration, elementDeclaration,
request);
item.setDocumentation(documentation);
response.addCompletionAttribute(item);
}
}
}

@Override
public void onAttributeValue(String valuePrefix, ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker)
public void onAttributeValue(String valuePrefix, ICompletionRequest request, ICompletionResponse response,
CancelChecker cancelChecker)
throws Exception {
DOMElement parentElement = request.getNode().isElement() ? (DOMElement) request.getNode() : null;
if (parentElement == null) {
Expand Down Expand Up @@ -352,15 +372,24 @@ private void fillAttributeValuesWithCMAttributeDeclarations(CMElementDeclaration
item.setKind(CompletionItemKind.Value);
item.setFilterText(insertText);
item.setTextEdit(Either.forLeft(new TextEdit(fullRange, insertText)));
MarkupContent documentation = XMLGenerator.createMarkupContent(cmAttribute, value, cmElement, request);
item.setDocumentation(documentation);

DOMDocument document = request.getNode().getOwnerDocument();
JsonObject data = DataEntryField.createData(document.getDocumentURI(),
AttributeValueCompletionResolver.PARTICIPANT_ID);
try {
data.addProperty(AttributeValueCompletionResolver.OFFSET_KEY,
Integer.toString(document.offsetAt(request.getPosition())));
} catch (BadLocationException e) {
}
item.setData(data);
response.addCompletionItem(item);
});
}
}

@Override
public void onXMLContent(ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker) throws Exception {
public void onXMLContent(ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker)
throws Exception {
try {
ContentModelManager contentModelManager = request.getComponent(ContentModelManager.class);
DOMElement parentElement = request.getParentElement();
Expand Down Expand Up @@ -404,4 +433,9 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons
// XML Schema, DTD is loading, ignore this error
}
}

@Override
public ICompletionItemResolveParticipant getResolveCompletionItemParticipant(String participantId) {
return completionResolvers.get(participantId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.lemminx.extensions.contentmodel.participants.completion;

import java.util.Collection;

import org.eclipse.lemminx.dom.DOMAttr;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
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.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.utils.XMLGenerator;
import org.eclipse.lemminx.services.extensions.completion.ICompletionItemResolveParticipant;
import org.eclipse.lemminx.services.extensions.completion.ICompletionItemResolverRequest;
import org.eclipse.lemminx.uriresolver.CacheResourceDownloadingException;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

/**
* Resolves the documentation for the completion of the attribute value from the
* content model.
*
*/
public class AttributeValueCompletionResolver implements ICompletionItemResolveParticipant {

public static final String PARTICIPANT_ID = AttributeValueCompletionResolver.class.getName();
public static final String OFFSET_KEY = "OFFSET";

@Override
public CompletionItem resolveCompletionItem(ICompletionItemResolverRequest request, CancelChecker cancelChecker) {

CompletionItem toResolve = request.getUnresolved();

DOMDocument document = request.getDocument();

String offsetString = request.getDataProperty(OFFSET_KEY);
Integer offset = null;
if (document == null || offsetString == null) {
return toResolve;
}
try {
offset = Integer.parseInt(offsetString);
} catch (NumberFormatException e) {
return toResolve;
}

DOMAttr attr = document.findAttrAt(offset);
if (attr == null) {
return toResolve;
}

DOMNode parentNode = document.findNodeAt(offset);
if (parentNode == null || !parentNode.isElement()) {
return toResolve;
}
DOMElement parentElement = (DOMElement) parentNode;

collectDocumentationFromContentModel(request, toResolve, parentElement, attr);
return toResolve;
}

private void collectDocumentationFromContentModel(ICompletionItemResolverRequest request, CompletionItem toResolve,
DOMElement parentElement, DOMAttr attr) {
String attributeName = attr.getName();
String attributeValue = toResolve.getLabel();
try {
ContentModelManager contentModelManager = request.getComponent(ContentModelManager.class);
Collection<CMDocument> cmDocuments = contentModelManager.findCMDocument(parentElement);
for (CMDocument cmDocument : cmDocuments) {
CMElementDeclaration cmElement = cmDocument.findCMElement(parentElement,
parentElement.getNamespaceURI());
if (cmElement != null) {
MarkupContent documentation = getDocumentationForAttributeValue(cmElement, attributeName,
attributeValue, request);
if (documentation != null) {
toResolve.setDocumentation(documentation);
return;
}
}
}
} catch (CacheResourceDownloadingException e) {
}
}

private MarkupContent getDocumentationForAttributeValue(CMElementDeclaration cmElement, String attributeName,
String attributeValue, ICompletionItemResolverRequest request) {
CMAttributeDeclaration cmAttribute = cmElement.findCMAttribute(attributeName);
if (cmAttribute != null) {
return XMLGenerator.createMarkupContent(cmAttribute, attributeValue, cmElement,
request);
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
import org.eclipse.lemminx.extensions.entities.participants.EntitiesCompletionParticipant;
import org.eclipse.lemminx.extensions.entities.participants.EntitiesDefinitionParticipant;
import org.eclipse.lemminx.extensions.entities.participants.EntitiesHoverParticipant;
import org.eclipse.lemminx.services.extensions.ICompletionParticipant;
import org.eclipse.lemminx.services.extensions.IDefinitionParticipant;
import org.eclipse.lemminx.services.extensions.IHoverParticipant;
import org.eclipse.lemminx.services.extensions.IXMLExtension;
import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry;
import org.eclipse.lemminx.services.extensions.completion.ICompletionParticipant;
import org.eclipse.lsp4j.InitializeParams;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import org.eclipse.lemminx.extensions.entities.EntitiesDocumentationUtils;
import org.eclipse.lemminx.extensions.entities.EntitiesDocumentationUtils.EntityOriginType;
import org.eclipse.lemminx.extensions.entities.EntitiesDocumentationUtils.PredefinedEntity;
import org.eclipse.lemminx.services.extensions.CompletionParticipantAdapter;
import org.eclipse.lemminx.services.extensions.ICompletionRequest;
import org.eclipse.lemminx.services.extensions.ICompletionResponse;
import org.eclipse.lemminx.services.extensions.completion.CompletionParticipantAdapter;
import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lemminx.utils.XMLPositionUtility.EntityReferenceRange;
import org.eclipse.lsp4j.CompletionItem;
Expand Down Expand Up @@ -64,7 +64,7 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons

/**
* Collect local entities declared in the DOCTYPE.
*
*
* @param document the DOM document.
* @param entityRange the entity range.
* @param markdown true if the documentation can be formatted as markdown and
Expand All @@ -91,7 +91,7 @@ private static void collectLocalEntityProposals(DOMDocument document, Range enti

/**
* 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
Expand All @@ -118,12 +118,12 @@ private static void collectExternalEntityProposals(DOMDocument document, Range e

/**
* Collect predefined entities.
*
*
* @param entityRange the entity range.
* @param markdown true if the documentation can be formatted as markdown and
* false otherwise.
* @param response the completion response.
*
*
* @see https://www.w3.org/TR/xml/#sec-predefined-ent
*/
private void collectPredefinedEntityProposals(Range entityRange, boolean markdown, ICompletionResponse response) {
Expand Down
Loading

0 comments on commit 8d8db70

Please sign in to comment.