Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*******************************************************************************
* Copyright (c) 2025 Vegard IT GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation
*******************************************************************************/
package org.eclipse.lsp4e.test.internal;

import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.ICodeMining;
import org.eclipse.lsp4e.internal.AbstractLSPCodeMiningProvider;
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
import org.eclipse.lsp4e.test.utils.TestUtils;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.Test;

/**
* Verifies that {@link AbstractLSPCodeMiningProvider} cancels the previous
* in-flight request for the same document when a new one starts.
*/
class AbstractLSPCodeMiningProviderTest extends AbstractTestWithProject {

private static final class StubProvider extends AbstractLSPCodeMiningProvider {
@Override
protected @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument doc,
TextDocumentIdentifier docId) {
return new CompletableFuture<>();
}
}

@Test
void cancelsPreviousRequestForSameDocument() throws Exception {
IFile file = TestUtils.createUniqueTestFile(project, "txt", "content");
ITextViewer viewer = TestUtils.openTextViewer(file);

var provider = new StubProvider();

var first = provider.provideCodeMinings(viewer, new NullProgressMonitor());
assertNotNull(first);
assertFalse(first.isCancelled(), "first request should start uncancelled");

var second = provider.provideCodeMinings(viewer, new NullProgressMonitor());
assertNotNull(second);

assertTrue(first.isCancelled(), "previous request should be cancelled when a new one starts");
assertFalse(second.isCancelled(), "new request should remain active");
}

@Test
void keepsRequestsIndependentAcrossDocuments() throws Exception {
IFile file1 = TestUtils.createUniqueTestFile(project, "txt", "one");
IFile file2 = TestUtils.createUniqueTestFile(project, "txt", "two");
ITextViewer viewer1 = TestUtils.openTextViewer(file1);
ITextViewer viewer2 = TestUtils.openTextViewer(file2);

var provider = new StubProvider();

var first = provider.provideCodeMinings(viewer1, new NullProgressMonitor());
var second = provider.provideCodeMinings(viewer2, new NullProgressMonitor());

assertNotNull(first);
assertNotNull(second);
assertFalse(first.isCancelled(), "request for doc1 must remain active when doc2 starts");
assertFalse(second.isCancelled(), "request for doc2 must start active");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*******************************************************************************
* Copyright (c) 2025 Vegard IT GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
*******************************************************************************/
package org.eclipse.lsp4e.internal;

import java.net.URI;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
import org.eclipse.jface.text.codemining.ICodeMining;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4j.TextDocumentIdentifier;

/**
* Base class for LSP-backed code mining providers that:
* <ul>
* <li>compute code minings asynchronously per document using LSP requests</li>
* <li>track at most one in-flight request per document and cancel the previous
* one when a new computation starts</li>
* </ul>
*/
public abstract class AbstractLSPCodeMiningProvider extends AbstractCodeMiningProvider {

private final ConcurrentMap<IDocument, CompletableFuture<List<? extends ICodeMining>>> pendingRequests = new ConcurrentHashMap<>();

/**
* Computes code minings for the given document.
*
* @return a future producing the list of code minings, or {@code null} if no
* code minings are available
*/
protected abstract @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument doc,
Comment thread
sebthom marked this conversation as resolved.
TextDocumentIdentifier docId);

@Override
public final @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(final ITextViewer viewer,
final IProgressMonitor monitor) {
final IDocument document = viewer.getDocument();
if (document == null) {
return null;
}

final URI docURI = LSPEclipseUtils.toUri(document);
if (docURI == null)
return null;

final TextDocumentIdentifier docId = LSPEclipseUtils.toTextDocumentIdentifier(docURI);

final var current = doProvideCodeMinings(document, docId);
final CompletableFuture<List<? extends ICodeMining>> previous;
if (current == null) {
previous = pendingRequests.remove(document);
} else {
previous = pendingRequests.put(document, current);
}
if (previous != null && !previous.isDone()) {
previous.cancel(true);
}

return current;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,36 @@
*******************************************************************************/
package org.eclipse.lsp4e.operations.codelens;

import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
import org.eclipse.jface.text.codemining.ICodeMining;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServerWrapper;
import org.eclipse.lsp4e.LanguageServers;
import org.eclipse.lsp4e.LanguageServers.LanguageServerDocumentExecutor;
import org.eclipse.lsp4e.internal.AbstractLSPCodeMiningProvider;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.TextDocumentIdentifier;

public class CodeLensProvider extends AbstractCodeMiningProvider {
public class CodeLensProvider extends AbstractLSPCodeMiningProvider {

private @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(IDocument document) {
URI docURI = LSPEclipseUtils.toUri(document);
if (docURI != null) {
final var param = new CodeLensParams(LSPEclipseUtils.toTextDocumentIdentifier(docURI));
LanguageServerDocumentExecutor executor = LanguageServers.forDocument(document)
.withFilter(sc -> sc.getCodeLensProvider() != null);
return executor
.collectAll((w, ls) -> ls.getTextDocumentService().codeLens(param)
.thenApply(codeLenses -> LanguageServers.streamSafely(codeLenses)
.map(codeLens -> toCodeMining(document, w, codeLens)).filter(Objects::nonNull)))
.thenApply(result -> result.stream().flatMap(s -> s).toList());
} else {
return null;
}
@Override
protected @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument document,
TextDocumentIdentifier docId) {
final var param = new CodeLensParams(docId);
LanguageServerDocumentExecutor executor = LanguageServers.forDocument(document)
.withFilter(sc -> sc.getCodeLensProvider() != null);
return executor
.collectAll((w, ls) -> ls.getTextDocumentService().codeLens(param)
.thenApply(codeLenses -> LanguageServers.streamSafely(codeLenses)
.map(codeLens -> toCodeMining(document, w, codeLens)).filter(Objects::nonNull)))
.thenApply(result -> result.stream().flatMap(s -> s).toList());
}

private @Nullable LSPCodeMining toCodeMining(IDocument document, LanguageServerWrapper languageServerWrapper,
Expand All @@ -58,12 +52,4 @@ public class CodeLensProvider extends AbstractCodeMiningProvider {
return null;
}
}

@Override
public @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer,
IProgressMonitor monitor) {
IDocument document = viewer.getDocument();
return document != null ? provideCodeMinings(document) : null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@
*/
package org.eclipse.lsp4e.operations.color;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
import org.eclipse.jface.text.codemining.ICodeMining;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServerWrapper;
import org.eclipse.lsp4e.LanguageServers;
import org.eclipse.lsp4e.internal.AbstractLSPCodeMiningProvider;
import org.eclipse.lsp4j.ColorInformation;
import org.eclipse.lsp4j.DocumentColorParams;
import org.eclipse.lsp4j.ServerCapabilities;
Expand All @@ -41,34 +37,28 @@
/**
* Consume the 'textDocument/documentColor' request to decorate color references
* in the editor.
*
*/
public class DocumentColorProvider extends AbstractCodeMiningProvider {
public class DocumentColorProvider extends AbstractLSPCodeMiningProvider {

private final Map<RGBA, Color> colorTable;

public DocumentColorProvider() {
colorTable = new HashMap<>();
}

private @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(IDocument document) {
URI docURI = LSPEclipseUtils.toUri(document);

if (docURI != null) {
final var textDocumentIdentifier = LSPEclipseUtils.toTextDocumentIdentifier(docURI);
final var param = new DocumentColorParams(textDocumentIdentifier);
return LanguageServers.forDocument(document)
.withCapability(ServerCapabilities::getColorProvider)
.collectAll(
// Need to do some of the result processing inside the function we supply to collectAll(...)
// as need the LSW to construct the ColorInformationMining
(wrapper, ls) -> ls.getTextDocumentService().documentColor(param)
.thenApply(colors -> LanguageServers.streamSafely(colors)
.map(color -> toMining(color, document, textDocumentIdentifier, wrapper))))
.thenApply(res -> res.stream().flatMap(Function.identity()).filter(Objects::nonNull).toList());
} else {
return null;
}
@Override
protected @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument document,
TextDocumentIdentifier docId) {
final var param = new DocumentColorParams(docId);
return LanguageServers.forDocument(document)
.withCapability(ServerCapabilities::getColorProvider)
.collectAll(
// Need to do some of the result processing inside the function we supply to collectAll(...)
// as need the LSW to construct the ColorInformationMining
(wrapper, ls) -> ls.getTextDocumentService().documentColor(param)
.thenApply(colors -> LanguageServers.streamSafely(colors)
.map(color -> toMining(color, document, docId, wrapper))))
.thenApply(res -> res.stream().flatMap(Function.identity()).filter(Objects::nonNull).toList());
}

private @Nullable ColorInformationMining toMining(ColorInformation color, IDocument document, TextDocumentIdentifier textDocumentIdentifier, LanguageServerWrapper wrapper) {
Expand All @@ -82,13 +72,6 @@ public DocumentColorProvider() {
return null;
}

@Override
public @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer,
IProgressMonitor monitor) {
IDocument document = viewer.getDocument();
return document != null ? provideCodeMinings(document) : null;
}

/**
* Returns the color from the given rgba.
*
Expand Down
Loading