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
Expand Up @@ -21,6 +21,7 @@
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

Expand Down Expand Up @@ -65,7 +66,8 @@ public void setUp() {

@Test
public void testHoverRegion() throws CoreException {
final var hoverResponse = new Hover(List.of(Either.forLeft("HoverContent")), new Range(new Position(0, 0), new Position(0, 10)));
final var hoverResponse = new Hover(List.of(Either.forLeft("HoverContent")),
new Range(new Position(0, 0), new Position(0, 10)));
MockLanguageServer.INSTANCE.setHover(hoverResponse);

IFile file = TestUtils.createUniqueTestFile(project, "HoverRange Other Text");
Expand All @@ -81,24 +83,30 @@ public void testHoverRegionInvalidOffset() throws CoreException {
IFile file = TestUtils.createUniqueTestFile(project, "HoverRange Other Text");
ITextViewer viewer = TestUtils.openTextViewer(file);

assertEquals(new Region(15, 0), hover.getHoverRegion(viewer, 15));
var region = hover.getHoverRegion(viewer, 15);
assertNotNull(region);
assertTrue("region should include the hover offset",
region.getOffset() <= 15 && (region.getOffset() + region.getLength()) >= 15);
}

@Test
public void testHoverInfo() throws CoreException {
final var hoverResponse = new Hover(List.of(Either.forLeft("HoverContent")), new Range(new Position(0, 0), new Position(0, 10)));
public void testHoverInfo() throws Exception {
final var hoverResponse = new Hover(List.of(Either.forLeft("HoverContent")),
new Range(new Position(0, 0), new Position(0, 10)));
MockLanguageServer.INSTANCE.setHover(hoverResponse);

IFile file = TestUtils.createUniqueTestFile(project, "HoverRange Other Text");
ITextViewer viewer = TestUtils.openTextViewer(file);

// TODO update test when MARKDOWN to HTML will be finished
assertTrue(hover.getHoverInfo(viewer, new Region(0, 10)).contains("HoverContent"));
String html = hover.getHoverInfoFuture(viewer, new Region(0, 10)).get(2, TimeUnit.SECONDS);
assertNotNull(html);
assertTrue(html.contains("HoverContent"));
}

@Test
public void testHoverInfoEmptyContentList() throws CoreException {
final var hoverResponse = new Hover(Collections.emptyList(), new Range(new Position(0, 0), new Position(0, 10)));
final var hoverResponse = new Hover(Collections.emptyList(),
new Range(new Position(0, 0), new Position(0, 10)));
MockLanguageServer.INSTANCE.setHover(hoverResponse);

IFile file = TestUtils.createUniqueTestFile(project, "HoverRange Other Text");
Expand All @@ -119,7 +127,8 @@ public void testHoverInfoInvalidOffset() throws CoreException {

@Test
public void testHoverEmptyContentItem() throws CoreException {
final var hoverResponse = new Hover(List.of(Either.forLeft("")), new Range(new Position(0, 0), new Position(0, 10)));
final var hoverResponse = new Hover(List.of(Either.forLeft("")),
new Range(new Position(0, 0), new Position(0, 10)));
MockLanguageServer.INSTANCE.setHover(hoverResponse);

IFile file = TestUtils.createUniqueTestFile(project, "HoverRange Other Text");
Expand All @@ -129,27 +138,28 @@ public void testHoverEmptyContentItem() throws CoreException {
}

@Test
public void testHoverOnExternalFile() throws CoreException, IOException {
public void testHoverOnExternalFile() throws Exception {
final var hoverResponse = new Hover(List.of(Either.forLeft("blah")),
new Range(new Position(0, 0), new Position(0, 0)));
MockLanguageServer.INSTANCE.setHover(hoverResponse);

File file = TestUtils.createTempFile("testHoverOnExternalfile", ".lspt");
ITextViewer viewer = LSPEclipseUtils.getTextViewer(IDE.openInternalEditorOnFileStore(
UI.getActivePage(), EFS.getStore(file.toURI())));
Assert.assertTrue(hover.getHoverInfo(viewer, new Region(0, 0)).contains("blah"));
ITextViewer viewer = LSPEclipseUtils
.getTextViewer(IDE.openInternalEditorOnFileStore(UI.getActivePage(), EFS.getStore(file.toURI())));
String html = hover.getHoverInfoFuture(viewer, new Region(0, 0)).get(2, TimeUnit.SECONDS);
Assert.assertTrue(html != null && html.contains("blah"));
}

@Test
public void testMultipleHovers() throws Exception {
final var hoverResponse = new Hover(List.of(Either.forLeft("HoverContent")), new Range(new Position(0, 0), new Position(0, 10)));
final var hoverResponse = new Hover(List.of(Either.forLeft("HoverContent")),
new Range(new Position(0, 0), new Position(0, 10)));
MockLanguageServer.INSTANCE.setHover(hoverResponse);

IFile file = TestUtils.createUniqueTestFileMultiLS(project, "HoverRange Other Text");
ITextViewer viewer = TestUtils.openTextViewer(file);

// TODO update test when MARKDOWN to HTML will be finished
String hoverInfo = hover.getHoverInfo(viewer, new Region(0, 10));
String hoverInfo = hover.getHoverInfoFuture(viewer, new Region(0, 10)).get(2, TimeUnit.SECONDS);
int index = hoverInfo.indexOf("HoverContent");
assertNotEquals("Hover content not found", -1, index);
index += "HoverContent".length();
Expand All @@ -167,12 +177,12 @@ public void testIntroUrlLink() throws Exception {

IFile file = TestUtils.createUniqueTestFile(project, "HoverRange Other Text");
IEditorPart editorPart = TestUtils.openEditor(file);

waitForAndAssertCondition(5_000, () -> LSPEclipseUtils.getTextViewer(editorPart) != null);
ITextViewer viewer = LSPEclipseUtils.getTextViewer(editorPart);
assertEquals(UI.getActivePart(), editorPart);

String hoverContent = hover.getHoverInfo(viewer, new Region(0, 10));
String hoverContent = hover.getHoverInfoFuture(viewer, new Region(0, 10)).get(2, TimeUnit.SECONDS);

final var hoverManager = new LSPTextHover();

Expand All @@ -188,8 +198,8 @@ public void testIntroUrlLink() throws Exception {

wrapperControl = (BrowserInformationControl) hoverManager.getHoverControlCreator()
.createInformationControl(shell);
control = (BrowserInformationControl) wrapperControl
.getInformationPresenterControlCreator().createInformationControl(shell);
control = (BrowserInformationControl) wrapperControl.getInformationPresenterControlCreator()
.createInformationControl(shell);
Field f = BrowserInformationControl.class.getDeclaredField("fBrowser"); //
f.setAccessible(true);

Expand All @@ -209,7 +219,7 @@ public void completed(ProgressEvent event) {
});

assertNotNull("Editor should be opened", viewer.getTextWidget());

UI.getActivePage().activate(editorPart);
browser.setText(hoverContent);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*******************************************************************************
* 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.operations.hover;

import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import org.eclipse.jdt.annotation.Nullable;

/**
* Lightweight carrier for asynchronous hover HTML content.
*
* Holds a placeholder HTML to show immediately and a future that will
* eventually provide the final HTML. The {@link #token} acts as an identity to
* guard UI updates against races when the control input changes quickly.
*/
@SuppressWarnings("javadoc")
final class AsyncHtmlHoverInput {

final UUID token = UUID.randomUUID();
final CompletableFuture<@Nullable String> future;
final String placeholderHtml;

AsyncHtmlHoverInput(CompletableFuture<@Nullable String> future, String placeholderHtml) {
this.future = future;
this.placeholderHtml = placeholderHtml;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial implementation
* Sebastian Thomschke (Vegard IT GmbH) - Prevent UI freezes through non-blocking hover rendering
*******************************************************************************/
package org.eclipse.lsp4e.operations.hover;

import java.net.URL;
import java.util.UUID;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
Expand Down Expand Up @@ -62,6 +64,8 @@ public void changed(LocationEvent event) {
}
};

private @Nullable UUID currentAsyncToken;

public FocusableBrowserInformationControl(Shell parent, String symbolicFontName, boolean resizable) {
super(parent, JFaceResources.DEFAULT_FONT, resizable);
}
Expand Down Expand Up @@ -155,6 +159,31 @@ private static boolean safeExecute(Browser browser, String expression) {

@Override
public void setInput(@Nullable Object input) {
if (input instanceof AsyncHtmlHoverInput async) {
this.currentAsyncToken = async.token;
super.setInput(styleHtml(async.placeholderHtml));
async.future.whenComplete((html, ex) -> UI.getDisplay().asyncExec(() -> {
Comment thread
rubenporras marked this conversation as resolved.
if (getShell() == null || getShell().isDisposed()) {
return;
}
final var currentAsyncToken = this.currentAsyncToken;
if (currentAsyncToken != null && !currentAsyncToken.equals(async.token)) {
return; // input changed; ignore stale update
}
if (ex != null) {
LanguageServerPlugin.logError(ex);
dispose();
return;
}
if (html != null && !html.isBlank()) {
super.setInput(styleHtml(html));
} else {
// No content from LS; hide placeholder
dispose();
}
}));
return;
}
if (input instanceof String html) {
input = styleHtml(html);
}
Expand Down Expand Up @@ -239,5 +268,4 @@ private static void appendAsHexString(StringBuilder buffer, int intValue) {
}
};
}

}
}
Loading
Loading