diff --git a/org.eclipse.jdt.ls.core/plugin.xml b/org.eclipse.jdt.ls.core/plugin.xml index ac9cac2097..6493455232 100644 --- a/org.eclipse.jdt.ls.core/plugin.xml +++ b/org.eclipse.jdt.ls.core/plugin.xml @@ -5,6 +5,7 @@ + @@ -225,7 +226,6 @@ id="org.eclipse.jdt.core.javanature"> - + + + + + + + + This extension point represents different kinds of code completion handlers for the JDT LS. +Each extension must implement <code>org.eclipse.jdt.ls.core.internal.handlers.ICompletionHandler</code>. + + + + + + + + + + + + + + + + + a fully qualified identifier of the target extension point + + + + + + + an optional identifier of the extension instance + + + + + + + an optional name of the extension instance + + + + + + + + + + + + A unique identifier that can be used to reference this ICompletionHandler. + + + + + + + The class that implements this completion handler. The class must implement ICompletionHandler. + + + + + + + + + + + + + + + + The following is an example of a language server content provider extension: + +<pre> + <extension + id="completionHandler" + point="org.eclipse.jdt.ls.core.completionHandler"> + <completionHandler + id="someCompletionHandler" + class="com.example.SomeCompletionHandler" /> + </extension> +</pre> + + + + + + + diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java index 60fc5f01f4..6ea508a7f8 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java @@ -35,7 +35,7 @@ import org.eclipse.jdt.ls.core.internal.commands.VmCommand; import org.eclipse.jdt.ls.core.internal.framework.protobuf.ProtobufSupport; import org.eclipse.jdt.ls.core.internal.handlers.BundleUtils; -import org.eclipse.jdt.ls.core.internal.handlers.CompletionHandler; +import org.eclipse.jdt.ls.core.internal.handlers.CompletionHandlers; import org.eclipse.jdt.ls.core.internal.handlers.CreateModuleInfoHandler; import org.eclipse.jdt.ls.core.internal.handlers.FormatterHandler; import org.eclipse.jdt.ls.core.internal.handlers.PasteEventHandler; @@ -191,10 +191,10 @@ public Object executeCommand(String commandId, List arguments, IProgress return false; } case "java.completion.onDidSelect": - CompletionHandler completionHandler = new CompletionHandler(JavaLanguageServerPlugin.getPreferencesManager()); + CompletionHandlers completionHandlers = JavaLanguageServerPlugin.getInstance().getCompletionHandlers(); String requestId = (String) arguments.get(0); String proposalId = (String) arguments.get(1); - completionHandler.onDidCompletionItemSelect(requestId, proposalId); + completionHandlers.onDidCompletionItemSelect(requestId, proposalId); return new Object(); case "java.decompile": String uri = (String) arguments.get(0); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java index dbc02eed0b..4314f55aea 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java @@ -55,6 +55,7 @@ import org.eclipse.jdt.ls.core.internal.corext.template.java.JavaLanguageServerTemplateStore; import org.eclipse.jdt.ls.core.internal.handlers.BundleUtils; import org.eclipse.jdt.ls.core.internal.handlers.CompletionContributionService; +import org.eclipse.jdt.ls.core.internal.handlers.CompletionHandlers; import org.eclipse.jdt.ls.core.internal.handlers.JDTLanguageServer; import org.eclipse.jdt.ls.core.internal.handlers.LogHandler; import org.eclipse.jdt.ls.core.internal.managers.ContentProviderManager; @@ -137,6 +138,8 @@ public class JavaLanguageServerPlugin extends Plugin { private CompletionContributionService completionContributionService; private LogHandler logHandler; + private CompletionHandlers completionHandlers; + public static LanguageServerApplication getLanguageServer() { return pluginInstance == null ? null : pluginInstance.languageServer; } @@ -167,6 +170,7 @@ public void start(BundleContext bundleContext) throws Exception { preferenceManager = new StandardPreferenceManager(); projectsManager = new StandardProjectsManager(preferenceManager); } + completionHandlers = new CompletionHandlers(preferenceManager); digestStore = new DigestStore(getStateLocation().toFile()); try { ResourcesPlugin.getWorkspace().addSaveParticipant(IConstants.PLUGIN_ID, projectsManager); @@ -427,6 +431,10 @@ public WorkingCopyOwner getWorkingCopyOwner() { return this.protocol.getWorkingCopyOwner(); } + public CompletionHandlers getCompletionHandlers() { + return completionHandlers; + } + public static JavaLanguageServerPlugin getInstance() { return pluginInstance; } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandler.java index 4a6172ebdb..ab5845e52e 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandler.java @@ -13,16 +13,13 @@ package org.eclipse.jdt.ls.core.internal.handlers; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.ProgressMonitorWrapper; @@ -37,9 +34,6 @@ import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.manipulation.SharedASTProviderCore; import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; -import org.eclipse.jdt.ls.core.contentassist.CompletionRanking; -import org.eclipse.jdt.ls.core.contentassist.ICompletionRankingProvider; -import org.eclipse.jdt.ls.core.internal.ExceptionFactory; import org.eclipse.jdt.ls.core.internal.JDTEnvironmentUtils; import org.eclipse.jdt.ls.core.internal.JDTUtils; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; @@ -51,7 +45,6 @@ import org.eclipse.jdt.ls.core.internal.contentassist.SortTextHelper; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; import org.eclipse.jdt.ls.core.internal.syntaxserver.ModelBasedCompletionEngine; -import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.CompletionItemOptions; @@ -60,10 +53,9 @@ import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.CompletionTriggerKind; import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -public class CompletionHandler{ +public class CompletionHandler implements ICompletionHandler { public final static CompletionOptions getDefaultCompletionOptions(PreferenceManager preferenceManager) { CompletionOptions completionOptions = new CompletionOptions(Boolean.TRUE, List.of(".", "@", "#", "*", " ")); @@ -101,19 +93,15 @@ public int compare(CompletionItem o1, CompletionItem o2) { }; - // TODO: we can consider to cache more detailed context so that the information can also - // be used by features like inlay hint. - public static CompletionProposal selectedProposal; - private PreferenceManager manager; public CompletionHandler(PreferenceManager manager) { this.manager = manager; } - public Either, CompletionList> completion(CompletionParams params, + @Override + public CompletionList completion(CompletionParams params, IProgressMonitor monitor) { - long startTime = System.currentTimeMillis(); CompletionList $ = null; try { ICompilationUnit unit = JDTUtils.resolveCompilationUnit(params.getTextDocument().getUri()); @@ -137,96 +125,7 @@ public Either, CompletionList> completion(CompletionParams } else { JavaLanguageServerPlugin.logInfo("Completion request completed"); } - long executionTime = System.currentTimeMillis() - startTime; - String lastRequestId = null; - for (CompletionItem item : $.getItems()) { - String requestId = ""; - String proposalId = ""; - @SuppressWarnings("unchecked") - Map data = (Map) item.getData(); - if (data != null) { - requestId = data.getOrDefault(CompletionResolveHandler.DATA_FIELD_REQUEST_ID, ""); - proposalId = data.getOrDefault(CompletionResolveHandler.DATA_FIELD_PROPOSAL_ID, ""); - } - if (requestId.isEmpty() || proposalId.isEmpty()) { - continue; - } - item.setCommand(new Command("", "java.completion.onDidSelect", Arrays.asList( - requestId, - proposalId - ))); - - if (Objects.equals(requestId, lastRequestId)) { - continue; - } - lastRequestId = requestId; - int pId = Integer.parseInt(proposalId); - long rId = Long.parseLong(requestId); - CompletionResponse completionResponse = CompletionResponses.get(rId); - if (completionResponse == null || completionResponse.getProposals().size() <= pId) { - JavaLanguageServerPlugin.logError("Failed to save common data for completion items."); - continue; - } - completionResponse.setCommonData(CompletionRanking.COMPLETION_EXECUTION_TIME, String.valueOf(executionTime)); - } - return Either.forRight($); - } - - @SuppressWarnings("unchecked") - public void onDidCompletionItemSelect(String requestId, String proposalId) throws CoreException { - triggerSignatureHelp(); - if (proposalId.isEmpty() || requestId.isEmpty()) { - return; - } - int pId = Integer.parseInt(proposalId); - long rId = Long.parseLong(requestId); - CompletionResponse completionResponse = CompletionResponses.get(rId); - if (completionResponse == null || completionResponse.getItems().size() <= pId - || completionResponse.getProposals().size() <= pId) { - throw ExceptionFactory.newException("Cannot get completion responses."); - } - - CompletionProposal proposal = completionResponse.getProposals().get(pId); - - // clear the cache if failed to get the selected proposal. - if (proposal == null) { - selectedProposal = null; - } else if (proposal.getKind() == CompletionProposal.METHOD_REF - || proposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION - || proposal.getKind() == CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER) { - selectedProposal = proposal; - } - CompletionItem item = completionResponse.getItems().get(pId); - if (item == null) { - throw ExceptionFactory.newException("Cannot get the completion item."); - } - - // get the cached completion execution time and set it to the selected item in case that providers need it. - String executionTime = completionResponse.getCommonData(CompletionRanking.COMPLETION_EXECUTION_TIME); - if (executionTime != null) { - ((Map)item.getData()).put(CompletionRanking.COMPLETION_EXECUTION_TIME, executionTime); - } - - Map contributedData = completionResponse.getCompletionItemData(pId); - if (contributedData != null) { - ((Map)item.getData()).putAll(contributedData); - } - - List providers = - ((CompletionContributionService) JavaLanguageServerPlugin.getCompletionContributionService()).getRankingProviders(); - for (ICompletionRankingProvider provider : providers) { - provider.onDidCompletionItemSelect(item); - } - } - - private void triggerSignatureHelp() { - if (manager.getPreferences().isSignatureHelpEnabled()) { - String onSelectedCommand = manager.getClientPreferences().getCompletionItemCommand(); - if (!onSelectedCommand.isEmpty() && manager.getClientPreferences().isExecuteClientCommandSupport()) { - JavaLanguageServerPlugin.getInstance().getClientConnection() - .executeClientCommand(onSelectedCommand); - } - } + return $; } private CompletionList computeContentAssist(ICompilationUnit unit, CompletionParams params, IProgressMonitor monitor) throws JavaModelException { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlers.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlers.java new file mode 100644 index 0000000000..d1b8318ae6 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlers.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 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 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + * Simeon Andreev - support completion handler extension + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.CompletionProposal; +import org.eclipse.jdt.ls.core.contentassist.CompletionRanking; +import org.eclipse.jdt.ls.core.internal.ExceptionFactory; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemDefaults; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + + +public final class CompletionHandlers extends ExtensionRepository { + + public static final String EXTENSION_POINT_ID = "org.eclipse.jdt.ls.core.completionHandler"; + + private static final String DATA_FIELD_HANDLER_ID = "hid"; + + private final PreferenceManager manager; + + // TODO: we can consider to cache more detailed context so that the information can also + // be used by features like inlay hint. + public static CompletionProposal selectedProposal; + + public CompletionHandlers(PreferenceManager manager) { + super(EXTENSION_POINT_ID, ICompletionHandler.class); + this.manager = manager; + CompletionHandler defaultHandler = new CompletionHandler(manager); + setDefaultImplementers(Arrays.asList(defaultHandler)); + } + + public Either, CompletionList> completion(CompletionParams params, IProgressMonitor monitor) { + boolean isIncomplete = false; + List completions = new ArrayList<>(); + CompletionItemDefaults defaults = new CompletionItemDefaults(); + List handlers = getCompletionHandlers(); + boolean defaultsSupport = manager.getClientPreferences().isCompletionListItemDefaultsSupport(); + for (int i = 0; i < handlers.size(); ++i) { + if (monitor.isCanceled()) { + break; + } + long startTime = System.currentTimeMillis(); + String handlerId = String.valueOf(i); + ICompletionHandler handler = handlers.get(i); + CompletionList c = handler.completion(params, monitor); + if (c != null) { + isIncomplete |= c.isIncomplete(); + if (defaults == null && defaultsSupport) { + defaults = c.getItemDefaults(); + } + long executionTime = System.currentTimeMillis() - startTime; + String lastRequestId = null; + for (CompletionItem item : c.getItems()) { + String requestId = ""; + String proposalId = ""; + @SuppressWarnings("unchecked") + Map data = (Map) item.getData(); + if (data != null) { + requestId = data.getOrDefault(CompletionResolveHandler.DATA_FIELD_REQUEST_ID, ""); + proposalId = data.getOrDefault(CompletionResolveHandler.DATA_FIELD_PROPOSAL_ID, ""); + } + if (requestId.isEmpty() || proposalId.isEmpty()) { + continue; + } + item.setCommand(new Command("", "java.completion.onDidSelect", Arrays.asList( + requestId, + proposalId + ))); + if (Objects.equals(requestId, lastRequestId)) { + continue; + } + lastRequestId = requestId; + int pId = Integer.parseInt(proposalId); + long rId = Long.parseLong(requestId); + CompletionResponse completionResponse = CompletionResponses.get(rId); + if (completionResponse == null || completionResponse.getProposals().size() <= pId) { + JavaLanguageServerPlugin.logError("Failed to save common data for completion items."); + continue; + } + completionResponse.setCommonData(CompletionRanking.COMPLETION_EXECUTION_TIME, String.valueOf(executionTime)); + completionResponse.setCommonData(DATA_FIELD_HANDLER_ID, handlerId); + } + completions.addAll(c.getItems()); + } + } + return Either.forRight(new CompletionList(isIncomplete, completions, defaults)); + } + + @SuppressWarnings("unchecked") + public void onDidCompletionItemSelect(String requestId, String proposalId) throws CoreException { + triggerSignatureHelp(); + if (proposalId.isEmpty() || requestId.isEmpty()) { + return; + } + int pId = Integer.parseInt(proposalId); + long rId = Long.parseLong(requestId); + CompletionResponse completionResponse = CompletionResponses.get(rId); + if (completionResponse == null || completionResponse.getItems().size() <= pId || completionResponse.getProposals().size() <= pId) { + throw ExceptionFactory.newException("Cannot get completion responses."); + } + + CompletionProposal proposal = completionResponse.getProposals().get(pId); + + // clear the cache if failed to get the selected proposal. + if (proposal == null) { + selectedProposal = null; + } else if (proposal.getKind() == CompletionProposal.METHOD_REF || proposal.getKind() == CompletionProposal.CONSTRUCTOR_INVOCATION || proposal.getKind() == CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER) { + selectedProposal = proposal; + } + CompletionItem item = completionResponse.getItems().get(pId); + if (item == null) { + throw ExceptionFactory.newException("Cannot get the completion item."); + } + + // get the cached completion execution time and set it to the selected item in case that providers need it. + String executionTime = completionResponse.getCommonData(CompletionRanking.COMPLETION_EXECUTION_TIME); + if (executionTime != null) { + ((Map) item.getData()).put(CompletionRanking.COMPLETION_EXECUTION_TIME, executionTime); + } + + Map contributedData = completionResponse.getCompletionItemData(pId); + if (contributedData != null) { + ((Map) item.getData()).putAll(contributedData); + } + String index = completionResponse.getCommonData(DATA_FIELD_HANDLER_ID); + if (index == null || index.isEmpty()) { + index = "0"; + } + int i = Integer.parseInt(index); + List handlers = getCompletionHandlers(); + if (0 <= i && i < handlers.size()) { + ICompletionHandler handler = handlers.get(i); + handler.onDidCompletionItemSelect(item); + } + } + + private void triggerSignatureHelp() { + if (manager.getPreferences().isSignatureHelpEnabled()) { + String onSelectedCommand = manager.getClientPreferences().getCompletionItemCommand(); + if (!onSelectedCommand.isEmpty() && manager.getClientPreferences().isExecuteClientCommandSupport()) { + JavaLanguageServerPlugin.getInstance().getClientConnection().executeClientCommand(onSelectedCommand); + } + } + } + + private List getCompletionHandlers() { + return getImplementers(); + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ExtensionRepository.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ExtensionRepository.java new file mode 100644 index 0000000000..658b24a814 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ExtensionRepository.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) Simeon Andreev and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtension; +import org.eclipse.core.runtime.IExtensionPoint; +import org.eclipse.core.runtime.IRegistryEventListener; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; + +/** + * @author Simeon Andreev + * + */ +public class ExtensionRepository implements IRegistryEventListener { + + private static final String CLASS = "class"; + + private final String extensionPointId; + private final Class interfaceType; + private final Object lock; + private List defaultImplementers; + private List implementers; + + public ExtensionRepository(String extensionPointId, Class interfaceType) { + this.extensionPointId = extensionPointId; + this.interfaceType = interfaceType; + lock = new Object(); + defaultImplementers = Collections.emptyList(); + Platform.getExtensionRegistry().addListener(this, extensionPointId); + } + + public void dispose() { + clear(); + Platform.getExtensionRegistry().removeListener(this); + } + + protected void setDefaultImplementers(List defaultImplementers) { + this.defaultImplementers = defaultImplementers; + } + + protected List getImplementers() { + synchronized (lock) { + if (implementers != null) { + return new ArrayList<>(implementers); + } + } + List contributedHandlers = loadImplementers(); + synchronized (lock) { + if (implementers == null) { + implementers = new ArrayList<>(contributedHandlers.size() + defaultImplementers.size()); + implementers.addAll(defaultImplementers); + implementers.addAll(contributedHandlers); + } + return new ArrayList<>(implementers); + } + } + + private List loadImplementers() { + IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(extensionPointId); + List implementers = new ArrayList<>(elements.length); + for (IConfigurationElement element : elements) { + try { + Object extension = element.createExecutableExtension(CLASS); + try { + @SuppressWarnings("unchecked") + T implementation = (T) extension; + implementers.add(implementation); + } catch (ClassCastException e) { + JavaLanguageServerPlugin.logError("Invalid extension to " + extensionPointId + ". Must extend " + interfaceType.getName()); + } + } catch (CoreException e) { + JavaLanguageServerPlugin.logException("Unable to create completion handler", e); + } + } + return implementers; + } + + @Override + public void added(IExtension[] extensions) { + clear(); + } + + @Override + public void removed(IExtension[] extensions) { + clear(); + } + + @Override + public void added(IExtensionPoint[] extensionPoints) { + } + + @Override + public void removed(IExtensionPoint[] extensionPoints) { + } + + private void clear() { + synchronized (lock) { + implementers = null; + } + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ICompletionHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ICompletionHandler.java new file mode 100644 index 0000000000..a78d7c3de7 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ICompletionHandler.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.ls.core.contentassist.ICompletionRankingProvider; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; + +/** + * @author Simeon Andreev + * + */ +public interface ICompletionHandler { + + CompletionList completion(CompletionParams params, IProgressMonitor monitor); + + default void onDidCompletionItemSelect(CompletionItem item) throws CoreException { + List providers = ((CompletionContributionService) JavaLanguageServerPlugin.getCompletionContributionService()).getRankingProviders(); + for (ICompletionRankingProvider provider : providers) { + provider.onDidCompletionItemSelect(item); + } + } + +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java index 80a883c6c7..a40c93129c 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java @@ -189,6 +189,7 @@ public class JDTLanguageServer extends BaseJDTLanguageServer implements Language private ClasspathUpdateHandler classpathUpdateHandler; private JVMConfigurator jvmConfigurator; private WorkspaceExecuteCommandHandler commandHandler; + private CompletionHandlers codeCompletion; private TypeHierarchyHandler typeHierarchyHandler = new TypeHierarchyHandler(); private ProgressReporterManager progressReporterManager; @@ -246,6 +247,7 @@ public JDTLanguageServer(ProjectsManager projects, PreferenceManager preferenceM JavaRuntime.addVMInstallChangedListener(jvmConfigurator); this.commandHandler = commandHandler; this.telemetryManager = telemetryManager; + this.codeCompletion = new CompletionHandlers(preferenceManager); } @Override @@ -260,6 +262,7 @@ public void connectClient(JavaLanguageClient client) { this.telemetryManager.setLanguageClient(client); this.telemetryManager.setPreferenceManager(preferenceManager); this.telemetryManager.setProjectseManager(pm); + this.codeCompletion = new CompletionHandlers(preferenceManager); } // For testing purpose @@ -648,12 +651,11 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { public CompletableFuture, CompletionList>> completion(CompletionParams position) { debugTrace(">> document/completion"); try { - CompletionHandler handler = new CompletionHandler(preferenceManager); IProgressMonitor monitor = new NullProgressMonitor(); if (Boolean.getBoolean(JAVA_LSP_JOIN_ON_COMPLETION)) { waitForLifecycleJobs(monitor); } - Either, CompletionList> result = handler.completion(position, monitor); + Either, CompletionList> result = codeCompletion.completion(position, monitor); return CompletableFuture.completedFuture(result); } catch (Exception ex) { return CompletableFuture.failedFuture(ex); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpUtils.java index bcf943da4b..cdafbd6ce4 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpUtils.java @@ -88,11 +88,11 @@ public static SignatureHelp getSignatureHelpFromASTNode(ICompilationUnit unit, i } // first check if we should use the proposal from the last selected completion item. - if (CompletionHandler.selectedProposal != null) { + if (CompletionHandlers.selectedProposal != null) { for (int i = 0; i < infos.size(); i++) { SignatureInformation signatureInformation = infos.get(i); CompletionProposal proposal = collector.getInfoProposals().get(signatureInformation); - if (Arrays.equals(proposal.getSignature(), CompletionHandler.selectedProposal.getSignature())) { + if (Arrays.equals(proposal.getSignature(), CompletionHandlers.selectedProposal.getSignature())) { int activeParameter = getActiveParameter(triggerOffset, proposal , context); if (activeParameter >= 0) { help.setActiveSignature(i); @@ -104,7 +104,7 @@ public static SignatureHelp getSignatureHelpFromASTNode(ICompilationUnit unit, i } // if not matching with the last selected proposal, clear the cache and fallback to guess strategy. - CompletionHandler.selectedProposal = null; + CompletionHandlers.selectedProposal = null; for (int i = 0; i < infos.size(); i++) { SignatureInformation signatureInformation = infos.get(i); CompletionProposal proposal = collector.getInfoProposals().get(signatureInformation); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/syntaxserver/SyntaxLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/syntaxserver/SyntaxLanguageServer.java index 0137116927..90fa8fcf44 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/syntaxserver/SyntaxLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/syntaxserver/SyntaxLanguageServer.java @@ -42,6 +42,7 @@ import org.eclipse.jdt.ls.core.internal.JobHelpers; import org.eclipse.jdt.ls.core.internal.ServiceStatus; import org.eclipse.jdt.ls.core.internal.handlers.BaseDocumentLifeCycleHandler; +import org.eclipse.jdt.ls.core.internal.handlers.CompletionHandlers; import org.eclipse.jdt.ls.core.internal.handlers.CompletionHandler; import org.eclipse.jdt.ls.core.internal.handlers.CompletionResolveHandler; import org.eclipse.jdt.ls.core.internal.handlers.DocumentHighlightHandler; @@ -109,6 +110,7 @@ public class SyntaxLanguageServer extends BaseJDTLanguageServer implements Langu private ContentProviderManager contentProviderManager; private ProjectsManager projectsManager; private PreferenceManager preferenceManager; + private CompletionHandlers codeCompletion; private Job shutdownJob = new Job("Shutdown...") { @Override @@ -136,6 +138,7 @@ public SyntaxLanguageServer(ContentProviderManager contentProviderManager, this.contentProviderManager = contentProviderManager; this.projectsManager = projectsManager; this.preferenceManager = preferenceManager; + this.codeCompletion = new CompletionHandlers(preferenceManager); this.documentLifeCycleHandler = new SyntaxDocumentLifeCycleHandler(null, projectsManager, preferenceManager, delayValidation); } @@ -413,14 +416,13 @@ public CompletableFuture hover(HoverParams position) { @Override public CompletableFuture, CompletionList>> completion(CompletionParams position) { logInfo(">> document/completion"); - CompletionHandler handler = new CompletionHandler(preferenceManager); final IProgressMonitor[] monitors = new IProgressMonitor[1]; CompletableFuture, CompletionList>> result = computeAsync((monitor) -> { monitors[0] = monitor; if (Boolean.getBoolean(JAVA_LSP_JOIN_ON_COMPLETION)) { waitForLifecycleJobs(monitor); } - return handler.completion(position, monitor); + return codeCompletion.completion(position, monitor); }); result.join(); if (monitors[0].isCanceled()) { diff --git a/org.eclipse.jdt.ls.tests/plugin.xml b/org.eclipse.jdt.ls.tests/plugin.xml index 5ba70a1f00..cd41a3cdf0 100644 --- a/org.eclipse.jdt.ls.tests/plugin.xml +++ b/org.eclipse.jdt.ls.tests/plugin.xml @@ -48,4 +48,11 @@ order ="300" class = "org.eclipse.jdt.ls.core.internal.managers.NoopImporter"/> + + + diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlerTest.java index 665f498a99..4d3d40f997 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlerTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionHandlerTest.java @@ -106,7 +106,7 @@ public class CompletionHandlerTest extends AbstractCompilationUnitBasedTest { private DocumentLifeCycleHandler lifeCycleHandler; private JavaClientConnection javaClient; - private static String COMPLETION_TEMPLATE = + static String COMPLETION_TEMPLATE = "{\n" + " \"id\": \"1\",\n" + " \"method\": \"textDocument/completion\",\n" + @@ -4150,8 +4150,12 @@ private CompletionList requestCompletions(ICompilationUnit unit, String complete return server.completion(JsonMessageHelper.getParams(createCompletionRequest(unit, loc[0], loc[1]))).join().getRight(); } - private String createCompletionRequest(ICompilationUnit unit, int line, int kar) { - return COMPLETION_TEMPLATE.replace("${file}", JDTUtils.toURI(unit)) + static String createCompletionRequest(ICompilationUnit unit, int line, int kar) { + return createCompletionRequest(JDTUtils.toURI(unit), line, kar); + } + + static String createCompletionRequest(String uri, int line, int kar) { + return COMPLETION_TEMPLATE.replace("${file}", uri) .replace("${line}", String.valueOf(line)) .replace("${char}", String.valueOf(kar)); } @@ -4165,6 +4169,10 @@ private void mockLSP2Client() { } private void mockLSPClient(boolean isSnippetSupported, boolean isSignatureHelpSupported) { + mockLSPClient(preferenceManager, isSnippetSupported, isSignatureHelpSupported); + } + + static void mockLSPClient(PreferenceManager preferenceManager, boolean isSnippetSupported, boolean isSignatureHelpSupported) { // Mock the preference manager to use LSP v3 support. when(preferenceManager.getClientPreferences().isCompletionSnippetsSupported()).thenReturn(isSnippetSupported); when(preferenceManager.getClientPreferences().isSignatureHelpSupported()).thenReturn(isSignatureHelpSupported); diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionRankingProviderTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionRankingProviderTest.java index 7b793317d2..c8098674b0 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionRankingProviderTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CompletionRankingProviderTest.java @@ -110,7 +110,7 @@ public void testOnDidCompletionItemSelect() throws Exception {ICompilationUnit u "}\n"); requestCompletions(unit, "Integer."); - CompletionHandler handler = new CompletionHandler(JavaLanguageServerPlugin.getPreferencesManager()); + CompletionHandlers handler = new CompletionHandlers(JavaLanguageServerPlugin.getPreferencesManager()); ArgumentCaptor argument = ArgumentCaptor.forClass(CompletionItem.class); handler.onDidCompletionItemSelect(String.valueOf((new CompletionResponse()).getId() - 1), "0"); diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ContributedCompletionHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ContributedCompletionHandlerTest.java new file mode 100644 index 0000000000..9aeeb487c8 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ContributedCompletionHandlerTest.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) Simeon Andreev and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.manipulation.CoreASTProvider; +import org.eclipse.jdt.ls.core.internal.JsonMessageHelper; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * @author Simeon Andreev + * + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ContributedCompletionHandlerTest extends AbstractCompilationUnitBasedTest { + + private IFile file; + + @BeforeEach + public void setUp() throws Exception { + ContributedTestCompletionHandler.item = ContributedTestCompletionHandler.testItem(); + mockLSP3Client(); + CoreASTProvider sharedASTProvider = CoreASTProvider.getInstance(); + sharedASTProvider.disposeAST(); + file = project.getFile("test.txt"); + file.create(new String("test").getBytes(), IResource.FORCE, monitor); + } + + @AfterEach + public void removeExtension() throws CoreException { + ContributedTestCompletionHandler.item = null; + file.delete(true, monitor); + } + + @Test + public void testCompletion_contributedHandler() throws Exception { + when(preferenceManager.getClientPreferences().isCompletionItemLabelDetailsSupport()).thenReturn(true); + CompletionList list = requestCompletions(file.getLocationURI().toString(), 0, 0); + assertNotNull(list); + List filtered = list.getItems().stream().filter((item) -> { + return item.getDetail() != null && item.getDetail().startsWith(ContributedTestCompletionHandler.TEST_DETAIL); + }).collect(Collectors.toList()); + assertEquals(1, filtered.size(), "No test proposals"); + CompletionItem oride = filtered.get(0); + assertEquals(ContributedTestCompletionHandler.TEST_CONTENT, oride.getInsertText()); + } + + private CompletionList requestCompletions(String uri, int line, int offset) throws JavaModelException { + return server.completion(JsonMessageHelper.getParams(CompletionHandlerTest.createCompletionRequest(uri, line, offset))).join().getRight(); + } + + private void mockLSP3Client() { + CompletionHandlerTest.mockLSPClient(preferenceManager, true, true); + } +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ContributedTestCompletionHandler.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ContributedTestCompletionHandler.java new file mode 100644 index 0000000000..6650900638 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ContributedTestCompletionHandler.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; + +/** + * @author Simeon Andreev + * + */ +public class ContributedTestCompletionHandler implements ICompletionHandler { + + public static final String TEST_DETAIL = "test completion item"; + public static final String TEST_CONTENT = "test string"; + + public static CompletionItem item = null; + + @Override + public CompletionList completion(CompletionParams params, IProgressMonitor monitor) { + List items = Collections.emptyList(); + if (item != null) { + items = Arrays.asList(item); + } + return new CompletionList(items); + } + + public static CompletionItem testItem() { + CompletionItem item = new CompletionItem(); + item.setInsertText(TEST_CONTENT); + item.setDetail(TEST_DETAIL); + return item; + } +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpHandlerTest.java index 07867c8eb8..0a5c83a649 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpHandlerTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SignatureHelpHandlerTest.java @@ -1223,8 +1223,8 @@ public foo() { CompletionProposalRequestor collector = new CompletionProposalRequestor(cu, offset, preferenceManager); cu.codeComplete(offset, collector, monitor); - CompletionHandler.selectedProposal = collector.getProposals().get(0); - StringBuilder description = CompletionProposalDescriptionProvider.createMethodProposalDescription(CompletionHandler.selectedProposal); + CompletionHandlers.selectedProposal = collector.getProposals().get(0); + StringBuilder description = CompletionProposalDescriptionProvider.createMethodProposalDescription(CompletionHandlers.selectedProposal); String fromProposal = description.toString(); String unnamedResult = "String(byte[] arg0, int arg1, int arg2, Charset arg3)"; String namedResult = "String(byte[] bytes, int offset, int length, Charset charset)";