349 lines
14 KiB
Java
349 lines
14 KiB
Java
package p.studio.lsp;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.io.TempDir;
|
|
import p.studio.lsp.messages.LspProjectContext;
|
|
import p.studio.lsp.dtos.LspDefinitionTargetDTO;
|
|
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
|
import p.studio.lsp.messages.LspDefinitionRequest;
|
|
import p.studio.vfs.*;
|
|
import p.studio.vfs.messages.*;
|
|
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
final class LspServiceImplTest {
|
|
@TempDir
|
|
Path tempDir;
|
|
|
|
private static final String OVERLAY_SOURCE = """
|
|
fn helper_call() -> void
|
|
{
|
|
if true {
|
|
helper();
|
|
}
|
|
while false {
|
|
helper();
|
|
}
|
|
let value = switch true {
|
|
true: { 1 },
|
|
default: { 2 }
|
|
};
|
|
let recovered = handle helper_result() {
|
|
_: {
|
|
ok(0)
|
|
}
|
|
};
|
|
}
|
|
""";
|
|
|
|
private static final String SERVICE_SOURCE = """
|
|
declare service Game {
|
|
fn tick(x: int) -> int { return x; }
|
|
}
|
|
|
|
fn main() -> void {
|
|
Game.tick(1);
|
|
}
|
|
""";
|
|
|
|
private static final String SDK_IMPORT_SOURCE = """
|
|
import { Gfx } from @sdk:gfx;
|
|
|
|
fn main() -> void {
|
|
Gfx.clear();
|
|
}
|
|
""";
|
|
|
|
private static final String PROJECT_IMPORT_SOURCE = """
|
|
import { Color } from @app:graphics;
|
|
|
|
fn main(color: Color) -> Color {
|
|
return color;
|
|
}
|
|
""";
|
|
|
|
@Test
|
|
void analyzeDocumentUsesVfsOverlayForRequestedDocumentAndFilesystemFallbackForClosedFiles() throws Exception {
|
|
final Path projectRoot = createProject();
|
|
final Path mainFile = projectRoot.resolve("src/main.pbs");
|
|
final Path helperFile = projectRoot.resolve("src/helper.pbs");
|
|
Files.writeString(mainFile, "fn broken( -> void {}\n");
|
|
Files.writeString(helperFile, "fn helper() -> void {}\n");
|
|
|
|
final VfsProjectDocument delegate = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
|
|
final LspService service = new LspServiceImpl(
|
|
new LspProjectContext("Example", "pbs", projectRoot),
|
|
new OverlayVfsProjectDocument(delegate, mainFile, OVERLAY_SOURCE));
|
|
|
|
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
|
|
|
|
assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper_call"));
|
|
assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper"));
|
|
assertEquals(List.of("/themes/pbs/semantic-highlighting.css"), analysis.semanticPresentation().resources());
|
|
assertTrue(analysis.semanticPresentation().semanticKeys().contains("pbs-function"));
|
|
|
|
assertTrue(
|
|
analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")),
|
|
analysis.toString());
|
|
assertTrue(
|
|
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.IF),
|
|
analysis.toString());
|
|
assertTrue(
|
|
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.WHILE),
|
|
analysis.toString());
|
|
assertTrue(
|
|
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SWITCH),
|
|
analysis.toString());
|
|
assertTrue(
|
|
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.HANDLE),
|
|
analysis.toString());
|
|
assertTrue(
|
|
analysis.workspaceSymbols().stream().anyMatch(symbol ->
|
|
symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))),
|
|
analysis.toString());
|
|
|
|
final int offset = OVERLAY_SOURCE.indexOf("helper();");
|
|
final var definition = service.definition(new LspDefinitionRequest(mainFile, offset));
|
|
final List<LspDefinitionTargetDTO> targets = definition.targets();
|
|
|
|
assertEquals(2, targets.size());
|
|
assertEquals(normalize(helperFile), targets.getFirst().documentPath());
|
|
assertEquals("helper", targets.getFirst().name());
|
|
}
|
|
|
|
@Test
|
|
void analyzeDocumentSurfacesDiagnosticsWithoutAbortingSemanticRead() throws Exception {
|
|
final Path projectRoot = createProject();
|
|
final Path mainFile = projectRoot.resolve("src/main.pbs");
|
|
Files.writeString(mainFile, """
|
|
fn main( -> void
|
|
{
|
|
helper();
|
|
}
|
|
""");
|
|
|
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
|
|
final LspService service = new LspServiceImpl(
|
|
new LspProjectContext("Example", "pbs", projectRoot),
|
|
vfs);
|
|
|
|
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
|
|
|
|
assertFalse(analysis.diagnostics().isEmpty(), analysis.toString());
|
|
assertTrue(analysis.diagnostics().stream().allMatch(diagnostic ->
|
|
diagnostic.documentPath().equals(normalize(mainFile))));
|
|
}
|
|
|
|
@Test
|
|
void analyzeDocumentHighlightsServiceIdentifiersWithServiceSemanticKey() throws Exception {
|
|
final Path projectRoot = createProject();
|
|
final Path mainFile = projectRoot.resolve("src/main.pbs");
|
|
Files.writeString(mainFile, SERVICE_SOURCE);
|
|
|
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
|
|
final LspService service = new LspServiceImpl(
|
|
new LspProjectContext("Example", "pbs", projectRoot),
|
|
vfs);
|
|
|
|
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
|
|
|
|
assertEquals("pbs-service", semanticKeyForLexeme(analysis, SERVICE_SOURCE, "Game"));
|
|
assertTrue(flatten(analysis.documentSymbols()).stream().anyMatch(symbol ->
|
|
symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SERVICE && symbol.name().equals("Game")));
|
|
}
|
|
|
|
@Test
|
|
void analyzeDocumentHighlightsSdkImportedHostIdentifiersWithHostSemanticKey() throws Exception {
|
|
final Path projectRoot = createProject();
|
|
final Path mainFile = projectRoot.resolve("src/main.pbs");
|
|
Files.writeString(mainFile, SDK_IMPORT_SOURCE);
|
|
|
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
|
|
final LspService service = new LspServiceImpl(
|
|
new LspProjectContext("Example", "pbs", projectRoot),
|
|
vfs);
|
|
|
|
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
|
|
|
|
assertEquals(List.of("pbs-service", "pbs-service"), semanticKeysForLexeme(analysis, SDK_IMPORT_SOURCE, "Gfx"));
|
|
}
|
|
|
|
@Test
|
|
void analyzeDocumentHighlightsProjectImportedStructIdentifiersWithStructSemanticKey() throws Exception {
|
|
final Path projectRoot = createProject();
|
|
final Path mainFile = projectRoot.resolve("src/main.pbs");
|
|
final Path graphicsDir = projectRoot.resolve("src/graphics");
|
|
final Path graphicsFile = graphicsDir.resolve("types.pbs");
|
|
final Path graphicsBarrel = graphicsDir.resolve("mod.barrel");
|
|
Files.createDirectories(graphicsDir);
|
|
Files.writeString(mainFile, PROJECT_IMPORT_SOURCE);
|
|
Files.writeString(graphicsFile, "declare struct Color(raw: int);\n");
|
|
Files.writeString(graphicsBarrel, "pub struct Color;\n");
|
|
|
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
|
|
final LspService service = new LspServiceImpl(
|
|
new LspProjectContext("Example", "pbs", projectRoot),
|
|
vfs);
|
|
|
|
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
|
|
|
|
assertEquals(List.of("pbs-struct", "pbs-struct", "pbs-struct"), semanticKeysForLexeme(analysis, PROJECT_IMPORT_SOURCE, "Color"));
|
|
}
|
|
|
|
private Path createProject() throws Exception {
|
|
final Path src = Files.createDirectories(tempDir.resolve("src"));
|
|
Files.writeString(tempDir.resolve("prometeu.json"), """
|
|
{
|
|
"name": "Example",
|
|
"version": "1.0.0",
|
|
"language": "pbs",
|
|
"stdlib": "1",
|
|
"dependencies": []
|
|
}
|
|
""");
|
|
Files.writeString(src.resolve("mod.barrel"), """
|
|
pub fn helper() -> void;
|
|
pub fn helper_result() -> result<MyError> int;
|
|
pub error MyError;
|
|
""");
|
|
Files.writeString(src.resolve("helper.pbs"), "fn helper() -> void {}\n");
|
|
Files.writeString(src.resolve("helper_result.pbs"), """
|
|
declare error MyError {
|
|
Failed
|
|
}
|
|
|
|
fn helper_result() -> result<MyError> int {
|
|
return ok(1);
|
|
}
|
|
""");
|
|
return tempDir;
|
|
}
|
|
|
|
private static List<p.studio.lsp.dtos.LspSymbolDTO> flatten(
|
|
final List<p.studio.lsp.dtos.LspSymbolDTO> symbols) {
|
|
final List<p.studio.lsp.dtos.LspSymbolDTO> flattened = new java.util.ArrayList<>();
|
|
for (final var symbol : symbols) {
|
|
flattened.add(symbol);
|
|
flattened.addAll(flatten(symbol.children()));
|
|
}
|
|
return flattened;
|
|
}
|
|
|
|
private VfsProjectContext projectContext(final Path projectRoot) {
|
|
return new VfsProjectContext("Example", "pbs", projectRoot);
|
|
}
|
|
|
|
private static Path normalize(final Path path) {
|
|
try {
|
|
return path.toAbsolutePath().normalize().toRealPath();
|
|
} catch (Exception exception) {
|
|
throw new IllegalStateException(exception);
|
|
}
|
|
}
|
|
|
|
private static String semanticKeyForLexeme(
|
|
final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis,
|
|
final String source,
|
|
final String lexeme) {
|
|
return semanticKeysForLexeme(analysis, source, lexeme).stream().findFirst().orElseThrow();
|
|
}
|
|
|
|
private static List<String> semanticKeysForLexeme(
|
|
final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis,
|
|
final String source,
|
|
final String lexeme) {
|
|
return analysis.semanticHighlights().stream()
|
|
.filter(highlight -> lexeme.equals(spanContent(source, highlight.range().startOffset(), highlight.range().endOffset())))
|
|
.map(p.studio.lsp.dtos.LspHighlightSpanDTO::semanticKey)
|
|
.toList();
|
|
}
|
|
|
|
private static String spanContent(
|
|
final String source,
|
|
final int start,
|
|
final int end) {
|
|
if (start < 0 || end > source.length() || start >= end) {
|
|
return "";
|
|
}
|
|
return source.substring(start, end);
|
|
}
|
|
|
|
private static final class OverlayVfsProjectDocument implements VfsProjectDocument {
|
|
private final VfsProjectDocument delegate;
|
|
private final Path overlayPath;
|
|
private final String overlayContent;
|
|
|
|
private OverlayVfsProjectDocument(
|
|
final VfsProjectDocument delegate,
|
|
final Path overlayPath,
|
|
final String overlayContent) {
|
|
this.delegate = delegate;
|
|
this.overlayPath = normalize(overlayPath);
|
|
this.overlayContent = overlayContent;
|
|
}
|
|
|
|
@Override
|
|
public VfsProjectContext projectContext() {
|
|
return delegate.projectContext();
|
|
}
|
|
|
|
@Override
|
|
public VfsProjectSnapshot snapshot() {
|
|
return delegate.snapshot();
|
|
}
|
|
|
|
@Override
|
|
public VfsProjectSnapshot refresh() {
|
|
return delegate.refresh();
|
|
}
|
|
|
|
@Override
|
|
public VfsProjectSnapshot refresh(final VfsRefreshRequest request) {
|
|
return delegate.refresh(request);
|
|
}
|
|
|
|
@Override
|
|
public VfsDocumentOpenResult openDocument(final Path path) {
|
|
final Path normalizedPath = normalize(path);
|
|
if (!overlayPath.equals(normalizedPath)) {
|
|
return delegate.openDocument(normalizedPath);
|
|
}
|
|
final VfsDocumentAccessContext accessContext = new VfsDocumentAccessContext(
|
|
normalizedPath,
|
|
"pbs",
|
|
true,
|
|
VfsDocumentAccessMode.READ_ONLY,
|
|
Map.of());
|
|
return new VfsDocumentOpenResult.VfsTextDocument(
|
|
normalizedPath,
|
|
normalizedPath.getFileName().toString(),
|
|
"pbs",
|
|
overlayContent,
|
|
"LF",
|
|
false,
|
|
accessContext);
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
delegate.close();
|
|
}
|
|
|
|
private static Path normalize(final Path path) {
|
|
final Path normalized = path.toAbsolutePath().normalize();
|
|
try {
|
|
return Files.exists(normalized) ? normalized.toRealPath() : normalized;
|
|
} catch (Exception ignored) {
|
|
return normalized;
|
|
}
|
|
}
|
|
}
|
|
}
|