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 9f2ecf911..8827a1bf9 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Angelo ZERR. + * Copyright (c) 2018, 2026 Angelo ZERR. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -85,6 +85,10 @@ import org.eclipse.lsp4j.FoldingRangeRequestParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; +import org.eclipse.lsp4j.InlineCompletionContext; +import org.eclipse.lsp4j.InlineCompletionItem; +import org.eclipse.lsp4j.InlineCompletionList; +import org.eclipse.lsp4j.InlineCompletionParams; import org.eclipse.lsp4j.LinkedEditingRangeParams; import org.eclipse.lsp4j.LinkedEditingRanges; import org.eclipse.lsp4j.Location; @@ -275,6 +279,16 @@ public CompletableFuture resolveCompletionItem(CompletionItem un }); } + @Override + public CompletableFuture, InlineCompletionList>> inlineCompletion(InlineCompletionParams params) { + return computeDOMAsync(params.getTextDocument(), (xmlDocument, cancelChecker) -> { + InlineCompletionContext context = params.getContext(); + InlineCompletionList list = getXMLLanguageService().doInlineCompletion(xmlDocument, params.getPosition(), context, + sharedSettings, cancelChecker); + return Either.forRight(list); + }); + } + @Override public CompletableFuture hover(HoverParams params) { return computeDOMAsync(params.getTextDocument(), (xmlDocument, cancelChecker) -> { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/core/XMLCorePlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/core/XMLCorePlugin.java new file mode 100644 index 000000000..d13996e1a --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/core/XMLCorePlugin.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.extensions.core; + +import org.eclipse.lemminx.extensions.core.participants.inlinecompletion.XMLCloseTagInlineCompletionParticipant; +import org.eclipse.lemminx.services.extensions.IXMLExtension; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionParticipant; +import org.eclipse.lemminx.services.extensions.save.ISaveContext; +import org.eclipse.lsp4j.InitializeParams; + +/** + * XML Core plugin extension to provide core XML editing features that are + * not specific to any grammar type (XSD, DTD, etc.). + * + *

+ * This plugin provides: + *

    + *
  • Inline completion for closing tags
  • + *
+ *

+ */ +public class XMLCorePlugin implements IXMLExtension { + + private IInlineCompletionParticipant inlineCompletionParticipant; + + @Override + public void start(InitializeParams params, XMLExtensionsRegistry registry) { + inlineCompletionParticipant = new XMLCloseTagInlineCompletionParticipant(); + registry.registerInlineCompletionParticipant(inlineCompletionParticipant); + } + + @Override + public void stop(XMLExtensionsRegistry registry) { + registry.unregisterInlineCompletionParticipant(inlineCompletionParticipant); + } + + @Override + public void doSave(ISaveContext context) { + // No settings to save + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/core/participants/inlinecompletion/XMLCloseTagInlineCompletionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/core/participants/inlinecompletion/XMLCloseTagInlineCompletionParticipant.java new file mode 100644 index 000000000..9bbc68bff --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/core/participants/inlinecompletion/XMLCloseTagInlineCompletionParticipant.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.extensions.core.participants.inlinecompletion; + +import static org.eclipse.lemminx.dom.parser.Constants._FSL; +import static org.eclipse.lemminx.dom.parser.Constants._LAN; +import static org.eclipse.lemminx.dom.parser.Constants._RAN; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionParticipant; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionRequest; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionResponse; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.InlineCompletionItem; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * Inline completion participant that suggests closing tags for open XML + * elements. + * + * This participant provides inline completion suggestions when the user is + * typing inside an XML element that needs to be closed. + */ +public class XMLCloseTagInlineCompletionParticipant implements IInlineCompletionParticipant { + + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + + DOMDocument document = request.getXMLDocument(); + int offset = request.getOffset(); + + // Find the node at the current position + DOMNode node = document.findNodeAt(offset); + if (node == null) { + return; + } + + // Check if the cursor is at a position where we should suggest a closing tag + String text = document.getText(); + if (offset > 0 && offset <= text.length()) { + int charBefore = text.codePointAt(offset - 1); + + // Suggest closing tag after '>' or after content + if (charBefore == _RAN || Character.isLetterOrDigit(charBefore) || Character.isWhitespace(charBefore)) { + // Check if we're inside an element that needs closing + DOMElement parentElement = XMLPositionUtility.findUnclosedParentElement(node, offset); + if (parentElement != null) { + String tagName = parentElement.getTagName(); + if (tagName != null && !tagName.isEmpty()) { + String closingTag = Character.toString(_LAN) + Character.toString(_FSL) + tagName + + Character.toString(_RAN); + + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText(closingTag); + response.addInlineCompletionItem(item); + } + } + } + } + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/InlineCompletionRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/InlineCompletionRequest.java new file mode 100644 index 000000000..8586a25d5 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/InlineCompletionRequest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * 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.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionRequest; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lsp4j.InlineCompletionContext; +import org.eclipse.lsp4j.Position; + +/** + * Inline completion request implementation. + */ +class InlineCompletionRequest extends AbstractPositionRequest implements IInlineCompletionRequest { + + private final InlineCompletionContext context; + private final SharedSettings sharedSettings; + + public InlineCompletionRequest(DOMDocument xmlDocument, Position position, + InlineCompletionContext context, + SharedSettings settings, + XMLExtensionsRegistry extensionsRegistry) throws BadLocationException { + super(xmlDocument, position, extensionsRegistry); + this.context = context; + this.sharedSettings = settings; + } + + @Override + public InlineCompletionContext getContext() { + return context; + } + + @Override + public SharedSettings getSharedSettings() { + return sharedSettings; + } + + @Override + public boolean canSupportMarkupKind(String kind) { + // Inline completion typically doesn't use markup documentation + return false; + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/InlineCompletionResponse.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/InlineCompletionResponse.java new file mode 100644 index 000000000..0e9e25354 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/InlineCompletionResponse.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.services; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionResponse; +import org.eclipse.lsp4j.InlineCompletionItem; + +/** + * Inline completion response implementation. + */ +class InlineCompletionResponse implements IInlineCompletionResponse { + + private final List items; + + public InlineCompletionResponse() { + this.items = new ArrayList<>(); + } + + @Override + public void addInlineCompletionItem(InlineCompletionItem item) { + items.add(item); + } + + /** + * Returns the list of inline completion items. + * + * @return the list of inline completion items + */ + public List getItems() { + return items; + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLInlineCompletion.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLInlineCompletion.java new file mode 100644 index 000000000..3f9956a14 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLInlineCompletion.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.services; + +import java.util.ArrayList; +import java.util.List; +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.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionParticipant; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lsp4j.InlineCompletionContext; +import org.eclipse.lsp4j.InlineCompletionItem; +import org.eclipse.lsp4j.InlineCompletionList; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * XML inline completion service. + */ +public class XMLInlineCompletion { + + private static final Logger LOGGER = Logger.getLogger(XMLInlineCompletion.class.getName()); + + private final XMLExtensionsRegistry extensionsRegistry; + + public XMLInlineCompletion(XMLExtensionsRegistry extensionsRegistry) { + this.extensionsRegistry = extensionsRegistry; + } + + /** + * Returns inline completion items for the given document and position. + * + * @param document the DOM document + * @param position the position where inline completion is requested + * @param context the inline completion context + * @param settings the shared settings + * @param cancelChecker the cancel checker + * @return the inline completion list + */ + public InlineCompletionList doInlineCompletion(DOMDocument document, Position position, + InlineCompletionContext context, + SharedSettings settings, + CancelChecker cancelChecker) { + InlineCompletionResponse response = new InlineCompletionResponse(); + + try { + InlineCompletionRequest request = new InlineCompletionRequest(document, position, context, settings, + extensionsRegistry); + + // Call all registered inline completion participants + for (IInlineCompletionParticipant participant : extensionsRegistry.getInlineCompletionParticipants()) { + try { + cancelChecker.checkCanceled(); + participant.onInlineCompletion(request, response, cancelChecker); + } catch (CancellationException e) { + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, + "Error while processing inline completion participant " + participant.getClass().getName(), + e); + } + } + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "Error while computing inline completion", e); + } + + InlineCompletionList result = new InlineCompletionList(); + result.setItems(response.getItems()); + return result; + } +} \ 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 2ca98f708..ea71c25f7 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018, 2023 Angelo ZERR + * Copyright (c) 2018, 2026 Angelo ZERR * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -47,6 +47,8 @@ import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.InlineCompletionContext; +import org.eclipse.lsp4j.InlineCompletionList; import org.eclipse.lsp4j.LinkedEditingRanges; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; @@ -95,6 +97,7 @@ public void checkCanceled() { private final XMLSelectionRanges selectionRanges; private final XMLLinkedEditing linkedEditing; private final XMLDocumentColor documentColor; + private final XMLInlineCompletion inlineCompletion; public XMLLanguageService() { this.formatter = new XMLFormatter(this); @@ -116,6 +119,7 @@ public XMLLanguageService() { this.rename = new XMLRename(this); this.selectionRanges = new XMLSelectionRanges(); this.linkedEditing = new XMLLinkedEditing(this); + this.inlineCompletion = new XMLInlineCompletion(this); } @Override @@ -194,6 +198,16 @@ public CompletionItem resolveCompletionItem(CompletionItem unresolved, DOMDocume SharedSettings sharedSettings, CancelChecker cancelChecker) { return completions.resolveCompletionItem(unresolved, xmlDocument, sharedSettings, cancelChecker); } + public InlineCompletionList doInlineCompletion(DOMDocument xmlDocument, Position position, + InlineCompletionContext context, SharedSettings settings) { + return doInlineCompletion(xmlDocument, position, context, settings, NULL_CHECKER); + } + + public InlineCompletionList doInlineCompletion(DOMDocument xmlDocument, Position position, + InlineCompletionContext context, SharedSettings settings, CancelChecker cancelChecker) { + return inlineCompletion.doInlineCompletion(xmlDocument, position, context, settings, cancelChecker); + } + public Hover doHover(DOMDocument xmlDocument, Position position, SharedSettings sharedSettings) { return doHover(xmlDocument, position, sharedSettings, NULL_CHECKER); 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 7cb1a458a..6bc8efead 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018, 2023 Angelo ZERR + * Copyright (c) 2018, 2026 Angelo ZERR * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -34,6 +34,7 @@ import org.eclipse.lemminx.services.extensions.diagnostics.IDiagnosticsParticipant; import org.eclipse.lemminx.services.extensions.format.IFormatterParticipant; import org.eclipse.lemminx.services.extensions.hover.IHoverParticipant; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionParticipant; import org.eclipse.lemminx.services.extensions.rename.IRenameParticipant; import org.eclipse.lemminx.services.extensions.save.ISaveContext; import org.eclipse.lemminx.services.extensions.save.ISaveContext.SaveContextType; @@ -68,6 +69,7 @@ public class XMLExtensionsRegistry implements IComponentProvider { private final List symbolsProviderParticipants; private final List workspaceServiceParticipants; private final List documentLifecycleParticipants; + private final List inlineCompletionParticipants; private IXMLDocumentProvider documentProvider; private IXMLValidationService validationService; private IXMLCommandService commandService; @@ -104,6 +106,7 @@ public XMLExtensionsRegistry() { symbolsProviderParticipants = new ArrayList<>(); workspaceServiceParticipants = new ArrayList<>(); documentLifecycleParticipants = new ArrayList<>(); + inlineCompletionParticipants = new ArrayList<>(); resolverExtensionManager = new URIResolverExtensionManager(); components = new HashMap<>(); telemetryManager = new TelemetryManager(null); @@ -248,6 +251,16 @@ public List getDocumentLifecycleParticipants() { return documentLifecycleParticipants; } + /** + * Return the registered inline completion participants. + * + * @return the registered inline completion participants. + */ + public List getInlineCompletionParticipants() { + initializeIfNeeded(); + return inlineCompletionParticipants; + } + public void initializeIfNeeded() { if (initialized) { return; @@ -470,6 +483,24 @@ public void registerDocumentLifecycleParticipant(IDocumentLifecycleParticipant d public void unregisterDocumentLifecycleParticipant(IDocumentLifecycleParticipant documentLifecycleParticipant) { documentLifecycleParticipants.remove(documentLifecycleParticipant); } + /** + * Register a new inline completion participant + * + * @param inlineCompletionParticipant the participant to register + */ + public void registerInlineCompletionParticipant(IInlineCompletionParticipant inlineCompletionParticipant) { + inlineCompletionParticipants.add(inlineCompletionParticipant); + } + + /** + * Unregister an inline completion participant. + * + * @param inlineCompletionParticipant the participant to unregister + */ + public void unregisterInlineCompletionParticipant(IInlineCompletionParticipant inlineCompletionParticipant) { + inlineCompletionParticipants.remove(inlineCompletionParticipant); + } + /** * Returns the XML Document provider and null otherwise. diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionParticipant.java new file mode 100644 index 000000000..53c8f5d54 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionParticipant.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.services.extensions.inlinecompletion; + +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * Inline completion participant API. + * + *

+ * This participant is called when inline completion is requested to provide + * context-aware suggestions as users type. + *

+ */ +public interface IInlineCompletionParticipant { + + /** + * Called when inline completion is requested. + * + * @param request the inline completion request + * @param response the response to add inline completion items to + * @param cancelChecker the cancel checker + */ + void onInlineCompletion(IInlineCompletionRequest request, + IInlineCompletionResponse response, + CancelChecker cancelChecker); +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionRequest.java new file mode 100644 index 000000000..3f415b165 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionRequest.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.services.extensions.inlinecompletion; + +import org.eclipse.lemminx.services.extensions.IPositionRequest; +import org.eclipse.lemminx.services.extensions.ISharedSettingsRequest; +import org.eclipse.lsp4j.InlineCompletionContext; + +/** + * Inline completion request API. + */ +public interface IInlineCompletionRequest extends IPositionRequest, ISharedSettingsRequest { + + /** + * Returns the inline completion context. + * + * @return the inline completion context + */ + InlineCompletionContext getContext(); +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionResponse.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionResponse.java new file mode 100644 index 000000000..f612ba035 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/inlinecompletion/IInlineCompletionResponse.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.services.extensions.inlinecompletion; + +import org.eclipse.lsp4j.InlineCompletionItem; + +/** + * Inline completion response API. + * + *

+ * This interface provides methods to add inline completion items to the response. + * It follows the same pattern as {@link org.eclipse.lemminx.services.extensions.completion.ICompletionResponse} + * to maintain API consistency. + *

+ */ +public interface IInlineCompletionResponse { + + /** + * Add an inline completion item to the response. + * + * @param item the inline completion item to add + */ + void addInlineCompletionItem(InlineCompletionItem item); +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ClientCapabilitiesWrapper.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ClientCapabilitiesWrapper.java index 8b99542c3..e84dadc19 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ClientCapabilitiesWrapper.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ClientCapabilitiesWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Red Hat, Inc. and others. + * Copyright (c) 2018, 2026 Red Hat, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -127,6 +127,10 @@ public boolean isLinkedEditingRangeDynamicRegistered() { return v3Supported && isDynamicRegistrationSupported(getTextDocument().getLinkedEditingRange()); } + public boolean isInlineCompletionDynamicRegistered() { + return v3Supported && isDynamicRegistrationSupported(getTextDocument().getInlineCompletion()); + } + private boolean isDynamicRegistrationSupported(DynamicRegistrationCapabilities capability) { return capability != null && capability.getDynamicRegistration() != null && capability.getDynamicRegistration().booleanValue(); 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 bbbef95e1..956694ad6 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Red Hat, Inc. and others. + * Copyright (c) 2018, 2026 Red Hat, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -52,6 +52,7 @@ private ServerCapabilitiesConstants() { public static final String TEXT_DOCUMENT_LINKED_EDITING_RANGE = "textDocument/linkedEditingRange"; public static final String TEXT_DOCUMENT_HIGHLIGHT = "textDocument/documentHighlight"; public static final String TEXT_DOCUMENT_SELECTION_RANGE = "textDocument/selectionRange"; + public static final String TEXT_DOCUMENT_INLINE_COMPLETION = "textDocument/inlineCompletion"; public static final String WORKSPACE_CHANGE_FOLDERS = "workspace/didChangeWorkspaceFolders"; public static final String WORKSPACE_EXECUTE_COMMAND = "workspace/executeCommand"; @@ -82,6 +83,7 @@ private ServerCapabilitiesConstants() { public static final String WORKSPACE_CHANGE_FOLDERS_ID = UUID.randomUUID().toString(); public static final String WORKSPACE_WATCHED_FILES_ID = UUID.randomUUID().toString(); public static final String LINKED_EDITING_RANGE_ID = UUID.randomUUID().toString(); + public static final String INLINE_COMPLETION_ID = UUID.randomUUID().toString(); public static final CompletionOptions DEFAULT_COMPLETION_OPTIONS = new CompletionOptions(true, Arrays.asList(".", ":", "<", "\"", "=", "/", "\\", "?", "\'", "&", "#")); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesInitializer.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesInitializer.java index 79652991c..5aacdf070 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesInitializer.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/settings/capabilities/ServerCapabilitiesInitializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018, 2023 Red Hat, Inc. and others. + * Copyright (c) 2018, 2026 Red Hat, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -61,6 +61,7 @@ public static ServerCapabilities getNonDynamicServerCapabilities(ClientCapabilit serverCapabilities.setLinkedEditingRangeProvider(!clientCapabilities.isLinkedEditingRangeDynamicRegistered()); serverCapabilities.setColorProvider(!clientCapabilities.isColorDynamicRegistrationSupported()); serverCapabilities.setSelectionRangeProvider(!clientCapabilities.isSelectionRangeDynamicRegistered()); + serverCapabilities.setInlineCompletionProvider(!clientCapabilities.isInlineCompletionDynamicRegistered()); if (clientCapabilities.isWorkspaceFoldersSupported()) { WorkspaceFoldersOptions workspaceFolders = new WorkspaceFoldersOptions(); 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 6fc5b454c..684c2f430 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Red Hat, Inc. and others. + * Copyright (c) 2018, 2026 Red Hat, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -28,6 +28,7 @@ import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.FORMATTING_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.FORMATTING_RANGE_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.HOVER_ID; +import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.INLINE_COMPLETION_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.LINKED_EDITING_RANGE_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.LINK_ID; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.REFERENCES_ID; @@ -43,6 +44,7 @@ import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_HIGHLIGHT; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_HOVER; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_LINK; +import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_INLINE_COMPLETION; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_LINKED_EDITING_RANGE; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_REFERENCES; import static org.eclipse.lemminx.settings.capabilities.ServerCapabilitiesConstants.TEXT_DOCUMENT_RENAME; @@ -185,6 +187,9 @@ public void initializeCapabilities() { if (this.getClientCapabilities().isLinkedEditingRangeDynamicRegistered()) { registerCapability(LINKED_EDITING_RANGE_ID, TEXT_DOCUMENT_LINKED_EDITING_RANGE); } + if (this.getClientCapabilities().isInlineCompletionDynamicRegistered()) { + registerCapability(INLINE_COMPLETION_ID, TEXT_DOCUMENT_INLINE_COMPLETION); + } if (this.getClientCapabilities().isDidChangeWatchedFilesRegistered()) { registerWatchedFiles(); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java index 39e315a04..58c280ac0 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 Angelo ZERR + * Copyright (c) 2018, 2026 Angelo ZERR * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -411,7 +411,40 @@ private static DOMNode findUnclosedChildNode(String childTag, List chil } /** - * Returns the range of the root start tag (excludes the '<') of the given + * Finds the nearest parent element that is not closed at the given offset. + * + * @param node the starting node + * @param offset the current cursor offset + * @return the unclosed parent element, or null if none found + */ + public static DOMElement findUnclosedParentElement(DOMNode node, int offset) { + DOMNode current = node; + + while (current != null) { + if (current.isElement()) { + DOMElement element = (DOMElement) current; + // Check if the element is not self-closed and not fully closed + if (!element.isSelfClosed() && !element.isClosed()) { + // Check if we're after the start tag but before any end tag + int startTagEnd = element.getStartTagCloseOffset(); + int endTagStart = element.hasEndTag() ? element.getEndTagOpenOffset() : -1; + + // If we're after the start tag close and either there's no end tag or we're + // before it + if (startTagEnd != -1 && offset > startTagEnd + && (endTagStart == -1 || offset < endTagStart)) { + return element; + } + } + } + current = current.getParentNode(); + } + + return null; + } + + /** + * Returns the range of the root start tag (excludes the '<') of the given * document and null otherwise. * * @param document the DOM document. diff --git a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension index 47ec500ac..d45f186fd 100644 --- a/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension +++ b/org.eclipse.lemminx/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension @@ -1,3 +1,4 @@ +org.eclipse.lemminx.extensions.core.XMLCorePlugin org.eclipse.lemminx.extensions.contentmodel.ContentModelPlugin org.eclipse.lemminx.extensions.references.XMLReferencesPlugin org.eclipse.lemminx.extensions.xsd.XSDPlugin 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 b40f5722b..2ef54127c 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 @@ -103,6 +103,10 @@ import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.InlineCompletionContext; +import org.eclipse.lsp4j.InlineCompletionItem; +import org.eclipse.lsp4j.InlineCompletionList; +import org.eclipse.lsp4j.InlineCompletionTriggerKind; import org.eclipse.lsp4j.SnippetTextEdit; import org.eclipse.lsp4j.TextDocumentEdit; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -2126,4 +2130,92 @@ public static void assertColorPresentation(List act public static ColorPresentation colorPres(String label, TextEdit textEdit) { return new ColorPresentation(label, textEdit); } + + // ------------------- Inline Completion assert + + // ------------------- Inline Completion assert + + /** + * Test inline completion with default language service, expecting specific insert texts + */ + public static void testInlineCompletionFor(String xml, String... expectedInsertTexts) throws BadLocationException { + testInlineCompletionFor(new XMLLanguageService(), xml, null, null, expectedInsertTexts); + } + + /** + * Test inline completion with default language service, expecting a specific count + */ + public static void testInlineCompletionFor(String xml, int expectedCount) throws BadLocationException { + testInlineCompletionFor(new XMLLanguageService(), xml, null, expectedCount); + } + + /** + * Test inline completion with custom language service, expecting specific insert texts + */ + public static void testInlineCompletionFor(XMLLanguageService xmlLanguageService, String xml, String fileURI, + String... expectedInsertTexts) throws BadLocationException { + testInlineCompletionFor(xmlLanguageService, xml, fileURI, null, expectedInsertTexts); + } + + /** + * Test inline completion with custom language service, expecting a specific count + */ + public static void testInlineCompletionFor(XMLLanguageService xmlLanguageService, String xml, String fileURI, + int expectedCount) throws BadLocationException { + testInlineCompletionFor(xmlLanguageService, xml, fileURI, expectedCount, (String[]) null); + } + + /** + * Test inline completion with custom language service, expecting both count and insert texts + */ + public static void testInlineCompletionFor(XMLLanguageService xmlLanguageService, String xml, String fileURI, + Integer expectedCount, String... expectedInsertTexts) throws BadLocationException { + int offset = xml.indexOf('|'); + xml = xml.substring(0, offset) + xml.substring(offset + 1); + + TextDocument document = new TextDocument(xml, fileURI != null ? fileURI : "test://test/test.xml"); + Position position = document.positionAt(offset); + DOMDocument xmlDoc = DOMParser.getInstance().parse(document, xmlLanguageService.getResolverExtensionManager()); + xmlLanguageService.setDocumentProvider((uri) -> xmlDoc); + + InlineCompletionContext context = new InlineCompletionContext(); + context.setTriggerKind(InlineCompletionTriggerKind.Invoked); + + SharedSettings settings = new SharedSettings(); + InlineCompletionList list = xmlLanguageService.doInlineCompletion(xmlDoc, position, context, + settings, NULL_CHECKER); + + if (expectedCount != null) { + assertEquals(expectedCount.intValue(), list.getItems().size()); + } + if (expectedInsertTexts != null && expectedInsertTexts.length > 0) { + for (String expectedText : expectedInsertTexts) { + assertInlineCompletion(list, expectedText); + } + } + } + + public static void assertInlineCompletion(InlineCompletionList completions, + String expectedInsertText) { + List matches = completions.getItems().stream() + .filter(item -> { + String insertText = getInlineCompletionInsertText(item); + return expectedInsertText.equals(insertText); + }) + .collect(Collectors.toList()); + + assertTrue(matches.size() > 0, + "No inline completion item found with insert text: " + expectedInsertText); + } + + private static String getInlineCompletionInsertText(InlineCompletionItem item) { + if (item.getInsertText() == null) { + return null; + } + if (item.getInsertText().isLeft()) { + return item.getInsertText().getLeft(); + } else { + return item.getInsertText().getRight().getValue(); + } + } } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/core/XMLCloseTagInlineCompletionTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/core/XMLCloseTagInlineCompletionTest.java new file mode 100644 index 000000000..07a54b1c1 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/core/XMLCloseTagInlineCompletionTest.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.extensions.core; + +import static org.eclipse.lemminx.XMLAssert.testInlineCompletionFor; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.junit.jupiter.api.Test; + +/** + * XML inline completion tests for closing tags. + * + * @see org.eclipse.lemminx.extensions.core.participants.inlinecompletion.XMLCloseTagInlineCompletionParticipant + */ +public class XMLCloseTagInlineCompletionTest { + + @Test + public void testCloseRootElement() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(xml, ""); + } + + @Test + public void testCloseNestedElement() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(xml, ""); + } + + @Test + public void testCloseElementWithAttributes() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(xml, ""); + } + + @Test + public void testNoSuggestionForClosedElement() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(xml, 0); + } + + @Test + public void testNoSuggestionForSelfClosedElement() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(xml, 0); + } + + @Test + public void testCloseMultipleNestedElements() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(xml, ""); + } + + @Test + public void testCloseAfterText() throws BadLocationException { + String xml = "text|"; + testInlineCompletionFor(xml, ""); + } + + @Test + public void testCloseAfterWhitespace() throws BadLocationException { + String xml = " |"; + testInlineCompletionFor(xml, ""); + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLInlineCompletionTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLInlineCompletionTest.java new file mode 100644 index 000000000..f14b6cbc0 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/XMLInlineCompletionTest.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + */ +package org.eclipse.lemminx.services; + +import static org.eclipse.lemminx.XMLAssert.testInlineCompletionFor; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionParticipant; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionRequest; +import org.eclipse.lemminx.services.extensions.inlinecompletion.IInlineCompletionResponse; +import org.eclipse.lsp4j.InlineCompletionItem; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * XML inline completion service tests with custom participants. + */ +public class XMLInlineCompletionTest { + + private XMLLanguageService languageService; + + @BeforeEach + public void initializeLanguageService() { + languageService = new XMLLanguageService(); + } + + @Test + public void testInlineCompletionWithNoParticipants() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(languageService, xml, null, 0); + } + + @Test + public void testInlineCompletionWithCustomParticipant() throws BadLocationException { + // Register a custom participant that always suggests "test" + languageService.registerInlineCompletionParticipant( + new IInlineCompletionParticipant() { + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText("test"); + response.addInlineCompletionItem(item); + } + } + ); + + String xml = "|"; + testInlineCompletionFor(languageService, xml, "test"); + } + + @Test + public void testInlineCompletionWithMultipleParticipants() throws BadLocationException { + // Register multiple participants + languageService.registerInlineCompletionParticipant( + new IInlineCompletionParticipant() { + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText("suggestion1"); + response.addInlineCompletionItem(item); + } + } + ); + + languageService.registerInlineCompletionParticipant( + new IInlineCompletionParticipant() { + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText("suggestion2"); + response.addInlineCompletionItem(item); + } + } + ); + + String xml = "|"; + testInlineCompletionFor(languageService, xml, "suggestion1", "suggestion2"); + } + + @Test + public void testInlineCompletionContext() throws BadLocationException { + // Register a participant that checks the context + languageService.registerInlineCompletionParticipant( + new IInlineCompletionParticipant() { + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText("context-aware"); + response.addInlineCompletionItem(item); + } + } + ); + + String xml = "|"; + testInlineCompletionFor(languageService, xml, "context-aware"); + } + + @Test + public void testInlineCompletionAtDifferentPositions() throws BadLocationException { + languageService.registerInlineCompletionParticipant( + new IInlineCompletionParticipant() { + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + int offset = request.getOffset(); + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText("offset:" + offset); + response.addInlineCompletionItem(item); + } + } + ); + + // Test at start of content + String xml1 = "|"; + testInlineCompletionFor(languageService, xml1, "offset:6"); + + // Test at end of content + String xml2 = "content|"; + testInlineCompletionFor(languageService, xml2, "offset:13"); + } + + @Test + public void testInlineCompletionWithEmptyDocument() throws BadLocationException { + String xml = "|"; + testInlineCompletionFor(languageService, xml, null, 0); + } + + @Test + public void testInlineCompletionWithNestedElements() throws BadLocationException { + languageService.registerInlineCompletionParticipant( + new IInlineCompletionParticipant() { + @Override + public void onInlineCompletion(IInlineCompletionRequest request, IInlineCompletionResponse response, + CancelChecker cancelChecker) { + InlineCompletionItem item = new InlineCompletionItem(); + item.setInsertText("nested"); + response.addInlineCompletionItem(item); + } + } + ); + + String xml = "|"; + testInlineCompletionFor(languageService, xml, "nested"); + } + +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/utils/XMLPositionUtilityTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/utils/XMLPositionUtilityTest.java index 96cffd1f4..51cd3895b 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/utils/XMLPositionUtilityTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/utils/XMLPositionUtilityTest.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2019 Red Hat Inc. and others. +* Copyright (c) 2019, 2026 Red Hat Inc. and others. * All rights reserved. This program and the accompanying materials * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v20.html @@ -201,4 +201,92 @@ private static void testMatchingTagPosition(String initialCursorText, String exp assertEquals(expectedCursorText, actualOutputString); } + + @Test + public void testFindUnclosedParentElementSimple() { + String xml = "|"; + testFindUnclosedParentElement(xml, "root"); + } + + @Test + public void testFindUnclosedParentElementWithContent() { + String xml = "content|"; + testFindUnclosedParentElement(xml, "root"); + } + + @Test + public void testFindUnclosedParentElementNested() { + String xml = "|"; + testFindUnclosedParentElement(xml, "child"); + } + + @Test + public void testFindUnclosedParentElementClosed() { + String xml = "|"; + testFindUnclosedParentElement(xml, null); + } + + @Test + public void testFindUnclosedParentElementSelfClosed() { + String xml = "|"; + testFindUnclosedParentElement(xml, null); + } + + @Test + public void testFindUnclosedParentElementWithAttributes() { + String xml = "|"; + testFindUnclosedParentElement(xml, "root"); + } + + @Test + public void testFindUnclosedParentElementBeforeStartTag() { + String xml = "|"; + testFindUnclosedParentElement(xml, null); + } + + @Test + public void testFindUnclosedParentElementInStartTag() { + String xml = ""; + testFindUnclosedParentElement(xml, null); + } + + @Test + public void testFindUnclosedParentElementMultipleNested() { + String xml = "|"; + testFindUnclosedParentElement(xml, "child"); + } + + @Test + public void testFindUnclosedParentElementAfterClosedChild() { + String xml = "|"; + testFindUnclosedParentElement(xml, "root"); + } + + /** + * Test findUnclosedParentElement for the given XML content. + * The '|' character marks the cursor position. + * + * @param xml the XML content with cursor position marked by '|' + * @param expectedTagName the expected tag name of the unclosed parent element, or null if none expected + */ + private static void testFindUnclosedParentElement(String xml, String expectedTagName) { + int offset = xml.indexOf('|'); + if (offset == -1) { + fail("XML must contain '|' to mark cursor position"); + } + + String xmlWithoutCursor = xml.substring(0, offset) + xml.substring(offset + 1); + DOMDocument document = DOMParser.getInstance().parse(xmlWithoutCursor, "test.xml", null); + + var node = document.findNodeAt(offset); + var unclosedElement = XMLPositionUtility.findUnclosedParentElement(node, offset); + + if (expectedTagName == null) { + Assertions.assertNull(unclosedElement, "Expected no unclosed parent element"); + } else { + Assertions.assertNotNull(unclosedElement, "Expected to find unclosed parent element"); + Assertions.assertEquals(expectedTagName, unclosedElement.getTagName(), + "Expected unclosed parent element tag name to be '" + expectedTagName + "'"); + } + } } \ No newline at end of file