Skip to content

Commit

Permalink
Improve CodeAction to insert expected elements to support choice
Browse files Browse the repository at this point in the history
Signed-off-by: Jessica He <jhe@redhat.com>
  • Loading branch information
JessicaJHee authored and angelozerr committed Jan 26, 2023
1 parent 1d51ef6 commit 5759686
Show file tree
Hide file tree
Showing 17 changed files with 1,001 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.lemminx.dom.DOMAttr;
import org.eclipse.lemminx.dom.DOMElement;
Expand Down Expand Up @@ -57,6 +58,8 @@ public class CMRelaxNGElementDeclaration implements CMElementDeclaration {

private Set<String> requiredElementNames;

private Set<String> possibleRequiredElementNames;

CMRelaxNGElementDeclaration(CMRelaxNGDocument document, ElementPattern pattern) {
this.cmDocument = document;
this.pattern = pattern;
Expand Down Expand Up @@ -283,4 +286,23 @@ private Set<String> getRequiredElementNames() {
}
return requiredElementNames;
}

public Collection<CMElementDeclaration> getPossibleRequiredElements() {
Set<CMElementDeclaration> possibelRequiredElements = new LinkedHashSet<>();
if (possibleRequiredElementNames == null) {
possibleRequiredElementNames = new LinkedHashSet<>();
PossibleRequiredElementsFunction elementsFunction = new PossibleRequiredElementsFunction(
possibleRequiredElementNames);
getPattern().getContent().apply(elementsFunction);
}
for (String elementName : possibleRequiredElementNames) {
possibelRequiredElements.add(findCMElement(elementName, null));
}

List<CMElementDeclaration> sortedElements = possibelRequiredElements.stream().collect(Collectors.toList());
Collections.sort(sortedElements, (e1, e2) -> {
return ((CMElementDeclaration) e1).getLocalName().compareTo(((CMElementDeclaration) e2).getLocalName());
});
return sortedElements;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*******************************************************************************
* Copyright (c) 2023 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 com.thaiopensource.relaxng.pattern;

import java.util.Set;

public class PossibleRequiredElementsFunction extends AbstractPatternFunction<Void> {

private final Set<String> requiredElementNames;

public PossibleRequiredElementsFunction(Set<String> requiredElementNames) {
this.requiredElementNames = requiredElementNames;
}

@Override
public Void caseOther(Pattern p) {
return null;
}

@Override
public Void caseElement(ElementPattern p) {
return caseNamed(p.getNameClass());
}

@Override
public Void caseGroup(GroupPattern p) {
return union(p);
}

@Override
public Void caseChoice(ChoicePattern p) {
return union(p);
}

@Override
public Void caseInterleave(InterleavePattern p) {
return union(p);
}

@Override
public Void caseAfter(AfterPattern p) {
return p.getOperand1().apply(this);
}

@Override
public Void caseOneOrMore(OneOrMorePattern p) {
return p.getOperand().apply(this);
}

private Void caseNamed(NameClass nc) {
if (!(nc instanceof SimpleNameClass))
return null;
requiredElementNames.add(((SimpleNameClass) nc).getName().getLocalName());
return null;
}

private Void union(BinaryPattern p) {
Pattern p1 = p.getOperand1();
if (!p1.isNullable()) {
p1.apply(this);
}
Pattern p2 = p.getOperand2();
if (!p2.isNullable()) {
p2.apply(this);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ public static CodeAction replace(String title, List<TextEdit> replace, TextDocum
return insertContentAction;
}

/**
* Returns the workspace edit of a given replacement text and range.
*
* @param replaceText the text to replace.
* @param fileContent the range to replace the text over.
* @param document
* @return the workspace edit of a given replacement text and range.
*/
public static WorkspaceEdit getReplaceWorkspaceEdit(String replaceText, Range range, TextDocumentItem document) {
TextEdit replace = new TextEdit(range, replaceText);
VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(
document.getUri(), document.getVersion());
TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier,
Collections.singletonList(replace));
return new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit)));
}

public static CodeAction replaceAt(String title, String replaceText, TextDocumentItem document,
Diagnostic diagnostic, Collection<Range> ranges) {
CodeAction insertContentAction = new CodeAction(title);
Expand All @@ -143,10 +160,10 @@ public static CodeAction replaceAt(String title, String replaceText, TextDocumen
/**
* Makes a CodeAction to create a file and add content to the file.
*
* @param title The displayed name of the CodeAction
* @param title The displayed name of the CodeAction
* @param fileURI The file to create
* @param fileContent The text to put into the newly created document.
* @param diagnostic The diagnostic that this CodeAction will fix
* @param fileContent The text to put into the newly created document.
* @param diagnostic The diagnostic that this CodeAction will fix
*/
public static CodeAction createFile(String title, String fileURI, String fileContent, Diagnostic diagnostic) {
WorkspaceEdit createAndAddContentEdit = createFileEdit(fileURI, fileContent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.utils.XMLGenerator;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest;
Expand Down Expand Up @@ -66,8 +67,8 @@ public void doCodeAction(ICodeActionRequest request, List<CodeAction> codeAction
Range targetRange = new Range(childElementPositionStartTag, childElementPositionEndTag);
XMLGenerator generator = request.getXMLGenerator();

String insertStrAll = generator.generateMissingElements(request, element, false);
String insertStrRequired = generator.generateMissingElements(request, element, true);
String insertStrAll = generator.generateMissingElements(request.getComponent(ContentModelManager.class), element, false);
String insertStrRequired = generator.generateMissingElements(request.getComponent(ContentModelManager.class), element, true);

CodeAction insertAllExpectedElement = CodeActionFactory.replace("Insert all expected elements",
targetRange, insertStrAll, document.getTextDocument(), diagnostic);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2023 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.codeactions.missingelement;

/**
* Data entry field used in the codeAction/resolve for inserting missing
* elements.
*
*/
public class MissingElementDataConstants {

/**
* The element name used to identify which choice element is to be generated.
*/
public static final String DATA_ELEMENT_FIELD = "element";

/**
* Indicates whether only required elements is to be generated or all.
*/
public static final String DATA_REQUIRED_FIELD = "onlyGenerateRequired";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Copyright (c) 2023 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* 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.codeactions.missingelement;

import static org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.missingelement.MissingElementDataConstants.DATA_REQUIRED_FIELD;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.commons.CodeActionFactory;
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.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.utils.XMLGenerator;
import org.eclipse.lemminx.services.data.DataEntryField;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionResolvesParticipant;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

import com.google.gson.JsonObject;

/**
* CodeAction to insert expected child elements within an element
*
* Given this XML where the expected child elements are not present as defined
* in the relaxNG schema:
*
* <servlet></servlet> Error: Child elements are missing from element: - servlet
*
*
* To fix the error, the code action will suggest inserting the expected
* elements inside the parent tag
*
*/

public class required_element_missingCodeAction implements ICodeActionParticipant {

private final Map<String, ICodeActionResolvesParticipant> resolveCodeActionParticipants;

public required_element_missingCodeAction() {
// Register available resolvers.
resolveCodeActionParticipants = new HashMap<>();
resolveCodeActionParticipants.put(required_element_missingCodeActionResolver.PARTICIPANT_ID,
new required_element_missingCodeActionResolver());
}

@Override
public void doCodeAction(ICodeActionRequest request, List<CodeAction> codeActions, CancelChecker cancelChecker) {
DOMDocument document = request.getDocument();
Diagnostic diagnostic = request.getDiagnostic();
try {
Range diagnosticRange = diagnostic.getRange();
int startOffset = document.offsetAt(diagnosticRange.getStart()) + 1;
DOMNode node = document.findNodeAt(startOffset);

if (node == null || !node.isElement()) {
return;
}

DOMElement element = (DOMElement) node;
XMLGenerator generator = request.getXMLGenerator();

String insertStrRequired = null;
String insertStrAll = null;

if (!request.canSupportResolve()) {
insertStrRequired = generator.generateMissingElements(request.getComponent(ContentModelManager.class),
element, true);
insertStrAll = generator.generateMissingElements(request.getComponent(ContentModelManager.class),
element, false);
}
CodeAction insertRequriedExpectedElementCodeAction = createInsertExpectedElementsCodeAction(true,
element, insertStrRequired, request, cancelChecker);

CodeAction insertAllExpectedElementCodeAction = createInsertExpectedElementsCodeAction(false,
element, insertStrAll, request, cancelChecker);

codeActions.add(insertRequriedExpectedElementCodeAction);
codeActions.add(insertAllExpectedElementCodeAction);

} catch (BadLocationException e) {
// do nothing
}
}

private static CodeAction createInsertExpectedElementsCodeAction(boolean isGenerateRequired, DOMElement domElement,
String insertStr, ICodeActionRequest request, CancelChecker cancelChecker)
throws BadLocationException {
Diagnostic diagnostic = request.getDiagnostic();
DOMDocument document = request.getDocument();
String title = isGenerateRequired ? "Insert only required elements" : "Insert all expected elements";
if (request.canSupportResolve()) {
JsonObject data = DataEntryField.createData(document.getDocumentURI(),
required_element_missingCodeActionResolver.PARTICIPANT_ID);
return insertExpectedElementsUnresolvedCodeAction(title, diagnostic, data, isGenerateRequired);
} else {
return insertExpectedElementCodeAction(document, title, domElement, insertStr, diagnostic);
}
}

private static CodeAction insertExpectedElementsUnresolvedCodeAction(String title,
Diagnostic diagnostic, JsonObject data, boolean isGenerateRequired) {
CodeAction codeAction = new CodeAction(title);
codeAction.setDiagnostics(Collections.singletonList(diagnostic));
codeAction.setKind(CodeActionKind.QuickFix);
String required_field = isGenerateRequired ? "true" : "false";
data.addProperty(DATA_REQUIRED_FIELD, required_field);
codeAction.setData(data);

return codeAction;
}

public ICodeActionResolvesParticipant getResolveCodeActionParticipant(String participantId) {
return resolveCodeActionParticipants.get(participantId);
}

private static CodeAction insertExpectedElementCodeAction(DOMDocument document, String title,
DOMElement element, String insertStr, Diagnostic diagnostic) throws BadLocationException {
Position childElementPositionStartTag = document.positionAt(element.getStartTagCloseOffset() + 1);
Position childElementPositionEndTag = document.positionAt(element.getEndTagOpenOffset());

Range targetRange = new Range(childElementPositionStartTag, childElementPositionEndTag);
return CodeActionFactory.replace(title, targetRange, insertStr, document.getTextDocument(), diagnostic);
}
}
Loading

0 comments on commit 5759686

Please sign in to comment.