Skip to content

Commit

Permalink
Bind to a schema from an empty document
Browse files Browse the repository at this point in the history
Allows you to use the code action to bind to a schema when
a document doesn't have a root element.
Also, the bind schema CodeLens is shown when the document doesn't
have a root element.
It generates an element `placeholder-element-name` when the binding
strategy requires a root element to function (eg. `schemaLocation`,
`noNamespaceSchemaLocation`).
It also uses `placeholder-element-name` when binding to a `.dtd` using a
DOCTYPE declaration.

Closes redhat-developer/vscode-xml#819

Signed-off-by: David Thompson <davthomp@redhat.com>
  • Loading branch information
datho7561 authored and angelozerr committed Jan 8, 2023
1 parent 2d5ebad commit 1663001
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 178 deletions.
Expand Up @@ -40,10 +40,10 @@
/**
* XML Command "xml.associate.grammar.insert" to associate a grammar to a given
* DOM document.
*
*
* The command parameters {@link ExecuteCommandParams} must be filled with 3
* parameters:
*
*
* <ul>
* <li>document URI (String) : the DOM document file URI to bind with a grammar.
* </li>
Expand All @@ -52,7 +52,7 @@
* <li>binding type (String) : which can takes values "standard", "xml-model" to
* know which binding type must be inserted in the DOM document.</li>
* </ul>
*
*
* @author Angelo ZERR
*
*/
Expand Down Expand Up @@ -112,14 +112,14 @@ protected Object executeCommand(DOMDocument document, ExecuteCommandParams param
// Insert inside <foo /> ->
// xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance"
// xsi:noNamespaceSchemaLocation=\"xsd/tag.xsd\"
return NoGrammarConstraintsCodeAction.createXSINoNamespaceSchemaLocationEdit(grammarURI, document);
return NoGrammarConstraintsCodeAction.createXSINoNamespaceSchemaLocationEdit(grammarURI, document, sharedSettings);
}
// Insert inside <foo /> ->
// xmlns="team_namespace"
// xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xsi:schemaLocation="team_namespace xsd/team.xsd"
return NoGrammarConstraintsCodeAction.createXSISchemaLocationEdit(grammarURI, targetNamespace,
document);
document, sharedSettings);
} else {
// DTD file
// Insert before <foo /> -> <!DOCTYPE foo SYSTEM "dtd/tag.dtd">
Expand Down
Expand Up @@ -12,7 +12,6 @@
package org.eclipse.lemminx.extensions.contentmodel.commands;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler;
import org.eclipse.lemminx.settings.SharedSettings;
Expand Down Expand Up @@ -42,17 +41,13 @@ protected Object executeCommand(DOMDocument document, ExecuteCommandParams param
/**
* Returns true if the given DOM document can be bound with a given grammar and
* false otherwise.
*
*
* @param document the DOM document.
*
*
* @return true if the given DOM document can be bound with a given grammar and
* false otherwise.
*/
public static boolean canBindWithGrammar(DOMDocument document) {
DOMElement documentElement = document.getDocumentElement();
if (documentElement == null) {
return false;
}
return !document.hasGrammar();
}
}
Expand Up @@ -146,7 +146,12 @@ private static void createBindToGrammarSchemaLenses(ICodeLensRequest request, Li
return;
}
String documentURI = document.getDocumentURI();
Range range = XMLPositionUtility.selectRootStartTag(document);
Range range;
if (document.getDocumentElement() != null) {
range = XMLPositionUtility.selectRootStartTag(document);
} else {
range = XMLPositionUtility.createRange(0, 0, document);
}

lenses.add(createAssociateLens(documentURI, "Bind to grammar/schema...", range));
}
Expand Down
Expand Up @@ -22,14 +22,14 @@

/**
* Code action resolver participant used to:
*
*
* <ul>
* <li>generate the XSD file for the given DOM document</li>
* <li>generate the association xsi:noNamespaceSchemaLocation in the XML to bind
* it with the generated XSD</li>
*
*
* </ul>
*
*
* @author Angelo ZERR
*
*/
Expand All @@ -42,7 +42,7 @@ public class GenerateXSINoNamespaceSchemaCodeActionResolver
@Override
protected TextDocumentEdit createFileEdit(String grammarFileName, DOMDocument document,
SharedSettings sharedSettings) throws BadLocationException {
return createXSINoNamespaceSchemaLocationEdit(grammarFileName, document);
return createXSINoNamespaceSchemaLocationEdit(grammarFileName, document, sharedSettings);
}

@Override
Expand Down
Expand Up @@ -46,7 +46,6 @@
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.eclipse.lsp4j.jsonrpc.messages.Either;

Expand All @@ -58,6 +57,8 @@
public class NoGrammarConstraintsCodeAction implements ICodeActionParticipant {

private static final Logger LOGGER = Logger.getLogger(NoGrammarConstraintsCodeAction.class.getName());
// FIXME: the element name should be derived from the content model if possible
private static final String PLACEHOLDER_ELEMENT_NAME = "root-element";
private final Map<String, ICodeActionResolvesParticipant> resolveCodeActionParticipants;

public NoGrammarConstraintsCodeAction() {
Expand Down Expand Up @@ -176,7 +177,7 @@ private static CodeAction createNoNamespaceSchemaLocationCodeAction(String schem
GenerateXSINoNamespaceSchemaCodeActionResolver.PARTICIPANT_ID);
} else {
TextDocumentEdit noNamespaceSchemaLocationEdit = createXSINoNamespaceSchemaLocationEdit(schemaFileName,
document);
document, request.getSharedSettings());
return createGrammarFileAndBindIt(title, schemaURI, schemaTemplate, noNamespaceSchemaLocationEdit,
diagnostic);
}
Expand Down Expand Up @@ -275,14 +276,21 @@ private static CodeAction createGrammarFileAndBindIt(String title, String gramma
return codeAction;
}

public static TextDocumentEdit createXSINoNamespaceSchemaLocationEdit(String schemaFileName, DOMDocument document)
public static TextDocumentEdit createXSINoNamespaceSchemaLocationEdit(String schemaFileName, DOMDocument document,
SharedSettings sharedSettings)
throws BadLocationException {
String delimiter = document.getTextDocument().lineDelimiter(0);
DOMElement documentElement = document.getDocumentElement();
int beforeTagOffset = documentElement.getStartTagOpenOffset();
int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length();
int beforeTagOffset = documentElement != null ? documentElement.getStartTagOpenOffset()
: document.getLastChild() != null ? document.getLastChild().getEnd() : 0;
int afterTagOffset = documentElement != null ? beforeTagOffset + 1 + documentElement.getTagName().length()
: beforeTagOffset;

StringBuilder insertText = new StringBuilder();
XMLBuilder insertText = new XMLBuilder(sharedSettings, null, delimiter);

if (documentElement == null) {
generateStartTag(insertText, document);
}

insertText.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
insertText.append(delimiter);
Expand All @@ -291,18 +299,29 @@ public static TextDocumentEdit createXSINoNamespaceSchemaLocationEdit(String sch
insertText.append(schemaFileName);
insertText.append("\"");

if (documentElement == null) {
generateEndTag(insertText);
}

Position position = document.positionAt(afterTagOffset);
return CodeActionFactory.insertEdit(insertText.toString(), position, document.getTextDocument());
}

public static TextDocumentEdit createXSISchemaLocationEdit(String schemaFileName, String targetNamespace,
DOMDocument document) throws BadLocationException {
DOMDocument document, SharedSettings sharedSettings) throws BadLocationException {
String delimiter = document.getTextDocument().lineDelimiter(0);
DOMElement documentElement = document.getDocumentElement();
int beforeTagOffset = documentElement.getStartTagOpenOffset();
int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length();
int beforeTagOffset = documentElement != null ? documentElement.getStartTagOpenOffset()
: document.getLastChild() != null ? document.getLastChild().getEnd() : 0;
int afterTagOffset = documentElement != null ? beforeTagOffset + 1 + documentElement.getTagName().length()
: beforeTagOffset;

XMLBuilder insertText = new XMLBuilder(sharedSettings, null, delimiter);

if (documentElement == null) {
generateStartTag(insertText, document);
}

StringBuilder insertText = new StringBuilder();
insertText.append(" xmlns=\"");
insertText.append(targetNamespace);
insertText.append("\"");
Expand All @@ -317,6 +336,10 @@ public static TextDocumentEdit createXSISchemaLocationEdit(String schemaFileName
insertText.append(schemaFileName);
insertText.append("\"");

if (documentElement == null) {
generateEndTag(insertText);
}

Position position = document.positionAt(afterTagOffset);
return CodeActionFactory.insertEdit(insertText.toString(), position, document.getTextDocument());
}
Expand All @@ -325,7 +348,8 @@ public static TextDocumentEdit createXmlModelEdit(String schemaFileName, String
DOMDocument document, SharedSettings sharedSettings) throws BadLocationException {
String delimiter = document.getTextDocument().lineDelimiter(0);
DOMElement documentElement = document.getDocumentElement();
int beforeTagOffset = documentElement.getStartTagOpenOffset();
int beforeTagOffset = documentElement != null ? documentElement.getStartTagOpenOffset()
: document.getLastChild() != null ? document.getLastChild().getEnd() : 0;

// Insert Text edit for xml-model
XMLBuilder xsdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter);
Expand All @@ -334,47 +358,85 @@ public static TextDocumentEdit createXmlModelEdit(String schemaFileName, String
xsdWithXmlModel.endPrologOrPI();
xsdWithXmlModel.linefeed();

String xmlModelInsertText = xsdWithXmlModel.toString();
String xmlModelInsertText = (documentElement == null && document.getLastChild() != null ? delimiter : "")
+ xsdWithXmlModel.toString();
Position xmlModelPosition = document.positionAt(beforeTagOffset);

if (StringUtils.isEmpty(targetNamespace)) {
if (documentElement != null && StringUtils.isEmpty(targetNamespace)) {
return CodeActionFactory.insertEdit(xmlModelInsertText, xmlModelPosition, document.getTextDocument());
}

StringBuilder xmlNamespaceInsertText = new StringBuilder();
xmlNamespaceInsertText.append(" xmlns=\"");
xmlNamespaceInsertText.append(targetNamespace);
xmlNamespaceInsertText.append("\" ");
// Generate root element (if needed) and insert namespace in root element (if
// needed)
XMLBuilder xmlNamespaceInsertText = new XMLBuilder(sharedSettings, null, delimiter);

if (documentElement == null) {
generateStartTag(xmlNamespaceInsertText, document);
}

if (!StringUtils.isEmpty(targetNamespace)) {
xmlNamespaceInsertText.append(" xmlns=\"");
xmlNamespaceInsertText.append(targetNamespace);
xmlNamespaceInsertText.append("\"");
}

if (documentElement == null) {
generateEndTag(xmlNamespaceInsertText);
}

int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length();
int afterTagOffset = beforeTagOffset
+ (documentElement != null ? 1 + documentElement.getTagName().length() : 0);
Position xmlNamespacePosition = document.positionAt(afterTagOffset);

List<TextEdit> edits = Arrays.asList( //
// insert xml-model before root tag element
CodeActionFactory.insertEdit(xmlModelInsertText, xmlModelPosition),
// insert xml namespace inside root tag element
CodeActionFactory.insertEdit(xmlNamespaceInsertText.toString(), xmlNamespacePosition));
return CodeActionFactory.insertEdits(document.getTextDocument(), edits);
return CodeActionFactory.insertEdits(document.getTextDocument(), //
Arrays.asList(CodeActionFactory.insertEdit(xmlModelInsertText, xmlModelPosition), //
CodeActionFactory.insertEdit(xmlNamespaceInsertText.toString(), xmlNamespacePosition)));
}

public static TextDocumentEdit createDocTypeEdit(String dtdFileName, DOMDocument document,
SharedSettings sharedSettings) throws BadLocationException {
String delimiter = document.getTextDocument().lineDelimiter(0);
DOMElement documentElement = document.getDocumentElement();
int beforeTagOffset = documentElement.getStartTagOpenOffset();

XMLBuilder docType = new XMLBuilder(sharedSettings, null, delimiter);
docType.startDoctype();
docType.addParameter(documentElement.getLocalName());
docType.addContent(" SYSTEM \"");
docType.addContent(dtdFileName);
docType.addContent("\"");
docType.endDoctype();
docType.linefeed();

String insertText = docType.toString();
int beforeTagOffset = documentElement != null ? documentElement.getStartTagOpenOffset()
: document.getLastChild() != null ? document.getLastChild().getEnd() : 0;

XMLBuilder builder = new XMLBuilder(sharedSettings, null, delimiter);
builder.startDoctype();
if (documentElement != null) {
builder.addParameter(documentElement.getLocalName());
} else {
builder.addParameter(PLACEHOLDER_ELEMENT_NAME);
}
builder.addContent(" SYSTEM \"");
builder.addContent(dtdFileName);
builder.addContent("\"");
builder.endDoctype();
builder.linefeed();

if (documentElement == null) {
builder.addContent("<");
builder.addContent(PLACEHOLDER_ELEMENT_NAME);
generateEndTag(builder);
}

String insertText = (documentElement == null && document.getLastChild() != null ? delimiter : "")
+ builder.toString();
Position position = document.positionAt(beforeTagOffset);
return CodeActionFactory.insertEdit(insertText, position, document.getTextDocument());
}

private static void generateStartTag(XMLBuilder builder, DOMDocument document) {
if (document.getLastChild() != null) {
builder.linefeed();
}
builder.addContent("<");
builder.addContent(PLACEHOLDER_ELEMENT_NAME);
}

private static void generateEndTag(XMLBuilder builder) {
builder.addContent("></");
builder.addContent(PLACEHOLDER_ELEMENT_NAME);
builder.addContent(">");
}

}
Expand Up @@ -133,7 +133,7 @@ public void associateWithXMLModelAXSDWithTargetNamespace() throws InterruptedExc

assertEquals(actual, tde(xmlPath, 1, //
te(1, 0, 1, 0, "<?xml-model href=\"xsd/team.xsd\"?>\r\n"), //
te(1, 4, 1, 4, " xmlns=\"team_namespace\" ")));
te(1, 4, 1, 4, " xmlns=\"team_namespace\"")));
}

@Test
Expand Down

0 comments on commit 1663001

Please sign in to comment.