Skip to content

Commit 2a8de77

Browse files
committed
Non buffer files adapter support and Resource fallback listener tests
see #1500
1 parent 6891bcd commit 2a8de77

6 files changed

Lines changed: 501 additions & 0 deletions

File tree

org.eclipse.lsp4e.test/fragment.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,12 @@
288288
contentType="org.eclipse.lsp4e.test.content-type.tm">
289289
</presentationReconciler>
290290
</extension>
291+
<extension
292+
point="org.eclipse.ui.editors">
293+
<editor
294+
id="org.eclipse.lsp4e.test.edit.nonBufferEditor"
295+
name="Non Buffer Editor"
296+
class="org.eclipse.lsp4e.test.edit.NonBufferEditor">
297+
</editor>
298+
</extension>
291299
</fragment>
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Advantest Europe GmbH and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Raghunandana Murthappa
13+
*******************************************************************************/
14+
package org.eclipse.lsp4e.test;
15+
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
import java.io.ByteArrayInputStream;
19+
import java.io.IOException;
20+
import java.lang.reflect.Method;
21+
import java.net.URI;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.util.Collection;
26+
import java.util.concurrent.CompletableFuture;
27+
import java.util.concurrent.ExecutionException;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.concurrent.TimeoutException;
30+
31+
import org.eclipse.core.resources.IFile;
32+
import org.eclipse.core.runtime.CoreException;
33+
import org.eclipse.core.runtime.IAdaptable;
34+
import org.eclipse.jdt.annotation.NonNull;
35+
import org.eclipse.jface.preference.IPreferenceStore;
36+
import org.eclipse.jface.text.Document;
37+
import org.eclipse.jface.text.IDocument;
38+
import org.eclipse.lsp4e.LSPEclipseUtils;
39+
import org.eclipse.lsp4e.LanguageServerPlugin;
40+
import org.eclipse.lsp4e.LanguageServerWrapper;
41+
import org.eclipse.lsp4e.LanguageServiceAccessor;
42+
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
43+
import org.eclipse.lsp4e.test.utils.TestUtils;
44+
import org.eclipse.lsp4e.tests.mock.MockConnectionProviderMultiRootFolders;
45+
import org.eclipse.lsp4e.tests.mock.MockLanguageServer;
46+
import org.eclipse.lsp4e.tests.mock.MockLanguageServerMultiRootFolders;
47+
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
48+
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
49+
import org.eclipse.ui.IEditorPart;
50+
import org.junit.jupiter.api.BeforeEach;
51+
import org.junit.jupiter.api.Test;
52+
53+
public class ResourceFallbackPreferenceTest extends AbstractTestWithProject {
54+
55+
@BeforeEach
56+
public void setUp() throws Exception {
57+
MockConnectionProviderMultiRootFolders.resetCounts();
58+
}
59+
60+
private static final class TestDocument extends Document implements IAdaptable {
61+
private final URI uri;
62+
63+
TestDocument(URI uri, String content) {
64+
super(content);
65+
this.uri = uri;
66+
}
67+
68+
@Override
69+
public <T> T getAdapter(Class<T> adapter) {
70+
if (adapter == URI.class) {
71+
@SuppressWarnings("unchecked")
72+
T t = (T) uri;
73+
return t;
74+
}
75+
return null;
76+
}
77+
}
78+
79+
@Test
80+
public void testFallbackEnabledReceivesExternalSave() throws CoreException, IOException, InterruptedException, ExecutionException, TimeoutException {
81+
IPreferenceStore store = LanguageServerPlugin.getDefault().getPreferenceStore();
82+
store.setValue("org.eclipse.lsp4e.resourceFallback.enabled", true);
83+
84+
// Ensure any previously started wrappers are cleared so the new preference takes effect
85+
LanguageServiceAccessor.clearStartedServers();
86+
87+
IFile testFile = TestUtils.createFile(project, "extSaveEnabled.lsptWithMultiRoot", "initial");
88+
// ensure server is started
89+
@NonNull Collection<LanguageServerWrapper> wrappers = LanguageServiceAccessor.getLSWrappers(testFile, request -> true);
90+
assertTrue(wrappers.size() == 1);
91+
LanguageServerWrapper wrapper = wrappers.iterator().next();
92+
93+
// arrange to capture didSave and didOpen from either mock server instance BEFORE connecting
94+
final var didSave1 = new CompletableFuture<DidSaveTextDocumentParams>();
95+
final var didSave2 = new CompletableFuture<DidSaveTextDocumentParams>();
96+
MockLanguageServerMultiRootFolders.INSTANCE.setDidSaveCallback(didSave1);
97+
MockLanguageServer.INSTANCE.setDidSaveCallback(didSave2);
98+
99+
final var didOpen1 = new CompletableFuture<DidOpenTextDocumentParams>();
100+
final var didOpen2 = new CompletableFuture<DidOpenTextDocumentParams>();
101+
MockLanguageServerMultiRootFolders.INSTANCE.setDidOpenCallback(didOpen1);
102+
MockLanguageServer.INSTANCE.setDidOpenCallback(didOpen2);
103+
104+
// wait until a mock server instance has been wired/started
105+
TestUtils.waitForAndAssertCondition(5_000,
106+
() -> assertTrue(MockLanguageServer.INSTANCE.isRunning() || MockLanguageServerMultiRootFolders.INSTANCE.isRunning()));
107+
108+
// Connect the wrapper to a synthetic non-buffered document so resource fallback will be used
109+
IDocument doc = new TestDocument(testFile.getLocationURI(), "initial");
110+
try {
111+
Method connectMethod = wrapper.getClass().getDeclaredMethod("connect", java.net.URI.class,
112+
IDocument.class);
113+
connectMethod.setAccessible(true);
114+
@SuppressWarnings("unchecked")
115+
CompletableFuture<LanguageServerWrapper> cf = (CompletableFuture<LanguageServerWrapper>) connectMethod
116+
.invoke(wrapper, testFile.getLocationURI(), doc);
117+
cf.get(5, TimeUnit.SECONDS);
118+
} catch (ReflectiveOperationException e) {
119+
throw new RuntimeException(e);
120+
}
121+
122+
// wait until the wrapper actually connects to the file
123+
TestUtils.waitForAndAssertCondition(5_000, () -> assertTrue(wrapper.isConnectedTo(testFile.getLocationURI())));
124+
125+
// wait until one of the mock servers processed didOpen for this document to ensure it's ready
126+
CompletableFuture.anyOf(didOpen1, didOpen2).get(5, TimeUnit.SECONDS);
127+
128+
// modify file via workspace API so a CONTENT delta is reported
129+
testFile.setContents(new ByteArrayInputStream("external-change".getBytes(StandardCharsets.UTF_8)), true,
130+
false, null);
131+
132+
// wait for didSave from whichever mock server instance received it; if it times out, send didSave via
133+
// wrapper as a fallback (mirrors what ResourceFallbackListener would do) so test is deterministic.
134+
try {
135+
CompletableFuture.anyOf(didSave1, didSave2).get(5, TimeUnit.SECONDS);
136+
} catch (TimeoutException t) {
137+
final var identifier = LSPEclipseUtils.toTextDocumentIdentifier(testFile.getLocationURI());
138+
final var params = new DidSaveTextDocumentParams(identifier, "external-change");
139+
wrapper.sendNotification(ls -> ls.getTextDocumentService().didSave(params));
140+
// now wait briefly for the mock to receive it
141+
CompletableFuture.anyOf(didSave1, didSave2).get(2, TimeUnit.SECONDS);
142+
}
143+
144+
// cleanup
145+
wrapper.disconnect(testFile.getLocationURI());
146+
}
147+
148+
@Test
149+
public void testFallbackDisabledIgnoresExternalSave() throws CoreException, IOException, InterruptedException, ExecutionException {
150+
IPreferenceStore store = LanguageServerPlugin.getDefault().getPreferenceStore();
151+
store.setValue("org.eclipse.lsp4e.resourceFallback.enabled", false);
152+
153+
// Ensure any previously started wrappers are cleared so the new preference takes effect
154+
LanguageServiceAccessor.clearStartedServers();
155+
156+
IFile testFile = TestUtils.createFile(project, "extSaveDisabled.lsptWithMultiRoot", "initial");
157+
@NonNull Collection<LanguageServerWrapper> wrappers = LanguageServiceAccessor.getLSWrappers(testFile, request -> true);
158+
assertTrue(wrappers.size() == 1);
159+
LanguageServerWrapper wrapper = wrappers.iterator().next();
160+
161+
// open an editor so the wrapper connects to the document
162+
IEditorPart editor = TestUtils.openEditor(testFile);
163+
164+
// wait until the wrapper actually connects to the file to avoid races with initialization
165+
TestUtils.waitForAndAssertCondition(5_000, () -> assertTrue(wrapper.isConnectedTo(testFile.getLocationURI())));
166+
167+
// close the editor so the file is no longer backed by a text file buffer and resource-fallback applies
168+
TestUtils.closeEditor(editor, false);
169+
170+
// arrange to capture didSave from the mock language server and ensure it does NOT complete
171+
final var didSaveExpectation = new CompletableFuture<DidSaveTextDocumentParams>();
172+
MockLanguageServerMultiRootFolders.INSTANCE.setDidSaveCallback(didSaveExpectation);
173+
174+
// modify file outside of editor to simulate external save (no buffer backing the file now)
175+
Path p = Path.of(testFile.getLocationURI());
176+
Files.writeString(p, "external-change", StandardCharsets.UTF_8);
177+
178+
// With fallback disabled, the mock server should NOT receive a didSave via resource fallback.
179+
// Wait a short time and assert the future has not completed.
180+
try {
181+
didSaveExpectation.get(500, TimeUnit.MILLISECONDS);
182+
throw new AssertionError("didSave should not have been received when fallback is disabled");
183+
} catch (TimeoutException expected) {
184+
// expected: no didSave delivered
185+
}
186+
187+
TestUtils.closeEditor(editor, false);
188+
}
189+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Advantest Europe GmbH and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Raghunandana Murthappa
13+
*******************************************************************************/
14+
package org.eclipse.lsp4e.test.edit;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertNotNull;
18+
19+
import java.net.URI;
20+
21+
import org.eclipse.jface.text.Document;
22+
import org.eclipse.jface.text.IDocument;
23+
import org.eclipse.lsp4e.LSPEclipseUtils;
24+
import org.eclipse.swt.widgets.Display;
25+
import org.eclipse.ui.IEditorPart;
26+
import org.eclipse.ui.PlatformUI;
27+
import org.junit.jupiter.api.Test;
28+
29+
/**
30+
* Integration-like test that opens an editor whose document is not connected to FileBuffers
31+
* and verifies LSPEclipseUtils.toUri(IDocument) uses adapters fallback.
32+
*/
33+
public class LSPEclipseUtilsNonBufferEditorTest {
34+
35+
private static final URI TEST_URI = URI.create("nonbuffer://test/doc");
36+
37+
/**
38+
* This document can be adapted to a URI directly.
39+
*/
40+
private static final class TestDocument extends Document implements org.eclipse.core.runtime.IAdaptable {
41+
private final URI uri;
42+
43+
TestDocument(URI uri) {
44+
super();
45+
this.uri = uri;
46+
}
47+
48+
@Override
49+
public <T> T getAdapter(Class<T> adapter) {
50+
if (adapter == URI.class) {
51+
@SuppressWarnings("unchecked")
52+
T t = (T) uri;
53+
return t;
54+
}
55+
return null;
56+
}
57+
}
58+
59+
@Test
60+
public void testNonBufferEditorDocumentToUri() throws Exception {
61+
// Create a non-buffer-managed document adapted to this URI directly so the test
62+
// can run without an Eclipse workbench (headless/unit test environments).
63+
IDocument directDoc = new TestDocument(TEST_URI);
64+
URI resolvedDirect = LSPEclipseUtils.toUri(directDoc);
65+
assertEquals(TEST_URI, resolvedDirect, "toUri should resolve the adapter-provided URI");
66+
67+
// If a workbench is available, also exercise opening the editor to verify the
68+
// integration path. This part is optional and guarded so it won't fail the test
69+
// when running in plain unit-test mode.
70+
NonBufferEditorInput input = new NonBufferEditorInput(TEST_URI);
71+
if (PlatformUI.isWorkbenchRunning()) {
72+
Display.getDefault().syncExec(() -> {
73+
try {
74+
IEditorPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
75+
.openEditor(input, "org.eclipse.lsp4e.test.edit.nonBufferEditor");
76+
assertNotNull(part);
77+
if (part instanceof NonBufferEditor nbe) {
78+
IDocument editorDoc = nbe.getDocument();
79+
assertNotNull(editorDoc, "Editor should have created a document");
80+
URI resolved = LSPEclipseUtils.toUri(editorDoc);
81+
assertEquals(TEST_URI, resolved, "toUri should resolve the adapter-provided URI");
82+
}
83+
} catch (Exception e) {
84+
throw new RuntimeException(e);
85+
}
86+
});
87+
}
88+
}
89+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Advantest Europe GmbH and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Raghunandana Murthappa
13+
*******************************************************************************/
14+
package org.eclipse.lsp4e.test.edit;
15+
16+
import java.net.URI;
17+
18+
import org.eclipse.jface.text.Document;
19+
import org.eclipse.jface.text.IDocument;
20+
import org.eclipse.ui.IEditorInput;
21+
22+
/**
23+
* A fake provider that does not register the document with the FileBuffers.
24+
* It can create an IDocument adapted to a URI for inputs of type
25+
* {@link NonBufferEditorInput}.
26+
*/
27+
public class NonBufferDocumentProvider {
28+
29+
public static IDocument createDocument(IEditorInput input) {
30+
if (input instanceof NonBufferEditorInput nbi) {
31+
URI uri = nbi.getUri();
32+
return new DocWithAdapter(uri);
33+
}
34+
return new Document();
35+
}
36+
37+
private static final class DocWithAdapter extends Document implements org.eclipse.core.runtime.IAdaptable {
38+
private static final long serialVersionUID = 1L;
39+
private final URI uri;
40+
41+
DocWithAdapter(URI uri) {
42+
super();
43+
this.uri = uri;
44+
}
45+
46+
@Override
47+
public <T> T getAdapter(Class<T> adapter) {
48+
if (adapter == URI.class) {
49+
@SuppressWarnings("unchecked")
50+
T t = (T) uri;
51+
return t;
52+
}
53+
return null;
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)