Skip to content

Commit 1168eb4

Browse files
raghucssitrubenporras
authored andcommitted
Adapt document to IFile when unable to retrieve file from buffer manager
Some framework like Xtext does not connect their document to buffer manager. So LSPEclipseUtils returns null for URI and IFile in such cases. So we can try to adapt the IDocument to URI and IFile. see #1500
1 parent ca73481 commit 1168eb4

8 files changed

Lines changed: 688 additions & 1 deletion

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

0 commit comments

Comments
 (0)