diff --git a/docs/LemMinX-Extensions.md b/docs/LemMinX-Extensions.md index 70ee112323..366a2f38dd 100644 --- a/docs/LemMinX-Extensions.md +++ b/docs/LemMinX-Extensions.md @@ -101,6 +101,7 @@ The [LemMinx Extensions API](https://github.com/eclipse/lemminx/tree/master/org. - Formatter with [IFormatterParticipant](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/format/IFormatterParticipant.java) - Symbols with [ISymbolsProviderParticipant](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ISymbolsProviderParticipant.java) - Monitoring workspace folders with [IWorkspaceServiceParticipant](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IWorkspaceServiceParticipant.java) +- Monitoring document lifecycle (didOpen, didChange, etc) with [IDocumentLifecycleParticipant](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IDocumentLifecycleParticipant.java) ## XML Language Server services available for extensions XML Language Server extension may need to use standard Language Server features such as commands, documents and ability to manipulate documents. These are available to extensions indirectly via specialized service API. 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 683b6561c0..b13468d625 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 @@ -26,10 +26,10 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; -import com.google.gson.JsonPrimitive; - import org.eclipse.lemminx.client.ExtendedClientCapabilities; import org.eclipse.lemminx.client.LimitExceededWarner; import org.eclipse.lemminx.client.LimitFeature; @@ -91,6 +91,7 @@ import org.eclipse.lsp4j.SelectionRangeParams; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentClientCapabilities; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.TypeDefinitionParams; @@ -100,17 +101,30 @@ import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.eclipse.lsp4j.services.TextDocumentService; +import com.google.gson.JsonPrimitive; + /** * XML text document service. * */ public class XMLTextDocumentService implements TextDocumentService { + private static final Logger LOGGER = Logger.getLogger(XMLTextDocumentService.class.getName()); + private final XMLLanguageServer xmlLanguageServer; private final TextDocuments> documents; private SharedSettings sharedSettings; private LimitExceededWarner limitExceededWarner; + /** + * Enumeration for Validation triggered by. + * + */ + private static enum TriggeredBy { + didOpen, // + didChange, Other; + } + /** * Save context. */ @@ -341,7 +355,7 @@ public CompletableFuture rename(RenameParams params) { @Override public void didOpen(DidOpenTextDocumentParams params) { TextDocument document = documents.onDidOpenTextDocument(params); - triggerValidationFor(document); + triggerValidationFor(document, TriggeredBy.didOpen); } /** @@ -350,17 +364,39 @@ public void didOpen(DidOpenTextDocumentParams params) { @Override public void didChange(DidChangeTextDocumentParams params) { TextDocument document = documents.onDidChangeTextDocument(params); - triggerValidationFor(document); + triggerValidationFor(document, TriggeredBy.didChange, params.getContentChanges()); } @Override public void didClose(DidCloseTextDocumentParams params) { + TextDocumentIdentifier identifier = params.getTextDocument(); + String uri = identifier.getUri(); + DOMDocument xmlDocument = getNowDOMDocument(uri); + // Remove the document from the cache documents.onDidCloseTextDocument(params); - TextDocumentIdentifier document = params.getTextDocument(); - String uri = document.getUri(); + // Publish empty errors from the document xmlLanguageServer.getLanguageClient() .publishDiagnostics(new PublishDiagnosticsParams(uri, Collections.emptyList())); getLimitExceededWarner().evictValue(uri); + // Manage didClose document lifecycle participants + if (xmlDocument != null) { + getXMLLanguageService().getDocumentLifecycleParticipants().forEach(participant -> { + try { + participant.didClose(xmlDocument); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing didClose for the participant '" + + participant.getClass().getName() + "'.", e); + } + }); + } + } + + private DOMDocument getNowDOMDocument(String uri) { + TextDocument document = documents.get(uri); + if (document != null) { + return ((ModelTextDocument) document).getModel().getNow(null); + } + return null; } @Override @@ -483,6 +519,19 @@ public void didSave(DidSaveTextDocumentParams params) { // A document was saved, collect documents to revalidate SaveContext context = new SaveContext(params.getTextDocument().getUri()); doSave(context); + + // Manage didSave document lifecycle participants + final DOMDocument xmlDocument = getNowDOMDocument(params.getTextDocument().getUri()); + if (xmlDocument != null) { + getXMLLanguageService().getDocumentLifecycleParticipants().forEach(participant -> { + try { + participant.didSave(xmlDocument); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing didSave for the participant '" + + participant.getClass().getName() + "'.", e); + } + }); + } return null; }); } @@ -526,11 +575,43 @@ private void triggerValidationFor(Collection> doc } } + private void triggerValidationFor(TextDocument document, TriggeredBy triggeredBy) { + triggerValidationFor(document, triggeredBy, null); + } + @SuppressWarnings("unchecked") - private void triggerValidationFor(TextDocument document) { - ((ModelTextDocument) document).getModel().thenAcceptAsync(xmlDocument -> { - validate(xmlDocument); - }); + private void triggerValidationFor(TextDocument document, TriggeredBy triggeredBy, + List changeEvents) { + ((ModelTextDocument) document).getModel()// + .thenAcceptAsync(xmlDocument -> { + // Validate the DOM document + validate(xmlDocument); + // Manage didOpen, didChange document lifecycle participants + switch (triggeredBy) { + case didOpen: + getXMLLanguageService().getDocumentLifecycleParticipants().forEach(participant -> { + try { + participant.didOpen(xmlDocument); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing didOpen for the participant '" + + participant.getClass().getName() + "'.", e); + } + }); + break; + case didChange: + getXMLLanguageService().getDocumentLifecycleParticipants().forEach(participant -> { + try { + participant.didChange(xmlDocument); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing didChange for the participant '" + + participant.getClass().getName() + "'.", e); + } + }); + break; + default: + // Do nothing + } + }); } void validate(DOMDocument xmlDocument) throws CancellationException { @@ -538,7 +619,8 @@ void validate(DOMDocument xmlDocument) throws CancellationException { cancelChecker.checkCanceled(); getXMLLanguageService().publishDiagnostics(xmlDocument, params -> xmlLanguageServer.getLanguageClient().publishDiagnostics(params), - (doc) -> triggerValidationFor(doc), sharedSettings.getValidationSettings(), cancelChecker); + (doc) -> triggerValidationFor(doc, TriggeredBy.Other), sharedSettings.getValidationSettings(), + cancelChecker); } private XMLLanguageService getXMLLanguageService() { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IDocumentLifecycleParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IDocumentLifecycleParticipant.java new file mode 100644 index 0000000000..d8ac80b5c4 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/IDocumentLifecycleParticipant.java @@ -0,0 +1,54 @@ +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.extensions; + +import java.util.List; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; + +/** + * Document LifecycleService participant API. + * + * @since 0.18.0 + */ +public interface IDocumentLifecycleParticipant { + + /** + * Handler called when a XML document is opened. + * + * @param document the DOM document + */ + void didOpen(DOMDocument document); + + /** + * Handler called when a XML document is changed. + * + * @param document the DOM document + */ + void didChange(DOMDocument document); + + /** + * Handler called when a XML document is saved. + * + * @param document the DOM document + */ + void didSave(DOMDocument document); + + /** + * Handler called when a XML document is closed. + * + * @param document the DOM document + */ + void didClose(DOMDocument document); + +} 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 ad7a196765..80369fca62 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 @@ -59,6 +59,7 @@ public class XMLExtensionsRegistry implements IComponentProvider { private final List formatterParticipants; private final List symbolsProviderParticipants; private final List workspaceServiceParticipants; + private final List documentLifecycleParticipants; private IXMLDocumentProvider documentProvider; private IXMLValidationService validationService; private IXMLCommandService commandService; @@ -89,6 +90,7 @@ public XMLExtensionsRegistry() { formatterParticipants = new ArrayList<>(); symbolsProviderParticipants = new ArrayList<>(); workspaceServiceParticipants = new ArrayList<>(); + documentLifecycleParticipants = new ArrayList<>(); resolverExtensionManager = new URIResolverExtensionManager(); components = new HashMap<>(); registerComponent(resolverExtensionManager); @@ -201,6 +203,8 @@ public Collection getSymbolsProviderParticipants() } /** + * Return the registered workspace service participants. + * * @return the registered workspace service participants. * @since 0.14.2 */ @@ -209,6 +213,17 @@ public Collection getWorkspaceServiceParticipants( return workspaceServiceParticipants; } + /** + * Return the registered document lifecycle participants. + * + * @return the registered document lifecycle participants. + * @since 0.18.0 + */ + public List getDocumentLifecycleParticipants() { + initializeIfNeeded(); + return documentLifecycleParticipants; + } + public void initializeIfNeeded() { if (initialized) { return; @@ -373,9 +388,10 @@ public void registerSymbolsProviderParticipant(ISymbolsProviderParticipant symbo public void unregisterSymbolsProviderParticipant(ISymbolsProviderParticipant symbolsProviderParticipant) { symbolsProviderParticipants.remove(symbolsProviderParticipant); } - + /** * Register a new workspace service participant + * * @param workspaceServiceParticipant the participant to register * @since 0.14.2 */ @@ -385,6 +401,7 @@ public void registerWorkspaceServiceParticipant(IWorkspaceServiceParticipant wor /** * Unregister a new workspace service participant. + * * @param workspaceServiceParticipant the participant to unregister * @since 0.14.2 */ @@ -392,6 +409,26 @@ public void unregisterWorkspaceServiceParticipant(IWorkspaceServiceParticipant w workspaceServiceParticipants.remove(workspaceServiceParticipant); } + /** + * Register a new document lifecycle participant + * + * @param documentLifecycleParticipant the participant to register + * @since 0.18.0 + */ + public void registerDocumentLifecycleParticipant(IDocumentLifecycleParticipant documentLifecycleParticipant) { + documentLifecycleParticipants.add(documentLifecycleParticipant); + } + + /** + * Unregister a new document lifecycle participant. + * + * @param documentLifecycleParticipant the participant to unregister + * @since 0.18.0 + */ + public void unregisterDocumentLifecycleParticipant(IDocumentLifecycleParticipant documentLifecycleParticipant) { + documentLifecycleParticipants.remove(documentLifecycleParticipant); + } + /** * Returns the XML Document provider and null otherwise. * diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java index e83b463436..91b7c4d826 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java @@ -17,12 +17,17 @@ import org.eclipse.lemminx.customservice.ActionableNotification; import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService.IDelegateCommandHandler; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; /** * Mock XML Language server which helps to track show messages, actionable @@ -82,4 +87,41 @@ public TextDocumentIdentifier didOpen(String fileURI, String xml) { } return xmlIdentifier; } + + public TextDocumentIdentifier didChange(String fileURI, List contentChanges) { + TextDocumentIdentifier xmlIdentifier = new TextDocumentIdentifier(fileURI); + DidChangeTextDocumentParams params = new DidChangeTextDocumentParams( + new VersionedTextDocumentIdentifier(xmlIdentifier.getUri(), 1), contentChanges); + XMLTextDocumentService textDocumentService = (XMLTextDocumentService) super.getTextDocumentService(); + textDocumentService.didChange(params); + try { + // Force the parse of DOM document + textDocumentService.getDocument(params.getTextDocument().getUri()).getModel().get(); + } catch (Exception e) { + e.printStackTrace(); + } + return xmlIdentifier; + } + + public TextDocumentIdentifier didClose(String fileURI) { + TextDocumentIdentifier xmlIdentifier = new TextDocumentIdentifier(fileURI); + DidCloseTextDocumentParams params = new DidCloseTextDocumentParams(xmlIdentifier); + XMLTextDocumentService textDocumentService = (XMLTextDocumentService) super.getTextDocumentService(); + textDocumentService.didClose(params); + return xmlIdentifier; + } + + public TextDocumentIdentifier didSave(String fileURI) { + TextDocumentIdentifier xmlIdentifier = new TextDocumentIdentifier(fileURI); + DidSaveTextDocumentParams params = new DidSaveTextDocumentParams(xmlIdentifier); + XMLTextDocumentService textDocumentService = (XMLTextDocumentService) super.getTextDocumentService(); + textDocumentService.didSave(params); + try { + // Force the parse of DOM document + textDocumentService.getDocument(params.getTextDocument().getUri()).getModel().get(); + } catch (Exception e) { + e.printStackTrace(); + } + return xmlIdentifier; + } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/DocumentLifecycleParticipantTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/DocumentLifecycleParticipantTest.java new file mode 100644 index 0000000000..9ddf2b4cbd --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/DocumentLifecycleParticipantTest.java @@ -0,0 +1,131 @@ +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.services.extensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; + +import org.eclipse.lemminx.MockXMLLanguageServer; +import org.eclipse.lemminx.dom.DOMDocument; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link IDocumentLifecycleParticipant} + */ +public class DocumentLifecycleParticipantTest { + + private static class CaptureDocumentLifecycleCalls implements IDocumentLifecycleParticipant { + + private int didOpen; + private int didChange; + private int didSave; + private int didClose; + + @Override + public void didOpen(DOMDocument document) { + if (document != null) { + this.didOpen++; + } + } + + @Override + public void didChange(DOMDocument document) { + if (document != null) { + this.didChange++; + } + } + + @Override + public void didSave(DOMDocument document) { + if (document != null) { + this.didSave++; + } + } + + @Override + public void didClose(DOMDocument document) { + if (document != null) { + this.didClose++; + } + } + + public int getDidOpen() { + return didOpen; + } + + public int getDidChange() { + return didChange; + } + + public int getDidSave() { + return didSave; + } + + public int getDidClose() { + return didClose; + } + + } + + private CaptureDocumentLifecycleCalls documentLifecycleParticipant; + private MockXMLLanguageServer server; + + @BeforeEach + public void initializeLanguageService() { + this.server = new MockXMLLanguageServer(); + this.documentLifecycleParticipant = new CaptureDocumentLifecycleCalls(); + server.getXMLLanguageService().registerDocumentLifecycleParticipant(this.documentLifecycleParticipant); + } + + @Test + public void didOpen() { + assertEquals(0, documentLifecycleParticipant.getDidOpen()); + server.didOpen("test.xml", "