Skip to content

Commit

Permalink
Feature request: buttons to associate XML documents with schema files
Browse files Browse the repository at this point in the history
Fixes eclipse#395

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jun 3, 2021
1 parent 4d1b436 commit ab7c34a
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
}
return CompletableFutures.computeAsync(cancelChecker -> {
try {
return handler.executeCommand(params, cancelChecker);
return handler.executeCommand(params, xmlLanguageServer.getSharedSettings(), cancelChecker);
} catch (Exception e) {
if (e instanceof ResponseErrorException) {
throw (ResponseErrorException) e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import java.util.Objects;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand;
import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationAllFilesCommand;
import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationFileCommand;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCodeActionParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCodeLensParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCompletionParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelDocumentLinkParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelHoverParticipant;
Expand All @@ -36,6 +38,7 @@
import org.eclipse.lemminx.services.extensions.ITypeDefinitionParticipant;
import org.eclipse.lemminx.services.extensions.IXMLExtension;
import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant;
import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService;
import org.eclipse.lemminx.services.extensions.diagnostics.IDiagnosticsParticipant;
import org.eclipse.lemminx.services.extensions.save.ISaveContext;
Expand Down Expand Up @@ -68,6 +71,8 @@ public class ContentModelPlugin implements IXMLExtension {

private ContentModelSymbolsProviderParticipant symbolsProviderParticipant;

private final ICodeLensParticipant codeLensParticipant;

ContentModelManager contentModelManager;

private ContentModelSettings cmSettings;
Expand All @@ -80,6 +85,7 @@ public ContentModelPlugin() {
diagnosticsParticipant = new ContentModelDiagnosticsParticipant(this);
codeActionParticipant = new ContentModelCodeActionParticipant();
typeDefinitionParticipant = new ContentModelTypeDefinitionParticipant();
codeLensParticipant = new ContentModelCodeLensParticipant();
}

@Override
Expand Down Expand Up @@ -176,6 +182,7 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) {
registry.registerTypeDefinitionParticipant(typeDefinitionParticipant);
symbolsProviderParticipant = new ContentModelSymbolsProviderParticipant(contentModelManager);
registry.registerSymbolsProviderParticipant(symbolsProviderParticipant);
registry.registerCodeLensParticipant(codeLensParticipant);

// Register custom commands to re-validate XML files
IXMLCommandService commandService = registry.getCommandService();
Expand All @@ -186,6 +193,8 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) {
new XMLValidationFileCommand(contentModelManager, documentProvider, validationService));
commandService.registerCommand(XMLValidationAllFilesCommand.COMMAND_ID,
new XMLValidationAllFilesCommand(contentModelManager, documentProvider, validationService));
commandService.registerCommand(AssociateGrammarCommand.COMMAND_ID,
new AssociateGrammarCommand(documentProvider));
}
}

Expand All @@ -198,12 +207,14 @@ public void stop(XMLExtensionsRegistry registry) {
registry.unregisterDocumentLinkParticipant(documentLinkParticipant);
registry.unregisterTypeDefinitionParticipant(typeDefinitionParticipant);
registry.unregisterSymbolsProviderParticipant(symbolsProviderParticipant);
registry.unregisterCodeLensParticipant(codeLensParticipant);

// Un-register custom commands to re-validate XML files
IXMLCommandService commandService = registry.getCommandService();
if (commandService != null) {
commandService.unregisterCommand(XMLValidationFileCommand.COMMAND_ID);
commandService.unregisterCommand(XMLValidationAllFilesCommand.COMMAND_ID);
commandService.unregisterCommand(AssociateGrammarCommand.COMMAND_ID);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.eclipse.lemminx.extensions.contentmodel.commands;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.NoGrammarConstraintsCodeAction;
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler;
import org.eclipse.lemminx.services.extensions.commands.ArgumentsUtils;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

public class AssociateGrammarCommand extends AbstractDOMDocumentCommandHandler {

public static final String COMMAND_ID = "xml.associate.grammar.insert";

public AssociateGrammarCommand(IXMLDocumentProvider documentProvider) {
super(documentProvider);
}

@Override
protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings,
CancelChecker cancelChecker) throws Exception {
String grammarURI = ArgumentsUtils.getArgAt(params, 1, String.class);
String type = ArgumentsUtils.getArgAt(params, 2, String.class);
String bindingKind = ArgumentsUtils.getArgAt(params, 3, String.class);

if ("xml-model".equals(bindingKind)) {
return NoGrammarConstraintsCodeAction.createXmlModelEdit(grammarURI, document, sharedSettings);
}

if ("XSD".equals(type)) {
return NoGrammarConstraintsCodeAction.createXSINoNamespaceSchemaLocationEdit(grammarURI, document);
} else if ("DTD".equals(type)) {
return NoGrammarConstraintsCodeAction.createDocTypeEdit(grammarURI, document, sharedSettings);
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService.IDelegateCommandHandler;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

Expand Down Expand Up @@ -52,7 +53,8 @@ public XMLValidationAllFilesCommand(ContentModelManager contentModelManager, IXM
}

@Override
public Object executeCommand(ExecuteCommandParams params, CancelChecker cancelChecker) throws Exception {
public Object executeCommand(ExecuteCommandParams params, SharedSettings sharedSettings,
CancelChecker cancelChecker) throws Exception {
// 1. clear the Xerces grammar pool
// (used by the Xerces validation) and the content model documents cache (used
// by the XML completion/hover based on the grammar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

Expand Down Expand Up @@ -48,8 +49,8 @@ public XMLValidationFileCommand(ContentModelManager contentModelManager, IXMLDoc
}

@Override
protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, CancelChecker cancelChecker)
throws Exception {
protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings,
CancelChecker cancelChecker) throws Exception {
// 1. remove the referenced grammar in the XML file from the Xerces grammar pool
// (used by the Xerces validation) and the content model documents cache (used
// by the XML completion/hover based on the grammar)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (c) 2021 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;

import java.util.Arrays;
import java.util.List;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensRequest;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

public class ContentModelCodeLensParticipant implements ICodeLensParticipant {

private static final String COMMAND_ID = "xml.associate.grammar.selectFile";

@Override
public void doCodeLens(ICodeLensRequest request, List<CodeLens> lenses, CancelChecker cancelChecker) {
DOMDocument document = request.getDocument();
DOMElement documentElement = document.getDocumentElement();
if (documentElement == null || document.hasGrammar()) {
return;
}
String documentURI = document.getDocumentURI();
Range range = XMLPositionUtility.selectRootStartTag(document);

lenses.add(createAssociateLens(documentURI, "Associate with XSD and schemaLocation", "XSD", "schemaLocation",
range));
lenses.add(createAssociateLens(documentURI, "Associate with XSD and xml-model", "XSD", "xml-model", range));
lenses.add(createAssociateLens(documentURI, "Associate with DTD and DOCTYPE", "DTD", "DOCTYPE", range));
lenses.add(createAssociateLens(documentURI, "Associate with DTD and xml-model", "DTD", "xml-model", range));
}

private static CodeLens createAssociateLens(String documentURI, String title, String type, String bindingKind,
Range range) {
Command command = new Command(title, COMMAND_ID, Arrays.asList(documentURI, type, bindingKind));
return new CodeLens(range, command, null);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.apache.xerces.xni.XMLLocator;
import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMRange;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementDeclUnterminatedCodeAction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen
}

FileContentGeneratorManager generator = componentProvider.getComponent(FileContentGeneratorManager.class);
String delimiter = document.lineDelimiter(0);
int beforeTagOffset = documentElement.getStartTagOpenOffset();
int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length();

// ---------- XSD

Expand All @@ -67,23 +64,18 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen

// xsi:noNamespaceSchemaLocation
// Create code action to create the XSD file with the generated XSD content
String insertText = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+ document.getTextDocument().lineDelimiter(0);
insertText += " xsi:noNamespaceSchemaLocation=\"" + schemaFileName + "\"";
TextDocumentEdit noNamespaceSchemaLocationEdit = createXSINoNamespaceSchemaLocationEdit(schemaFileName,
document);
CodeAction noNamespaceSchemaLocationAction = createGrammarFileAndBindIt(
"Generate '" + schemaFileName + "' and bind with xsi:noNamespaceSchemaLocation", schemaURI,
schemaTemplate, insertText, afterTagOffset, document, diagnostic);
schemaTemplate, noNamespaceSchemaLocationEdit, diagnostic);
codeActions.add(noNamespaceSchemaLocationAction);

// xml-model
XMLBuilder xsdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter);
xsdWithXmlModel.startPrologOrPI("xml-model");
xsdWithXmlModel.addSingleAttribute("href", schemaFileName, true);
xsdWithXmlModel.endPrologOrPI();
xsdWithXmlModel.linefeed();
TextDocumentEdit xsdWithXMLModelEdit = createXmlModelEdit(schemaFileName, document, sharedSettings);
CodeAction xsdWithXmlModelAction = createGrammarFileAndBindIt(
"Generate '" + schemaFileName + "' and bind with xml-model", schemaURI, schemaTemplate,
xsdWithXmlModel.toString(), beforeTagOffset, document, diagnostic);
xsdWithXMLModelEdit, diagnostic);
codeActions.add(xsdWithXmlModelAction);

// ---------- DTD
Expand All @@ -93,28 +85,17 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen
String dtdTemplate = generator.generate(document, sharedSettings, new DTDGeneratorSettings());

// <!DOCTYPE ${1:root-element} SYSTEM \"${2:file}.dtd\">
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();
TextDocumentEdit dtdWithDocType = createDocTypeEdit(dtdFileName, document, sharedSettings);
CodeAction docTypeAction = createGrammarFileAndBindIt(
"Generate '" + dtdFileName + "' and bind with DOCTYPE", dtdURI, dtdTemplate, docType.toString(),
beforeTagOffset, document, diagnostic);
"Generate '" + dtdFileName + "' and bind with DOCTYPE", dtdURI, dtdTemplate, dtdWithDocType,
diagnostic);
codeActions.add(docTypeAction);

// xml-model
XMLBuilder dtdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter);
dtdWithXmlModel.startPrologOrPI("xml-model");
dtdWithXmlModel.addSingleAttribute("href", dtdFileName, true);
dtdWithXmlModel.endPrologOrPI();
dtdWithXmlModel.linefeed();
TextDocumentEdit dtdWithXMLModelEdit = createXmlModelEdit(dtdFileName, document, sharedSettings);
CodeAction dtdWithXmlModelAction = createGrammarFileAndBindIt(
"Generate '" + dtdFileName + "' and bind with xml-model", dtdURI, dtdTemplate,
dtdWithXmlModel.toString(), beforeTagOffset, document, diagnostic);
"Generate '" + dtdFileName + "' and bind with xml-model", dtdURI, dtdTemplate, dtdWithXMLModelEdit,
diagnostic);
codeActions.add(dtdWithXmlModelAction);

} catch (BadLocationException e) {
Expand Down Expand Up @@ -153,18 +134,60 @@ static String getFileName(String schemaURI) {
return new File(schemaURI).getName();
}

private static CodeAction createGrammarFileAndBindIt(String title, String grammarURI, String grammarContent,
String insertText, int insertOffset, DOMDocument document, Diagnostic diagnostic)
throws BadLocationException {
Position position = document.positionAt(insertOffset);
TextDocumentEdit insertEdit = CodeActionFactory.insertEdit(insertText, position, document.getTextDocument());
return createGrammarFileAndBindIt(title, grammarURI, grammarContent, insertEdit, diagnostic);
}

private static CodeAction createGrammarFileAndBindIt(String title, String grammarURI, String grammarContent,
TextDocumentEdit boundEdit, Diagnostic diagnostic) {
CodeAction codeAction = CodeActionFactory.createFile(title, grammarURI, grammarContent, diagnostic);
codeAction.getEdit().getDocumentChanges().add(Either.forLeft(boundEdit));
return codeAction;
}

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

String insertText = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" + delimiter;
insertText += " xsi:noNamespaceSchemaLocation=\"" + schemaFileName + "\"";
Position position = document.positionAt(afterTagOffset);
return CodeActionFactory.insertEdit(insertText, position, document.getTextDocument());
}

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

XMLBuilder xsdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter);
xsdWithXmlModel.startPrologOrPI("xml-model");
xsdWithXmlModel.addSingleAttribute("href", schemaFileName, true);
xsdWithXmlModel.endPrologOrPI();
xsdWithXmlModel.linefeed();

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

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();
Position position = document.positionAt(beforeTagOffset);
return CodeActionFactory.insertEdit(insertText, position, document.getTextDocument());
}
}
Loading

0 comments on commit ab7c34a

Please sign in to comment.