Skip to content

Commit 3a0c311

Browse files
committed
Merge branch 'feature/1010-pom-leading-for-dsls/final' into feature/1010-pom-leading-for-dsls/remote-routing
2 parents 8713e45 + 73cfbee commit 3a0c311

14 files changed

Lines changed: 412 additions & 279 deletions

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ jobs:
214214
extensionFile: ${{ steps.publishToOpenVSX.outputs.vsixPath }} # copy exact same vsix from the previous step
215215

216216
- name: Prepare Draft Release
217-
uses: softprops/action-gh-release@v2
217+
uses: softprops/action-gh-release@v3
218218
continue-on-error: true
219219
if: startsWith(github.ref, 'refs/tags/v')
220220
with:

rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.time.Duration;
3030
import java.util.List;
3131
import java.util.concurrent.CompletableFuture;
32-
import org.checkerframework.checker.nullness.qual.Nullable;
3332
import org.eclipse.lsp4j.ClientCapabilities;
3433
import org.eclipse.lsp4j.CreateFilesParams;
3534
import org.eclipse.lsp4j.DeleteFilesParams;
@@ -39,14 +38,12 @@
3938
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
4039
import org.eclipse.lsp4j.services.LanguageClient;
4140
import org.eclipse.lsp4j.services.TextDocumentService;
42-
import org.rascalmpl.util.locations.ColumnMaps;
43-
import org.rascalmpl.util.locations.LineColumnOffsetMap;
4441
import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter;
4542

4643
import io.usethesource.vallang.ISourceLocation;
4744
import io.usethesource.vallang.IValue;
4845

49-
public interface IBaseTextDocumentService extends TextDocumentService {
46+
public interface IBaseTextDocumentService extends TextDocumentService, ITextDocumentStateManager {
5047
static final Duration NO_DEBOUNCE = Duration.ZERO;
5148
static final Duration NORMAL_DEBOUNCE = Duration.ofMillis(800);
5249

@@ -63,11 +60,6 @@ public interface IBaseTextDocumentService extends TextDocumentService {
6360

6461
@JsonRequest("executeRascalCommand")
6562
CompletableFuture<IValue> executeCommand(String languageName, String command);
66-
LineColumnOffsetMap getColumnMap(ISourceLocation file);
67-
ColumnMaps getColumnMaps();
68-
@Nullable TextDocumentState getDocumentState(ISourceLocation file);
69-
70-
boolean isManagingFile(ISourceLocation file);
7163

7264
void didCreateFiles(CreateFilesParams params);
7365
void didRenameFiles(RenameFilesParams params, List<WorkspaceFolder> workspaceFolders);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice,
9+
* this list of conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following disclaimer in the documentation
13+
* and/or other materials provided with the distribution.
14+
*
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package org.rascalmpl.vscode.lsp;
28+
29+
import java.io.FileNotFoundException;
30+
import org.rascalmpl.util.locations.ColumnMaps;
31+
import org.rascalmpl.util.locations.LineColumnOffsetMap;
32+
33+
import io.usethesource.vallang.ISourceLocation;
34+
35+
public interface ITextDocumentStateManager {
36+
LineColumnOffsetMap getColumnMap(ISourceLocation file);
37+
ColumnMaps getColumnMaps();
38+
TextDocumentState getEditorState(ISourceLocation file) throws FileNotFoundException;
39+
boolean isManagingFile(ISourceLocation file);
40+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice,
9+
* this list of conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following disclaimer in the documentation
13+
* and/or other materials provided with the distribution.
14+
*
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package org.rascalmpl.vscode.lsp;
28+
29+
import java.io.FileNotFoundException;
30+
import java.io.IOException;
31+
import java.io.Reader;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Set;
35+
import java.util.concurrent.CompletableFuture;
36+
import java.util.concurrent.ConcurrentHashMap;
37+
import java.util.concurrent.ExecutorService;
38+
import java.util.function.BiFunction;
39+
import java.util.function.Function;
40+
import java.util.stream.Collectors;
41+
import org.apache.commons.io.IOUtils;
42+
import org.apache.logging.log4j.LogManager;
43+
import org.apache.logging.log4j.Logger;
44+
import org.checkerframework.checker.nullness.qual.KeyFor;
45+
import org.checkerframework.checker.nullness.qual.Nullable;
46+
import org.eclipse.lsp4j.TextDocumentItem;
47+
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
48+
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
49+
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
50+
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
51+
import org.rascalmpl.uri.URIResolverRegistry;
52+
import org.rascalmpl.util.locations.ColumnMaps;
53+
import org.rascalmpl.util.locations.LineColumnOffsetMap;
54+
import org.rascalmpl.values.parsetrees.ITree;
55+
import org.rascalmpl.vscode.lsp.model.DiagnosticsReporter;
56+
import org.rascalmpl.vscode.lsp.rascal.conversion.Diagnostics;
57+
import org.rascalmpl.vscode.lsp.util.Versioned;
58+
import org.rascalmpl.vscode.lsp.util.locations.Locations;
59+
60+
import io.usethesource.vallang.ISourceLocation;
61+
62+
/**
63+
* Manages open files and their contents.
64+
*
65+
* This class maintains a set of open files, their state, and information derived from their contents, like column maps.
66+
* This functionality is shared by implementations of {@link IBaseTextDocumentService}.
67+
*/
68+
public abstract class TextDocumentStateManager implements ITextDocumentStateManager {
69+
70+
private static final Logger logger = LogManager.getLogger(TextDocumentStateManager.class);
71+
72+
private final Map<ISourceLocation, TextDocumentState> files = new ConcurrentHashMap<>();
73+
private final ColumnMaps columns;
74+
75+
@SuppressWarnings({"methodref.receiver.bound"}) // this::getContents
76+
protected TextDocumentStateManager() {
77+
this.columns = new ColumnMaps(this::getContents);
78+
}
79+
80+
protected static ResponseError unknownFileError(ISourceLocation loc, @Nullable Object data) {
81+
return new ResponseError(ResponseErrorCode.RequestFailed, "Unknown file: " + loc, data);
82+
}
83+
84+
protected static ResponseError unknownFileError(VersionedTextDocumentIdentifier doc, @Nullable Object data) {
85+
return unknownFileError(Locations.toLoc(doc), data);
86+
}
87+
88+
public String getContents(ISourceLocation file) {
89+
file = file.top();
90+
var ideState = files.get(file);
91+
if (ideState != null) {
92+
return ideState.getCurrentContent().get();
93+
}
94+
if (!URIResolverRegistry.getInstance().isFile(file)) {
95+
logger.error("Trying to get the contents of a directory: {}", file);
96+
return "";
97+
}
98+
try (Reader src = URIResolverRegistry.getInstance().getCharacterReader(file)) {
99+
return IOUtils.toString(src);
100+
}
101+
catch (IOException e) {
102+
logger.error("Error opening file {} to get contents", file, e);
103+
return "";
104+
}
105+
}
106+
107+
@Override
108+
public ColumnMaps getColumnMaps() {
109+
return columns;
110+
}
111+
112+
@Override
113+
public boolean isManagingFile(ISourceLocation loc) {
114+
return files.containsKey(loc.top());
115+
}
116+
117+
@Override
118+
public LineColumnOffsetMap getColumnMap(ISourceLocation loc) {
119+
return columns.get(loc.top());
120+
}
121+
122+
/**
123+
* Get open file state.
124+
* @param loc The location of the file.
125+
* @return The current state in the editor.
126+
* @throws FileNotFoundException If the file is not open.
127+
*/
128+
@Override
129+
public TextDocumentState getEditorState(ISourceLocation loc) throws FileNotFoundException {
130+
loc = loc.top();
131+
TextDocumentState file = files.get(loc);
132+
if (file == null) {
133+
throw new FileNotFoundException(String.format("Unknown file: %s", loc));
134+
}
135+
return file;
136+
}
137+
138+
/**
139+
* Get open file state.
140+
*
141+
* Intentionally protected function, only to be used from LSP endpoints. Users outside of the LSP context should call {@link TextDocumentStateManager#getEditorState}.
142+
* @param loc The location of the file.
143+
* @return The current state in the editor.
144+
* @throws ResponseErrorException If the file is not open.
145+
*/
146+
protected TextDocumentState getFile(ISourceLocation loc) {
147+
try {
148+
return getEditorState(loc);
149+
} catch (FileNotFoundException ignored) {
150+
throw new ResponseErrorException(unknownFileError(loc, loc));
151+
}
152+
}
153+
154+
protected TextDocumentState openFile(TextDocumentItem doc, Function<ISourceLocation, BiFunction<ISourceLocation, String, CompletableFuture<ITree>>> parserGetter, long timestamp, ExecutorService exec) {
155+
return files.computeIfAbsent(Locations.toLoc(doc),
156+
l -> new TextDocumentState(parserGetter.apply(l), l, doc.getVersion(), doc.getText(), timestamp, exec));
157+
}
158+
159+
private void invalidateColumnMaps(ISourceLocation loc) {
160+
columns.clear(loc.top());
161+
}
162+
163+
/**
164+
* Close a file/editor.
165+
* @param loc The location of the file.
166+
* @throws ResponseErrorException If the file was not open.
167+
*/
168+
protected void closeFile(ISourceLocation loc) {
169+
invalidateColumnMaps(loc);
170+
if (files.remove(loc.top()) == null) {
171+
throw new ResponseErrorException(unknownFileError(loc, loc));
172+
}
173+
}
174+
175+
protected @Nullable TextDocumentState changeParser(ISourceLocation f, BiFunction<ISourceLocation, String, CompletableFuture<ITree>> parser) {
176+
f = f.top();
177+
logger.trace("Updating state: {}", f);
178+
179+
// Since we cannot know what happened to this file before we were called, we need to be careful about races.
180+
// It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value.
181+
var state = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(parser));
182+
if (state == null) {
183+
logger.debug("Updating the parser of {} failed, since it was closed.", f);
184+
}
185+
return state;
186+
}
187+
188+
protected Set<@KeyFor("this.files") ISourceLocation> getOpenFiles() {
189+
return files.keySet();
190+
}
191+
192+
protected void updateContents(VersionedTextDocumentIdentifier doc, String newContents, long timestamp) {
193+
logger.trace("New contents for {}", doc);
194+
TextDocumentState file = getFile(Locations.toLoc(doc));
195+
invalidateColumnMaps(file.getLocation());
196+
handleParsingErrors(file, file.update(doc.getVersion(), newContents, timestamp));
197+
}
198+
199+
protected void handleParsingErrors(TextDocumentState file, CompletableFuture<Versioned<List<Diagnostics.Template>>> diagnosticsAsync) {
200+
diagnosticsAsync.thenAccept(diagnostics -> {
201+
var parseErrors = diagnostics.map(d -> d.stream()
202+
.map(diagnostic -> diagnostic.instantiate(getColumnMaps()))
203+
.collect(Collectors.toList()));
204+
205+
var loc = file.getLocation();
206+
logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, loc);
207+
getDiagnosticsReporter(loc).reportParseErrors(loc, parseErrors);
208+
});
209+
}
210+
211+
protected abstract DiagnosticsReporter getDiagnosticsReporter(ISourceLocation file);
212+
213+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice,
9+
* this list of conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following disclaimer in the documentation
13+
* and/or other materials provided with the distribution.
14+
*
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
* POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package org.rascalmpl.vscode.lsp.model;
28+
29+
import java.util.List;
30+
import org.eclipse.lsp4j.Diagnostic;
31+
import org.rascalmpl.vscode.lsp.parametric.model.ParametricFileFacts;
32+
import org.rascalmpl.vscode.lsp.rascal.model.FileFacts;
33+
import org.rascalmpl.vscode.lsp.util.Versioned;
34+
35+
import io.usethesource.vallang.ISourceLocation;
36+
37+
/**
38+
* Interface for objects that can report diagnostics on files.
39+
*
40+
* Encapsulates common behavior of {@link FileFacts} and {@link ParametricFileFacts}.
41+
*/
42+
public interface DiagnosticsReporter {
43+
void reportParseErrors(ISourceLocation file, Versioned<List<Diagnostic>> msgs);
44+
}

0 commit comments

Comments
 (0)