implements PLN-0137 (2/7) document-links
Advertise document links only when the frontend supports them. A link covers the ModuleRef span when module assembly already bound that module to a regular file. Virtual stdlib and unresolved modules produce no link.
This commit is contained in:
parent
099857b636
commit
3320f222bb
@ -288,6 +288,8 @@ Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Dia
|
||||
|
||||
When tooling publishes an editor diagnostic, `source` MUST be the `languageId` of the bound frontend project. `code` MUST be the stable compiler diagnostic code when one exists, and MUST be empty when the compiler diagnostic has none. Tooling MUST NOT copy that code into `source`. Tooling MUST NOT publish related locations, compiler phase, or a repair payload on the editor diagnostic.
|
||||
|
||||
When a frontend exposes document links, each link MUST cover the span of a module reference whose resolved destination is a compiler-known regular file. A module reference that does not resolve to a regular file MUST produce no link. Tooling MUST NOT target virtual, untitled, or synthetic URIs, including virtual stdlib documents. Document links MUST NOT replace definition. A missing document-link capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
`FrontendSpec` remains the source of static frontend-owned presentation metadata such as semantic vocabularies, host projections, and visual themes. Producing semantic tokens for a live document is an optional editor-facing capability; the existence of static presentation metadata MUST NOT imply that every frontend can provide live semantic-token results.
|
||||
|
||||
The frontend registry MUST resolve providers by `languageId`. A lookup for an unknown `languageId` MUST fail explicitly with a diagnostic-friendly error. Unknown languages MUST NOT silently fall back to PBS or to any other frontend.
|
||||
|
||||
@ -21,6 +21,7 @@ import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
|
||||
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDocumentLink;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||
@ -40,6 +41,7 @@ import p.studio.compiler.source.Span;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.FileTableReader;
|
||||
import p.studio.compiler.source.tables.ModuleReference;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Files;
|
||||
@ -401,6 +403,46 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return FrontendRenameResult.applied(List.copyOf(edits.values()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean documentLinksSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendDocumentLink> documentLinks(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
if (request == null
|
||||
|| editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface)
|
||||
|| surface.physicalModuleFileByReference() == null
|
||||
|| ast.imports() == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<FrontendDocumentLink> links = new ArrayList<>();
|
||||
for (final PbsAst.ImportDecl importDecl : ast.imports()) {
|
||||
if (importDecl == null || importDecl.moduleRef() == null) {
|
||||
continue;
|
||||
}
|
||||
final PbsAst.ModuleRef moduleRef = importDecl.moduleRef();
|
||||
final Span span = moduleRef.span();
|
||||
if (span == null || span.isNone() || moduleRef.project() == null || moduleRef.project().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
final Path target = surface.physicalModuleFileByReference().get(
|
||||
new ModuleReference(moduleRef.project(), moduleRef.pathSegments()));
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
links.add(new FrontendDocumentLink(
|
||||
target,
|
||||
toOffset(span.getStart()),
|
||||
toOffset(span.getEnd())));
|
||||
}
|
||||
return List.copyOf(links);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean codeActionsSupported() {
|
||||
return true;
|
||||
|
||||
@ -15,9 +15,12 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.identifiers.ModuleId;
|
||||
import p.studio.compiler.source.tables.ModuleReference;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@ -153,14 +156,46 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
||||
Map.copyOf(astByFile),
|
||||
Map.copyOf(supplementalTopDeclsByFile),
|
||||
Map.copyOf(inlineHintsByFile),
|
||||
Map.copyOf(sourceKindByFile));
|
||||
Map.copyOf(sourceKindByFile),
|
||||
physicalModuleFiles(ctx, assembly));
|
||||
}
|
||||
|
||||
private static Map<ModuleReference, Path> physicalModuleFiles(
|
||||
final FrontendPhaseContext ctx,
|
||||
final PbsModuleAssembly assembly) {
|
||||
final Map<ModuleReference, Path> files = new LinkedHashMap<>();
|
||||
if (ctx == null || ctx.fileTable == null || assembly == null || assembly.parsedSourceFiles() == null) {
|
||||
return Map.of();
|
||||
}
|
||||
for (final PbsParsedSourceFile parsed : assembly.parsedSourceFiles()) {
|
||||
if (parsed.fileId() == null || parsed.fileId().isNone()
|
||||
|| parsed.moduleId() == null || parsed.moduleId().isNone()) {
|
||||
continue;
|
||||
}
|
||||
final var handle = ctx.fileTable.get(parsed.fileId());
|
||||
if (handle == null || handle.getCanonPath() == null) {
|
||||
continue;
|
||||
}
|
||||
final Path path = handle.getCanonPath().toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(path)) {
|
||||
continue;
|
||||
}
|
||||
final ModuleReference reference = assembly.moduleTable().get(parsed.moduleId());
|
||||
if (reference == null) {
|
||||
continue;
|
||||
}
|
||||
files.merge(reference, path, (left, right) ->
|
||||
left.toString().compareTo(right.toString()) <= 0 ? left : right);
|
||||
}
|
||||
return Map.copyOf(files);
|
||||
}
|
||||
|
||||
public record PbsSemanticReadSurface(
|
||||
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
|
||||
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile,
|
||||
Map<FileId, SourceKind> sourceKindByFile) {
|
||||
Map<FileId, SourceKind> sourceKindByFile,
|
||||
Map<ModuleReference, Path> physicalModuleFileByReference) {
|
||||
}
|
||||
|
||||
private IRBackend mergeCompiledSources(
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package p.studio.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.AppMode;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.BuildStack;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.SourceHandle;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.FileTable;
|
||||
import p.studio.compiler.source.tables.ProjectTable;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PBSFrontendDocumentLinkTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void documentLinksUseTheAssembledPhysicalModuleFile() throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceRoot = projectRoot.resolve("src");
|
||||
final Path sourceA = sourceRoot.resolve("a").resolve("source.pbs");
|
||||
final Path sourceB = sourceRoot.resolve("b").resolve("source.pbs");
|
||||
Files.createDirectories(sourceA.getParent());
|
||||
Files.createDirectories(sourceB.getParent());
|
||||
Files.writeString(sourceA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { target } from @app:a;
|
||||
import { Log } from @sdk:log;
|
||||
import { Missing } from @app:missing;
|
||||
|
||||
fn caller() -> int {
|
||||
return target();
|
||||
}
|
||||
""";
|
||||
Files.writeString(sourceB, caller);
|
||||
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
|
||||
|
||||
final var projectTable = new ProjectTable();
|
||||
final var fileTable = new FileTable(1);
|
||||
final var projectId = projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(projectRoot)
|
||||
.name("app")
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
|
||||
.build());
|
||||
register(projectId, projectRoot, sourceA, fileTable);
|
||||
register(projectId, projectRoot, sourceA.getParent().resolve("mod.barrel"), fileTable);
|
||||
final FileId callerId = register(projectId, projectRoot, sourceB, fileTable);
|
||||
register(projectId, projectRoot, sourceB.getParent().resolve("mod.barrel"), fileTable);
|
||||
|
||||
final var phaseContext = new FrontendPhaseContext(
|
||||
projectTable,
|
||||
fileTable,
|
||||
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
|
||||
AppMode.Game);
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var editorialContext = service.prepareEditorialContext(phaseContext, callerId).orElseThrow();
|
||||
final var links = service.documentLinks(
|
||||
new FrontendDocumentRequest(projectRoot, sourceB, caller),
|
||||
editorialContext);
|
||||
|
||||
assertEquals(1, links.size());
|
||||
assertEquals(sourceA.toAbsolutePath().normalize(), links.getFirst().path());
|
||||
final byte[] bytes = caller.getBytes(StandardCharsets.UTF_8);
|
||||
assertEquals("@app:a", new String(
|
||||
bytes,
|
||||
links.getFirst().startOffset(),
|
||||
links.getFirst().endOffset() - links.getFirst().startOffset(),
|
||||
StandardCharsets.UTF_8));
|
||||
assertTrue(service.documentLinksSupported());
|
||||
}
|
||||
|
||||
private static FileId register(
|
||||
final p.studio.compiler.source.identifiers.ProjectId projectId,
|
||||
final Path projectRoot,
|
||||
final Path file,
|
||||
final FileTable fileTable) throws IOException {
|
||||
final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
return fileTable.register(new SourceHandle(
|
||||
projectId,
|
||||
projectRoot.relativize(file),
|
||||
file,
|
||||
attributes.size(),
|
||||
attributes.lastModifiedTime().toMillis(),
|
||||
SourceProviderFactory.filesystem()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendDocumentLink(
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendDocumentLink {
|
||||
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,6 +139,20 @@ public interface FrontendLanguageService {
|
||||
return rename(request, offset, newName);
|
||||
}
|
||||
|
||||
default boolean documentLinksSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default List<FrontendDocumentLink> documentLinks(final FrontendDocumentRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendDocumentLink> documentLinks(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return documentLinks(request);
|
||||
}
|
||||
|
||||
default boolean codeActionsSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -30,6 +30,9 @@ class FrontendLanguageServiceTest {
|
||||
assertTrue(service.prepareRename(request, 0).isEmpty());
|
||||
assertEquals(FrontendRenameResult.Status.UNSUPPORTED, service.rename(request, 0, "renamed").status());
|
||||
assertTrue(service.rename(request, 0, "renamed").edits().isEmpty());
|
||||
assertFalse(service.documentLinksSupported());
|
||||
assertTrue(service.documentLinks(request).isEmpty());
|
||||
assertTrue(service.documentLinks(request, null).isEmpty());
|
||||
assertFalse(service.codeActionsSupported());
|
||||
assertTrue(service.codeActions(request).isEmpty());
|
||||
assertTrue(service.codeActions(request, null).isEmpty());
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineDocumentLink(
|
||||
String targetUri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineDocumentLink {
|
||||
targetUri = requireText(targetUri, "targetUri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("document link range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = Objects.requireNonNull(value, field).trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineDocumentLinks(List<BaselineDocumentLink> links) {
|
||||
public BaselineDocumentLinks {
|
||||
links = links == null ? List.of() : List.copyOf(links);
|
||||
}
|
||||
|
||||
public static BaselineDocumentLinks empty() {
|
||||
return new BaselineDocumentLinks(List.of());
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,7 @@ public record BaselineServerDescription(
|
||||
boolean workspaceSymbolsSupported,
|
||||
boolean renameSupported,
|
||||
boolean codeActionsSupported,
|
||||
boolean documentLinksSupported,
|
||||
List<String> semanticTokenTypes,
|
||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||
List<BaselineVisualTheme> visualThemes,
|
||||
|
||||
@ -9,6 +9,7 @@ import p.studio.lsp.messages.BaselineReferences;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
@ -60,6 +61,10 @@ public interface LanguageServiceBridge {
|
||||
|
||||
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
default BaselineDocumentLinks documentLinks(LspProjectContext context, String documentUri, String text) {
|
||||
return BaselineDocumentLinks.empty();
|
||||
}
|
||||
|
||||
BaselineCodeActions codeActions(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
|
||||
@ -6,6 +6,7 @@ import p.studio.compiler.messages.*;
|
||||
import p.studio.compiler.models.*;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDocumentLink;
|
||||
import p.studio.compiler.services.FrontendLanguageService;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
@ -57,6 +58,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
final boolean codeActionsSupported = languageService
|
||||
.map(FrontendLanguageService::codeActionsSupported)
|
||||
.orElse(false);
|
||||
final boolean documentLinksSupported = languageService
|
||||
.map(FrontendLanguageService::documentLinksSupported)
|
||||
.orElse(false);
|
||||
return new BaselineServerDescription(
|
||||
"Prometeu Studio LSP",
|
||||
"0.1.0",
|
||||
@ -70,6 +74,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
codeActionsSupported,
|
||||
documentLinksSupported,
|
||||
presentation.semanticKeys(),
|
||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||
@ -365,6 +370,29 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
.orElseGet(() -> BaselineRename.refused("not renameable"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineDocumentLinks documentLinks(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty() || !languageService.orElseThrow().documentLinksSupported()) {
|
||||
return BaselineDocumentLinks.empty();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
document.text());
|
||||
return new BaselineDocumentLinks(mapDocumentLinks(
|
||||
languageService.orElseThrow().documentLinks(request, document.editorialContext()),
|
||||
document.text()));
|
||||
})
|
||||
.orElseGet(BaselineDocumentLinks::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
@ -830,6 +858,28 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDocumentLink> mapDocumentLinks(
|
||||
final List<FrontendDocumentLink> links,
|
||||
final String currentDocumentText) {
|
||||
if (links == null || links.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(
|
||||
currentDocumentText == null ? "" : currentDocumentText);
|
||||
final ArrayList<BaselineDocumentLink> mapped = new ArrayList<>();
|
||||
for (final FrontendDocumentLink link : links) {
|
||||
final DocumentPosition start = mapper.positionOf(link.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(link.endOffset());
|
||||
mapped.add(new BaselineDocumentLink(
|
||||
link.path().toUri().toString(),
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDefinitionLocation> mapDefinitionLocations(
|
||||
final List<FrontendDefinitionLocation> locations,
|
||||
final Path currentDocumentPath,
|
||||
|
||||
@ -168,6 +168,14 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<DocumentLink>> documentLink(final DocumentLinkParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapDocumentLinks(
|
||||
languageServiceBridge.documentLinks(project, uri, text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(final CodeActionParams params) {
|
||||
if (!quickFixRequested(params.getContext())) {
|
||||
|
||||
@ -53,6 +53,11 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.QuickFix));
|
||||
capabilities.setCodeActionProvider(codeActionOptions);
|
||||
}
|
||||
if (description.documentLinksSupported()) {
|
||||
final DocumentLinkOptions documentLinkOptions = new DocumentLinkOptions();
|
||||
documentLinkOptions.setResolveProvider(false);
|
||||
capabilities.setDocumentLinkProvider(documentLinkOptions);
|
||||
}
|
||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||
semanticTokens.setFull(true);
|
||||
@ -159,6 +164,23 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
return edit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DocumentLink> mapDocumentLinks(final BaselineDocumentLinks links) {
|
||||
if (links == null || links.links().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<DocumentLink> mapped = new ArrayList<>();
|
||||
for (final BaselineDocumentLink link : links.links()) {
|
||||
final DocumentLink documentLink = new DocumentLink();
|
||||
documentLink.setRange(new Range(
|
||||
new Position(link.startLine(), link.startCharacter()),
|
||||
new Position(link.endLine(), link.endCharacter())));
|
||||
documentLink.setTarget(link.targetUri());
|
||||
mapped.add(documentLink);
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Either<Command, CodeAction>> mapCodeActions(
|
||||
final BaselineCodeActions actions,
|
||||
|
||||
@ -9,6 +9,7 @@ import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
import org.eclipse.lsp4j.Diagnostic;
|
||||
import org.eclipse.lsp4j.DocumentLink;
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.Location;
|
||||
import org.eclipse.lsp4j.Range;
|
||||
@ -19,6 +20,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import org.eclipse.lsp4j.PrepareRenameDefaultBehavior;
|
||||
import org.eclipse.lsp4j.PrepareRenameResult;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
@ -59,6 +61,10 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
List<Either<Command, CodeAction>> mapCodeActions(BaselineCodeActions actions, List<Diagnostic> contextDiagnostics);
|
||||
|
||||
default List<DocumentLink> mapDocumentLinks(BaselineDocumentLinks links) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.lsp.services.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.FrontendSpec;
|
||||
@ -14,6 +15,7 @@ import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlySet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@ -74,6 +76,7 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(bridge.codeActions(context, documentUri, "fn main() {}", List.of(
|
||||
new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1)))
|
||||
.actions().isEmpty());
|
||||
assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -768,6 +771,7 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(description.workspaceSymbolsSupported());
|
||||
assertTrue(description.renameSupported());
|
||||
assertTrue(description.codeActionsSupported());
|
||||
assertTrue(description.documentLinksSupported());
|
||||
assertEquals(1, description.semanticHostProjections().size());
|
||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||
@ -781,6 +785,77 @@ class CompilerLanguageServiceBridgeTest {
|
||||
&& tokenStyle.foreground().equals("#7fb8ff")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksSkipVirtualStdlibImports() throws IOException {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
.resolve("test-projects")
|
||||
.resolve("main")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
|
||||
final String text = Files.readString(documentPath);
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var links = bridge.documentLinks(
|
||||
new LspProjectContext("main", "pbs", projectRoot),
|
||||
documentPath.toUri().toString(),
|
||||
text);
|
||||
|
||||
assertTrue(links.links().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksPointAtThePhysicalModuleFile(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceA = projectRoot.resolve("src").resolve("a").resolve("source.pbs");
|
||||
final Path sourceB = projectRoot.resolve("src").resolve("b").resolve("source.pbs");
|
||||
Files.createDirectories(sourceA.getParent());
|
||||
Files.createDirectories(sourceB.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { target } from @app:a;
|
||||
import { Log } from @sdk:log;
|
||||
import { Missing } from @app:missing;
|
||||
|
||||
fn caller() -> int {
|
||||
return target();
|
||||
}
|
||||
""";
|
||||
Files.writeString(sourceB, caller);
|
||||
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
|
||||
final Path realRoot = projectRoot.toRealPath();
|
||||
final Path realA = sourceA.toRealPath();
|
||||
final Path realB = sourceB.toRealPath();
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var links = bridge.documentLinks(
|
||||
new LspProjectContext("app", "pbs", realRoot),
|
||||
realB.toUri().toString(),
|
||||
caller);
|
||||
|
||||
assertEquals(1, links.links().size());
|
||||
final var link = links.links().getFirst();
|
||||
assertEquals(realA.toUri().toString(), link.targetUri());
|
||||
final int start = new DocumentPositionMapper(caller).byteOffsetOf(link.startLine(), link.startCharacter());
|
||||
final int end = new DocumentPositionMapper(caller).byteOffsetOf(link.endLine(), link.endCharacter());
|
||||
assertEquals("@app:a", new String(caller.getBytes(StandardCharsets.UTF_8), start, end - start, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private Path findRepoRoot(final Path start) {
|
||||
var current = start;
|
||||
while (current != null) {
|
||||
|
||||
@ -105,6 +105,7 @@ class PrometeuLanguageServerTest {
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
List.of(),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
|
||||
@ -10,6 +10,8 @@ import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentIssue;
|
||||
import p.studio.lsp.messages.BaselineDocumentLink;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.BaselineCompletionItem;
|
||||
import p.studio.lsp.messages.BaselineCompletionItemKind;
|
||||
@ -55,6 +57,7 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -82,6 +85,7 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider());
|
||||
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().getResolveProvider());
|
||||
|
||||
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||
@ -298,4 +302,53 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(1, action.getDiagnostics().size());
|
||||
assertEquals(diagnostic, action.getDiagnostics().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksMapRangeAndTargetWithoutResolve() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
final var unsupported = mapper.mapInitializeResult(serverDescription(false));
|
||||
assertNull(unsupported.getCapabilities().getDocumentLinkProvider());
|
||||
|
||||
final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of(
|
||||
new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25))));
|
||||
assertEquals(1, mapped.size());
|
||||
assertEquals("file:///tmp/a/source.pbs", mapped.getFirst().getTarget());
|
||||
assertEquals(0, mapped.getFirst().getRange().getStart().getLine());
|
||||
assertEquals(19, mapped.getFirst().getRange().getStart().getCharacter());
|
||||
assertEquals(25, mapped.getFirst().getRange().getEnd().getCharacter());
|
||||
assertNull(mapped.getFirst().getData());
|
||||
assertTrue(mapper.mapDocumentLinks(BaselineDocumentLinks.empty()).isEmpty());
|
||||
}
|
||||
|
||||
private static BaselineServerDescription serverDescription(final boolean documentLinksSupported) {
|
||||
return new BaselineServerDescription(
|
||||
"demo",
|
||||
"1",
|
||||
"pbs",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
documentLinksSupported,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
List.of(new BaselineSemanticHostProjectionEntry(
|
||||
"demo-keyword",
|
||||
"keyword",
|
||||
List.of(),
|
||||
"variable",
|
||||
List.of())))),
|
||||
List.of(new BaselineVisualTheme(
|
||||
"demo-default",
|
||||
"Demo Default",
|
||||
new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"),
|
||||
List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))),
|
||||
"demo-default");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user