diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/MinifyPlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/MinifyPlugin.java new file mode 100644 index 000000000..9444b8503 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/MinifyPlugin.java @@ -0,0 +1,61 @@ +/******************************************************************************* +* Copyright (c) 2026 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.minify; + +import org.eclipse.lemminx.extensions.minify.commands.MinifyDocumentCommand; +import org.eclipse.lemminx.extensions.minify.participants.MinifyCodeActionParticipant; +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lemminx.services.extensions.IXMLExtension; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService; +import org.eclipse.lemminx.services.extensions.save.ISaveContext; +import org.eclipse.lsp4j.InitializeParams; + +/** + * Minify plugin to register the minify command and code action. + */ +public class MinifyPlugin implements IXMLExtension { + + private MinifyCodeActionParticipant codeActionParticipant; + + @Override + public void start(InitializeParams params, XMLExtensionsRegistry registry) { + // Register minify command + IXMLCommandService commandService = registry.getCommandService(); + if (commandService != null) { + IXMLDocumentProvider documentProvider = registry.getDocumentProvider(); + commandService.registerCommand(MinifyDocumentCommand.COMMAND_ID, + new MinifyDocumentCommand(documentProvider)); + } + + // Register minify code action + codeActionParticipant = new MinifyCodeActionParticipant(); + registry.registerCodeActionParticipant(codeActionParticipant); + } + + @Override + public void stop(XMLExtensionsRegistry registry) { + // Unregister minify command + IXMLCommandService commandService = registry.getCommandService(); + if (commandService != null) { + commandService.unregisterCommand(MinifyDocumentCommand.COMMAND_ID); + } + + // Unregister minify code action + registry.unregisterCodeActionParticipant(codeActionParticipant); + } + + @Override + public void doSave(ISaveContext context) { + // Nothing to do on save + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/XMLMinifierDocument.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/XMLMinifierDocument.java new file mode 100644 index 000000000..b10bd2d65 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/XMLMinifierDocument.java @@ -0,0 +1,426 @@ +/******************************************************************************* +* Copyright (c) 2026 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.minify; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.commons.TextDocument; +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMComment; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMProcessingInstruction; +import org.eclipse.lemminx.dom.DOMText; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lemminx.utils.StringUtils; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.w3c.dom.Node; + +/** + * XML minifier which generates several text edits to remove unnecessary spaces. + * + * @author Angelo ZERR + * + */ +public class XMLMinifierDocument { + + private static final Logger LOGGER = Logger.getLogger(XMLMinifierDocument.class.getName()); + + private static final String XML_SPACE_ATTR = "xml:space"; + private static final String XML_SPACE_ATTR_PRESERVE = "preserve"; + + private final DOMDocument xmlDocument; + private final TextDocument textDocument; + private final SharedSettings sharedSettings; + + private int startOffset = -1; + private int endOffset = -1; + + /** + * XML minifier document. + */ + public XMLMinifierDocument(DOMDocument xmlDocument, Range range, SharedSettings sharedSettings) { + this.xmlDocument = xmlDocument; + this.textDocument = xmlDocument.getTextDocument(); + this.sharedSettings = sharedSettings; + + if (range != null) { + try { + this.startOffset = textDocument.offsetAt(range.getStart()); + this.endOffset = textDocument.offsetAt(range.getEnd()); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + } + + /** + * Returns the list of TextEdits to minify the XML document. + * + * @return the list of TextEdits to minify the XML document. + * @throws BadLocationException + */ + public List minify() throws BadLocationException { + return minify(xmlDocument, startOffset, endOffset); + } + + public List minify(DOMDocument document, int start, int end) { + // Pre-allocate list capacity based on document size + int estimatedCapacity = Math.min(textDocument.getText().length() / 100, 10000); + List edits = new ArrayList<>(estimatedCapacity); + + // Get initial document region + DOMNode currentDOMNode = getDOMNodeToMinify(document, start, end); + + if (currentDOMNode != null) { + // Minify all siblings (and their children) as long as they overlap with + // start/end offset + minifySiblings(edits, currentDOMNode, start, end); + } + + return edits; + } + + /** + * Returns the DOM node to minify. + * + * @param document the DOM document + * @param start the start offset + * @param end the end offset + * @return the DOM node to minify + */ + private DOMNode getDOMNodeToMinify(DOMDocument document, int start, int end) { + if (start == -1) { + // No range specified, minify entire document + // Start from the first child of the document (could be PI, comment, or element) + return document.getFirstChild(); + } + + // Find the node at the start offset + DOMNode node = document.findNodeAt(start); + if (node == null) { + return document.getDocumentElement(); + } + + // Navigate up to find the first element or significant node + while (node != null && node.getParentNode() != null) { + if (node.isElement() || node.getParentNode() == document) { + return node; + } + node = node.getParentNode(); + } + + return node; + } + + /** + * Minify siblings of the given node. + * + * @param edits the list of text edits + * @param node the starting node + * @param start the start offset + * @param end the end offset + */ + private void minifySiblings(List edits, DOMNode node, int start, int end) { + DOMNode current = node; + + while (current != null) { + // Check if we're still within the range + if (end != -1 && current.getStart() > end) { + break; + } + + // Minify the current node + minifyNode(edits, current, start, end); + + // Remove whitespace between current node and next sibling + DOMNode nextSibling = current.getNextSibling(); + if (nextSibling != null) { + int afterCurrent = current.getEnd(); + int beforeNext = nextSibling.getStart(); + if (afterCurrent < beforeNext) { + removeWhitespace(edits, afterCurrent, beforeNext); + } + } + + // Move to next sibling + current = nextSibling; + } + } + + /** + * Minify a single DOM node. + * + * @param edits the list of text edits + * @param node the node to minify + * @param start the start offset + * @param end the end offset + */ + private void minifyNode(List edits, DOMNode node, int start, int end) { + if (node == null) { + return; + } + + short nodeType = node.getNodeType(); + + switch (nodeType) { + case Node.ELEMENT_NODE: + minifyElement(edits, (DOMElement) node, start, end); + break; + case Node.TEXT_NODE: + minifyText(edits, (DOMText) node); + break; + case Node.COMMENT_NODE: + // Remove comments during minification + minifyComment(edits, (DOMComment) node); + break; + case Node.CDATA_SECTION_NODE: + // Keep CDATA as-is (content must be preserved) + break; + case Node.PROCESSING_INSTRUCTION_NODE: + minifyProcessingInstruction(edits, (DOMProcessingInstruction) node); + break; + default: + break; + } + } + + /** + * Minify a comment node by removing it. + * + * @param edits the list of text edits + * @param comment the comment node + */ + private void minifyComment(List edits, DOMComment comment) { + // Remove the entire comment + removeContent(edits, comment.getStart(), comment.getEnd()); + } + + /** + * Minify a processing instruction node. + * + * @param edits the list of text edits + * @param pi the processing instruction + */ + private void minifyProcessingInstruction(List edits, DOMProcessingInstruction pi) { + // Processing instruction content is preserved + // Whitespace after it is handled in minifySiblings + } + + /** + * Minify an element node. + * + * @param edits the list of text edits + * @param element the element to minify + * @param start the start offset + * @param end the end offset + */ + private void minifyElement(List edits, DOMElement element, int start, int end) { + // Check if xml:space="preserve" is set + if (shouldPreserveSpace(element)) { + return; + } + + // Minify attributes (remove spaces between attributes if needed) + minifyAttributes(edits, element); + + // Minify whitespace between start tag and content + if (element.hasChildNodes()) { + DOMNode firstChild = element.getFirstChild(); + if (firstChild != null) { + // Remove whitespace between opening tag and first child + int afterStartTag = element.getStartTagCloseOffset() + 1; + int beforeFirstChild = firstChild.getStart(); + if (afterStartTag < beforeFirstChild) { + removeWhitespace(edits, afterStartTag, beforeFirstChild); + } + } + + // Minify children + DOMNode child = element.getFirstChild(); + while (child != null) { + minifyNode(edits, child, start, end); + + // Remove whitespace between siblings + DOMNode nextSibling = child.getNextSibling(); + if (nextSibling != null) { + int afterChild = child.getEnd(); + int beforeNext = nextSibling.getStart(); + if (afterChild < beforeNext) { + removeWhitespace(edits, afterChild, beforeNext); + } + } + + child = nextSibling; + } + + // Remove whitespace between last child and closing tag + DOMNode lastChild = element.getLastChild(); + if (lastChild != null && element.getEndTagOpenOffset() != DOMNode.NULL_VALUE) { + int afterLastChild = lastChild.getEnd(); + int beforeEndTag = element.getEndTagOpenOffset(); + if (afterLastChild < beforeEndTag) { + removeWhitespace(edits, afterLastChild, beforeEndTag); + } + } + } + } + + /** + * Minify attributes by removing unnecessary spaces. + * + * @param edits the list of text edits + * @param element the element + */ + private void minifyAttributes(List edits, DOMElement element) { + if (!element.hasAttributes()) { + return; + } + + List attributes = element.getAttributeNodes(); + + // Remove extra spaces between element name and first attribute + if (!attributes.isEmpty()) { + DOMAttr firstAttr = attributes.get(0); + int afterElementName = element.getStartTagOpenOffset() + element.getTagName().length() + 1; + int beforeFirstAttr = firstAttr.getStart(); + + if (afterElementName < beforeFirstAttr) { + int length = beforeFirstAttr - afterElementName; + if (StringUtils.isWhitespace(textDocument.getText(), afterElementName, beforeFirstAttr) && length > 1) { + // Replace multiple spaces with single space + try { + Range range = new Range(textDocument.positionAt(afterElementName), + textDocument.positionAt(beforeFirstAttr)); + edits.add(new TextEdit(range, " ")); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + } + } + + // Remove extra spaces between attributes, keep only one space + for (int i = 0; i < attributes.size() - 1; i++) { + DOMAttr attr = attributes.get(i); + DOMAttr nextAttr = attributes.get(i + 1); + + int afterAttr = attr.getEnd(); + int beforeNextAttr = nextAttr.getStart(); + + if (afterAttr < beforeNextAttr) { + int length = beforeNextAttr - afterAttr; + if (StringUtils.isWhitespace(textDocument.getText(), afterAttr, beforeNextAttr) && length > 1) { + // Replace multiple spaces with single space + try { + Range range = new Range(textDocument.positionAt(afterAttr), + textDocument.positionAt(beforeNextAttr)); + edits.add(new TextEdit(range, " ")); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + } + } + } + + /** + * Minify text node by trimming unnecessary whitespace. + * + * @param edits the list of text edits + * @param text the text node + */ + private void minifyText(List edits, DOMText text) { + DOMNode parent = text.getParentNode(); + if (parent != null && shouldPreserveSpace((DOMElement) parent)) { + return; + } + + String content = text.getData(); + if (content == null || content.trim().isEmpty()) { + // Remove entirely if only whitespace + removeWhitespace(edits, text.getStart(), text.getEnd()); + } else { + // Trim leading and trailing whitespace, and normalize internal whitespace + // Replace any sequence of whitespace characters (spaces, tabs, newlines) with a + // single space + String normalized = content.trim().replaceAll("\\s+", " "); + if (!normalized.equals(content)) { + try { + Range range = new Range(textDocument.positionAt(text.getStart()), + textDocument.positionAt(text.getEnd())); + edits.add(new TextEdit(range, normalized)); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + } + } + + /** + * Remove whitespace in the given range. + * + * @param edits the list of text edits + * @param start the start offset + * @param end the end offset + */ + private void removeWhitespace(List edits, int start, int end) { + if (start >= end) { + return; + } + if (StringUtils.isWhitespace(textDocument.getText(), start, end)) { + // Only whitespace, remove it + removeContent(edits, start, end); + } + } + + private void removeContent(List edits, int start, int end) { + try { + Range range = new Range(textDocument.positionAt(start), textDocument.positionAt(end)); + edits.add(new TextEdit(range, "")); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + + /** + * Check if whitespace should be preserved for this element. + * + * @param element the element + * @return true if whitespace should be preserved + */ + private boolean shouldPreserveSpace(DOMElement element) { + if (element == null) { + return false; + } + + // Check xml:space attribute + String xmlSpace = element.getAttribute(XML_SPACE_ATTR); + if (XML_SPACE_ATTR_PRESERVE.equals(xmlSpace)) { + return true; + } + + // Check parent elements + DOMNode parent = element.getParentNode(); + if (parent != null && parent.isElement()) { + return shouldPreserveSpace((DOMElement) parent); + } + + return false; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/commands/MinifyDocumentCommand.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/commands/MinifyDocumentCommand.java new file mode 100644 index 000000000..1bc919965 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/commands/MinifyDocumentCommand.java @@ -0,0 +1,48 @@ +/******************************************************************************* +* Copyright (c) 2026 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.minify.commands; + +import java.util.List; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lemminx.services.XMLMinifier; +import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * XML Command "xml.minify.document" to minify an XML document by removing + * unnecessary whitespace. + * + */ +public class MinifyDocumentCommand extends AbstractDOMDocumentCommandHandler { + + public static final String COMMAND_ID = "xml.minify.document"; + + private final XMLMinifier minifier; + + public MinifyDocumentCommand(IXMLDocumentProvider documentProvider) { + super(documentProvider); + this.minifier = new XMLMinifier(); + } + + @Override + protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings, + CancelChecker cancelChecker) throws Exception { + // Get the minification text edits + List edits = minifier.minify(document, null, sharedSettings); + return edits; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/participants/MinifyCodeActionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/participants/MinifyCodeActionParticipant.java new file mode 100644 index 000000000..658d230ba --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/participants/MinifyCodeActionParticipant.java @@ -0,0 +1,74 @@ +/******************************************************************************* +* Copyright (c) 2026 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.minify.participants; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.services.XMLMinifier; +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.jsonrpc.CancelChecker; + +import com.google.gson.JsonObject; + +/** + * Code action participant to provide minify as a source action. + */ +public class MinifyCodeActionParticipant implements ICodeActionParticipant { + + private static final String SOURCE_MINIFY_KIND = CodeActionKind.Source + ".minify"; + + private final Map resolveCodeActionParticipants; + private final XMLMinifier minifier; + + public MinifyCodeActionParticipant() { + this.minifier = new XMLMinifier(); + this.resolveCodeActionParticipants = new HashMap<>(); + this.resolveCodeActionParticipants.put(MinifyCodeActionResolver.PARTICIPANT_ID, + new MinifyCodeActionResolver(minifier)); + } + + @Override + public void doCodeActionUnconditional(ICodeActionRequest request, List codeActions, + CancelChecker cancelChecker) throws CancellationException { + + // Only support clients that can resolve code actions + // The edits will be computed only when the user clicks on the action + if (!request.canSupportResolve()) { + return; + } + + DOMDocument document = request.getDocument(); + + CodeAction minifyAction = new CodeAction("Minify XML"); + minifyAction.setKind(SOURCE_MINIFY_KIND); + + JsonObject data = DataEntryField.createData(document.getDocumentURI(), + MinifyCodeActionResolver.PARTICIPANT_ID); + minifyAction.setData(data); + + codeActions.add(minifyAction); + } + + @Override + public ICodeActionResolvesParticipant getResolveCodeActionParticipant(String participantId) { + return resolveCodeActionParticipants.get(participantId); + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/participants/MinifyCodeActionResolver.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/participants/MinifyCodeActionResolver.java new file mode 100644 index 000000000..ae73f131b --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/minify/participants/MinifyCodeActionResolver.java @@ -0,0 +1,72 @@ +/******************************************************************************* +* Copyright (c) 2026 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.minify.participants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.commons.CodeActionFactory; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.services.XMLMinifier; +import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionResolverRequest; +import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionResolvesParticipant; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +/** + * CodeAction resolver for the minify action. + */ +public class MinifyCodeActionResolver implements ICodeActionResolvesParticipant { + + private static final Logger LOGGER = Logger.getLogger(MinifyCodeActionResolver.class.getName()); + + public static final String PARTICIPANT_ID = MinifyCodeActionResolver.class.getName(); + + private final XMLMinifier minifier; + + public MinifyCodeActionResolver(XMLMinifier minifier) { + this.minifier = minifier; + } + + @Override + public CodeAction resolveCodeAction(ICodeActionResolverRequest request, CancelChecker cancelChecker) { + DOMDocument document = request.getDocument(); + CodeAction resolved = request.getUnresolved(); + + try { + // Compute the minify edits + List edits = minifier.minify(document, null, request.getSharedSettings()); + + // Create workspace edit + if (edits != null && !edits.isEmpty()) { + // Convert to List + List textEdits = new ArrayList<>(edits); + TextDocumentEdit textDocumentEdit = CodeActionFactory.insertEdits(document.getTextDocument(), + textEdits); + WorkspaceEdit workspaceEdit = new WorkspaceEdit( + Collections.singletonList(Either.forLeft(textDocumentEdit))); + resolved.setEdit(workspaceEdit); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while resolving minify code action", e); + } + + return resolved; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/IXMLMinifier.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/IXMLMinifier.java new file mode 100644 index 000000000..fd244ed85 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/IXMLMinifier.java @@ -0,0 +1,34 @@ +/******************************************************************************* +* Copyright (c) 2026 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.services; + +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * XML minifier API. + * + */ +public interface IXMLMinifier { + + /** + * Minify the given text document. + * + * @param text the text. + * @param uri the uri. + * @param sharedSettings the shared settings. + * @param cancelChecker the cancel checker. + * + * @return the minified text document. + */ + String minify(String text, String uri, SharedSettings sharedSettings, CancelChecker cancelChecker); +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLanguageService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLanguageService.java index e1116ff56..2ca98f708 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLanguageService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLanguageService.java @@ -66,7 +66,7 @@ * XML Language service. * */ -public class XMLLanguageService extends XMLExtensionsRegistry implements IXMLFullFormatter { +public class XMLLanguageService extends XMLExtensionsRegistry implements IXMLFullFormatter, IXMLMinifier { private static final CancelChecker NULL_CHECKER = new CancelChecker() { @@ -77,6 +77,7 @@ public void checkCanceled() { }; private final XMLFormatter formatter; + private final XMLMinifier minifier; private final XMLHighlighting highlighting; private final XMLSymbolsProvider symbolsProvider; private final XMLCompletions completions; @@ -97,6 +98,7 @@ public void checkCanceled() { public XMLLanguageService() { this.formatter = new XMLFormatter(this); + this.minifier = new XMLMinifier(); this.highlighting = new XMLHighlighting(this); this.symbolsProvider = new XMLSymbolsProvider(this); this.completions = new XMLCompletions(this); @@ -134,6 +136,24 @@ public List format(DOMDocument xmlDocument, Range range, Sha return formatter.format(xmlDocument, range, sharedSettings); } + @Override + public String minify(String text, String uri, SharedSettings sharedSettings, CancelChecker cancelChecker) { + DOMDocument xmlDocument = DOMParser.getInstance().parse(new TextDocument(text, uri), null); + List edits = this.minify(xmlDocument, null, sharedSettings); + try { + return TextEditUtils.applyEdits(xmlDocument.getTextDocument(), edits); + } catch (Exception e) { + if (edits != null && edits.size() == 1) { + return edits.get(0).getNewText(); + } + return text; + } + } + + public List minify(DOMDocument xmlDocument, Range range, SharedSettings sharedSettings) { + return minifier.minify(xmlDocument, range, sharedSettings); + } + public List findDocumentHighlights(DOMDocument xmlDocument, Position position) { return findDocumentHighlights(xmlDocument, position, NULL_CHECKER); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLMinifier.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLMinifier.java new file mode 100644 index 000000000..af1ba8571 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLMinifier.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2026 Angelo ZERR + * 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: + * Angelo Zerr - initial API and implementation + */ +package org.eclipse.lemminx.services; + +import static org.eclipse.lemminx.utils.TextEditUtils.applyEdits; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.commons.TextDocument; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.minify.XMLMinifierDocument; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; + +/** + * XML minifier support. + * + */ +public class XMLMinifier { + private static final Logger LOGGER = Logger.getLogger(XMLMinifier.class.getName()); + + public XMLMinifier() { + } + + /** + * Returns a List containing multiple TextEdits to remove spaces. + * + * @param xmlDocument document to perform minification on + * @param range specified range in which minification will be done + * @param sharedSettings settings containing minification preferences + * @return List containing TextEdit with minification changes + */ + public List minify(DOMDocument xmlDocument, Range range, SharedSettings sharedSettings) { + try { + XMLMinifierDocument minifierDocument = new XMLMinifierDocument(xmlDocument, range, sharedSettings); + List result = minifierDocument.minify(); + + // For large files, merge all TextEdits into a single one to avoid OutOfMemory + // This is more memory efficient as we don't keep thousands of TextEdit objects + if (shouldMergeEdits(result, xmlDocument)) { + String minified = applyEdits(xmlDocument.getTextDocument(), result); + Range editRange = range != null ? range : getFullDocumentRange(xmlDocument); + return List.of(new TextEdit(editRange, minified)); + } + return result; + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "Minification failed due to BadLocation", e); + } + return null; + } + + /** + * Determines if TextEdits should be merged into a single edit. + * Merging is beneficial for large files to reduce memory consumption. + * + * @param edits the list of text edits + * @param xmlDocument the XML document + * @return true if edits should be merged + */ + private boolean shouldMergeEdits(List edits, DOMDocument xmlDocument) { + // Merge if there are many edits (> 1000) or if the document is large (> 100KB) + int editCount = edits != null ? edits.size() : 0; + int documentSize = xmlDocument.getTextDocument().getText().length(); + return editCount > 1000 || documentSize > 100_000; + } + + /** + * Returns the full document range. + * + * @param xmlDocument the XML document + * @return the full document range + * @throws BadLocationException if position calculation fails + */ + private Range getFullDocumentRange(DOMDocument xmlDocument) throws BadLocationException { + TextDocument textDocument = xmlDocument.getTextDocument(); + Position start = new Position(0, 0); + Position end = textDocument.positionAt(textDocument.getText().length()); + return new Range(start, end); + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesConstants.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesConstants.java index d80d8bccd..bbbef95e1 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesConstants.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesConstants.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.UUID; +import org.eclipse.lsp4j.CodeActionKind; import org.eclipse.lsp4j.CodeActionOptions; import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.ColorProviderOptions; @@ -94,6 +95,10 @@ private ServerCapabilitiesConstants() { private static CodeActionOptions createDefaultCodeActionOptions() { CodeActionOptions options = new CodeActionOptions(); options.setResolveProvider(Boolean.TRUE); + options.setCodeActionKinds(Arrays.asList( + CodeActionKind.QuickFix, + CodeActionKind.Source + )); return options; } } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension index abdc794ec..47ec500ac 100644 --- a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension +++ b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension @@ -12,4 +12,5 @@ org.eclipse.lemminx.extensions.xmlmodel.XMLModelPlugin org.eclipse.lemminx.extensions.generators.FileContentGeneratorPlugin org.eclipse.lemminx.extensions.relaxng.RelaxNGPlugin org.eclipse.lemminx.extensions.xinclude.XIncludePlugin -org.eclipse.lemminx.extensions.colors.XMLColorsPlugin \ No newline at end of file +org.eclipse.lemminx.extensions.colors.XMLColorsPlugin +org.eclipse.lemminx.extensions.minify.MinifyPlugin \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java index abff9df5f..3dcbe36ec 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java @@ -182,16 +182,14 @@ public static CompletionList testCompletionFor(String value, int expectedCount, } public static CompletionList testCompletionFor(String value, Integer expectedCount, boolean enableItemDefaults, - CompletionItem... expectedItems) - throws BadLocationException { + CompletionItem... expectedItems) throws BadLocationException { SharedSettings settings = new SharedSettings(); return testCompletionFor(new XMLLanguageService(), value, null, null, null, expectedCount, settings, enableItemDefaults, expectedItems); } public static CompletionList testCompletionFor(String value, String catalogPath, String fileURI, - Integer expectedCount, - CompletionItem... expectedItems) throws BadLocationException { + Integer expectedCount, CompletionItem... expectedItems) throws BadLocationException { return testCompletionFor(new XMLLanguageService(), value, catalogPath, null, fileURI, expectedCount, true, expectedItems); } @@ -204,8 +202,7 @@ public static CompletionList testCompletionFor(String value, boolean autoCloseTa public static CompletionList testCompletionFor(XMLLanguageService xmlLanguageService, String value, String catalogPath, Consumer customConfiguration, String fileURI, Integer expectedCount, boolean autoCloseTags, CompletionItem... expectedItems) throws BadLocationException { - return testCompletionFor(xmlLanguageService, value, - catalogPath, customConfiguration, fileURI, expectedCount, + return testCompletionFor(xmlLanguageService, value, catalogPath, customConfiguration, fileURI, expectedCount, autoCloseTags, false, expectedItems); } @@ -228,8 +225,7 @@ public static CompletionList testCompletionFor(XMLLanguageService xmlLanguageSer } public static CompletionList testCompletionFor(XMLLanguageService xmlLanguageService, String value, - String catalogPath, - Consumer customConfiguration, String fileURI, Integer expectedCount, + String catalogPath, Consumer customConfiguration, String fileURI, Integer expectedCount, SharedSettings sharedSettings, boolean enableItemDefaults, CompletionItem... expectedItems) throws BadLocationException { if (enableItemDefaults) { @@ -327,10 +323,8 @@ public static void assertCompletion(CompletionList completions, CompletionItem e List matchedAdditionalTextEdits = match.getAdditionalTextEdits() != null ? match.getAdditionalTextEdits() : Collections.emptyList(); - assertEquals(expected.getAdditionalTextEdits().size(), - matchedAdditionalTextEdits.size()); - assertArrayEquals(expected.getAdditionalTextEdits().toArray(), - matchedAdditionalTextEdits.toArray()); + assertEquals(expected.getAdditionalTextEdits().size(), matchedAdditionalTextEdits.size()); + assertArrayEquals(expected.getAdditionalTextEdits().toArray(), matchedAdditionalTextEdits.toArray()); } } else { assertNull(match.getTextEdit()); @@ -522,8 +516,7 @@ public static void testCompletionItemUnresolvedFor(String value, String catalogP } public static void testCompletionItemUnresolvedFor(XMLLanguageService xmlLanguageService, String value, - String catalogPath, - Consumer customConfiguration, String fileURI, Integer expectedCount, + String catalogPath, Consumer customConfiguration, String fileURI, Integer expectedCount, SharedSettings sharedSettings, CompletionItem... expectedItems) throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); @@ -574,8 +567,7 @@ public static void testCompletionItemUnresolvedFor(XMLLanguageService xmlLanguag public static void testCompletionItemResolveFor(String value, String catalogPath, String fileURI, Integer expectedCount, CompletionItem... expectedItems) throws BadLocationException { CompletionItemResolveSupportCapabilities completionItemResolveSupportCapabilities = new CompletionItemResolveSupportCapabilities(); - completionItemResolveSupportCapabilities - .setProperties(Arrays.asList("documentation")); + completionItemResolveSupportCapabilities.setProperties(Arrays.asList("documentation")); CompletionItemCapabilities completionItemCapabilities = new CompletionItemCapabilities(); completionItemCapabilities.setResolveSupport(completionItemResolveSupportCapabilities); CompletionCapabilities completionCapabilities = new CompletionCapabilities(); @@ -588,8 +580,7 @@ public static void testCompletionItemResolveFor(String value, String catalogPath } public static void testCompletionItemResolveFor(XMLLanguageService xmlLanguageService, String value, - String catalogPath, - Consumer customConfiguration, String fileURI, Integer expectedCount, + String catalogPath, Consumer customConfiguration, String fileURI, Integer expectedCount, SharedSettings sharedSettings, CompletionItem... expectedItems) throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); @@ -630,15 +621,13 @@ public static void testCompletionItemResolveFor(XMLLanguageService xmlLanguageSe assertEquals(expectedCount.intValue(), list.getItems().size()); } - CompletionList resolved = new CompletionList( - list.getItems().stream() // - .map((item) -> { - return (CompletionItem) xmlLanguageService.resolveCompletionItem(item, htmlDoc, - sharedSettings, - () -> { - }); - }) // - .collect(Collectors.toList())); + CompletionList resolved = new CompletionList(list.getItems().stream() // + .map((item) -> { + return (CompletionItem) xmlLanguageService.resolveCompletionItem(item, htmlDoc, sharedSettings, + () -> { + }); + }) // + .collect(Collectors.toList())); if (expectedItems != null) { for (CompletionItem item : expectedItems) { @@ -838,14 +827,13 @@ public static void testPublishDiagnosticsFor(String xml, String fileURI, Consume } public static void testPublishDiagnosticsFor(String xml, String fileURI, - XMLValidationRootSettings validationSettings, - PublishDiagnosticsParams... expected) { + XMLValidationRootSettings validationSettings, PublishDiagnosticsParams... expected) { testPublishDiagnosticsFor(xml, fileURI, validationSettings, (Consumer) null, expected); } public static void testPublishDiagnosticsFor(String xml, String fileURI, - XMLValidationRootSettings validationSettings, - Consumer configuration, PublishDiagnosticsParams... expected) { + XMLValidationRootSettings validationSettings, Consumer configuration, + PublishDiagnosticsParams... expected) { XMLLanguageService xmlLanguageService = new XMLLanguageService(); if (configuration != null) { xmlLanguageService.initializeIfNeeded(); @@ -860,8 +848,8 @@ public static void testPublishDiagnosticsFor(String xml, String fileURI, XMLLang } public static void testPublishDiagnosticsFor(String xml, String fileURI, - XMLValidationRootSettings validationSettings, - XMLLanguageService xmlLanguageService, PublishDiagnosticsParams... expected) { + XMLValidationRootSettings validationSettings, XMLLanguageService xmlLanguageService, + PublishDiagnosticsParams... expected) { List actual = new ArrayList<>(); DOMDocument xmlDocument = DOMParser.getInstance().parse(xml, fileURI, @@ -876,7 +864,8 @@ public static void testPublishDiagnosticsFor(String xml, String fileURI, public static void assertPublishDiagnostics(List actual, PublishDiagnosticsParams... expected) { assertEquals(expected.length, actual.size(), () -> { - return "Unexpected diagnostics. Expected:"+ getMessages(Arrays.stream(expected)) + ",\nReceived: " +getMessages(actual.stream()); + return "Unexpected diagnostics. Expected:" + getMessages(Arrays.stream(expected)) + ",\nReceived: " + + getMessages(actual.stream()); }); for (int i = 0; i < expected.length; i++) { assertEquals(expected[i].getUri(), actual.get(i).getUri()); @@ -892,8 +881,7 @@ public static void assertPublishDiagnostics(List actua private static List getMessages(Stream diagParams) { return diagParams.flatMap(d -> d.getDiagnostics().stream()) - .map(d -> cleanExceptionMessage.apply(d.getMessage())) - .collect(Collectors.toList()); + .map(d -> cleanExceptionMessage.apply(d.getMessage())).collect(Collectors.toList()); } private static final Function cleanExceptionMessage = (message) -> { @@ -962,8 +950,7 @@ public static List testCodeActionsFor(String xml, Diagnostic diagnos } public static List testCodeActionsFor(String xml, Diagnostic diagnostic, int index, - CodeAction... expected) - throws BadLocationException { + CodeAction... expected) throws BadLocationException { SharedSettings settings = new SharedSettings(); settings.getFormattingSettings().setTabSize(4); settings.getFormattingSettings().setInsertSpaces(false); @@ -1013,9 +1000,8 @@ public static List testCodeActionsFor(String xml, Range range, Strin } public static List testCodeActionsFor(String xml, Diagnostic diagnostic, Range range, - String catalogPath, - String fileURI, SharedSettings sharedSettings, XMLLanguageService xmlLanguageService, int index, - CodeAction... expected) throws BadLocationException { + String catalogPath, String fileURI, SharedSettings sharedSettings, XMLLanguageService xmlLanguageService, + int index, CodeAction... expected) throws BadLocationException { int offset = xml.indexOf('|'); if (offset != -1) { xml = xml.substring(0, offset) + xml.substring(offset + 1); @@ -1145,7 +1131,9 @@ public static CodeAction ca(Diagnostic d, JsonObject data, Either... ops) { CodeAction codeAction = new CodeAction(); - codeAction.setDiagnostics(Collections.singletonList(d)); + if (d != null) { + codeAction.setDiagnostics(Collections.singletonList(d)); + } if (ops != null && ops.length > 0) { codeAction.setEdit(new WorkspaceEdit(Arrays.asList(ops))); } @@ -1679,15 +1667,13 @@ public static void testHighlightsFor(String xml, DocumentHighlight... expected) testHighlightsFor(xml, null, expected); } - public static void testHighlightsFor(String value, String fileURI, - DocumentHighlight... expected) + public static void testHighlightsFor(String value, String fileURI, DocumentHighlight... expected) throws BadLocationException { testHighlightsFor(new XMLLanguageService(), value, fileURI, expected); } public static void testHighlightsFor(XMLLanguageService xmlLanguageService, String value, String fileURI, - DocumentHighlight... expected) - throws BadLocationException { + DocumentHighlight... expected) throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); @@ -1811,6 +1797,24 @@ public static void assertFormat(XMLLanguageService languageService, String unfor } } + // ------------------- Minify assert + + public static String minify(String unminified, SharedSettings sharedSettings) throws BadLocationException { + return minify(unminified, sharedSettings, "test.xml", null); + } + + public static String minify(String unminified, SharedSettings sharedSettings, String uri, Range range) + throws BadLocationException { + TextDocument document = new TextDocument(unminified, uri); + document.setIncremental(true); + DOMDocument xmlDocument = DOMParser.getInstance().parse(document, null); + + XMLLanguageService languageService = new XMLLanguageService(); + List edits = languageService.minify(xmlDocument, range, sharedSettings); + + return applyEdits(document, edits); + } + // ------------------- Prepare rename assert public static PrepareRenameResult pr(Range range, String placeholder) { @@ -1821,8 +1825,7 @@ public static void assertPrepareRename(String value) throws BadLocationException assertRename(value, null); } - public static void assertPrepareRename(String value, PrepareRenameResult expected) - throws BadLocationException { + public static void assertPrepareRename(String value, PrepareRenameResult expected) throws BadLocationException { assertPrepareRename(null, value, expected); } @@ -1837,8 +1840,7 @@ public static void assertPrepareRename(XMLLanguageService languageService, Strin value = value.substring(0, offset) + value.substring(offset + 1); fileURI = fileURI != null ? fileURI : "test://test/test.html"; - DOMDocument document = DOMParser.getInstance().parse(value, fileURI, - null); + DOMDocument document = DOMParser.getInstance().parse(value, fileURI, null); Position position = document.positionAt(offset); @@ -1873,8 +1875,7 @@ public static void assertRename(XMLLanguageService languageService, String value value = value.substring(0, offset) + value.substring(offset + 1); fileURI = fileURI != null ? fileURI : "test://test/test.html"; - DOMDocument document = DOMParser.getInstance().parse(value, fileURI, - null); + DOMDocument document = DOMParser.getInstance().parse(value, fileURI, null); Position position = document.positionAt(offset); @@ -1884,10 +1885,8 @@ public static void assertRename(XMLLanguageService languageService, String value WorkspaceEdit workspaceEdit = languageService.doRename(document, position, newText, () -> { }); final String uri = fileURI; - Optional documentChange = workspaceEdit.getDocumentChanges() - .stream().filter(Either::isLeft) - .filter(e -> uri.equals(e.getLeft().getTextDocument().getUri())) - .map(Either::getLeft).findFirst(); + Optional documentChange = workspaceEdit.getDocumentChanges().stream().filter(Either::isLeft) + .filter(e -> uri.equals(e.getLeft().getTextDocument().getUri())).map(Either::getLeft).findFirst(); List actualEdits = documentChange.isPresent() ? documentChange.get().getEdits() : Collections.emptyList(); assertArrayEquals(expectedEdits.toArray(), actualEdits.toArray()); @@ -1905,8 +1904,7 @@ public static void testLinkedEditingFor(String value, String fileURI, LinkedEdit } public static void testLinkedEditingFor(XMLLanguageService xmlLanguageService, String value, String fileURI, - LinkedEditingRanges expected) - throws BadLocationException { + LinkedEditingRanges expected) throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); @@ -2005,15 +2003,15 @@ public static SelectionRange sr(List ranges) { return selectionRange; } - public static void assertSurroundWith(String xml, SurroundWithKind kind, boolean snippetsSupported, - String expected) throws BadLocationException, InterruptedException, ExecutionException { + public static void assertSurroundWith(String xml, SurroundWithKind kind, boolean snippetsSupported, String expected) + throws BadLocationException, InterruptedException, ExecutionException { assertSurroundWith(xml, kind, snippetsSupported, (service) -> { }, "src/test/resources/test.xml", expected); } public static void assertSurroundWith(String xml, SurroundWithKind kind, boolean snippetsSupported, - Consumer configuration, String uri, - String expected) throws BadLocationException, InterruptedException, ExecutionException { + Consumer configuration, String uri, String expected) + throws BadLocationException, InterruptedException, ExecutionException { MockXMLLanguageServer languageServer = new MockXMLLanguageServer(); configuration.accept(languageServer.getXMLLanguageService()); @@ -2035,10 +2033,8 @@ public static void assertSurroundWith(String xml, SurroundWithKind kind, boolean TextDocumentIdentifier xmlIdentifier = languageServer.didOpen(uri, x.toString()); // Execute surround with tags command - SurroundWithResponse response = (SurroundWithResponse) languageServer - .executeCommand(SurroundWithCommand.COMMAND_ID, xmlIdentifier, selection, kind.name(), - snippetsSupported) - .get(); + SurroundWithResponse response = (SurroundWithResponse) languageServer.executeCommand( + SurroundWithCommand.COMMAND_ID, xmlIdentifier, selection, kind.name(), snippetsSupported).get(); String actual = applyEdits(document, Arrays.asList(response.getStart(), response.getEnd())); assertEquals(expected, actual); @@ -2077,8 +2073,7 @@ public static void assertColorInformation(List actua // ------------------- ColorInformation assert public static void testColorPresentationFor(String value, String fileURI, Color color, Range range, - XMLColorsSettings colorSettings, - ColorPresentation... expected) { + XMLColorsSettings colorSettings, ColorPresentation... expected) { TextDocument document = new TextDocument(value, fileURI != null ? fileURI : "test://test/test.xml"); XMLLanguageService xmlLanguageService = new XMLLanguageService(); diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLProblemsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLProblemsTest.java index d9d495b4a..d9dd72a16 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLProblemsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLProblemsTest.java @@ -35,6 +35,7 @@ import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.nogrammarconstraints.GenerateXSINoNamespaceSchemaCodeActionResolver; import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings; import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationRootSettings; +import org.eclipse.lemminx.extensions.minify.participants.MinifyCodeActionResolver; import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lemminx.services.data.DataEntryField; import org.eclipse.lemminx.settings.SharedSettings; @@ -239,7 +240,9 @@ public void withCodeActionResolverSupport() throws BadLocationException { ca(d, createData("test.xml", GenerateRelaxNGSchemaCodeActionResolver.PARTICIPANT_ID, "test.rng")), // Open binding wizard command ca(d, new Command("Bind to existing grammar/schema", OPEN_BINDING_WIZARD, - Arrays.asList(new Object[] { "test.xml" })))); + Arrays.asList(new Object[] { "test.xml" }))), // + // XML Minify + ca(null, createData("test.xml", MinifyCodeActionResolver.PARTICIPANT_ID, null))); // Test resolve of // Code action to generate DTD, XSD @@ -294,7 +297,9 @@ public void withCodeActionResolverSupport() throws BadLocationException { private JsonObject createData(String uri, String particpantId, String file) { JsonObject data = DataEntryField.createData(uri, particpantId); - data.addProperty("file", file); + if (file != null) { + data.addProperty("file", file); + } return data; } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/minify/XMLMinifierTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/minify/XMLMinifierTest.java new file mode 100644 index 000000000..f3f04fb0b --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/minify/XMLMinifierTest.java @@ -0,0 +1,232 @@ +/******************************************************************************* +* Copyright (c) 2026 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.minify; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.settings.SharedSettings; +import org.junit.jupiter.api.Test; + +/** + * XML minifier tests. + * + */ +public class XMLMinifierTest { + + @Test + public void testMinifySimpleElement() throws BadLocationException { + String content = "\n" + // + " text\n" + // + ""; + String expected = "text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyWithAttributes() throws BadLocationException { + String content = "\n" + // + " text\n" + // + ""; + String expected = "text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyPreserveXmlSpace() throws BadLocationException { + String content = "\n" + // + " text with spaces \n" + // + ""; + String expected = " text with spaces "; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyNestedElements() throws BadLocationException { + String content = "\n" + // + " \n" + // + " text1\n" + // + " text2\n" + // + " \n" + // + ""; + String expected = "text1text2"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyMixedContent() throws BadLocationException { + String content = "\n" + // + " Some text\n" + // + " text\n" + // + " More text\n" + // + ""; + String expected = "Some texttextMore text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyNormalizeWhitespace() throws BadLocationException { + String content = "\n" + // + " Text with multiple\n" + // + " spaces and\n" + // + " newlines.\n" + // + ""; + String expected = "Text with multiple spaces and newlines."; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyRemovesComments() throws BadLocationException { + String content = "\n" + // + " \n" + // + " text\n" + // + ""; + String expected = "text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyRemovesMultipleComments() throws BadLocationException { + String content = "\n" + // + "\n" + // + " \n" + // + " text\n" + // + " \n" + // + " text2\n" + // + " \n" + // + ""; + String expected = "texttext2"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyRemovesNestedComments() throws BadLocationException { + String content = "\n" + // + " \n" + // + " \n" + // + " text\n" + // + " \n" + // + ""; + String expected = "text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyRemovesMultilineComments() throws BadLocationException { + String content = "\n" + // + " \n" + // + " text\n" + // + " \n" + // + ""; + String expected = "text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyWithCDATA() throws BadLocationException { + String content = "\n" + // + " \n" + // + ""; + String expected = ""; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyProcessingInstruction() throws BadLocationException { + String content = "\n" + // + "\n" + // + " text\n" + // + ""; + String expected = "text"; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } + + @Test + public void testMinifyCatalog() throws BadLocationException { + String content = "\n" + // + "\n" + // + " \n" + // + " Gambardella, Matthew\n" + // + " XML Developer's Guide\n" + // + " Computer\n" + // + " 44.95\n" + // + " 2000-10-01\n" + // + " An in-depth look at creating applications \n" + // + " with XML.\n" + // + " \n" + // + " \n" + // + " Ralls, Kim\n" + // + " Midnight Rain\n" + // + " Fantasy\n" + // + " 5.95\n" + // + " 2000-12-16\n" + // + " A former architect battles corporate zombies, \n" + // + " an evil sorceress, and her own childhood to become queen \n" + // + " of the world.\n" + // + " \n" + // + ""; + String expected = "" + // + "" + // + "" + // + "Gambardella, Matthew" + // + "XML Developer's Guide" + // + "Computer" + // + "44.95" + // + "2000-10-01" + // + "An in-depth look at creating applications with XML." + // + "" + // + "" + // + "Ralls, Kim" + // + "Midnight Rain" + // + "Fantasy" + // + "5.95" + // + "2000-12-16" + // + "A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world." + // + "" + // + ""; + SharedSettings settings = new SharedSettings(); + String actual = XMLAssert.minify(content, settings); + assertEquals(expected, actual); + } +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/relaxng/xml/codeaction/MissingChildElementCodeActionTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/relaxng/xml/codeaction/MissingChildElementCodeActionTest.java index 3b10d232e..7651bbdf2 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/relaxng/xml/codeaction/MissingChildElementCodeActionTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/relaxng/xml/codeaction/MissingChildElementCodeActionTest.java @@ -26,6 +26,7 @@ import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.missingelement.required_element_missingCodeActionResolver; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.missingelement.required_elements_missing_expectedCodeActionResolver; +import org.eclipse.lemminx.extensions.minify.participants.MinifyCodeActionResolver; import org.eclipse.lemminx.extensions.relaxng.xml.validator.RelaxNGErrorCode; import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lemminx.services.data.DataEntryField; @@ -54,13 +55,12 @@ public void incomplete_element_required_element_missing_only_root() throws Excep ""; Diagnostic d = d(1, 1, 1, 12, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(1, 13, 2, 0, // - "\r\n" + // - "\t\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n" + // - "\t\r\n")), + testCodeActionsFor(xml, d, ca(d, te(1, 13, 2, 0, // + "\r\n" + // + "\t\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n" + // + "\t\r\n")), ca(d, te(1, 13, 2, 0, // "\r\n" + // "\t\r\n" + // @@ -78,11 +78,10 @@ public void incomplete_element_required_element_missing_simple() throws Exceptio ""; Diagnostic d = d(2, 2, 2, 6, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(2, 7, 3, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n\t")), + testCodeActionsFor(xml, d, ca(d, te(2, 7, 3, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n\t")), ca(d, te(2, 7, 3, 1, // "\r\n" + // "\t\t\r\n" + // @@ -102,11 +101,10 @@ public void incomplete_element_required_element_missing_newlines() throws Except ""; Diagnostic d = d(2, 2, 2, 6, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(2, 7, 7, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n\t")), + testCodeActionsFor(xml, d, ca(d, te(2, 7, 7, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n\t")), ca(d, te(2, 7, 7, 1, // "\r\n" + // "\t\t\r\n" + // @@ -123,11 +121,10 @@ public void incomplete_element_required_element_missing_with_existing() throws E ""; Diagnostic d = d(2, 2, 2, 6, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(2, 7, 4, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n\t")), + testCodeActionsFor(xml, d, ca(d, te(2, 7, 4, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n\t")), ca(d, te(2, 7, 4, 1, // "\r\n" + // "\t\t\r\n" + // @@ -143,11 +140,10 @@ public void incomplete_element_required_element_missing_with_ref() throws Except ""; Diagnostic d = d(2, 2, 2, 6, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(2, 7, 3, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n\t")), + testCodeActionsFor(xml, d, ca(d, te(2, 7, 3, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n\t")), ca(d, te(2, 7, 3, 1, // "\r\n" + // "\t\t\r\n" + // @@ -161,27 +157,26 @@ public void incomplete_element_required_element_missing_TEI_teiHeader() throws E ""; Diagnostic d = d(1, 1, 1, 4, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, 0, - ca(d, te(1, 41, 2, 0, // - "\r\n" + // - "\t\r\n" + // - "\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\t

\r\n" + // - "\t\t\t
\r\n" + // - "\t\t
\r\n" + // - "\t
\r\n" + // - "\t\r\n" + // - "\t\t\r\n" + // - "\t\t\t
\r\n" + // - "\t\t\r\n" + // - "\t
\r\n"))); + testCodeActionsFor(xml, d, 0, ca(d, te(1, 41, 2, 0, // + "\r\n" + // + "\t\r\n" + // + "\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\t

\r\n" + // + "\t\t\t
\r\n" + // + "\t\t
\r\n" + // + "\t
\r\n" + // + "\t\r\n" + // + "\t\t\r\n" + // + "\t\t\t
\r\n" + // + "\t\t\r\n" + // + "\t
\r\n"))); } @Test @@ -223,20 +218,20 @@ public void incomplete_element_required_element_missing_optional_element_with_re XMLLanguageService ls = new XMLLanguageService(); List actual = testCodeActionsFor(xml, d, null, settings, ls, - ca(d, createData("test.xml", required_element_missingCodeActionResolver.PARTICIPANT_ID, - true)), - ca(d, createData("test.xml", required_element_missingCodeActionResolver.PARTICIPANT_ID, - false))); + ca(d, createData("test.xml", required_element_missingCodeActionResolver.PARTICIPANT_ID, true)), + ca(d, createData("test.xml", required_element_missingCodeActionResolver.PARTICIPANT_ID, false)), // + // XML Minify + ca(null, createData("test.xml", MinifyCodeActionResolver.PARTICIPANT_ID, null))); CodeAction unresolved1 = actual.get(0); - testResolveCodeActionsFor(xml, unresolved1, settings, ls, ca(d, - createData("test.xml", required_element_missingCodeActionResolver.PARTICIPANT_ID, true), - teOp("test.xml", 2, 7, 3, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\r\n\t"))); + testResolveCodeActionsFor(xml, unresolved1, settings, ls, + ca(d, createData("test.xml", required_element_missingCodeActionResolver.PARTICIPANT_ID, true), + teOp("test.xml", 2, 7, 3, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\r\n\t"))); CodeAction unresolved2 = actual.get(1); testResolveCodeActionsFor(xml, unresolved2, settings, ls, @@ -277,7 +272,7 @@ public void incomplete_element_required_element_missing_optional_element_zeroOrM "\t\t\r\n\t"))); } - //https://github.com/eclipse/lemminx/issues/1458 + // https://github.com/eclipse/lemminx/issues/1458 @Test public void incomplete_element_required_element_missing_optional_element_zeroOrMore_autoCloseTags() throws Exception { @@ -317,10 +312,9 @@ public void incomplete_element_required_element_missing_choice() throws Exceptio ""; Diagnostic d = d(1, 1, 1, 8, RelaxNGErrorCode.incomplete_element_required_element_missing); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(1, 9, 2, 0, // - "\r\n" + // - "\t\r\n")), + testCodeActionsFor(xml, d, ca(d, te(1, 9, 2, 0, // + "\r\n" + // + "\t\r\n")), // Generate all elements will generate duplicate elemenst in this case. Needs // fix in future. ca(d, te(1, 9, 2, 0, // @@ -338,10 +332,9 @@ public void incomplete_element_required_elements_missing_choice() throws Excepti ""; Diagnostic d = d(1, 1, 1, 8, RelaxNGErrorCode.incomplete_element_required_elements_missing_expected); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(1, 9, 2, 0, // - "\r\n" + // - "\t\r\n")), + testCodeActionsFor(xml, d, ca(d, te(1, 9, 2, 0, // + "\r\n" + // + "\t\r\n")), ca(d, te(1, 9, 2, 0, // "\r\n" + // "\t\r\n" + // @@ -374,12 +367,11 @@ public void incomplete_element_required_elements_missing_choice_TEItext() throws ""; Diagnostic d = d(15, 2, 15, 6, RelaxNGErrorCode.incomplete_element_required_elements_missing_expected); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, - ca(d, te(15, 7, 16, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\t
\r\n" + // - "\t\t\r\n\t")), + testCodeActionsFor(xml, d, ca(d, te(15, 7, 16, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\t
\r\n" + // + "\t\t\r\n\t")), ca(d, te(15, 7, 16, 1, // "\r\n" + // "\t\t\r\n" + // @@ -408,23 +400,27 @@ public void with_codeAction_resolver_support_choice() throws BadLocationExceptio ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "title2")), ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, - "titleChoice"))); + "titleChoice")), // + // XML Minify + ca(null, createData("test.xml", MinifyCodeActionResolver.PARTICIPANT_ID, null))); CodeAction unresolved1 = actual.get(0); - testResolveCodeActionsFor(xml, unresolved1, settings, ls, ca(d, - createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "title"), - teOp("test.xml", 1, 9, 2, 0, // - "\r\n" + // - "\t\r\n"))); + testResolveCodeActionsFor(xml, unresolved1, settings, ls, + ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, + "title"), + teOp("test.xml", 1, 9, 2, 0, // + "\r\n" + // + "\t\r\n"))); CodeAction unresolved2 = actual.get(1); - testResolveCodeActionsFor(xml, unresolved2, settings, ls, ca(d, - createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "title2"), - teOp("test.xml", 1, 9, 2, 0, // - "\r\n" + // - "\t\r\n" + // - "\t\t\r\n" + // - "\t\r\n"))); + testResolveCodeActionsFor(xml, unresolved2, settings, ls, + ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, + "title2"), + teOp("test.xml", 1, 9, 2, 0, // + "\r\n" + // + "\t\r\n" + // + "\t\t\r\n" + // + "\t\r\n"))); CodeAction unresolved3 = actual.get(2); testResolveCodeActionsFor(xml, unresolved3, settings, ls, @@ -456,23 +452,26 @@ public void with_codeAction_resolver_support_choice_pretext() throws BadLocation ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "letter")), ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, - "memo"))); + "memo")), // + // XML Minify + ca(null, createData("test.xml", MinifyCodeActionResolver.PARTICIPANT_ID, null))); CodeAction unresolved1 = actual.get(1); - testResolveCodeActionsFor(xml, unresolved1, settings, ls, ca(d, - createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "book"), - teOp("test.xml", 1, 9, 2, 0, // - "\r\n" + // - "\t\r\n" + // - "\t\t\r\n" + // - "\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\t\r\n" + // - "\t\t\t\t

\r\n" + // - "\t\t\t
\r\n" + // - "\t\t
\r\n" + // - "\t
\r\n"))); + testResolveCodeActionsFor(xml, unresolved1, settings, ls, + ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, + "book"), + teOp("test.xml", 1, 9, 2, 0, // + "\r\n" + // + "\t\r\n" + // + "\t\t\r\n" + // + "\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\t\r\n" + // + "\t\t\t\t

\r\n" + // + "\t\t\t
\r\n" + // + "\t\t
\r\n" + // + "\t
\r\n"))); } @Test @@ -505,34 +504,40 @@ public void with_codeAction_resolver_TEI_text() throws BadLocationException { ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "body")), ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, - "group"))); + "group")), // + // XML Minify + ca(null, createData("test.xml", MinifyCodeActionResolver.PARTICIPANT_ID, null))); CodeAction unresolved1 = actual.get(0); - testResolveCodeActionsFor(xml, unresolved1, settings, ls, ca(d, - createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "body"), - teOp("test.xml", 15, 7, 16, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\t
\r\n" + // - "\t\t\r\n\t"))); + testResolveCodeActionsFor(xml, unresolved1, settings, ls, + ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, + "body"), + teOp("test.xml", 15, 7, 16, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\t
\r\n" + // + "\t\t\r\n\t"))); CodeAction unresolved2 = actual.get(1); - testResolveCodeActionsFor(xml, unresolved2, settings, ls, ca(d, - createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, "group"), - teOp("test.xml", 15, 7, 16, 1, // - "\r\n" + // - "\t\t\r\n" + // - "\t\t\t\r\n" + // - "\t\t\t\t\r\n" + // - "\t\t\t\t\t
\r\n" + // - "\t\t\t\t\r\n" + // - "\t\t\t
\r\n" + // - "\t\t
\r\n\t"))); + testResolveCodeActionsFor(xml, unresolved2, settings, ls, + ca(d, createData("test.xml", required_elements_missing_expectedCodeActionResolver.PARTICIPANT_ID, + "group"), + teOp("test.xml", 15, 7, 16, 1, // + "\r\n" + // + "\t\t\r\n" + // + "\t\t\t\r\n" + // + "\t\t\t\t\r\n" + // + "\t\t\t\t\t
\r\n" + // + "\t\t\t\t\r\n" + // + "\t\t\t
\r\n" + // + "\t\t
\r\n\t"))); } private JsonObject createData(String uri, String particpantId, String elementName) { JsonObject data = DataEntryField.createData(uri, particpantId); - data.addProperty("element", elementName); + if (elementName != null) { + data.addProperty("element", elementName); + } return data; }