From fa8f51b17ec3409c89e4d6f23679aaac4a8d540e Mon Sep 17 00:00:00 2001 From: azerr Date: Wed, 4 Jan 2023 10:32:39 +0100 Subject: [PATCH] CodeLens, References, Rename support for XML references Signed-off-by: azerr --- .../lemminx/XMLTextDocumentService.java | 17 +- .../org/eclipse/lemminx/dom/DOMElement.java | 11 +- .../java/org/eclipse/lemminx/dom/DOMNode.java | 11 + .../references/XMLReferencesPlugin.java | 24 ++ .../XMLReferencesCodeLensParticipant.java | 128 ++++++ .../XMLReferencesCompletionParticipant.java | 101 +++-- .../XMLReferencesDefinitionParticipant.java | 34 +- .../XMLReferencesHighlightingParticipant.java | 57 ++- ...erencesLinkedEditingRangesParticipant.java | 78 ++++ .../XMLReferencesReferenceParticipant.java | 66 ++++ .../XMLReferencesRenameParticipant.java | 153 +++++++ .../IXMLReferenceCollector.java} | 21 +- .../references/search/SearchEngine.java | 305 ++++++++++++++ .../references/search/SearchNode.java | 147 +++++++ .../references/search/SearchNodeFactory.java | 134 +++++++ .../references/search/SearchQuery.java | 108 +++++ .../references/search/SearchQueryFactory.java | 179 +++++++++ .../settings/XMLReferenceExpression.java | 10 + .../utils/XMLReferencesSearchContext.java | 77 ---- .../references/utils/XMLReferencesUtils.java | 372 ------------------ .../grammar/rng/RNGRenameParticipant.java | 95 +++-- .../participants/XSDRenameParticipant.java | 111 ++++-- .../services/LinkedEditingRangesRequest.java | 31 ++ .../services/PrepareRenameRequest.java | 25 ++ .../lemminx/services/XMLLanguageService.java | 16 +- .../lemminx/services/XMLLinkedEditing.java | 55 ++- .../lemminx/services/XMLPrepareRename.java | 91 +++++ .../eclipse/lemminx/services/XMLRename.java | 22 +- .../ILinkedEditingRangesParticipant.java | 37 ++ .../ILinkedEditingRangesRequest.java | 19 + .../extensions/IPrepareRenameRequest.java | 20 + .../extensions/IRenameParticipant.java | 8 +- .../services/extensions/IRenameRequest.java | 2 +- .../extensions/XMLExtensionsRegistry.java | 17 + .../ServerCapabilitiesConstants.java | 4 +- .../capabilities/XMLCapabilityManager.java | 3 +- .../eclipse/lemminx/utils/StringUtils.java | 29 +- .../java/org/eclipse/lemminx/XMLAssert.java | 36 +- .../XMLReferencesCodeLensExtensionsTest.java | 125 ++++++ ...XMLReferencesCompletionExtensionsTest.java | 285 +++++++++++--- ...XMLReferencesDefinitionExtensionsTest.java | 104 +++-- ...LReferencesHighlightingExtensionsTest.java | 163 +++++--- ...ncesLinkedEditingRangesExtensionsTest.java | 199 ++++++++++ .../XMLReferencesReferenceExtensionsTest.java | 97 +++++ .../XMLReferencesRenameExtensionsTest.java | 150 +++++++ .../XMLReferencesSettingsForTest.java | 100 +++++ .../ErrorParticipantLanguageServiceTest.java | 19 +- 47 files changed, 3048 insertions(+), 848 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCodeLensParticipant.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesLinkedEditingRangesParticipant.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesReferenceParticipant.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesRenameParticipant.java rename org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/{utils/IXMLReferenceTosCollector.java => search/IXMLReferenceCollector.java} (50%) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchEngine.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNode.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNodeFactory.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQuery.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQueryFactory.java delete mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesSearchContext.java delete mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesUtils.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/LinkedEditingRangesRequest.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/PrepareRenameRequest.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLPrepareRename.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesParticipant.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesRequest.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IPrepareRenameRequest.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCodeLensExtensionsTest.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesLinkedEditingRangesExtensionsTest.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesReferenceExtensionsTest.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesRenameExtensionsTest.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesSettingsForTest.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java index 7e4db3f20c..8bed29a902 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java @@ -89,7 +89,10 @@ import org.eclipse.lsp4j.LinkedEditingRanges; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.PrepareRenameParams; +import org.eclipse.lsp4j.PrepareRenameResult; import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ReferenceParams; import org.eclipse.lsp4j.RenameParams; import org.eclipse.lsp4j.SelectionRange; @@ -349,10 +352,18 @@ public CompletableFuture> rangeFormatting(DocumentRange }); } + @Override + public CompletableFuture> prepareRename(PrepareRenameParams params) { + return computeDOMAsync(params.getTextDocument(), (xmlDocument, cancelChecker) -> { + return getXMLLanguageService().prepareRename(xmlDocument, params.getPosition(), cancelChecker); + }); + } + @Override public CompletableFuture rename(RenameParams params) { return computeDOMAsync(params.getTextDocument(), (xmlDocument, cancelChecker) -> { - return getXMLLanguageService().doRename(xmlDocument, params.getPosition(), params.getNewName()); + return getXMLLanguageService().doRename(xmlDocument, params.getPosition(), params.getNewName(), + cancelChecker); }); } @@ -563,14 +574,14 @@ public CompletableFuture> documentColor(DocumentColorPara return getXMLLanguageService().findDocumentColors(xmlDocument, cancelChecker); }); } - + @Override public CompletableFuture> colorPresentation(ColorPresentationParams params) { return computeDOMAsync(params.getTextDocument(), (xmlDocument, cancelChecker) -> { return getXMLLanguageService().getColorPresentations(xmlDocument, params, cancelChecker); }); } - + @Override public void didSave(DidSaveTextDocumentParams params) { computeAsync((monitor) -> { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java index 9705bc9d4c..f0351c635a 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMElement.java @@ -631,15 +631,6 @@ public DOMText findTextAt(int offset) { text.parent = this; return text; } - DOMNode node = super.findNodeAt(offset); - if (node != null) { - if (node.isText()) { - return (DOMText) node; - } - if (node.isElement() && node != this) { - return ((DOMElement) node).findTextAt(offset); - } - } - return null; + return findTextAt(this, offset); } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMNode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMNode.java index 0e1f49b1d3..e9e6b8af3c 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMNode.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMNode.java @@ -289,6 +289,17 @@ public static DOMAttr findAttrAt(DOMNode node, int offset) { return null; } + public static DOMText findTextAt(DOMNode node, int offset) { + if (node != null && node.hasChildNodes()) { + for (DOMNode child : node.getChildren()) { + if (child.isText() && isIncluded(child, offset)) { + return (DOMText) child; + } + } + } + return null; + } + public static DOMNode findNodeOrAttrAt(DOMDocument document, int offset) { DOMNode node = document.findNodeAt(offset); if (node != null) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/XMLReferencesPlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/XMLReferencesPlugin.java index a42a653dd5..a58968c387 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/XMLReferencesPlugin.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/XMLReferencesPlugin.java @@ -11,14 +11,22 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.references; +import org.eclipse.lemminx.extensions.references.participants.XMLReferencesCodeLensParticipant; import org.eclipse.lemminx.extensions.references.participants.XMLReferencesCompletionParticipant; import org.eclipse.lemminx.extensions.references.participants.XMLReferencesDefinitionParticipant; import org.eclipse.lemminx.extensions.references.participants.XMLReferencesHighlightingParticipant; +import org.eclipse.lemminx.extensions.references.participants.XMLReferencesLinkedEditingRangesParticipant; +import org.eclipse.lemminx.extensions.references.participants.XMLReferencesReferenceParticipant; +import org.eclipse.lemminx.extensions.references.participants.XMLReferencesRenameParticipant; import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; import org.eclipse.lemminx.services.extensions.IDefinitionParticipant; import org.eclipse.lemminx.services.extensions.IHighlightingParticipant; +import org.eclipse.lemminx.services.extensions.ILinkedEditingRangesParticipant; +import org.eclipse.lemminx.services.extensions.IReferenceParticipant; +import org.eclipse.lemminx.services.extensions.IRenameParticipant; 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.completion.ICompletionParticipant; import org.eclipse.lemminx.services.extensions.save.ISaveContext; import org.eclipse.lsp4j.InitializeParams; @@ -66,14 +74,22 @@ public class XMLReferencesPlugin implements IXMLExtension { private final ICompletionParticipant completionParticipant; private final IDefinitionParticipant definitionParticipant; + private final IReferenceParticipant referenceParticipant; + private final ICodeLensParticipant codeLensParticipant; private final IHighlightingParticipant highlightingParticipant; + private final IRenameParticipant renameParticipant; + private final ILinkedEditingRangesParticipant linkedEditingRangesParticipant; private XMLReferencesSettings referencesSettings; public XMLReferencesPlugin() { completionParticipant = new XMLReferencesCompletionParticipant(this); definitionParticipant = new XMLReferencesDefinitionParticipant(this); + referenceParticipant = new XMLReferencesReferenceParticipant(this); + codeLensParticipant = new XMLReferencesCodeLensParticipant(this); highlightingParticipant = new XMLReferencesHighlightingParticipant(this); + renameParticipant = new XMLReferencesRenameParticipant(this); + linkedEditingRangesParticipant = new XMLReferencesLinkedEditingRangesParticipant(this); } @Override @@ -99,14 +115,22 @@ private void updateSettings(XMLReferencesSettings settings, ISaveContext context public void start(InitializeParams params, XMLExtensionsRegistry registry) { registry.registerCompletionParticipant(completionParticipant); registry.registerDefinitionParticipant(definitionParticipant); + registry.registerReferenceParticipant(referenceParticipant); + registry.registerCodeLensParticipant(codeLensParticipant); registry.registerHighlightingParticipant(highlightingParticipant); + registry.registerRenameParticipant(renameParticipant); + registry.registerLinkedEditingRangesParticipants(linkedEditingRangesParticipant); } @Override public void stop(XMLExtensionsRegistry registry) { registry.unregisterCompletionParticipant(completionParticipant); registry.unregisterDefinitionParticipant(definitionParticipant); + registry.unregisterReferenceParticipant(referenceParticipant); + registry.unregisterCodeLensParticipant(codeLensParticipant); registry.unregisterHighlightingParticipant(highlightingParticipant); + registry.unregisterRenameParticipant(renameParticipant); + registry.unregisterLinkedEditingRangesParticipants(linkedEditingRangesParticipant); } public XMLReferencesSettings getReferencesSettings() { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCodeLensParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCodeLensParticipant.java new file mode 100644 index 0000000000..9e4c0eaecf --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCodeLensParticipant.java @@ -0,0 +1,128 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.participants; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.lemminx.client.CodeLensKind; +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchNode; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQuery.Direction; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; +import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; +import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant; +import org.eclipse.lemminx.services.extensions.codelens.ICodeLensRequest; +import org.eclipse.lemminx.services.extensions.codelens.ReferenceCommand; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * XML references codelens support. + * + */ +public class XMLReferencesCodeLensParticipant implements ICodeLensParticipant { + + private static class Link { + + public final List froms; + public final List tos; + + public Link() { + froms = new ArrayList<>(); + tos = new ArrayList<>(); + } + + public void addTo(SearchNode to) { + tos.add(to); + } + + public void addFrom(SearchNode from) { + froms.add(from); + } + } + + private final XMLReferencesPlugin plugin; + + public XMLReferencesCodeLensParticipant(XMLReferencesPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void doCodeLens(ICodeLensRequest request, List lenses, CancelChecker cancelChecker) { + DOMDocument document = request.getDocument(); + SearchQuery query = SearchQueryFactory.createQuery(document, + plugin.getReferencesSettings(), Direction.BOTH); + if (query != null) { + final Map linksMap = new HashMap<>(); + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> { + Link link = linksMap.get(expression); + if (link == null) { + link = new Link(); + linksMap.put(expression, link); + } + if (fromSearchNode != null) { + link.addFrom(fromSearchNode); + } + if (toSearchNode != null) { + link.addTo(toSearchNode); + } + }, cancelChecker); + if (!linksMap.isEmpty()) { + boolean supportedByClient = request.isSupportedByClient(CodeLensKind.References); + Map cache = new HashMap<>(); + Collection links = linksMap.values(); + for (Link link : links) { + for (SearchNode to : link.tos) { + // Increment references count Codelens for the given target element + DOMNode toNode = to.getNode(); + DOMElement toElement = toNode.isAttribute() ? ((DOMAttr) toNode).getOwnerElement() + : toNode.getParentElement(); + if (toElement != null) { + for (SearchNode from : link.froms) { + if (from.matchesValue(to)) { + + CodeLens codeLens = cache.get(toElement); + if (codeLens == null) { + Range range = XMLPositionUtility.createRange(toNode); + codeLens = new CodeLens(range); + codeLens.setCommand( + new ReferenceCommand(document.getDocumentURI(), range.getStart(), + supportedByClient)); + cache.put(toElement, codeLens); + lenses.add(codeLens); + } else { + ((ReferenceCommand) codeLens.getCommand()).increment(); + } + + } + } + } + } + } + } + } + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCompletionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCompletionParticipant.java index e33c0f67f3..47bbb9a39d 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCompletionParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesCompletionParticipant.java @@ -11,15 +11,16 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.references.participants; -import org.eclipse.lemminx.dom.DOMElement; -import org.eclipse.lemminx.dom.DOMNode; +import java.util.concurrent.atomic.AtomicReference; + import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; -import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; -import org.eclipse.lemminx.extensions.references.utils.XMLReferencesSearchContext; -import org.eclipse.lemminx.extensions.references.utils.XMLReferencesUtils; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; import org.eclipse.lemminx.services.extensions.completion.CompletionParticipantAdapter; import org.eclipse.lemminx.services.extensions.completion.ICompletionRequest; import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse; +import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.Range; @@ -44,52 +45,70 @@ public XMLReferencesCompletionParticipant(XMLReferencesPlugin plugin) { @Override public void onXMLContent(ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker) throws Exception { - DOMNode fromNode = request.getNode(); - if (fromNode.isElement()) { - fromNode = ((DOMElement) fromNode).findTextAt(request.getOffset()); - } - searchToNodes(fromNode, request, response); + searchToNodes(request, response, cancelChecker); } @Override public void onAttributeValue(String valuePrefix, ICompletionRequest request, ICompletionResponse response, CancelChecker cancelChecker) throws Exception { - DOMNode node = request.getNode(); - DOMNode fromNode = node.findAttrAt(request.getOffset()); - searchToNodes(fromNode, request, response); + searchToNodes(request, response, cancelChecker); } - private void searchToNodes(DOMNode fromNode, ICompletionRequest request, ICompletionResponse response) { - XMLReferencesSearchContext searchContext = XMLReferencesUtils.findExpressionsWhichMatchFrom(fromNode, + private void searchToNodes(ICompletionRequest request, ICompletionResponse response, + CancelChecker cancelChecker) { + // Create the from query for the node which needs to perform the search. + SearchQuery query = SearchQueryFactory.createFromQuery(request.getNode(), request.getOffset(), plugin.getReferencesSettings()); - if (searchContext != null) { - XMLReferencesUtils.searchToNodes(fromNode, searchContext, false, true, - (toNamespacePrefix, toNode, expression) -> { - CompletionItem item = new CompletionItem(); - String value = createReferenceValue(toNode, toNamespacePrefix, expression); - String insertText = request.getInsertAttrValue(value); - item.setLabel(value); - item.setKind(CompletionItemKind.Value); - item.setFilterText(insertText); - Range fullRange = request.getReplaceRange(); - item.setTextEdit(Either.forLeft(new TextEdit(fullRange, insertText))); - response.addCompletionItem(item); - }); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return; } - } + query.setMatchNode(false); + query.setSearchInIncludedFiles(true); - private static String createReferenceValue(DOMNode toNode, String toNamespacePrefix, - XMLReferenceExpression expression) { - StringBuilder value = new StringBuilder(); - if (expression.getPrefix() != null) { - value.append(expression.getPrefix()); - } - if (toNamespacePrefix != null) { - value.append(toNamespacePrefix); - value.append(":"); - } - value.append(XMLReferencesUtils.getNodeValue(toNode)); - return value.toString(); + AtomicReference replaceRange = new AtomicReference<>(null); + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> { + CompletionItem item = new CompletionItem(); + String value = toSearchNode.getValue(fromSearchNode.getPrefix()); + String insertText = request.getInsertAttrValue(value); + item.setLabel(value); + item.setKind(CompletionItemKind.Value); + item.setFilterText(insertText); + Range fullRange = replaceRange.get(); + if (fullRange == null) { + replaceRange.set(XMLPositionUtility.createRange(fromSearchNode)); + fullRange = replaceRange.get(); + } + item.setTextEdit(Either.forLeft(new TextEdit(fullRange, insertText))); + response.addCompletionItem(item); + }, cancelChecker); + +// XMLReferencesSearchContext searchContext = XMLReferencesUtils.findExpressionsWhichMatchFrom(request.getNode(), +// request.getOffset(), +// plugin.getReferencesSettings()); +// if (searchContext != null) { +// AtomicReference replaceRangex = new AtomicReference<>(null); +// XMLReferencesUtils.searchToNodes(searchContext, false, true, +// (fromSearchNode, toSearchNode) -> { +// CompletionItem item = new CompletionItem(); +// String value = toSearchNode.getValue(); +// String insertText = request.getInsertAttrValue(value); +// item.setLabel(value); +// item.setKind(CompletionItemKind.Value); +// item.setFilterText(insertText); +// Range fullRange = replaceRange.get(); +// if (fullRange == null) { +// replaceRange.set(XMLPositionUtility.createRange(fromSearchNode)); +// fullRange = replaceRange.get(); +// } +// item.setTextEdit(Either.forLeft(new TextEdit(fullRange, insertText))); +// response.addCompletionItem(item); +// }, cancelChecker); +// } } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesDefinitionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesDefinitionParticipant.java index 53f4a61869..b232b0b33f 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesDefinitionParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesDefinitionParticipant.java @@ -14,10 +14,10 @@ import java.util.List; import org.eclipse.lemminx.dom.DOMDocument; -import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; -import org.eclipse.lemminx.extensions.references.utils.XMLReferencesSearchContext; -import org.eclipse.lemminx.extensions.references.utils.XMLReferencesUtils; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; import org.eclipse.lemminx.services.extensions.AbstractDefinitionParticipant; import org.eclipse.lemminx.services.extensions.IDefinitionRequest; import org.eclipse.lemminx.utils.XMLPositionUtility; @@ -46,18 +46,26 @@ protected boolean match(DOMDocument document) { @Override protected void doFindDefinition(IDefinitionRequest request, List locations, CancelChecker cancelChecker) { - DOMNode fromNode = request.getNode(); - XMLReferencesSearchContext searchContext = XMLReferencesUtils.findExpressionsWhichMatchFrom(fromNode, + // Create the from query for the node which needs to perform the search. + SearchQuery query = SearchQueryFactory.createFromQuery(request.getNode(), request.getOffset(), plugin.getReferencesSettings()); - if (searchContext != null) { - XMLReferencesUtils.searchToNodes(fromNode, searchContext, true, true, - (toNamespacePrefix, toNode, expression) -> { - LocationLink location = XMLPositionUtility.createLocationLink( - XMLReferencesUtils.getNodeRange(fromNode), - XMLReferencesUtils.getNodeRange(toNode)); - locations.add(location); - }); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return; } + query.setMatchNode(true); + query.setSearchInIncludedFiles(true); + + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> { + LocationLink location = XMLPositionUtility.createLocationLink( + XMLPositionUtility.createRange(fromSearchNode), + toSearchNode); + locations.add(location); + }, cancelChecker); } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesHighlightingParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesHighlightingParticipant.java index b64134f350..158651255a 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesHighlightingParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesHighlightingParticipant.java @@ -12,11 +12,14 @@ package org.eclipse.lemminx.extensions.references.participants; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; -import org.eclipse.lemminx.extensions.references.utils.XMLReferencesSearchContext; -import org.eclipse.lemminx.extensions.references.utils.XMLReferencesUtils; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQuery.Direction; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; import org.eclipse.lemminx.services.extensions.IHighlightingParticipant; import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.DocumentHighlight; @@ -41,24 +44,44 @@ public XMLReferencesHighlightingParticipant(XMLReferencesPlugin plugin) { @Override public void findDocumentHighlights(DOMNode node, Position position, int offset, List highlights, CancelChecker cancelChecker) { - DOMNode fromNode = node; - if (fromNode.isElement()) { - fromNode = fromNode.findAttrAt(offset); + // Create the from query for the node which needs to perform the search. + SearchQuery query = SearchQueryFactory.createQuery(node, offset, plugin.getReferencesSettings()); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return; } - XMLReferencesSearchContext searchContext = XMLReferencesUtils.findExpressionsWhichMatchFrom(fromNode, - plugin.getReferencesSettings()); - if (searchContext != null) { - highlights - .add(new DocumentHighlight( - XMLPositionUtility.createRange(XMLReferencesUtils.getNodeRange(fromNode)), - DocumentHighlightKind.Read)); - XMLReferencesUtils.searchToNodes(fromNode, searchContext, true, false, - (toNamespacePrefix, toNode, expression) -> { + + query.setMatchNode(true); + query.setSearchInIncludedFiles(false); + + AtomicBoolean added = new AtomicBoolean(false); + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> { + if (!added.get()) { + if (query.getDirection() == Direction.FROM_2_TO) { + highlights.add(new DocumentHighlight( + XMLPositionUtility.createRange(fromSearchNode), + DocumentHighlightKind.Read)); + } else { + highlights.add(new DocumentHighlight( + XMLPositionUtility.createRange(toSearchNode), + DocumentHighlightKind.Write)); + } + added.set(true); + } + if (query.getDirection() == Direction.FROM_2_TO) { highlights.add(new DocumentHighlight( - XMLPositionUtility.createRange(XMLReferencesUtils.getNodeRange(toNode)), + XMLPositionUtility.createRange(toSearchNode), DocumentHighlightKind.Write)); - }); - } + } else { + highlights.add(new DocumentHighlight( + XMLPositionUtility.createRange(fromSearchNode), + DocumentHighlightKind.Read)); + } + }, cancelChecker); } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesLinkedEditingRangesParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesLinkedEditingRangesParticipant.java new file mode 100644 index 0000000000..bd91c85aaf --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesLinkedEditingRangesParticipant.java @@ -0,0 +1,78 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.participants; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchNode; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; +import org.eclipse.lemminx.services.extensions.ILinkedEditingRangesParticipant; +import org.eclipse.lemminx.services.extensions.ILinkedEditingRangesRequest; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * XML references linked edting ranges. + * + * @author Angelo ZERR + * + */ +public class XMLReferencesLinkedEditingRangesParticipant implements ILinkedEditingRangesParticipant { + + private final XMLReferencesPlugin plugin; + + public XMLReferencesLinkedEditingRangesParticipant(XMLReferencesPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void findLinkedEditingRanges(ILinkedEditingRangesRequest request, List ranges, + CancelChecker cancelChecker) { + final AtomicBoolean added = new AtomicBoolean(false); + SearchQuery query = SearchQueryFactory.createToQuery(request.getNode(), request.getOffset(), + plugin.getReferencesSettings()); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return; + } + query.setMatchNode(true); + query.setSearchInIncludedFiles(true); + + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> { + if (!added.get()) { + ranges.add(XMLPositionUtility.createRange(toSearchNode)); + added.set(true); + } + ranges.add(createRange(fromSearchNode)); + }, + cancelChecker); + + } + + private static Range createRange(SearchNode node) { + Range range = XMLPositionUtility.createRange(node); + String prefix = node.getPrefix(); + if (prefix != null) { + range.getStart().setCharacter(range.getStart().getCharacter() + prefix.length()); + } + return range; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesReferenceParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesReferenceParticipant.java new file mode 100644 index 0000000000..d003533400 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesReferenceParticipant.java @@ -0,0 +1,66 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.participants; + +import java.util.List; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; +import org.eclipse.lemminx.services.extensions.AbstractReferenceParticipant; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.ReferenceContext; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * XML references reference support. + * + */ +public class XMLReferencesReferenceParticipant extends AbstractReferenceParticipant { + + private final XMLReferencesPlugin plugin; + + public XMLReferencesReferenceParticipant(XMLReferencesPlugin plugin) { + this.plugin = plugin; + } + + @Override + protected boolean match(DOMDocument document) { + return true; + } + + @Override + protected void findReferences(DOMNode node, Position position, int offset, ReferenceContext context, + List locations, CancelChecker cancelChecker) { + SearchQuery query = SearchQueryFactory.createToQuery(node, offset, plugin.getReferencesSettings()); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return; + } + query.setMatchNode(true); + query.setSearchInIncludedFiles(true); + + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> locations + .add(XMLPositionUtility.createLocation(fromSearchNode)), + cancelChecker); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesRenameParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesRenameParticipant.java new file mode 100644 index 0000000000..7972756a04 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/participants/XMLReferencesRenameParticipant.java @@ -0,0 +1,153 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.participants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.extensions.references.XMLReferencesPlugin; +import org.eclipse.lemminx.extensions.references.search.SearchEngine; +import org.eclipse.lemminx.extensions.references.search.SearchNode; +import org.eclipse.lemminx.extensions.references.search.SearchQuery; +import org.eclipse.lemminx.extensions.references.search.SearchQueryFactory; +import org.eclipse.lemminx.services.extensions.IPrepareRenameRequest; +import org.eclipse.lemminx.services.extensions.IRenameParticipant; +import org.eclipse.lemminx.services.extensions.IRenameRequest; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +/** + * XML references rename support. + * + */ +public class XMLReferencesRenameParticipant implements IRenameParticipant { + + private final XMLReferencesPlugin plugin; + + public XMLReferencesRenameParticipant(XMLReferencesPlugin plugin) { + this.plugin = plugin; + } + + @Override + public Either prepareRename(IPrepareRenameRequest request, + CancelChecker cancelChecker) { + // FIXME: when rename from 'from' node will be avalaible, we need just replace with SearchQueryFactory.createQuery + SearchQuery query = SearchQueryFactory.createToQuery(request.getNode(), request.getOffset(), + plugin.getReferencesSettings()); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return null; + } + SearchNode searchNode = query.getSearchNode(); + if (searchNode == null) { + return null; + } + Range range = XMLPositionUtility.createRange(searchNode); + String placeholder = searchNode.getValue(null); + return Either.forRight(new PrepareRenameResult(range, placeholder)); + } + + @Override + public void doRename(IRenameRequest request, List locations, CancelChecker cancelChecker) { + locations.addAll(getRenameTextEdits(request, cancelChecker)); + } + + private List getRenameTextEdits(IRenameRequest request, CancelChecker cancelChecker) { + SearchQuery query = SearchQueryFactory.createToQuery(request.getNode(), request.getOffset(), + plugin.getReferencesSettings()); + if (query == null) { + // The query cannot be created because: + // - the node is neither a text nor an attribute + // - it doesn't exists some expressions for the DOM document of the node. + // - there are none expressions which matches the node. + return Collections.emptyList(); + } + query.setMatchNode(true); + query.setSearchInIncludedFiles(true); + + List locations = new ArrayList<>(); + SearchEngine.getInstance().search(query, + (fromSearchNode, toSearchNode, expression) -> locations + .add(XMLPositionUtility.createLocation(fromSearchNode)), + cancelChecker); + if (locations.isEmpty()) { + return Collections.emptyList(); + } + + String newText = request.getNewText(); + DOMNode node = query.getNode(); + return renameAttributeValueTextEdits(node, newText, locations); + } + + private List renameAttributeValueTextEdits(DOMNode node, String newText, + List locations) { + DOMRange attrValue = getNodeRange(node); + List textEdits = new ArrayList<>(); + Range range = XMLPositionUtility.createRange(attrValue); + + // make range not cover " on both ends + reduceRangeFromBothEnds(range, 1); + + textEdits.add(new TextEdit(range, newText)); + + for (Location location : locations) { + Range textEditRange = location.getRange(); + reduceRangeFromBothEnds(textEditRange, 1); + + TextEdit textEdit = new TextEdit(textEditRange, newText); + textEdits.add(textEdit); + } + + return textEdits; + } + + private void reduceRangeFromBothEnds(Range range, int reduce) { + increaseStartRange(range, reduce); + decreaseEndRange(range, reduce); + } + + private void increaseStartRange(Range range, int increase) { + int startCharacter = range.getStart().getCharacter(); + range.getStart().setCharacter(startCharacter + increase); + } + + private void decreaseEndRange(Range range, int decrease) { + int endCharacter = range.getEnd().getCharacter(); + range.getEnd().setCharacter(endCharacter - decrease); + } + + /** + * Returns the range of the given DOM node. + * + * @param node the DOM node. + * + * @return the range of the given DOM node. + */ + public static DOMRange getNodeRange(DOMNode node) { + if (node.isAttribute()) { + return ((DOMAttr) node).getNodeAttrValue(); + } + return node; + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/IXMLReferenceTosCollector.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/IXMLReferenceCollector.java similarity index 50% rename from org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/IXMLReferenceTosCollector.java rename to org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/IXMLReferenceCollector.java index b4b848830a..4a8494b339 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/IXMLReferenceTosCollector.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/IXMLReferenceCollector.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2022 Red Hat Inc. and others. +* Copyright (c) 2023 Red Hat Inc. and others. * All rights reserved. This program and the accompanying materials * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v20.html @@ -9,28 +9,27 @@ * Contributors: * Red Hat Inc. - initial API and implementation *******************************************************************************/ -package org.eclipse.lemminx.extensions.references.utils; +package org.eclipse.lemminx.extensions.references.search; -import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; /** - * API to collect to attribute which matches + * API to collect from attribute which matches * {@link XMLReferenceExpression#matchTo(org.w3c.dom.Node)} * * @author Angelo ZERR * */ @FunctionalInterface -public interface IXMLReferenceTosCollector { +public interface IXMLReferenceCollector { /** - * Collect the given to attribute which matches the given expression. + * Collect the from / to search node which matches the given expression. * - * @param namespacePrefix namespace prefix. - * @param toNode the to attribute, text node to collect. - * @param expression the reference expression which matches the to - * node. + * @param fromSearchNode the from attribute, text node to collect. + * @param toSearchNode the to attribute, text node to collect. + * @param expression the reference expression which matches the from / to + * node. */ - void collect(String namespacePrefix, DOMNode toNode, XMLReferenceExpression expression); + void collect(SearchNode fromSearchNode, SearchNode toSearchNode, XMLReferenceExpression expression); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchEngine.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchEngine.java new file mode 100644 index 0000000000..1f7981abd9 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchEngine.java @@ -0,0 +1,305 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.search; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMText; +import org.eclipse.lemminx.extensions.references.search.SearchQuery.Direction; +import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; +import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager; +import org.eclipse.lemminx.utils.DOMUtils; +import org.eclipse.lemminx.utils.URIUtils; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; + +/** + * XML references search engine to collect attribute , text which matches XML + * references expression {@link XMLReferenceExpression}. + * + * @author Angelo ZERR + * + */ +public class SearchEngine { + + private static final SearchEngine INSTANCE = new SearchEngine(); + + public static SearchEngine getInstance() { + return INSTANCE; + } + + private static enum MatchNodeStatus { + FROM, TO, NONE; + } + + private static final String INCLUDE_TAG = "include"; + + private static final String HREF_ATTR = "href"; + + /** + * Perform the XML references search by using the given search query. + * + * @param query the search query. + * @param collector the collector used to collect attribute, text nodes. + * + * @param cancelChecker the cancel checker. + */ + public final void search(SearchQuery query, IXMLReferenceCollector collector, CancelChecker cancelChecker) { + DOMDocument document = query.getNode().getOwnerDocument(); + Set visitedURIs = query.isSearchInIncludedFiles() ? new HashSet<>() : null; + searchInDocument(document, query, collector, visitedURIs, cancelChecker); + } + + /** + * Perform the search in the given DOM document. + * + * @param document the DOM document. + * @param query the search query. + * @param collector the collector used to collect attribute, text nodes. + * @param visitedURIs visited URis used to avoid document loading recursion + * when document contains some xi:include. + * @param cancelChecker the cancel checker. + */ + private void searchInDocument(DOMDocument document, SearchQuery query, IXMLReferenceCollector collector, + Set visitedURIs, CancelChecker cancelChecker) { + + // Perform the search by using the DOM document + Set externalURIsForDocument = query.isSearchInIncludedFiles() ? new HashSet<>() : null; + searchInNode(document, query, collector, externalURIsForDocument, cancelChecker); + + if (externalURIsForDocument != null && !externalURIsForDocument.isEmpty()) { + // The search for the document has collected some external document, URIs, + // perform the search for each of them. + URIResolverExtensionManager resolverExtensionManager = document.getResolverExtensionManager(); + for (String externalURI : externalURIsForDocument) { + String baseURI = document.getDocumentURI(); + String resourceURI = resolverExtensionManager.resolve(baseURI, null, externalURI); + if (URIUtils.isFileResource(resourceURI) && canPerformSearch(resourceURI, visitedURIs)) { + // The search was never done for the exterlam document, perform the search by + // using this external document. + if (visitedURIs != null) { + visitedURIs.add(baseURI); + } + DOMDocument externalDocument = DOMUtils.loadDocument(resourceURI, + document.getResolverExtensionManager()); + if (externalDocument != null) { + searchInDocument(externalDocument, query, collector, visitedURIs, cancelChecker); + } + } + } + } + } + + /** + * Returns true if the search can be performed for the given document URI and + * false otherwise. + * + * @param documentURI the document URI. + * @param visitedURIs the visited URIs. + * + * @return true if the search can be performed for the given document URI and + * false otherwise. + */ + private boolean canPerformSearch(String documentURI, Set visitedURIs) { + return visitedURIs == null || !visitedURIs.contains(documentURI); + } + + /** + * Perform the search in the given DOM node. + * + * @param node the DOM node. + * @param query the search query. + * @param collector the collector used to collect attribute, text nodes. + * @param externalURIs the collected external URIs (xi:include/@href) + * @param cancelChecker the cancel checker. + */ + private void searchInNode(DOMNode node, SearchQuery query, IXMLReferenceCollector collector, + Set externalURIs, CancelChecker cancelChecker) { + // Stop the search if required + if (cancelChecker != null) { + cancelChecker.checkCanceled(); + } + + if (node.isElement()) { + // Search in the attributes of the element + DOMElement element = (DOMElement) node; + searchInAttributes(element, query, collector, cancelChecker); + if (externalURIs != null) { + if (isInclude(element)) { + // collect xi:include + String includedFile = element.getAttribute(HREF_ATTR); + if (includedFile != null) { + externalURIs.add(includedFile); + } + } + } + + } else if (node.isText()) { + // Search in the text + searchInText((DOMText) node, query, collector, cancelChecker); + } + if (node.hasChildNodes()) { + // Search in the children + for (DOMNode child : node.getChildren()) { + searchInNode(child, query, collector, externalURIs, cancelChecker); + } + } + } + + /** + * Perform the search in the given DOM text node. + * + * @param text the DOM text node. + * @param query the search query. + * @param collector the collector used to collect attribute, text nodes. + * @param cancelChecker the cancel checker. + */ + private void searchInText(DOMText text, SearchQuery query, IXMLReferenceCollector collector, + CancelChecker cancelChecker) { + if (query.isSearchInText()) { + if (cancelChecker != null) { + cancelChecker.checkCanceled(); + } + collectNodes(text, query, collector); + } + } + + /** + * Perform the search in the attributes of the given DOM element node. + * + * @param element the DOM element node. + * @param query the search query. + * @param collector the collector used to collect attribute, text nodes. + * @param cancelChecker the cancel checker. + */ + private void searchInAttributes(DOMElement element, SearchQuery query, IXMLReferenceCollector collector, + CancelChecker cancelChecker) { + if (query.isSearchInAttribute()) { + // Search to reference in attribute nodes + if (element.hasAttributes()) { + NamedNodeMap toAttributes = element.getAttributes(); + if (toAttributes != null) { + for (int j = 0; j < toAttributes.getLength(); j++) { + DOMAttr toAttr = (DOMAttr) toAttributes.item(j); + if (cancelChecker != null) { + cancelChecker.checkCanceled(); + } + collectNodes(toAttr, query, collector); + } + } + } + } + } + + private void collectNodes(DOMNode node, SearchQuery query, IXMLReferenceCollector collector) { + Direction direction = query.getDirection(); + // Loop for reference expressions of the query + for (XMLReferenceExpression expression : query.getExpressions()) { + MatchNodeStatus status = matchNode(node, expression, direction); + if (status != MatchNodeStatus.NONE) { + // The DOM node matches the XPath (from / to) declared in the reference expression + // get the search nodes for this attribute / text node + List searchNodes = findSearchNodes(node, expression, status, direction); + for (SearchNode searchNode : searchNodes) { + // Collect the current search node + collect(query, searchNode, expression, collector); + } + } + } + } + + private void collect(SearchQuery query, SearchNode searchNode, XMLReferenceExpression expression, + IXMLReferenceCollector collector) { + SearchNode requestedNode = query.getSearchNode(); + if (query.isMatchNode()) { + if (!requestedNode.matchesValue(searchNode)) { + return; + } + } + Direction direction = query.getDirection(); + switch (direction) { + case FROM_2_TO: + collector.collect(requestedNode, searchNode, expression); + break; + case TO_2_FROM: + collector.collect(searchNode, requestedNode, expression); + break; + default: + Direction nodeDirection = searchNode.getDirection(); + if (nodeDirection == Direction.FROM_2_TO) { + collector.collect(searchNode, requestedNode, expression); + } else { + collector.collect(requestedNode, searchNode, expression); + } + } + } + + private List findSearchNodes(DOMNode node, XMLReferenceExpression expression, MatchNodeStatus status, + Direction direction) { + switch (direction) { + case FROM_2_TO: + return SearchNodeFactory.findSearchNodes(node, null, false, direction); + case TO_2_FROM: + return SearchNodeFactory.findSearchNodes(node, expression.getPrefix(), expression.isMultiple(), + direction); + default: + if (status == MatchNodeStatus.FROM) { + return SearchNodeFactory.findSearchNodes(node, expression.getPrefix(), expression.isMultiple(), + Direction.FROM_2_TO); + } + return SearchNodeFactory.findSearchNodes(node, null, false, Direction.TO_2_FROM); + } + } + + private MatchNodeStatus matchNode(DOMNode node, XMLReferenceExpression expression, Direction direction) { + switch (direction) { + case FROM_2_TO: + if (expression.matchTo(node)) { + return MatchNodeStatus.TO; + } + return MatchNodeStatus.NONE; + case TO_2_FROM: + if (expression.matchFrom(node)) { + return MatchNodeStatus.FROM; + } + return MatchNodeStatus.NONE; + default: + if (expression.matchFrom(node)) { + return MatchNodeStatus.FROM; + } + if (expression.matchTo(node)) { + return MatchNodeStatus.TO; + } + return MatchNodeStatus.NONE; + } + } + + /** + * Returns true if the given element is an include element (ex : xi:include) and + * false otherwise. + * + * @param element the DOM element. + * + * @return true if the given element is an include element (ex : xi:include) and + * false otherwise. + */ + private static boolean isInclude(Element element) { + return element != null && INCLUDE_TAG.equals(element.getLocalName()); + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNode.java new file mode 100644 index 0000000000..ba431eac3a --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNode.java @@ -0,0 +1,147 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.search; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.extensions.references.search.SearchQuery.Direction; + +/** + * Search node result. + * + * @author Angelo ZERR + * + */ +public class SearchNode implements DOMRange { + + private final int start; + + private final int end; + + private final DOMNode node; + + private final String prefix; + + private final Direction direction; + + public SearchNode(int start, int end, DOMNode node, String prefix, Direction direction) { + this.start = start; + this.end = end; + this.node = node; + this.prefix = prefix; + this.direction = direction; + } + + public String getValue(String prefix) { + StringBuilder value = new StringBuilder(); + if (prefix != null) { + value.append(prefix); + } + String text = getOwnerDocument().getText(); + for (int i = getStart(); i < getEnd(); i++) { + value.append(text.charAt(i)); + } + return value.toString(); + } + + public String getPrefix() { + return prefix; + } + + public boolean matchesValue(SearchNode toSearchNode) { + int fromStart = getStart(); + int fromEnd = getEnd(); + String fromText = getOwnerDocument().getText(); + if (direction == Direction.FROM_2_TO) { + int adjust = adjustWithPrefix(this); + if (adjust == -1) { + return false; + } + fromStart = fromStart + adjust; + } + int toStart = toSearchNode.getStart(); + int toEnd = toSearchNode.getEnd(); + String toText = toSearchNode.getOwnerDocument().getText(); + if (direction == Direction.TO_2_FROM) { + int adjust = adjustWithPrefix(toSearchNode); + if (adjust == -1) { + return false; + } + toStart = toStart + adjust; + } + + int length = fromEnd - fromStart; + + if (length != toEnd - toStart) { + return false; + } + for (int i = 0; i < length; i++) { + if (fromText.charAt(i + fromStart) != toText.charAt(i + toStart)) { + return false; + } + } + return true; + } + + private static int adjustWithPrefix(SearchNode node) { + String prefix = node.getPrefix(); + if (prefix == null) { + return 0; + } + int start = node.getStart(); + int end = node.getEnd(); + String text = node.getOwnerDocument().getText(); + if (prefix.length() > (end - start)) { + return -1; + } + // check the from search node starts with prefix (ex : '#') + for (int i = 0; i < prefix.length(); i++) { + if (text.charAt(start + i) != prefix.charAt(i)) { + return -1; + } + } + return prefix.length(); + } + + public DOMNode getNode() { + return node; + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getEnd() { + return end; + } + + @Override + public DOMDocument getOwnerDocument() { + return node.getOwnerDocument(); + } + + public Direction getDirection() { + return direction; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + String text = node.getOwnerDocument().getText(); + result.append(text.substring(start, end)); + result.append(direction == Direction.FROM_2_TO ? " -->" : " <--"); + return result.toString(); + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNodeFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNodeFactory.java new file mode 100644 index 0000000000..b39946824e --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchNodeFactory.java @@ -0,0 +1,134 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.search; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.extensions.references.search.SearchQuery.Direction; +import org.eclipse.lemminx.utils.StringUtils; + +/** + * Search node factory. + * + * @author Angelo ZERR + * + */ +public class SearchNodeFactory { + + private static final Predicate NAME_PREDICATE = ch -> { + return !Character.isWhitespace(ch); + }; + + public static List findSearchNodes(DOMNode node, String prefix, boolean multiple, + Direction direction) { + int startNode = getStartNode(node); + if (startNode == -1) { + return Collections.emptyList(); + } + int endNode = getEndNode(node); + if (endNode == -1) { + return Collections.emptyList(); + } + + if (multiple) { + String text = node.getOwnerDocument().getText(); + List searchNodes = new ArrayList<>(); + int itemStart = -1; + for (int j = startNode; j < endNode; j++) { + char c = text.charAt(j); + if (itemStart == -1) { + if (!Character.isWhitespace(c)) { + itemStart = j; + } + } else if (Character.isWhitespace(c)) { + searchNodes.add(new SearchNode(itemStart, j, node, prefix, direction)); + itemStart = -1; + } + } + if (itemStart != -1) { + searchNodes.add(new SearchNode(itemStart, endNode, node, prefix, direction)); + } + return searchNodes; + + } + return Arrays.asList(new SearchNode(startNode, endNode, node, prefix, direction)); + } + + public static SearchNode getSearchNodeAt(DOMNode node, int offset, String prefix, boolean multiple, + Direction direction) { + int startNode = getStartNode(node); + if (startNode == -1) { + return null; + } + int endNode = getEndNode(node); + if (endNode == -1) { + return null; + } + if (multiple) { + String text = node.getOwnerDocument().getText(); + if (offset != startNode) { + int left = StringUtils.findStartWord(text, offset, startNode, NAME_PREDICATE); + if (left != -1) { + startNode = left; + } else { + left = StringUtils.findStartWord(text, offset - 1, startNode, NAME_PREDICATE); + if (left != -1) { + startNode = left; + } else { + startNode = offset; + } + } + } + if (offset != endNode - 1) { + int right = StringUtils.findEndWord(text, offset, endNode, NAME_PREDICATE); + if (right != -1) { + endNode = right; + } else { + endNode = offset; + } + } + } + return new SearchNode(startNode, endNode, node, prefix, direction); + } + + private static int getStartNode(DOMNode node) { + DOMRange range = getDOMRange(node); + if (range == null) { + return -1; + } + return range.getStart() + (node.isAttribute() ? 1 : 0); + } + + private static int getEndNode(DOMNode node) { + DOMRange range = getDOMRange(node); + if (range == null) { + return -1; + } + return range.getEnd() - (node.isAttribute() ? 1 : 0); + } + + private static DOMRange getDOMRange(DOMNode node) { + if (node.isAttribute()) { + DOMAttr attr = (DOMAttr) node; + return attr.getNodeAttrValue(); + } + return node; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQuery.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQuery.java new file mode 100644 index 0000000000..dba2aa4149 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQuery.java @@ -0,0 +1,108 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.search; + +import java.util.List; + +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; + +/** + * XML references search Query. + * + * @author Angelo ZERR + * + */ +public class SearchQuery { + + public static enum Direction { + FROM_2_TO, TO_2_FROM, BOTH; + } + + private final DOMNode node; + private final List expressions; + private final Direction direction; + private boolean searchInAttribute; + private boolean searchInText; + private boolean matchNode; + private boolean searchInIncludedFiles; + private SearchNode searchNode; + + public SearchQuery(DOMNode node, int offset, List expressions, Direction direction) { + this.node = node; + this.expressions = expressions; + this.direction = direction; + searchInAttribute = false; + searchInText = false; + boolean hasMultiple = false; + String prefix = null; + for (XMLReferenceExpression expression : expressions) { + if (!searchInAttribute) { + searchInAttribute = direction == Direction.FROM_2_TO ? expression.isFromSearchInAttribute() + : expression.isToSearchInAttribute(); + } + if (!searchInText) { + searchInText = direction == Direction.FROM_2_TO ? expression.isFromSearchInText() + : expression.isToSearchInText(); + } + if (!hasMultiple) { + hasMultiple = expression.isMultiple(); + } + if (prefix == null) { + prefix = expression.getPrefix(); + } + } + if (offset != -1) { + searchNode = SearchNodeFactory.getSearchNodeAt(node, offset, prefix, hasMultiple, direction); + } + } + + public DOMNode getNode() { + return node; + } + + public List getExpressions() { + return expressions; + } + + public boolean isMatchNode() { + return matchNode; + } + + public boolean isSearchInIncludedFiles() { + return searchInIncludedFiles; + } + + public void setSearchInIncludedFiles(boolean searchInIncludedFiles) { + this.searchInIncludedFiles = searchInIncludedFiles; + } + + public void setMatchNode(boolean matchNode) { + this.matchNode = matchNode; + } + + public boolean isSearchInAttribute() { + return searchInAttribute; + } + + public boolean isSearchInText() { + return searchInText; + } + + public SearchNode getSearchNode() { + return searchNode; + } + + public Direction getDirection() { + return direction; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQueryFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQueryFactory.java new file mode 100644 index 0000000000..c66dcde575 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/search/SearchQueryFactory.java @@ -0,0 +1,179 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references.search; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMText; +import org.eclipse.lemminx.extensions.references.search.SearchQuery.Direction; +import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; +import org.eclipse.lemminx.extensions.references.settings.XMLReferences; +import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; + +/** + * XML references search Query factory. + * + * @author Angelo ZERR + * + */ +public class SearchQueryFactory { + + public static SearchQuery createQuery(DOMNode node, int offset, XMLReferencesSettings settings) { + DOMNode adjustedNode = findAttrOrTextNode(node, offset); + if (adjustedNode == null) { + return null; + } + SearchQuery query = internalCreateQuery(adjustedNode, offset, settings, Direction.FROM_2_TO); + if (query != null) { + return query; + } + return internalCreateQuery(adjustedNode, offset, settings, Direction.TO_2_FROM); + } + + public static SearchQuery createFromQuery(DOMNode node, int offset, XMLReferencesSettings settings) { + return createQuery(node, offset, settings, Direction.FROM_2_TO); + } + + public static SearchQuery createToQuery(DOMNode node, int offset, XMLReferencesSettings settings) { + return createQuery(node, offset, settings, Direction.TO_2_FROM); + } + + public static SearchQuery createQuery(DOMNode node, XMLReferencesSettings settings, + Direction direction) { + return createQuery(node, -1, settings, direction); + } + + public static SearchQuery createQuery(DOMNode node, int offset, XMLReferencesSettings settings, + Direction direction) { + DOMNode adjustedNode = findAttrOrTextNode(node, offset); + if (adjustedNode == null) { + return null; + } + return internalCreateQuery(adjustedNode, offset, settings, direction); + } + + private static SearchQuery internalCreateQuery(DOMNode adjustedNode, int offset, XMLReferencesSettings settings, + Direction direction) { + List expressions = findExpressions(adjustedNode, settings, + direction); + if (expressions == null) { + return null; + } + return new SearchQuery(adjustedNode, offset, expressions, direction); + } + + private static DOMNode findAttrOrTextNode(DOMNode node, int offset) { + if (node == null || node.isText() || node.isAttribute() || node.isOwnerDocument()) { + return node; + } + if (node.isElement()) { + DOMText text = ((DOMElement) node).findTextAt(offset); + if (text != null) { + return text; + } + DOMAttr attr = node.findAttrAt(offset); + if (attr != null) { + return attr; + } + } + return null; + } + + /** + * Returns XML references expressions which match the given DOM + * document and null otherwise. + * + * @param document the DOM document. + * @param xmlReferencesSettings the XML references settings which hosts all + * references expressions. + * + * @return XML references expressions which match the given DOM + * document and null otherwise. + */ + private static List findExpressions(DOMNode node, + XMLReferencesSettings xmlReferencesSettings, Direction direction) { + List allReferences = xmlReferencesSettings != null ? xmlReferencesSettings.getReferences() + : null; + if (allReferences == null) { + return null; + } + DOMDocument document = node.getOwnerDocument(); + String uri = document.getDocumentURI(); + List matchedExpressions = null; + for (XMLReferences references : allReferences) { + // Given this XML references sample + + /** + * + * "xml.references": [ + * // references for docbook.xml files + * { + * "pattern": "*.xml", + * "expressions": [ + * { + * "from": "xref/@linkend", + * "to": "@id" + * } + * ] + * } + *] + * + * + * + */ + if (references.matches(uri)) { + // here uri matches the "*.xml" pattern + List expressions = references.getExpressions(); + if (expressions != null) { + if (node.isOwnerDocument()) { + if (matchedExpressions == null) { + matchedExpressions = new ArrayList<>(); + } + matchedExpressions.addAll(expressions); + } else { + for (XMLReferenceExpression expression : expressions) { + // Given this XML reference expression sample + /** + * + * "expressions": [ + * { + * "from": "xref/@linkend", + * "to": "@id" + * } + * ] + * + * + * + */ + boolean add = direction == Direction.FROM_2_TO ? expression.matchFrom(node) + : expression.matchTo(node); + if (add) { + // here the attribute matches xref/@linkend + if (matchedExpressions == null) { + matchedExpressions = new ArrayList<>(); + } + matchedExpressions.add(expression); + } + } + } + } + } + } + return matchedExpressions; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/settings/XMLReferenceExpression.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/settings/XMLReferenceExpression.java index 3f5d8e9055..acd7030e18 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/settings/XMLReferenceExpression.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/settings/XMLReferenceExpression.java @@ -41,6 +41,8 @@ public class XMLReferenceExpression { private String to; + private boolean multiple; + public String getFrom() { return from; } @@ -65,6 +67,14 @@ public void setPrefix(String prefix) { this.prefix = prefix; } + public void setMultiple(boolean multiple) { + this.multiple = multiple; + } + + public boolean isMultiple() { + return multiple; + } + /** * Returns true if the given DOM Node match the XPath expression of the 'from' * XPath matcher and false otherwise. diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesSearchContext.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesSearchContext.java deleted file mode 100644 index 6be033f84b..0000000000 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesSearchContext.java +++ /dev/null @@ -1,77 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2023 Red Hat Inc. and others. -* All rights reserved. This program and the accompanying materials -* which accompanies this distribution, and is available at -* http://www.eclipse.org/legal/epl-v20.html -* -* SPDX-License-Identifier: EPL-2.0 -* -* Contributors: -* Red Hat Inc. - initial API and implementation -*******************************************************************************/ -package org.eclipse.lemminx.extensions.references.utils; - -import java.util.List; - -import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; - -/** - * The XML reference searcher. - * - * @author Angelo ZERR - * - */ -public class XMLReferencesSearchContext { - - private final List expressions; - - private final boolean searchInAttribute; - - private final boolean searchInText; - - public XMLReferencesSearchContext(List expressions, boolean searchForFromNode) { - this.expressions = expressions; - if (searchForFromNode) { - searchInAttribute = expressions.stream() - .anyMatch(expr -> expr.isFromSearchInAttribute()); - searchInText = expressions.stream() - .anyMatch(expr -> expr.isFromSearchInText()); - } else { - searchInAttribute = expressions.stream() - .anyMatch(expr -> expr.isToSearchInAttribute()); - searchInText = expressions.stream() - .anyMatch(expr -> expr.isToSearchInText()); - } - } - - /** - * Returns true if the search of nodes must be done in attribute nodes and false - * otherwise. - * - * @return true if the search of nodes must be done in attribute nodes and false - * otherwise. - */ - public boolean isSearchInAttribute() { - return searchInAttribute; - } - - /** - * Returns true if the search of nodes must be done in text nodes and false - * otherwise. - * - * @return true if the search of nodes must be done in text nodes and false - * otherwise. - */ - public boolean isSearchInText() { - return searchInText; - } - - /** - * Returns the list of reference expressions. - * - * @return the list of reference expressions. - */ - public List getExpressions() { - return expressions; - } -} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesUtils.java deleted file mode 100644 index e3f9a961ff..0000000000 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/references/utils/XMLReferencesUtils.java +++ /dev/null @@ -1,372 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2022 Red Hat Inc. and others. -* All rights reserved. This program and the accompanying materials -* which accompanies this distribution, and is available at -* http://www.eclipse.org/legal/epl-v20.html -* -* SPDX-License-Identifier: EPL-2.0 -* -* Contributors: -* Red Hat Inc. - initial API and implementation -*******************************************************************************/ -package org.eclipse.lemminx.extensions.references.utils; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.eclipse.lemminx.dom.DOMAttr; -import org.eclipse.lemminx.dom.DOMDocument; -import org.eclipse.lemminx.dom.DOMElement; -import org.eclipse.lemminx.dom.DOMNode; -import org.eclipse.lemminx.dom.DOMRange; -import org.eclipse.lemminx.dom.DOMText; -import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; -import org.eclipse.lemminx.extensions.references.settings.XMLReferences; -import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; -import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager; -import org.eclipse.lemminx.utils.DOMUtils; -import org.eclipse.lemminx.utils.StringUtils; -import org.eclipse.lemminx.utils.URIUtils; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import com.google.common.base.Objects; - -/** - * XML references utilities. - * - * @author Angelo ZERR - * - */ -public class XMLReferencesUtils { - - private static final String INCLUDE_TAG = "include"; - - private static final String HREF_ATTR = "href"; - - /** - * Collect 'to' attributes which matches 'to' declared in - * {@link XMLReferencesSearchContext} which contains a list of - * {@link XMLReferenceExpression}. - * - * @param fromNode the from attribute, text node. - * @param searchContext the references search context. - * @param matchNode true if the value of the from and to nodes must - * be checked, and false otherwise. - * @param searchInIncludedFile true if search must be done in included XML - * file (ex : xi:include) and false otherwise. - * @param collector collector to collect to attribute, text nodes. - */ - public static void searchToNodes(DOMNode fromNode, XMLReferencesSearchContext searchContext, - boolean matchNode, boolean searchInIncludedFile, IXMLReferenceTosCollector collector) { - - DOMDocument document = fromNode.getOwnerDocument(); - DOMElement documentElement = document != null ? document.getDocumentElement() : null; - if (documentElement == null) { - return; - } - String fromValue = getNodeValue(fromNode); - if (matchNode && StringUtils.isEmpty(fromValue)) { - return; - } - String namespacePrefix = null; - int index = fromValue.indexOf(':'); - if (index != -1) { - // ex : jakartaee:applicationType - namespacePrefix = fromValue.substring(0, index); - } - - String fromName = null; - if (matchNode) { - fromName = getFromName(fromValue, namespacePrefix); - } - - // Collect attribute, text nodes to for document element - collectToNode(fromNode, searchContext, matchNode, collector, documentElement, - namespacePrefix); - - // Collect attribute, text nodes to for children of document element - searchToNodes(fromNode, searchContext, matchNode, collector, documentElement, - namespacePrefix, - fromName, new HashSet<>(), searchInIncludedFile); - } - - private static void searchToNodes(DOMNode fromNode, XMLReferencesSearchContext referenceSearcher, - boolean matchNode, - IXMLReferenceTosCollector collector, DOMElement documentElement, String toNamespacePrefix, - String fromName, Set visitedURIs, boolean searchInExternalDocument) { - if (visitedURIs != null) { - DOMDocument document = documentElement.getOwnerDocument(); - String documentURI = document.getDocumentURI(); - if (visitedURIs.contains(documentURI)) { - return; - } - visitedURIs.add(documentURI); - } - Set externalURIS = null; - Node parent = documentElement; - NodeList children = parent.getChildNodes(); - for (int i = 0; i < children.getLength(); i++) { - Node node = children.item(i); - if (node.getNodeType() == Node.ELEMENT_NODE) { - DOMElement toElement = (DOMElement) node; - collectToNode(fromNode, referenceSearcher, matchNode, collector, toElement, - toNamespacePrefix); - - if (isInclude(toElement)) { - // collect xi:include - String schemaLocation = toElement.getAttribute(HREF_ATTR); - if (schemaLocation != null) { - if (externalURIS == null) { - externalURIS = new HashSet<>(); - } - externalURIS.add(schemaLocation); - } - } else { - searchToNodes(fromNode, referenceSearcher, matchNode, collector, toElement, - toNamespacePrefix, fromName, null, false); - } - } - } - if (searchInExternalDocument && externalURIS != null) { - // Search in include location - DOMDocument document = documentElement.getOwnerDocument(); - String documentURI = document.getDocumentURI(); - URIResolverExtensionManager resolverExtensionManager = document.getResolverExtensionManager(); - for (String externalURI : externalURIS) { - String resourceURI = resolverExtensionManager.resolve(documentURI, null, externalURI); - if (URIUtils.isFileResource(resourceURI)) { - DOMDocument externalDocument = DOMUtils.loadDocument(resourceURI, - document.getResolverExtensionManager()); - if (externalDocument != null) { - searchToNodes(fromNode, referenceSearcher, matchNode, collector, - externalDocument.getDocumentElement(), toNamespacePrefix, fromName, visitedURIs, - searchInExternalDocument); - } - } - } - } - } - - private static void collectToNode(DOMNode fromNode, XMLReferencesSearchContext searchContext, - boolean matchNode, IXMLReferenceTosCollector collector, DOMElement toElement, - String toNamespacePrefix) { - if (searchContext.isSearchInAttribute()) { - // Search to reference in attribute nodes - if (toElement.hasAttributes()) { - NamedNodeMap toAttributes = toElement.getAttributes(); - if (toAttributes != null) { - for (int j = 0; j < toAttributes.getLength(); j++) { - DOMAttr toAttr = (DOMAttr) toAttributes.item(j); - XMLReferenceExpression expression = findExpressionWhichMatchesTo(fromNode, toAttr, - searchContext, - matchNode); - if (expression != null) { - collector.collect(toNamespacePrefix, toAttr, expression); - } - } - } - } - } - if (searchContext.isSearchInText()) { - // Search to reference in text node. - DOMNode firstChild = toElement.getFirstChild(); - if (firstChild != null && firstChild.isText()) { - XMLReferenceExpression expression = findExpressionWhichMatchesTo(fromNode, firstChild, - searchContext, - matchNode); - if (expression != null) { - collector.collect(toNamespacePrefix, firstChild, expression); - } - } - } - } - - /** - * Returns the reference expression where the given attributes - * fromNode and - * toNode matches an expression from the given - * referenceSearcher. - * - * @param fromNode the from attribute, text node. - * @param toNode the to attribute, text node. - * @param searchContext reference searcher which matches the from - * node. - * @param matchNode true if the test of the value of from and to - * attribute, text must be checked and false - * otherwise. - * @return the reference expression where the given attributes - * fromNode and - * toNode matches an expression from the given - * referenceSearcher. - */ - private static XMLReferenceExpression findExpressionWhichMatchesTo(DOMNode fromNode, DOMNode toNode, - XMLReferencesSearchContext searchContext, boolean matchNode) { - for (XMLReferenceExpression expression : searchContext.getExpressions()) { - if (expression.matchTo(toNode)) { - // The current expression can be applied for the to attribute. - if (!matchNode) { - // No need to match the attribute value - return expression; - } - // The expression is returned only if the from attribute value, text content is - // equals to to - // attribute value, text content. - String fromValue = getNodeValue(fromNode); - if (fromValue != null) { - String prefix = expression.getPrefix(); - if (prefix != null) { - if (!fromValue.startsWith(prefix)) { - continue; - } - fromValue = fromValue.substring(prefix.length(), fromValue.length()); - } - if (fromValue.equals(getNodeValue(toNode))) { - return expression; - } - } - } - } - return null; - } - - /** - * Returns the value of the given DOM node and null otherwise. - * - * @param node the DOM node. - * - * @return the value of the given DOM node and null otherwise. - */ - public static String getNodeValue(DOMNode node) { - if (node.isAttribute()) { - return ((DOMAttr) node).getValue(); - } - if (node.isText()) { - return ((DOMText) node).getData(); - } - return null; - } - - /** - * Returns the range of the given DOM node. - * - * @param node the DOM node. - * - * @return the range of the given DOM node. - */ - public static DOMRange getNodeRange(DOMNode node) { - if (node.isAttribute()) { - return ((DOMAttr) node).getNodeAttrValue(); - } - return node; - } - - /** - * Returns true if the given element is an include element (ex : xi:include) and - * false otherwise. - * - * @param element the DOM element. - * - * @return true if the given element is an include element (ex : xi:include) and - * false otherwise. - */ - private static boolean isInclude(Element element) { - return element != null && INCLUDE_TAG.equals(element.getLocalName()); - } - - private static String getFromName(String fromValue, String toNamespacePrefix) { - int index = fromValue.indexOf(":"); - if (index != -1) { - String prefix = fromValue.substring(0, index); - if (!Objects.equal(prefix, toNamespacePrefix)) { - return null; - } - return fromValue.substring(index + 1, fromValue.length()); - } - return fromValue; - } - - /** - * Returns all XML reference expressions where the given DOM node - * matches the - * from expression and null otherwise. - * - * @param node the DOM attribute. - * - * @param xmlReferencesSettings the XML references settings. - * @return all XML reference expressions where the given attribute matches the - * from - * expression and an empty list otherwise. - */ - public static XMLReferencesSearchContext findExpressionsWhichMatchFrom(DOMNode node, - XMLReferencesSettings xmlReferencesSettings) { - List allReferences = xmlReferencesSettings != null ? xmlReferencesSettings.getReferences() - : null; - if (allReferences == null) { - return null; - } - if (node == null || (!node.isAttribute() && !node.isText())) { - return null; - } - String uri = node.getOwnerDocument().getDocumentURI(); - List matchedExpressions = null; - for (XMLReferences references : allReferences) { - // Given this XML references sample - - /** - * - * "xml.references": [ - * // references for docbook.xml files - * { - * "pattern": "*.xml", - * "expressions": [ - * { - * "from": "xref/@linkend", - * "to": "@id" - * } - * ] - * } - *] - * - * - * - */ - if (references.matches(uri)) { - // here uri matches the "*.xml" pattern - List expressions = references.getExpressions(); - if (expressions != null) { - for (XMLReferenceExpression expression : expressions) { - // Given this XML reference expression sample - /** - * - * "expressions": [ - * { - * "from": "xref/@linkend", - * "to": "@id" - * } - * ] - * - * - * - */ - if (expression.matchFrom(node)) { - // here the attribute matches xref/@linkend - if (matchedExpressions == null) { - matchedExpressions = new ArrayList<>(); - } - matchedExpressions.add(expression); - } - } - } - } - } - if (matchedExpressions == null) { - return null; - } - return new XMLReferencesSearchContext(matchedExpressions, true); - } -} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/relaxng/grammar/rng/RNGRenameParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/relaxng/grammar/rng/RNGRenameParticipant.java index 7c6a016dd3..f8e9cb4619 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/relaxng/grammar/rng/RNGRenameParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/relaxng/grammar/rng/RNGRenameParticipant.java @@ -21,13 +21,18 @@ import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.dom.DOMRange; import org.eclipse.lemminx.extensions.relaxng.utils.RelaxNGUtils; +import org.eclipse.lemminx.services.extensions.IPositionRequest; +import org.eclipse.lemminx.services.extensions.IPrepareRenameRequest; import org.eclipse.lemminx.services.extensions.IRenameParticipant; import org.eclipse.lemminx.services.extensions.IRenameRequest; import org.eclipse.lemminx.utils.DOMUtils; import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.PrepareRenameResult; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; /** * RNG rename @@ -35,53 +40,50 @@ */ public class RNGRenameParticipant implements IRenameParticipant { - @Override - public void doRename(IRenameRequest request, List locations) { - DOMDocument xmlDocument = request.getXMLDocument(); + // --------------- Prepare rename - if (!DOMUtils.isRelaxNGXMLSyntax(xmlDocument)) { - return; + @Override + public Either prepareRename(IPrepareRenameRequest request, + CancelChecker cancelChecker) { + // RNG rename can be applied for: + // - define/@name + DOMAttr attr = findAttrToRename(request); + if (attr != null) { + Range range = XMLPositionUtility.selectAttributeValue(attr, true); + String placeholder = attr.getValue(); + return Either.forRight(new PrepareRenameResult(range, placeholder)); } - locations.addAll(getRenameTextEdits(request)); - + return null; } - private List getRenameTextEdits(IRenameRequest request) { - DOMDocument document = request.getXMLDocument(); - DOMNode node = request.getNode(); - - if (!node.isAttribute()) { - return Collections.emptyList(); - } + // --------------- Rename - DOMAttr attr = (DOMAttr) node; - DOMElement ownerElement = attr.getOwnerElement(); + @Override + public void doRename(IRenameRequest request, List locations, CancelChecker cancelChecker) { + locations.addAll(getRenameTextEdits(request, cancelChecker)); + } - if (ownerElement == null) { + private List getRenameTextEdits(IRenameRequest request, CancelChecker cancelChecker) { + // RNG rename can be applied for: + // - define/@name + DOMAttr attr = findAttrToRename(request); + if (attr == null) { return Collections.emptyList(); } - + DOMElement ownerElement = attr.getOwnerElement(); + DOMDocument document = request.getXMLDocument(); String newText = request.getNewText(); - - if (RelaxNGUtils.isDefine(ownerElement)) { - // locations = getReferenceLocations(ownerElement); - return renameAttributeValueTextEdits(document, attr, newText, locations); - } - } - - return Collections.emptyList(); + List locations = getReferenceLocations(ownerElement, cancelChecker); + return renameAttributeValueTextEdits(document, attr, newText, locations); } - private List getReferenceLocations(DOMNode node) { + private List getReferenceLocations(DOMNode node, CancelChecker cancelChecker) { List locations = new ArrayList<>(); RelaxNGUtils.searchRNGOriginAttributes(node, (origin, target) -> locations.add(XMLPositionUtility.createLocation(origin.getNodeAttrValue())), - null); + cancelChecker); return locations; } @@ -126,4 +128,35 @@ private void decreaseEndRange(Range range, int decrease) { range.getEnd().setCharacter(endCharacter - decrease); } + /** + * Returns the xsd:complexType/@name or xs:simpleType/@name to rename or null + * otherwise. + * + * @param request the position request. + * + * @return the xsd:complexType/@name or xs:simpleType/@name to rename or null + * otherwise. + */ + private static DOMAttr findAttrToRename(IPositionRequest request) { + DOMDocument xmlDocument = request.getXMLDocument(); + if (!DOMUtils.isRelaxNGXMLSyntax(xmlDocument)) { + return null; + } + DOMNode node = request.getNode(); + if (node == null || !node.isAttribute()) { + return null; + } + + DOMAttr attr = (DOMAttr) node; + DOMElement ownerElement = attr.getOwnerElement(); + if (ownerElement == null) { + return null; + } + if (RelaxNGUtils.isDefine(ownerElement)) { + if (attr.getName().equals("name")) { + return attr; + } + } + return null; + } } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/XSDRenameParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/XSDRenameParticipant.java index c66414ce65..ccb286e567 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/XSDRenameParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/XSDRenameParticipant.java @@ -22,14 +22,19 @@ import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.dom.DOMRange; import org.eclipse.lemminx.extensions.xsd.utils.XSDUtils; +import org.eclipse.lemminx.services.extensions.IPositionRequest; +import org.eclipse.lemminx.services.extensions.IPrepareRenameRequest; import org.eclipse.lemminx.services.extensions.IRenameParticipant; import org.eclipse.lemminx.services.extensions.IRenameRequest; import org.eclipse.lemminx.utils.DOMUtils; import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PrepareRenameResult; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; /** * XSD rename @@ -37,57 +42,58 @@ */ public class XSDRenameParticipant implements IRenameParticipant { - @Override - public void doRename(IRenameRequest request, List locations) { - DOMDocument xmlDocument = request.getXMLDocument(); + // --------------- Prepare rename - if (!DOMUtils.isXSD(xmlDocument)) { - return; + @Override + public Either prepareRename(IPrepareRenameRequest request, + CancelChecker cancelChecker) { + // XSD rename can be applied for: + // - xsd:complexType/@name + // - xs:simpleType/@name + DOMAttr attr = findAttrToRename(request); + if (attr != null) { + Range range = XMLPositionUtility.selectAttributeValue(attr, true); + String placeholder = attr.getValue(); + return Either.forRight(new PrepareRenameResult(range, placeholder)); } - locations.addAll(getRenameTextEdits(request)); - + return null; } - private List getRenameTextEdits(IRenameRequest request) { - DOMDocument document = request.getXMLDocument(); - DOMNode node = request.getNode(); + // --------------- Rename - if (!node.isAttribute()) { - return Collections.emptyList(); - } - - DOMAttr attr = (DOMAttr) node; - DOMElement ownerElement = attr.getOwnerElement(); + @Override + public void doRename(IRenameRequest request, List locations, CancelChecker cancelChecker) { + locations.addAll(getRenameTextEdits(request, cancelChecker)); + } - if (ownerElement == null) { + private List getRenameTextEdits(IRenameRequest request, CancelChecker cancelChecker) { + // XSD rename can be applied for: + // - xsd:complexType/@name + // - xs:simpleType/@name + DOMAttr attr = findAttrToRename(request); + if (attr == null) { return Collections.emptyList(); } - + DOMElement ownerElement = attr.getOwnerElement(); + DOMDocument document = request.getXMLDocument(); String newText = request.getNewText(); - - if (XSDUtils.isXSComplexType(ownerElement) || XSDUtils.isXSSimpleType(ownerElement)) { - - if (attr.getName().equals("name")) { - List locations = getReferenceLocations(ownerElement); - return renameAttributeValueTextEdits(document, attr, newText, locations); - } - } - - return Collections.emptyList(); + List locations = getReferenceLocations(ownerElement, cancelChecker); + return renameAttributeValueTextEdits(document, attr, newText, locations); } - private List getReferenceLocations(DOMNode node) { + private List getReferenceLocations(DOMNode node, CancelChecker cancelChecker) { List locations = new ArrayList<>(); XSDUtils.searchXSOriginAttributes(node, - (origin, target) -> locations.add(XMLPositionUtility.createLocation(origin.getNodeAttrValue())), - null); + (origin, target) -> locations.add(XMLPositionUtility.createLocation(origin.getNodeAttrValue())), + cancelChecker); return locations; } - private List renameAttributeValueTextEdits(DOMDocument document, DOMAttr attribute, String newText, List locations) { + private List renameAttributeValueTextEdits(DOMDocument document, DOMAttr attribute, String newText, + List locations) { DOMRange attrValue = attribute.getNodeAttrValue(); List textEdits = new ArrayList<>(); @@ -100,7 +106,7 @@ private List renameAttributeValueTextEdits(DOMDocument document, DOMAt textEdits.add(new TextEdit(range, newText)); - for (Location location: locations) { + for (Location location : locations) { Range textEditRange = location.getRange(); reduceRangeFromBothEnds(textEditRange, 1); @@ -110,9 +116,9 @@ private List renameAttributeValueTextEdits(DOMDocument document, DOMAt } catch (BadLocationException e1) { return Collections.emptyList(); } - + int colonIndex = oldAttrValue.indexOf(":"); - + if (colonIndex > 0) { increaseStartRange(textEditRange, colonIndex + 1); } @@ -143,5 +149,36 @@ private String getAttrTextValueFromPosition(DOMDocument document, Position posit int offset = document.offsetAt(position); return document.findAttrAt(offset).getValue(); } - -} \ No newline at end of file + + /** + * Returns the xsd:complexType/@name or xs:simpleType/@name to rename or null + * otherwise. + * + * @param request the position request. + * + * @return the xsd:complexType/@name or xs:simpleType/@name to rename or null + * otherwise. + */ + private static DOMAttr findAttrToRename(IPositionRequest request) { + DOMDocument xmlDocument = request.getXMLDocument(); + if (!DOMUtils.isXSD(xmlDocument)) { + return null; + } + DOMNode node = request.getNode(); + if (node == null || !node.isAttribute()) { + return null; + } + + DOMAttr attr = (DOMAttr) node; + DOMElement ownerElement = attr.getOwnerElement(); + if (ownerElement == null) { + return null; + } + if (XSDUtils.isXSComplexType(ownerElement) || XSDUtils.isXSSimpleType(ownerElement)) { + if (attr.getName().equals("name")) { + return attr; + } + } + return null; + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/LinkedEditingRangesRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/LinkedEditingRangesRequest.java new file mode 100644 index 0000000000..319f61430a --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/LinkedEditingRangesRequest.java @@ -0,0 +1,31 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.services.extensions.ILinkedEditingRangesRequest; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lsp4j.Position; + +/** + * Linked editing ranges request implementation. + * + */ +class LinkedEditingRangesRequest extends AbstractPositionRequest implements ILinkedEditingRangesRequest { + + public LinkedEditingRangesRequest(DOMDocument xmlDocument, Position position, + XMLExtensionsRegistry extensionsRegistry) throws BadLocationException { + super(xmlDocument, position, extensionsRegistry); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/PrepareRenameRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/PrepareRenameRequest.java new file mode 100644 index 0000000000..1b62bdbb94 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/PrepareRenameRequest.java @@ -0,0 +1,25 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.services.extensions.IPrepareRenameRequest; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lsp4j.Position; + +class PrepareRenameRequest extends AbstractPositionRequest implements IPrepareRenameRequest { + + public PrepareRenameRequest(DOMDocument document, Position position, XMLExtensionsRegistry extensionsRegistry) throws BadLocationException { + super(document, position, extensionsRegistry); + } +} \ No newline at end of file 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 93042080c8..1e62fb6391 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 @@ -52,6 +52,7 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PrepareRenameResult; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ReferenceContext; @@ -60,6 +61,7 @@ import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; /** * XML Language service. @@ -88,6 +90,7 @@ public void checkCanceled() { private final XMLReference reference; private final XMLCodeLens codelens; private final XMLCodeActions codeActions; + private final XMLPrepareRename prepareRename; private final XMLRename rename; private final XMLSelectionRanges selectionRanges; private final XMLLinkedEditing linkedEditing; @@ -108,9 +111,10 @@ public XMLLanguageService() { this.reference = new XMLReference(this); this.codelens = new XMLCodeLens(this); this.codeActions = new XMLCodeActions(this); + this.prepareRename = new XMLPrepareRename(this); this.rename = new XMLRename(this); this.selectionRanges = new XMLSelectionRanges(); - this.linkedEditing = new XMLLinkedEditing(); + this.linkedEditing = new XMLLinkedEditing(this); } @Override @@ -232,8 +236,14 @@ public List getSelectionRanges(DOMDocument xmlDocument, List prepareRename(DOMDocument xmlDocument, Position position, + CancelChecker cancelChecker) { + return prepareRename.prepareRename(xmlDocument, position, cancelChecker); + } + + public WorkspaceEdit doRename(DOMDocument xmlDocument, Position position, String newText, + CancelChecker cancelChecker) { + return rename.doRename(xmlDocument, position, newText, cancelChecker); } public List findDocumentLinks(DOMDocument document) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLinkedEditing.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLinkedEditing.java index 32a8f05d3f..d3105bd667 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLinkedEditing.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLLinkedEditing.java @@ -11,6 +11,7 @@ *******************************************************************************/ package org.eclipse.lemminx.services; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; @@ -20,6 +21,9 @@ import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.ILinkedEditingRangesParticipant; +import org.eclipse.lemminx.services.extensions.ILinkedEditingRangesRequest; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.LinkedEditingRanges; import org.eclipse.lsp4j.Position; @@ -33,7 +37,13 @@ class XMLLinkedEditing { private static Logger LOGGER = Logger.getLogger(XMLLinkedEditing.class.getName()); - + + private final XMLExtensionsRegistry extensionsRegistry; + + public XMLLinkedEditing(XMLExtensionsRegistry extensionsRegistry) { + this.extensionsRegistry = extensionsRegistry; + } + /** * Returns the linked editing ranges for the given xmlDocument at * the given position and null otherwise. @@ -48,25 +58,40 @@ public LinkedEditingRanges findLinkedEditingRanges(DOMDocument document, Positio CancelChecker cancelChecker) { try { cancelChecker.checkCanceled(); - - int offset = document.offsetAt(position); - DOMNode node = document.findNodeAt(offset); - if (node == null || !node.isElement()) { + + ILinkedEditingRangesRequest request = new LinkedEditingRangesRequest(document, position, + extensionsRegistry); + DOMNode node = request.getNode(); + if (node == null) { return null; } - DOMElement element = (DOMElement) node; - if (element.isOrphanEndTag() || !element.hasEndTag()) { - return null; + + final List ranges = new ArrayList<>(); + for (ILinkedEditingRangesParticipant participant : extensionsRegistry + .getLinkedEditingRangesParticipants()) { + try { + participant.findLinkedEditingRanges(request, ranges, cancelChecker); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, + "Error while processing linked editing ranges for the participant '" + + participant.getClass().getName() + "'.", + e); + } } - if (element.isInStartTag(offset) || element.isInEndTag(offset, true)) { - List ranges = Arrays.asList(XMLPositionUtility.selectStartTagName(element), - XMLPositionUtility.selectEndTagName(element)); - - cancelChecker.checkCanceled(); - - return new LinkedEditingRanges(ranges); + if (node.isElement()) { + DOMElement element = (DOMElement) node; + if (!(element.isOrphanEndTag() || !element.hasEndTag())) { + int offset = request.getOffset(); + if (element.isInStartTag(offset) || element.isInEndTag(offset, true)) { + ranges.addAll(Arrays.asList(XMLPositionUtility.selectStartTagName(element), + XMLPositionUtility.selectEndTagName(element))); + + } + } } + cancelChecker.checkCanceled(); + return !ranges.isEmpty() ? new LinkedEditingRanges(ranges) : null; } catch (BadLocationException e) { LOGGER.log(Level.SEVERE, "In XMLLinkedEditing, position error", e); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLPrepareRename.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLPrepareRename.java new file mode 100644 index 0000000000..ab2d05a337 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLPrepareRename.java @@ -0,0 +1,91 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services; + +import java.util.concurrent.CancellationException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.IPrepareRenameRequest; +import org.eclipse.lemminx.services.extensions.IRenameParticipant; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +/** + * XML prepare rename support. + * + */ +public class XMLPrepareRename { + + private static final Logger LOGGER = Logger.getLogger(XMLPrepareRename.class.getName()); + + private final XMLExtensionsRegistry extensionsRegistry; + + public XMLPrepareRename(XMLExtensionsRegistry extensionsRegistry) { + this.extensionsRegistry = extensionsRegistry; + } + + public Either prepareRename(DOMDocument xmlDocument, Position position, + CancelChecker cancelChecker) { + + IPrepareRenameRequest request = null; + + try { + request = new PrepareRenameRequest(xmlDocument, position, extensionsRegistry); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "Failed creating prperare rename request", e); + return null; + } + + for (IRenameParticipant participant : extensionsRegistry.getRenameParticipants()) { + try { + Either result = participant.prepareRename(request, cancelChecker); + if (result != null) { + return result; + } + } catch (CancellationException e) { + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, + "Error while processing prepare rename for the participant '" + participant.getClass().getName() + + "'.", + e); + } + } + + DOMNode node = request.getNode(); + if (node == null) { + return null; + } + int offset = request.getOffset(); + if (node.isElement()) { + // By default rename can be applied to rename tag name + DOMElement element = (DOMElement) node; + if (element.isInStartTag(offset)) { + return Either.forLeft(XMLPositionUtility.selectStartTagName(element)); + } + if (element.isInEndTag(offset)) { + return Either.forLeft(XMLPositionUtility.selectEndTagName(element)); + } + } + return null; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLRename.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLRename.java index bd13a6b962..8b01877963 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLRename.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLRename.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.logging.Level; import java.util.logging.Logger; @@ -39,6 +40,7 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; /** * Handle all rename requests @@ -55,8 +57,11 @@ public XMLRename(XMLExtensionsRegistry extensionsRegistry) { this.extensionsRegistry = extensionsRegistry; } - public WorkspaceEdit doRename(DOMDocument xmlDocument, Position position, String newText) { + public WorkspaceEdit doRename(DOMDocument xmlDocument, Position position, String newText, + CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); + RenameRequest renameRequest = null; try { @@ -68,7 +73,7 @@ public WorkspaceEdit doRename(DOMDocument xmlDocument, Position position, String DOMNode node = renameRequest.getNode(); - if (node == null || (!node.isAttribute() && !node.isElement()) + if (node == null || (!node.isAttribute() && !node.isElement() && !node.isText()) || (node.isElement() && !((DOMElement) node).hasTagName())) { return createWorkspaceEdit(xmlDocument.getDocumentURI(), Collections.emptyList()); @@ -78,7 +83,9 @@ public WorkspaceEdit doRename(DOMDocument xmlDocument, Position position, String for (IRenameParticipant participant : extensionsRegistry.getRenameParticipants()) { try { - participant.doRename(renameRequest, textEdits); + participant.doRename(renameRequest, textEdits, cancelChecker); + } catch (CancellationException e) { + throw e; } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error while processing rename for the participant '" + participant.getClass().getName() + "'.", @@ -88,16 +95,17 @@ public WorkspaceEdit doRename(DOMDocument xmlDocument, Position position, String textEdits.addAll(getRenameTextEdits(xmlDocument, node, position, newText)); + cancelChecker.checkCanceled(); + return createWorkspaceEdit(xmlDocument.getDocumentURI(), textEdits); } private List getRenameTextEdits(DOMDocument xmlDocument, DOMNode node, Position position, String newText) { - - DOMElement element = getAssociatedElement(node); - if (node == null) { + if (node == null || node.isText()) { return Collections.emptyList(); } + DOMElement element = getAssociatedElement(node); if (node.isCDATA()) { return getCDATARenameTextEdits(xmlDocument, element, position, newText); @@ -121,7 +129,7 @@ private List getRenameTextEdits(DOMDocument xmlDocument, DOMNode node, * @return associated DOMElement */ private DOMElement getAssociatedElement(DOMNode node) { - if (node == null || (!node.isElement() && !node.isAttribute())) { + if (!node.isElement() && !node.isAttribute()) { return null; } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesParticipant.java new file mode 100644 index 0000000000..838a4c1525 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesParticipant.java @@ -0,0 +1,37 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.extensions; + +import java.util.List; + +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * Linked editing ranges participant API. + * + * @author Angelo ZERR + * + */ +public interface ILinkedEditingRangesParticipant { + + /** + * Find linked editing ranges for DOM document and position defined in the given + * request. + * + * @param request the linked editing ranges request. + * @param ranges the ranges to update. + * @param cancelChecker the cancel checker. + */ + void findLinkedEditingRanges(ILinkedEditingRangesRequest request, List ranges, CancelChecker cancelChecker); + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesRequest.java new file mode 100644 index 0000000000..49e08cfa2d --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ILinkedEditingRangesRequest.java @@ -0,0 +1,19 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.extensions; + +/** + * Linked editing ranges request API. + * + */ +public interface ILinkedEditingRangesRequest extends IPositionRequest { +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IPrepareRenameRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IPrepareRenameRequest.java new file mode 100644 index 0000000000..44060f3f17 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IPrepareRenameRequest.java @@ -0,0 +1,20 @@ +/******************************************************************************* +* Copyright (c) 2019 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.extensions; + +/** + * Prepare rename request API. + * + */ +public interface IPrepareRenameRequest extends IPositionRequest { + +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameParticipant.java index dc7a70dd0a..c213713b4b 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameParticipant.java @@ -13,7 +13,11 @@ import java.util.List; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; /** * Rename participant API. @@ -21,5 +25,7 @@ */ public interface IRenameParticipant { - void doRename(IRenameRequest request, List locations); + void doRename(IRenameRequest request, List locations, CancelChecker cancelChecker); + + Either prepareRename(IPrepareRenameRequest request, CancelChecker cancelChecker); } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameRequest.java index 38a1a98292..a22e1370d1 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameRequest.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IRenameRequest.java @@ -12,7 +12,7 @@ package org.eclipse.lemminx.services.extensions; /** - * Definition request API. + * Rename request API. * */ public interface IRenameRequest extends IPositionRequest { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/XMLExtensionsRegistry.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/XMLExtensionsRegistry.java index ea4c93b57f..0f74f7b056 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/XMLExtensionsRegistry.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/XMLExtensionsRegistry.java @@ -60,6 +60,7 @@ public class XMLExtensionsRegistry implements IComponentProvider { private final List codeLensParticipants; private final List highlightingParticipants; private final List renameParticipants; + private final List linkedEditingRangesParticipants; private final List formatterParticipants; private final List symbolsProviderParticipants; private final List workspaceServiceParticipants; @@ -94,6 +95,7 @@ public XMLExtensionsRegistry() { codeLensParticipants = new ArrayList<>(); highlightingParticipants = new ArrayList<>(); renameParticipants = new ArrayList<>(); + linkedEditingRangesParticipants = new ArrayList<>(); formatterParticipants = new ArrayList<>(); symbolsProviderParticipants = new ArrayList<>(); workspaceServiceParticipants = new ArrayList<>(); @@ -205,6 +207,11 @@ public Collection getRenameParticipants() { return renameParticipants; } + public List getLinkedEditingRangesParticipants() { + initializeIfNeeded(); + return linkedEditingRangesParticipants; + } + public Collection getFormatterParticipants() { initializeIfNeeded(); return formatterParticipants; @@ -394,6 +401,16 @@ public void unregisterRenameParticipant(IRenameParticipant renameParticipant) { renameParticipants.remove(renameParticipant); } + public void registerLinkedEditingRangesParticipants( + ILinkedEditingRangesParticipant linkedEditingRangesParticipant) { + linkedEditingRangesParticipants.add(linkedEditingRangesParticipant); + } + + public void unregisterLinkedEditingRangesParticipants( + ILinkedEditingRangesParticipant linkedEditingRangesParticipant) { + linkedEditingRangesParticipants.remove(linkedEditingRangesParticipant); + } + public void registerFormatterParticipant(IFormatterParticipant formatterParticipant) { formatterParticipants.add(formatterParticipant); } 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 332720ab2c..d80d8bccd1 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 @@ -20,6 +20,7 @@ import org.eclipse.lsp4j.ColorProviderOptions; import org.eclipse.lsp4j.CompletionOptions; import org.eclipse.lsp4j.DocumentLinkOptions; +import org.eclipse.lsp4j.RenameOptions; import org.eclipse.lsp4j.TextDocumentSyncKind; /** @@ -82,9 +83,10 @@ private ServerCapabilitiesConstants() { public static final String LINKED_EDITING_RANGE_ID = UUID.randomUUID().toString(); public static final CompletionOptions DEFAULT_COMPLETION_OPTIONS = new CompletionOptions(true, - Arrays.asList(".", ":", "<", "\"", "=", "/", "\\", "?", "\'", "&")); + Arrays.asList(".", ":", "<", "\"", "=", "/", "\\", "?", "\'", "&", "#")); public static final TextDocumentSyncKind DEFAULT_SYNC_OPTION = TextDocumentSyncKind.Full; public static final DocumentLinkOptions DEFAULT_LINK_OPTIONS = new DocumentLinkOptions(true); + public static final RenameOptions DEFAULT_RENAME_OPTIONS = new RenameOptions(true); public static final ColorProviderOptions DEFAULT_COLOR_OPTIONS = new ColorProviderOptions(); public static final CodeLensOptions DEFAULT_CODELENS_OPTIONS = new CodeLensOptions(); public static final CodeActionOptions DEFAULT_CODEACTION_OPTIONS = createDefaultCodeActionOptions(); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/XMLCapabilityManager.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/XMLCapabilityManager.java index 99acc25da6..3a37648eed 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/XMLCapabilityManager.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/XMLCapabilityManager.java @@ -20,6 +20,7 @@ import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DEFAULT_COLOR_OPTIONS; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DEFAULT_COMPLETION_OPTIONS; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DEFAULT_LINK_OPTIONS; +import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DEFAULT_RENAME_OPTIONS; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DEFINITION_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DOCUMENT_HIGHLIGHT_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.DOCUMENT_SYMBOL_ID; @@ -170,7 +171,7 @@ public void initializeCapabilities() { registerCapability(COLOR_ID, TEXT_DOCUMENT_COLOR, DEFAULT_COLOR_OPTIONS); } if (this.getClientCapabilities().isRenameDynamicRegistrationSupported()) { - registerCapability(RENAME_ID, TEXT_DOCUMENT_RENAME); + registerCapability(RENAME_ID, TEXT_DOCUMENT_RENAME, DEFAULT_RENAME_OPTIONS); } if (this.getClientCapabilities().isDefinitionDynamicRegistered()) { registerCapability(DEFINITION_ID, TEXT_DOCUMENT_DEFINITION); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java index e97745b1bc..d4c174548c 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/StringUtils.java @@ -70,11 +70,11 @@ public static boolean isWhitespace(String value) { * * @param value The string to check * @return true if any of the below hold, and false otherwise: - *
    - *
  • The String is null
  • - *
  • The String is of length 0
  • - *
  • The String contains only whitespace characters
  • - *
+ *
    + *
  • The String is null
  • + *
  • The String is of length 0
  • + *
  • The String contains only whitespace characters
  • + *
*/ public static boolean isBlank(String value) { return isEmpty(value) || isWhitespace(value); @@ -422,15 +422,19 @@ public static String getString(Object obj) { * and -1 if no word. */ public static int findStartWord(String text, int offset, Predicate isValidChar) { + return findStartWord(text, offset, 0, isValidChar); + } + + public static int findStartWord(String text, int offset, int min, Predicate isValidChar) { if (offset < 0 || offset >= text.length() || !isValidChar.test(text.charAt(offset))) { return -1; } - for (int i = offset - 1; i >= 0; i--) { + for (int i = offset - 1; i >= min; i--) { if (!isValidChar.test(text.charAt(i))) { return i + 1; } } - return 0; + return min; } /** @@ -444,16 +448,21 @@ public static int findStartWord(String text, int offset, Predicate is * @return the start word offset from the right of the given offset * and -1 if no word. */ + public static int findEndWord(String text, int offset, Predicate isValidChar) { + return findEndWord(text, offset, text.length(), isValidChar); + } + + public static int findEndWord(String text, int offset, int max, Predicate isValidChar) { if (offset < 0 || offset >= text.length() || !isValidChar.test(text.charAt(offset))) { return -1; } - for (int i = offset + 1; i < text.length(); i++) { + for (int i = offset + 1; i < max; i++) { if (!isValidChar.test(text.charAt(i))) { return i; } } - return -1; + return max; } /** @@ -498,7 +507,7 @@ public static boolean isQuoted(String value) { * * @param reference the string being compared to * @param current the string compared - * @return true if the two strings are similar, false otherwise + * @return true if the two strings are similar, false otherwise */ public static boolean isSimilar(String reference, String current) { int threshold = Math.round(MAX_DISTANCE_DIFF_RATIO * reference.length()); 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 37de6e37df..e512257732 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 @@ -745,6 +745,10 @@ public static Diagnostic d(int startLine, int startCharacter, int endLine, int e code != null ? code.getCode() : null); } + public static Range r(int line, int startCharacter, int endCharacter) { + return r(line, startCharacter, line, endCharacter); + } + public static Range r(int startLine, int startCharacter, int endLine, int endCharacter) { return new Range(new Position(startLine, startCharacter), new Position(endLine, endCharacter)); } @@ -1443,9 +1447,9 @@ public static void testReferencesFor(XMLLanguageService xmlLanguageService, Stri xmlLanguageService = new XMLLanguageService(); } - ContentModelSettings settings = new ContentModelSettings(); - settings.setUseCache(false); - xmlLanguageService.doSave(new SettingsSaveContext(settings)); +// ContentModelSettings settings = new ContentModelSettings(); +// settings.setUseCache(false); +// xmlLanguageService.doSave(new SettingsSaveContext(settings)); DOMDocument xmlDocument = DOMParser.getInstance().parse(document, xmlLanguageService.getResolverExtensionManager()); @@ -1691,18 +1695,26 @@ public static void assertRename(String value, String newText, List exp public static void assertRename(XMLLanguageService languageService, String value, String newText, List expectedEdits) throws BadLocationException { + assertRename(languageService, value, null, newText, expectedEdits); + } + + public static void assertRename(XMLLanguageService languageService, String value, String fileURI, String newText, + List expectedEdits) throws BadLocationException { int offset = value.indexOf("|"); value = value.substring(0, offset) + value.substring(offset + 1); - DOMDocument document = DOMParser.getInstance().parse(value, "test://test/test.html", null); + fileURI = fileURI != null ? fileURI : "test://test/test.html"; + DOMDocument document = DOMParser.getInstance().parse(value, fileURI, + null); Position position = document.positionAt(offset); if (languageService == null) { languageService = new XMLLanguageService(); } - WorkspaceEdit workspaceEdit = languageService.doRename(document, position, newText); - List actualEdits = workspaceEdit.getChanges().get("test://test/test.html"); + WorkspaceEdit workspaceEdit = languageService.doRename(document, position, newText, () -> { + }); + List actualEdits = workspaceEdit.getChanges().get(fileURI); assertArrayEquals(expectedEdits.toArray(), actualEdits.toArray()); } @@ -1714,18 +1726,18 @@ public static void testLinkedEditingFor(String xml, LinkedEditingRanges expected public static void testLinkedEditingFor(String value, String fileURI, LinkedEditingRanges expected) throws BadLocationException { + testLinkedEditingFor(new XMLLanguageService(), value, fileURI, expected); + } + + public static void testLinkedEditingFor(XMLLanguageService xmlLanguageService, String value, String fileURI, + LinkedEditingRanges expected) + throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); TextDocument document = new TextDocument(value, fileURI != null ? fileURI : "test://test/test.xml"); Position position = document.positionAt(offset); - XMLLanguageService xmlLanguageService = new XMLLanguageService(); - - ContentModelSettings settings = new ContentModelSettings(); - settings.setUseCache(false); - xmlLanguageService.doSave(new SettingsSaveContext(settings)); - DOMDocument xmlDocument = DOMParser.getInstance().parse(document, xmlLanguageService.getResolverExtensionManager()); xmlLanguageService.setDocumentProvider((uri) -> xmlDocument); diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCodeLensExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCodeLensExtensionsTest.java new file mode 100644 index 0000000000..afe5eaf5cd --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCodeLensExtensionsTest.java @@ -0,0 +1,125 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references; + +import static org.eclipse.lemminx.XMLAssert.cl; +import static org.eclipse.lemminx.XMLAssert.r; +import static org.eclipse.lemminx.client.ClientCommands.SHOW_REFERENCES; + +import java.util.Arrays; +import java.util.function.Consumer; + +import org.eclipse.lemminx.AbstractCacheBasedTest; +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; +import org.eclipse.lemminx.client.CodeLensKind; +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lsp4j.CodeLens; +import org.junit.jupiter.api.Test; + +/** + * XML references codelens tests + * + */ +public class XMLReferencesCodeLensExtensionsTest extends AbstractCacheBasedTest { + + @Test + public void tei() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + "
\r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + "
\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" + + "

Some text here.

\r\n" + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + testCodeLensFor(xml, "file:///test/tei.xml", // + cl(r(16, 10, 16, 26), "1 reference", SHOW_REFERENCES)); + } + + @Test + public void teiTargetMulti() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + // [2 references] + + "

\r\n" + // [1 reference] + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCodeLensFor(xml, "file:///test/tei.xml", // + cl(r(5, 6, 5, 16), "1 reference", SHOW_REFERENCES), // + cl(r(6, 6, 6, 16), "2 references", SHOW_REFERENCES)); + } + + @Test + public void web() throws BadLocationException { + // completion on <| + String xml = "\r\n" + + " \r\n" + + " comingsoon\r\n" + + " mysite.server.ComingSoonServlet\r\n" + + " \r\n" + + " \r\n" + + " comingsoon\r\n" + + " /*\r\n" + + " \r\n" + + "\r\n" + + ""; + testCodeLensFor(xml, "file:///test/web.xml", // + cl(r(4, 18, 4, 28), "1 reference", SHOW_REFERENCES)); + } + + private static void testCodeLensFor(String value, String fileURI, CodeLens... expected) { + XMLLanguageService xmlLanguageService = new XMLLanguageService(); + xmlLanguageService.getExtensions(); + Consumer customConfiguration = ls -> { + ls.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); + }; + XMLAssert.testCodeLensFor(value, fileURI, xmlLanguageService, Arrays.asList(CodeLensKind.References), + customConfiguration, expected); + } + +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCompletionExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCompletionExtensionsTest.java index 161e494ddf..ec14aba69d 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCompletionExtensionsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesCompletionExtensionsTest.java @@ -14,17 +14,10 @@ import static org.eclipse.lemminx.XMLAssert.c; import static org.eclipse.lemminx.XMLAssert.te; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import org.eclipse.lemminx.XMLAssert; import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.extensions.contentmodel.BaseFileTempTest; -import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; -import org.eclipse.lemminx.extensions.references.settings.XMLReferences; -import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lsp4j.CompletionItem; import org.junit.jupiter.api.Test; @@ -36,7 +29,7 @@ public class XMLReferencesCompletionExtensionsTest extends BaseFileTempTest { @Test - public void tei() throws BadLocationException { + public void teiCorresp() throws BadLocationException { // completion on <| String xml = "\r\n" // + "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 16, 8, 16, "#A"), "#A"), // + c("#B", te(8, 16, 8, 16, "#B"), "#B")); + } + + @Test + public void teiTargetMulti1() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 16, 8, 18, "#A"), "#A"), // + c("#B", te(8, 16, 8, 18, "#B"), "#B")); + } + + @Test + public void teiTargetMulti2() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 16, 8, 18, "#A"), "#A"), // + c("#B", te(8, 16, 8, 18, "#B"), "#B")); + } + + @Test + public void teiTargetMulti3() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 16, 8, 18, "#A"), "#A"), // + c("#B", te(8, 16, 8, 18, "#B"), "#B")); + } + + @Test + public void teiTargetMulti4() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 16, 8, 19, "#A"), "#A"), // + c("#B", te(8, 16, 8, 19, "#B"), "#B")); + } + + @Test + public void teiTargetMulti5() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 16, 8, 18, "#A"), "#A"), // + c("#B", te(8, 16, 8, 18, "#B"), "#B")); + } + + @Test + public void teiTargetMulti6() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 19, 8, 21, "#A"), "#A"), // + c("#B", te(8, 19, 8, 21, "#B"), "#B")); + } + + @Test + public void teiTargetMulti7() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 19, 8, 21, "#A"), "#A"), // + c("#B", te(8, 19, 8, 21, "#B"), "#B")); + } + + @Test + public void teiTargetMulti8() throws BadLocationException { + // completion on <| + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testCompletionFor(xml, "file:///test/tei.xml", // + 2, // + c("#A", te(8, 19, 8, 19, "#A"), "#A"), // + c("#B", te(8, 19, 8, 19, "#B"), "#B")); + } + @Test public void docbook() throws BadLocationException { // completion on <| @@ -98,7 +307,8 @@ public void web() throws BadLocationException { // completion on <| String xml = "\r\n" + " \r\n" + " comingsoon\r\n" @@ -120,7 +330,8 @@ public void webInEmptyText() throws BadLocationException { // completion on <| String xml = "\r\n" + " \r\n" + " comingsoon\r\n" @@ -136,64 +347,14 @@ public void webInEmptyText() throws BadLocationException { 1 + XMLAssert.CDATA_SNIPPETS + XMLAssert.COMMENT_SNIPPETS, // c("comingsoon", te(8, 18, 8, 18, "comingsoon"), "comingsoon")); } + private static void testCompletionFor(String value, String fileURI, Integer expectedCount, CompletionItem... expectedItems) throws BadLocationException { XMLAssert.testCompletionFor(new XMLLanguageService(), value, null, ls -> { - - XMLReferencesSettings referencesSettings = new XMLReferencesSettings(); - referencesSettings.setReferences(createReferences()); - ls.doSave(new SettingsSaveContext(referencesSettings)); + ls.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); }, fileURI, expectedCount, true, expectedItems); } - - private static List createReferences() { - List references = new ArrayList<>(); - /* - * { - * "prefix": "#", - * "from": "@corresp", - * "to": "@xml:id" - * } - */ - XMLReferences tei = new XMLReferences(); - tei.setPattern("**/*tei.xml"); - XMLReferenceExpression corresp = new XMLReferenceExpression(); - corresp.setPrefix("#"); - corresp.setFrom("@corresp"); - corresp.setTo("@xml:id"); - tei.setExpressions(Arrays.asList(corresp)); - references.add(tei); - /* - * { - * "from": "xref/@linkend", - * "to": "@id" - * } - */ - XMLReferences docbook = new XMLReferences(); - docbook.setPattern("**/*docbook.xml"); - XMLReferenceExpression linkend = new XMLReferenceExpression(); - linkend.setFrom("xref/@linkend"); - linkend.setTo("@id"); - docbook.setExpressions(Arrays.asList(linkend)); - references.add(docbook); - - /* - * { - * "from": "servlet-mapping/servlet-name/text()", - * "to": "servlet/servlet-name/text()" - * } - */ - XMLReferences web = new XMLReferences(); - web.setPattern("**/web.xml"); - XMLReferenceExpression servletName = new XMLReferenceExpression(); - servletName.setFrom("servlet-mapping/servlet-name/text()"); - servletName.setTo("servlet/servlet-name/text()"); - web.setExpressions(Arrays.asList(servletName)); - references.add(web); - - return references; - } } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesDefinitionExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesDefinitionExtensionsTest.java index 43fd3bb1df..6317e32b2b 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesDefinitionExtensionsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesDefinitionExtensionsTest.java @@ -14,17 +14,10 @@ import static org.eclipse.lemminx.XMLAssert.ll; import static org.eclipse.lemminx.XMLAssert.r; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import org.eclipse.lemminx.AbstractCacheBasedTest; import org.eclipse.lemminx.XMLAssert; import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; import org.eclipse.lemminx.commons.BadLocationException; -import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; -import org.eclipse.lemminx.extensions.references.settings.XMLReferences; -import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lsp4j.LocationLink; import org.junit.jupiter.api.Test; @@ -65,9 +58,51 @@ public void tei() throws BadLocationException { + " \r\n" + ""; testDefinitionFor(xml, "file:///test/tei.xml", - ll("file:///test/tei.xml", r(18, 22, 18, 32), r(16, 17, 16, 26))); + ll("file:///test/tei.xml", r(18, 23, 18, 31), r(16, 18, 16, 25))); + } + + @Test + public void teiTargetMulti1() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testDefinitionFor(xml, "file:///test/tei.xml", + ll("file:///test/tei.xml", r(8, 16, 8, 18), r(5, 14, 5, 15))); } + @Test + public void teiTargetMulti2() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + testDefinitionFor(xml, "file:///test/tei.xml", + ll("file:///test/tei.xml", r(8, 19, 8, 21), r(6, 14, 6, 15))); + } + @Test public void docbook() throws BadLocationException { String xml = "\r\n" @@ -85,7 +120,7 @@ public void docbook() throws BadLocationException { + " \r\n" + ""; testDefinitionFor(xml, "file:///test/docbook.xml", - ll("file:///test/docbook.xml", r(4, 22, 4, 33), r(2, 16, 2, 27))); + ll("file:///test/docbook.xml", r(4, 23, 4, 32), r(2, 17, 2, 26))); } @Test @@ -175,60 +210,11 @@ public void noDefinitionWithTEIAndNotHash() throws BadLocationException { private static void testDefinitionFor(String xml, String fileURI, LocationLink... expectedItems) throws BadLocationException { - XMLReferencesSettings referencesSettings = new XMLReferencesSettings(); - referencesSettings.setReferences(createReferences()); XMLLanguageService xmlLanguageService = new XMLLanguageService(); xmlLanguageService.getExtensions(); - xmlLanguageService.doSave(new SettingsSaveContext(referencesSettings)); + xmlLanguageService.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); XMLAssert.testDefinitionFor(xmlLanguageService, xml, fileURI, expectedItems); } - private static List createReferences() { - List references = new ArrayList<>(); - /* - * { - * "prefix": "#", - * "from": "@corresp", - * "to": "@xml:id" - * } - */ - XMLReferences tei = new XMLReferences(); - tei.setPattern("**/*tei.xml"); - XMLReferenceExpression corresp = new XMLReferenceExpression(); - corresp.setPrefix("#"); - corresp.setFrom("@corresp"); - corresp.setTo("@xml:id"); - tei.setExpressions(Arrays.asList(corresp)); - references.add(tei); - /* - * { - * "from": "xref/@linkend", - * "to": "@id" - * } - */ - XMLReferences docbook = new XMLReferences(); - docbook.setPattern("**/*docbook.xml"); - XMLReferenceExpression linkend = new XMLReferenceExpression(); - linkend.setFrom("xref/@linkend"); - linkend.setTo("@id"); - docbook.setExpressions(Arrays.asList(linkend)); - references.add(docbook); - - /* - * { - * "from": "servlet-mapping/servlet-name/text()", - * "to": "servlet/servlet-name/text()" - * } - */ - XMLReferences web = new XMLReferences(); - web.setPattern("**/web.xml"); - XMLReferenceExpression servletName = new XMLReferenceExpression(); - servletName.setFrom("servlet-mapping/servlet-name/text()"); - servletName.setTo("servlet/servlet-name/text()"); - web.setExpressions(Arrays.asList(servletName)); - references.add(web); - - return references; - } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesHighlightingExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesHighlightingExtensionsTest.java index f63b361584..4121ad6cf3 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesHighlightingExtensionsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesHighlightingExtensionsTest.java @@ -16,17 +16,10 @@ import static org.eclipse.lsp4j.DocumentHighlightKind.Read; import static org.eclipse.lsp4j.DocumentHighlightKind.Write; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import org.eclipse.lemminx.AbstractCacheBasedTest; import org.eclipse.lemminx.XMLAssert; import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; import org.eclipse.lemminx.commons.BadLocationException; -import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; -import org.eclipse.lemminx.extensions.references.settings.XMLReferences; -import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lsp4j.DocumentHighlight; import org.junit.jupiter.api.Test; @@ -39,7 +32,7 @@ public class XMLReferencesHighlightingExtensionsTest extends AbstractCacheBasedTest { @Test - public void tei() throws BadLocationException { + public void teiOnCorresp() throws BadLocationException { String xml = "\r\n" // + "Some text here.

\r\n" + " \r\n" // <-- // highlighting here should highlight // body/@xml-id="body-id" + + " \r\n" + " \r\n" + "
\r\n" + "
"; testHighlightsFor(xml, "file:///test/tei.xml", // - hl(r(18, 22, 18, 32), Read), hl(r(16, 17, 16, 26), Write)); + hl(r(18, 23, 18, 31), Read), hl(r(16, 18, 16, 25), Write)); + } + + @Test + public void teiOnXMLId() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + "
\r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + "
\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" // <-- // highlighting here should highlight the 2 + // anchor/@corresp attributes which reference this xml:id + + "

Some text here.

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + testHighlightsFor(xml, "file:///test/tei.xml", // + hl(r(16, 18, 16, 25), Write), // + hl(r(18, 23, 18, 31), Read), // + hl(r(19, 23, 19, 31), Read)); + } + + @Test + public void teiTargetMulti() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" // [0] + + " \r\n" + + " \r\n" // [1] + + " \r\n" // [2] + + " \r\n" + + " \r\n" // [3] + + " \r\n" + + " \r\n" + + ""; + testHighlightsFor(xml, "file:///test/tei.xml", // + hl(r(6, 14, 6, 15), Write), // [0] + hl(r(8, 19, 8, 21), Read), // [1] + hl(r(9, 19, 9, 21), Read), // [2] + hl(r(11, 16, 11, 18), Read)); // [3] } @Test @@ -90,11 +148,11 @@ public void docbook() throws BadLocationException { + " \r\n" + ""; testHighlightsFor(xml, "file:///test/docbook.xml", // - hl(r(4, 22, 4, 33), Read), hl(r(2, 16, 2, 27), Write)); + hl(r(4, 23, 4, 32), Read), hl(r(2, 17, 2, 26), Write)); } @Test - public void web() throws BadLocationException { + public void webOnServletMapping() throws BadLocationException { String xml = "\r\n" + ""; testHighlightsFor(xml, "file:///test/web.xml", // - hl(r(8, 18, 8, 28), Read), hl(r(4, 18, 4, 28), Write)); + hl(r(8, 18, 8, 28), Read), // + hl(r(4, 18, 4, 28), Write)); + } + + @Test + public void webOnServlet() throws BadLocationException { + String xml = "\r\n" + + " \r\n" + + " comi|ngsoon\r\n" // <-- highlight on servlet/servlet-name + // text should highlight the two servlet-mapping/servlet-name text nodes + + " mysite.server.ComingSoonServlet\r\n" + + " \r\n" + + " \r\n" + + " comingsoon\r\n" + + " /*\r\n" + + " \r\n" + + " \r\n" + + " comingsoon\r\n" + + " /*\r\n" + + " \r\n" + + "\r\n" + + ""; + testHighlightsFor(xml, "file:///test/web.xml", // + hl(r(4, 18, 4, 28), Write), // + hl(r(8, 18, 8, 28), Read), + hl(r(12, 18, 12, 28), Read)); } + private static void testHighlightsFor(String value, String fileURI, DocumentHighlight... expected) throws BadLocationException { - XMLReferencesSettings referencesSettings = new XMLReferencesSettings(); - referencesSettings.setReferences(createReferences()); XMLLanguageService xmlLanguageService = new XMLLanguageService(); xmlLanguageService.getExtensions(); - xmlLanguageService.doSave(new SettingsSaveContext(referencesSettings)); + xmlLanguageService.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); XMLAssert.testHighlightsFor(xmlLanguageService, value, fileURI, expected); } - private static List createReferences() { - List references = new ArrayList<>(); - /* - * { - * "prefix": "#", - * "from": "@corresp", - * "to": "@xml:id" - * } - */ - XMLReferences tei = new XMLReferences(); - tei.setPattern("**/*tei.xml"); - XMLReferenceExpression corresp = new XMLReferenceExpression(); - corresp.setPrefix("#"); - corresp.setFrom("@corresp"); - corresp.setTo("@xml:id"); - tei.setExpressions(Arrays.asList(corresp)); - references.add(tei); - /* - * { - * "from": "xref/@linkend", - * "to": "@id" - * } - */ - XMLReferences docbook = new XMLReferences(); - docbook.setPattern("**/*docbook.xml"); - XMLReferenceExpression linkend = new XMLReferenceExpression(); - linkend.setFrom("xref/@linkend"); - linkend.setTo("@id"); - docbook.setExpressions(Arrays.asList(linkend)); - references.add(docbook); - - /* - * { - * "from": "servlet-mapping/servlet-name/text()", - * "to": "servlet/servlet-name/text()" - * } - */ - XMLReferences web = new XMLReferences(); - web.setPattern("**/web.xml"); - XMLReferenceExpression servletName = new XMLReferenceExpression(); - servletName.setFrom("servlet-mapping/servlet-name/text()"); - servletName.setTo("servlet/servlet-name/text()"); - web.setExpressions(Arrays.asList(servletName)); - references.add(web); - - return references; - } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesLinkedEditingRangesExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesLinkedEditingRangesExtensionsTest.java new file mode 100644 index 0000000000..7a00924b40 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesLinkedEditingRangesExtensionsTest.java @@ -0,0 +1,199 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references; + +import static org.eclipse.lemminx.XMLAssert.le; +import static org.eclipse.lemminx.XMLAssert.r; + +import org.eclipse.lemminx.AbstractCacheBasedTest; +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lsp4j.LinkedEditingRanges; +import org.junit.jupiter.api.Test; + +/** + * XML references linked editing ranges tests. + * + * @author Angelo ZERR + */ +public class XMLReferencesLinkedEditingRangesExtensionsTest extends AbstractCacheBasedTest { + + @Test + public void teiOnCorresp() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + " \r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

Some text here.

\r\n" + + " \r\n" // <-- // linked editing ranges should do noting + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + testLinkedEditingFor(xml, "file:///test/tei.xml", null); + } + + @Test + public void teiOnXMLId() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + "
\r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + "
\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" // <-- linked editing ranges on xml:id should get the ranges of + // the two anchor/@corresp attributes + + "

Some text here.

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + testLinkedEditingFor(xml, "file:///test/tei.xml", // + le(r(16, 18, 16, 25), r(18, 24, 18, 31), r(19, 24, 19, 31))); + } + + @Test + public void docbookOnLinked() throws BadLocationException { + // highlighting on define/@name + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + "\r\n" + + " \r\n" // <-- // linked editing ranges should do noting + + "\r\n" + + " \r\n" + + "\r\n" + + " \r\n" + + "\r\n" + + " \r\n" + + ""; + testLinkedEditingFor(xml, "file:///test/docbook.xml", null); + } + + @Test + public void docbookOnChapterId() throws BadLocationException { + // highlighting on define/@name + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + "\r\n" + + " \r\n" // <-- // highlighting here should highlight + // chapter/@id="chapter-1" + + "\r\n" + + " \r\n" + + "\r\n" + + " \r\n" + + "\r\n" + + " \r\n" + + ""; + testLinkedEditingFor(xml, "file:///test/docbook.xml", // + le(r(2, 17, 2, 26), r(4, 23, 4, 32))); + } + + @Test + public void webOnServletMapping() throws BadLocationException { + String xml = "\r\n" + + " \r\n" + + " comingsoon\r\n" + + " mysite.server.ComingSoonServlet\r\n" + + " \r\n" + + " \r\n" + + " co|mingsoon\r\n" // <-- linked editing ranges on + // servlet-mapping/servlet-name + // text + + " /*\r\n" + + " \r\n" + + "\r\n" + + ""; + testLinkedEditingFor(xml, "file:///test/web.xml", null); + } + + @Test + public void webOnServlet() throws BadLocationException { + String xml = "\r\n" + + " \r\n" + + " comi|ngsoon\r\n" // <-- linked editing ranges on + // servlet/servlet-name + // text should get the ranges of the two servlet-mapping/servlet-name text nodes + + " mysite.server.ComingSoonServlet\r\n" + + " \r\n" + + " \r\n" + + " comingsoon\r\n" + + " /*\r\n" + + " \r\n" + + " \r\n" + + " comingsoon\r\n" + + " /*\r\n" + + " \r\n" + + "\r\n" + + ""; + testLinkedEditingFor(xml, "file:///test/web.xml", // + le(r(4, 18, 4, 28), r(8, 18, 8, 28), r(12, 18, 12, 28))); + } + + private static void testLinkedEditingFor(String value, String fileURI, + LinkedEditingRanges expected) + throws BadLocationException { + XMLLanguageService xmlLanguageService = new XMLLanguageService(); + xmlLanguageService.getExtensions(); + xmlLanguageService.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); + + XMLAssert.testLinkedEditingFor(xmlLanguageService, value, fileURI, expected); + } + +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesReferenceExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesReferenceExtensionsTest.java new file mode 100644 index 0000000000..92d8e50bb7 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesReferenceExtensionsTest.java @@ -0,0 +1,97 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references; + +import static org.eclipse.lemminx.XMLAssert.l; +import static org.eclipse.lemminx.XMLAssert.r; + +import org.eclipse.lemminx.AbstractCacheBasedTest; +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lsp4j.Location; +import org.junit.jupiter.api.Test; + +/** + * XML references tests + * + */ +public class XMLReferencesReferenceExtensionsTest extends AbstractCacheBasedTest { + + @Test + public void tei() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + "
\r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + "
\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" // find references here + + "

Some text here.

\r\n" + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + testReferencesFor(xml, "file:///test/tei.xml", + l("file:///test/tei.xml", r(18, 23, 18, 31))); + } + + @Test + public void teiTargetMulti() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "

\r\n" + + "

\r\n" + + " \r\n" + + " \r\n" // [1] + + " \r\n" // [2] + + " \r\n" + + " \r\n" // [3] + + " \r\n" + + " \r\n" + + ""; + testReferencesFor(xml, "file:///test/tei.xml", + l("file:///test/tei.xml", r(8, 19, 8, 21)), // [1] + l("file:///test/tei.xml", r(9, 19, 9, 21)), // [2] + l("file:///test/tei.xml", r(11, 16, 11, 18))); // [3] + } + + private void testReferencesFor(String xml, String fileURI, Location... expectedItems) throws BadLocationException { + XMLLanguageService xmlLanguageService = new XMLLanguageService(); + xmlLanguageService.getExtensions(); + xmlLanguageService.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); + + XMLAssert.testReferencesFor(xmlLanguageService, xml, fileURI, expectedItems); + } +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesRenameExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesRenameExtensionsTest.java new file mode 100644 index 0000000000..367aca0713 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesRenameExtensionsTest.java @@ -0,0 +1,150 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references; + +import static org.eclipse.lemminx.XMLAssert.r; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.lemminx.AbstractCacheBasedTest; +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.XMLAssert.SettingsSaveContext; +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * RNG rename tests. + * + */ +public class XMLReferencesRenameExtensionsTest extends AbstractCacheBasedTest { + + @Test + public void teiOnXmlId() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + " \r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" // rename here should should rename the 2 anchor/@corresp + // attributes + // value + + "

Some text here.

\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + assertRename(xml, "file:///test/tei.xml", "new-id", // + edits("new-id", + r(16, 18, 25), // + r(18, 24, 30), // + r(19, 24, 30))); + } + + @Disabled + @Test + public void teiOnCorresp() throws BadLocationException { + String xml = "\r\n" + // + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Title\r\n" + + " \r\n" + + " \r\n" + + "

Publication information

\r\n" + + "
\r\n" + + " \r\n" + + "

Information about the source

\r\n" + + "
\r\n" + + "
\r\n" + + "
\r\n" + + " \r\n" + + " \r\n" + + "

Some text here.

\r\n" + + " \r\n" // rename here should should rename the 2 + // anchor/@corresp attributes + // value + + " \r\n" + + " \r\n" + + "
\r\n" + + "
"; + assertRename(xml, "file:///test/tei.xml", "new-id", // + edits("new-id", + r(16, 18, 25), // + r(18, 24, 30), // + r(19, 24, 30))); + } + + @Test + public void web() throws BadLocationException { + String xml = "\r\n" + + " \r\n" + + " co|mingsoon\r\n" // rename here should should rename + // servlet-mapping/servlet-name text + // value + + " mysite.server.ComingSoonServlet\r\n" + + " \r\n" + + " \r\n" + + " comingsoon\r\n" + + " /*\r\n" + + " \r\n" + + "\r\n" + + ""; + assertRename(xml, "file:///test/web.xml", "new-name", edits("new-name", r(4, 19, 27), r(8, 19, 27))); + } + + private static void assertRename(String value, String fileURI, String newText, + List expectedEdits) throws BadLocationException { + XMLLanguageService xmlLanguageService = new XMLLanguageService(); + xmlLanguageService.getExtensions(); + xmlLanguageService.doSave(new SettingsSaveContext(XMLReferencesSettingsForTest.createXMLReferencesSettings())); + + XMLAssert.assertRename(xmlLanguageService, value, fileURI, newText, expectedEdits); + } + + private static List edits(String newText, Range... ranges) { + return Stream.of(ranges) // + .map(r -> new TextEdit(r, newText)) // + .collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesSettingsForTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesSettingsForTest.java new file mode 100644 index 0000000000..da405c0ba4 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/references/XMLReferencesSettingsForTest.java @@ -0,0 +1,100 @@ +/******************************************************************************* +* Copyright (c) 2023 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.references; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.lemminx.extensions.references.settings.XMLReferenceExpression; +import org.eclipse.lemminx.extensions.references.settings.XMLReferences; +import org.eclipse.lemminx.extensions.references.settings.XMLReferencesSettings; + +/** + * {@link XMLReferencesSettings} for tests. + * + */ +public class XMLReferencesSettingsForTest { + + public static XMLReferencesSettings createXMLReferencesSettings() { + XMLReferencesSettings referencesSettings = new XMLReferencesSettings(); + referencesSettings.setReferences(createReferences()); + return referencesSettings; + } + + private static List createReferences() { + List references = new ArrayList<>(); + + XMLReferences tei = new XMLReferences(); + tei.setPattern("**/*tei.xml"); + references.add(tei); + /* + * { + * "prefix": "#", + * "from": "@corresp", + * "to": "@xml:id" + * } + */ + XMLReferenceExpression corresp = new XMLReferenceExpression(); + corresp.setPrefix("#"); + corresp.setFrom("@corresp"); + corresp.setTo("@xml:id"); + + /* + * { + * "prefix": "#", + * "from": "@target", + * "to": "@xml:id", + * "multiple": true + * } + */ + XMLReferenceExpression target = new XMLReferenceExpression(); + target.setPrefix("#"); + target.setFrom("@target"); + target.setTo("@xml:id"); + target.setMultiple(true); + + tei.setExpressions(Arrays.asList(corresp, target)); + + + /* + * { + * "from": "xref/@linkend", + * "to": "@id" + * } + */ + XMLReferences docbook = new XMLReferences(); + docbook.setPattern("**/*docbook.xml"); + XMLReferenceExpression linkend = new XMLReferenceExpression(); + linkend.setFrom("xref/@linkend"); + linkend.setTo("@id"); + docbook.setExpressions(Arrays.asList(linkend)); + references.add(docbook); + + /* + * { + * "from": "servlet-mapping/servlet-name/text()", + * "to": "servlet/servlet-name/text()" + * } + */ + XMLReferences web = new XMLReferences(); + web.setPattern("**/web.xml"); + XMLReferenceExpression servletName = new XMLReferenceExpression(); + servletName.setFrom("servlet-mapping/servlet-name/text()"); + servletName.setTo("servlet/servlet-name/text()"); + web.setExpressions(Arrays.asList(servletName)); + references.add(web); + + return references; + } + +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java index a16d1e03e4..34692a859e 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/ErrorParticipantLanguageServiceTest.java @@ -38,6 +38,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.eclipse.lemminx.AbstractCacheBasedTest; import org.eclipse.lemminx.client.ClientCommands; @@ -68,9 +69,13 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.junit.jupiter.api.Test; /** @@ -269,8 +274,18 @@ public Hover onText(IHoverRequest request) throws Exception { this.registerReferenceParticipant( (document, position, context, locations, cancelChecker) -> locations.add(TEST_LOCATION)); - this.registerRenameParticipant((request, locations) -> { - throw new RuntimeException("This participant is broken"); + this.registerRenameParticipant(new IRenameParticipant() { + + @Override + public Either prepareRename(IPrepareRenameRequest request, + CancelChecker cancelChecker) { + return null; + } + + @Override + public void doRename(IRenameRequest request, List locations, CancelChecker cancelChecker) { + throw new RuntimeException("This participant is broken"); + } }); this.registerSymbolsProviderParticipant(new ISymbolsProviderParticipant() {