Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix LSP document synchronization #544

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
6 changes: 4 additions & 2 deletions rascal-lsp/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"console": "internalConsole",
"vmArgs": [
"-Dlog4j2.level=TRACE",
"-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar"
"-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar",
"-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver"
]
},
{
Expand All @@ -22,7 +23,8 @@
"console": "internalConsole",
"vmArgs": [
"-Dlog4j2.level=TRACE",
"-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar"
"-Drascal.compilerClasspath=${workspaceFolder}/target/lib/rascal.jar",
"-Drascal.fallbackResolver=org.rascalmpl.vscode.lsp.uri.FallbackResolver"
]
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.rascalmpl.vscode.lsp.uri.LSPOpenFileResolver
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ public interface IBaseTextDocumentService extends TextDocumentService {
void unregisterLanguage(LanguageParameter lang);
CompletableFuture<IValue> executeCommand(String languageName, String command);
LineColumnOffsetMap getColumnMap(ISourceLocation file);
TextDocumentState getDocumentState(ISourceLocation file);

boolean isManagingFile(ISourceLocation file);

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.rascalmpl.uri.URIResolverRegistry;
import org.rascalmpl.uri.URIUtil;
import org.rascalmpl.values.IRascalValueFactory;
import org.rascalmpl.vscode.lsp.util.locations.Locations;

import io.usethesource.vallang.ISourceLocation;
import io.usethesource.vallang.IValueFactory;
Expand All @@ -65,7 +66,7 @@ default CompletableFuture<SourceLocation> resolveLocation(SourceLocation loc) {
try {
ISourceLocation tmp = loc.toRascalLocation();

ISourceLocation resolved = reg.logicalToPhysical(tmp);
ISourceLocation resolved = Locations.toClientLocation(tmp);

if (resolved == null) {
return loc;
Expand All @@ -77,10 +78,6 @@ default CompletableFuture<SourceLocation> resolveLocation(SourceLocation loc) {
IRascalFileSystemServices__logger.warn("Could not resolve location {} due to {}.", loc, e.getMessage());
return loc;
}
catch (IOException e) {
// This is normal behavior (when its not a logical scheme)
return loc;
}
catch (Throwable e) {
IRascalFileSystemServices__logger.warn("Could not resolve location {} due to {}.", loc, e.getMessage());
return loc;
Expand Down Expand Up @@ -130,7 +127,7 @@ private static boolean readonly(ISourceLocation loc) throws IOException {
return false;
}
if (reg.getRegisteredLogicalSchemes().contains(loc.getScheme())) {
var resolved = reg.logicalToPhysical(loc);
var resolved = Locations.toClientLocation(loc);
if (resolved != null && resolved != loc) {
return readonly(resolved);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public void browse(URI uri, String title, int viewColumn) {
@Override
public void edit(ISourceLocation path) {
try {
ISourceLocation physical = URIResolverRegistry.getInstance().logicalToPhysical(path);
ISourceLocation physical = Locations.toClientLocation(path);
ShowDocumentParams params = new ShowDocumentParams(physical.getURI().toASCIIString());
params.setTakeFocus(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,8 @@ public ISourceLocation getLocation() {
public Versioned<String> getCurrentContent() {
return currentContent;
}

public long getLastModified() {
return currentContent.getTimestamp();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary;
import org.rascalmpl.vscode.lsp.parametric.model.ParametricSummary.SummaryLookup;
import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter;
import org.rascalmpl.vscode.lsp.uri.FallbackResolver;
import org.rascalmpl.vscode.lsp.util.CodeActions;
import org.rascalmpl.vscode.lsp.util.Diagnostics;
import org.rascalmpl.vscode.lsp.util.FoldingRanges;
Expand Down Expand Up @@ -160,6 +161,7 @@
this.dedicatedLanguageName = dedicatedLanguage.getName();
this.dedicatedLanguage = dedicatedLanguage;
}
FallbackResolver.getInstance().registerTextDocumentService(this);
}

@Override
Expand Down Expand Up @@ -683,4 +685,14 @@
return CompletableFuture.completedFuture(null);
}
}

@Override
public boolean isManagingFile(ISourceLocation file) {
return files.containsKey(file.top());
}

@Override
public TextDocumentState getDocumentState(ISourceLocation file) {
return files.get(file.top());

Check failure on line 696 in rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java

View workflow job for this annotation

GitHub Actions / checkstyle

com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck

'method def' child has incorrect indentation level 7, expected level should be 8.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import org.rascalmpl.vscode.lsp.rascal.model.FileFacts;
import org.rascalmpl.vscode.lsp.rascal.model.SummaryBridge;
import org.rascalmpl.vscode.lsp.terminal.ITerminalIDEServer.LanguageParameter;
import org.rascalmpl.vscode.lsp.uri.FallbackResolver;
import org.rascalmpl.vscode.lsp.util.CodeActions;
import org.rascalmpl.vscode.lsp.util.Diagnostics;
import org.rascalmpl.vscode.lsp.util.DocumentChanges;
Expand Down Expand Up @@ -135,6 +136,7 @@ public RascalTextDocumentService(ExecutorService exec) {
this.ownExecuter = exec;
this.documents = new ConcurrentHashMap<>();
this.columns = new ColumnMaps(this::getContents);
FallbackResolver.getInstance().registerTextDocumentService(this);
}

@Override
Expand Down Expand Up @@ -488,4 +490,14 @@ private static <T> CompletableFuture<T> recoverExceptions(CompletableFuture<T> f
return defaultValue.get();
});
}

@Override
public boolean isManagingFile(ISourceLocation file) {
return facts.containsFile(file);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this not looking at documents? (just like the Parametric one, where both look at the same container?)

}

@Override
public TextDocumentState getDocumentState(ISourceLocation file) {
return documents.get(file.top());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import org.rascalmpl.vscode.lsp.util.concurrent.LazyUpdateableReference;
import org.rascalmpl.vscode.lsp.util.concurrent.ReplaceableFuture;
import org.rascalmpl.vscode.lsp.util.locations.ColumnMaps;
import org.rascalmpl.vscode.lsp.util.locations.Locations;

import io.usethesource.vallang.ISourceLocation;

public class FileFacts {
Expand Down Expand Up @@ -87,7 +89,7 @@ public void reportParseErrors(ISourceLocation file, List<Diagnostic> msgs) {
private FileFact getFile(ISourceLocation l) {
ISourceLocation resolved = null;
try {
resolved = URIResolverRegistry.getInstance().logicalToPhysical(l);
resolved = Locations.toClientLocation(l);
if (resolved == null) {
resolved = l;
}
Expand All @@ -101,6 +103,10 @@ public PathConfig getPathConfig(ISourceLocation file) {
return confs.lookupConfig(file);
}

public boolean containsFile(ISourceLocation file) {
return files.containsKey(file.top());
}

private class FileFact {
private final ISourceLocation file;
private final LazyUpdateableReference<InterruptibleFuture<@Nullable SummaryBridge>> summary;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public void browse(URI uri, String title, int viewColumn) {
@Override
public void edit(ISourceLocation path) {
try {
ISourceLocation physical = URIResolverRegistry.getInstance().logicalToPhysical(path);
ISourceLocation physical = Locations.toClientLocation(path);
ShowDocumentParams params = new ShowDocumentParams(physical.getURI().toASCIIString());
params.setTakeFocus(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,55 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;

import org.rascalmpl.uri.ILogicalSourceLocationResolver;
import org.rascalmpl.uri.ISourceLocationInputOutput;
import org.rascalmpl.uri.ISourceLocationWatcher;
import org.rascalmpl.uri.URIUtil;
import org.rascalmpl.vscode.lsp.IBaseTextDocumentService;
import org.rascalmpl.vscode.lsp.TextDocumentState;
import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeUriResolverClient;
import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeUriResolverServer;
import org.rascalmpl.vscode.lsp.uri.jsonrpc.VSCodeVFS;
import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.IOResult;
import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.ISourceLocationRequest;
import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.WriteFileRequest;
import org.rascalmpl.vscode.lsp.util.Lazy;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import io.usethesource.vallang.ISourceLocation;

public class FallbackResolver implements ISourceLocationInputOutput, ISourceLocationWatcher {
public class FallbackResolver implements ISourceLocationInputOutput, ISourceLocationWatcher, ILogicalSourceLocationResolver {

private static FallbackResolver instance = null;

public static FallbackResolver getInstance() {
if (instance == null) {
throw new IllegalStateException("FallbackResolver accessed before initialization");
}
return instance;
}

public FallbackResolver() {
instance = this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add a small comment on why we're doing this

}

private static VSCodeUriResolverServer getServer() throws IOException {
var result = VSCodeVFS.INSTANCE.getServer();
Expand Down Expand Up @@ -250,5 +272,45 @@ public void unwatch(ISourceLocation root, Consumer<ISourceLocationChanged> watch
getClient().removeWatcher(root, watcher, getServer());

}

public boolean isFileManaged(ISourceLocation file) {
for (final var service : textDocumentServices) {
if (service.isManagingFile(file)) {
return true;
}
}
return false;
}

@Override
public ISourceLocation resolve(ISourceLocation input) throws IOException {
if (isFileManaged(input)) {
try {
return URIUtil.changeScheme(input, "lsp+" + input.getScheme()).top();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the .top? that would loose information on the URI that we cannot recover?

} catch (URISyntaxException e) {
// fall through
}
}
return input;
}

@Override
public String authority() {
throw new UnsupportedOperationException("'authority' not supported by fallback resolver");
}

private Set<IBaseTextDocumentService> textDocumentServices = new HashSet<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should be final

notnit: not a threadsafe container, since we're only iterating over it, never clearing any, or using the key of it, an CopyOnWriteArrayList might be the better option.


public void registerTextDocumentService(IBaseTextDocumentService service) {
textDocumentServices.add(service);
}

public TextDocumentState getDocumentState(ISourceLocation file) {
for (final var service : textDocumentServices) {
if (service.isManagingFile(file)) {
return service.getDocumentState(file);
}
}
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there an alternative to null? should we throw an exception at this point? since isFileManaged is true?

what happens to a uri that was generated while the editor was open, but refenced after it is closed? do we unresolve it to the original URI?

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2018-2023, NWO-I CWI and Swat.engineering
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.rascalmpl.vscode.lsp.uri;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import org.rascalmpl.uri.ISourceLocationInput;
import org.rascalmpl.uri.URIUtil;

import io.usethesource.vallang.ISourceLocation;

public class LSPOpenFileResolver implements ISourceLocationInput {

@Override
public InputStream getInputStream(ISourceLocation uri) throws IOException {
var fallbackResolver = FallbackResolver.getInstance();
uri = stripLspPrefix(uri);
if (fallbackResolver.isFileManaged(uri)) {
return new ByteArrayInputStream(fallbackResolver.getDocumentState(uri).getCurrentContent().get().getBytes(StandardCharsets.UTF_16));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a risk of a race between isFileManaged and getDocumentState. Maybe it gets better if we merge them? Would also cut down on the map lookups.

}
throw new IOException("File does not exist");
}

@Override
public Charset getCharset(ISourceLocation uri) throws IOException {
return StandardCharsets.UTF_16;
}

@Override
public boolean exists(ISourceLocation uri) {
return FallbackResolver.getInstance().isFileManaged(stripLspPrefix(uri));
}

@Override
public long lastModified(ISourceLocation uri) throws IOException {
return FallbackResolver.getInstance().getDocumentState(stripLspPrefix(uri)).getLastModified();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can throw a NPE in case getDocumentState returns a null.

}

@Override
public boolean isDirectory(ISourceLocation uri) {
return false;
}

@Override
public boolean isFile(ISourceLocation uri) {
return exists(uri);
}

private static ISourceLocation stripLspPrefix(ISourceLocation uri) {
if (uri.getScheme().startsWith("lsp+")) {
try {
return URIUtil.changeScheme(uri, uri.getScheme().substring("lsp+".length()));
} catch (URISyntaxException e) {
// fall through
}
}
return uri;
}

@Override
public String[] list(ISourceLocation uri) throws IOException {
throw new UnsupportedOperationException("Unimplemented method 'list'");
rodinaarssen marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public String scheme() {
return "lsp";
}

@Override
public boolean supportsHost() {
return false;
}

}
Loading
Loading