implements PLN-0076
This commit is contained in:
parent
7f40541e96
commit
7cbe61f5eb
@ -5,7 +5,10 @@ plugins {
|
||||
dependencies {
|
||||
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
||||
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||
implementation(project(":prometeu-compiler:prometeu-frontend-api"))
|
||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
|
||||
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
||||
implementation(project(":prometeu-compiler:prometeu-deps"))
|
||||
implementation(libs.lsp4j)
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCompletion(
|
||||
boolean incomplete,
|
||||
List<BaselineCompletionItem> items) {
|
||||
public BaselineCompletion {
|
||||
items = List.copyOf(Objects.requireNonNull(items, "items"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCompletionItem(
|
||||
String label,
|
||||
BaselineCompletionItemKind kind,
|
||||
String detail,
|
||||
String documentation) {
|
||||
public BaselineCompletionItem {
|
||||
label = requireText(label, "label");
|
||||
kind = Objects.requireNonNull(kind, "kind");
|
||||
detail = normalize(detail);
|
||||
documentation = normalize(documentation);
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = normalize(value);
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static String normalize(final String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public enum BaselineCompletionItemKind {
|
||||
KEYWORD,
|
||||
VARIABLE,
|
||||
FIELD,
|
||||
FUNCTION,
|
||||
METHOD,
|
||||
CONSTRUCTOR,
|
||||
STRUCT,
|
||||
BUILTIN_TYPE,
|
||||
SERVICE,
|
||||
HOST,
|
||||
CONTRACT,
|
||||
CALLBACK,
|
||||
ENUM,
|
||||
ERROR,
|
||||
GLOBAL,
|
||||
CONST
|
||||
}
|
||||
@ -8,6 +8,8 @@ public record BaselineServerDescription(
|
||||
String version,
|
||||
String frontendLanguageId,
|
||||
boolean hoverSupported,
|
||||
boolean completionSupported,
|
||||
boolean signatureHelpSupported,
|
||||
List<String> semanticTokenTypes,
|
||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||
List<BaselineVisualTheme> visualThemes,
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineSignature(
|
||||
String label,
|
||||
List<String> parameters,
|
||||
String documentation) {
|
||||
public BaselineSignature {
|
||||
label = requireText(label, "label");
|
||||
parameters = List.copyOf(Objects.requireNonNull(parameters, "parameters"));
|
||||
documentation = normalize(documentation);
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = normalize(value);
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static String normalize(final String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineSignatureHelp(
|
||||
List<BaselineSignature> signatures,
|
||||
int activeSignature,
|
||||
int activeParameter) {
|
||||
public BaselineSignatureHelp {
|
||||
signatures = List.copyOf(Objects.requireNonNull(signatures, "signatures"));
|
||||
if (!signatures.isEmpty() && (activeSignature < 0 || activeSignature >= signatures.size())) {
|
||||
throw new IllegalArgumentException("activeSignature must reference one of the provided signatures");
|
||||
}
|
||||
if (activeParameter < 0) {
|
||||
throw new IllegalArgumentException("activeParameter must be >= 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
package p.studio.lsp.services;
|
||||
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineHover;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
|
||||
@ -11,7 +13,11 @@ public interface LanguageServiceBridge {
|
||||
|
||||
BaselineDocumentAnalysis analyzeDocument(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
BaselineHover hover(LspProjectContext context, String documentUri, int line, int character);
|
||||
BaselineCompletion completion(LspProjectContext context, String documentUri, String text, int line, int character);
|
||||
|
||||
BaselineHover hover(LspProjectContext context, String documentUri, String text, int line, int character);
|
||||
|
||||
BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character);
|
||||
|
||||
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
|
||||
@ -3,7 +3,10 @@ package p.studio.lsp.services.compiler;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.compiler.messages.BuilderPipelineConfig;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.messages.BuildingIssue;
|
||||
import p.studio.compiler.messages.HostAdmissionContext;
|
||||
import p.studio.compiler.models.AnalysisSnapshot;
|
||||
import p.studio.compiler.models.BuilderPipelineContext;
|
||||
import p.studio.compiler.models.FrontendEditorPaletteSpec;
|
||||
@ -14,20 +17,33 @@ import p.studio.compiler.models.FrontendSemanticToken;
|
||||
import p.studio.compiler.models.FrontendTokenStyleSpec;
|
||||
import p.studio.compiler.models.FrontendVisualThemeSpec;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.services.PBSFrontendPhaseService;
|
||||
import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.compiler.workspaces.BuilderPipelineService;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSignature;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService;
|
||||
import p.studio.lsp.messages.*;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class CompilerLanguageServiceBridge implements LanguageServiceBridge {
|
||||
private final AssetSurfaceContextLoader assetSurfaceContextLoader = new AssetSurfaceContextLoader();
|
||||
private final PbsEditorialSupportService editorialSupportService = new PbsEditorialSupportService();
|
||||
|
||||
@Override
|
||||
public BaselineServerDescription describeServer(final LspProjectContext context) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
@ -38,6 +54,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
"0.1.0",
|
||||
context.languageId(),
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
presentation.semanticKeys(),
|
||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||
@ -57,19 +75,76 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineHover hover(
|
||||
public BaselineCompletion completion(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
return new BaselineHover("""
|
||||
**Prometeu Studio LSP**
|
||||
|
||||
Compiler-backed baseline.
|
||||
|
||||
Semantic enrichment will be layered later.
|
||||
""");
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
|
||||
final List<PbsEditorialCompletionCandidate> candidates = editorialSupportService.completion(
|
||||
document.text(),
|
||||
document.ast(),
|
||||
document.supplementalTopDecls(),
|
||||
document.feSurfaceContext(),
|
||||
offset);
|
||||
return new BaselineCompletion(
|
||||
false,
|
||||
candidates.stream().map(this::mapCompletionItem).toList());
|
||||
})
|
||||
.orElseGet(() -> new BaselineCompletion(false, List.of()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineHover hover(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.flatMap(document -> {
|
||||
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
|
||||
return editorialSupportService.hover(
|
||||
document.text(),
|
||||
document.ast(),
|
||||
document.supplementalTopDecls(),
|
||||
document.feSurfaceContext(),
|
||||
offset);
|
||||
})
|
||||
.map(symbol -> new BaselineHover(formatHoverMarkdown(symbol)))
|
||||
.orElseGet(() -> new BaselineHover("No symbol information."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.flatMap(document -> {
|
||||
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
|
||||
return editorialSupportService.signatureHelp(
|
||||
document.text(),
|
||||
document.ast(),
|
||||
document.supplementalTopDecls(),
|
||||
document.feSurfaceContext(),
|
||||
offset);
|
||||
})
|
||||
.map(signatureHelp -> new BaselineSignatureHelp(
|
||||
signatureHelp.signatures().stream()
|
||||
.map(signature -> new BaselineSignature(signature.label(), signature.parameterLabels(), ""))
|
||||
.toList(),
|
||||
signatureHelp.activeSignature(),
|
||||
signatureHelp.activeParameter()))
|
||||
.orElseGet(() -> new BaselineSignatureHelp(List.of(), 0, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -119,6 +194,71 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty());
|
||||
}
|
||||
|
||||
private Optional<EditorialDocument> editorialDocument(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
if (!"pbs".equals(context.languageId())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig(
|
||||
new BuilderPipelineConfig(
|
||||
false,
|
||||
context.projectRoot().toString(),
|
||||
"core-v1",
|
||||
SourceProviderFactory.overlayUtf8(text == null ? Map.of() : Map.of(documentPath, text))));
|
||||
BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty());
|
||||
final FileId fileId = findFileId(pipelineContext, documentPath);
|
||||
if (fileId == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext(
|
||||
pipelineContext.resolvedWorkspace.graph().projectTable(),
|
||||
pipelineContext.fileTable,
|
||||
pipelineContext.resolvedWorkspace.stack(),
|
||||
pipelineContext.resolvedWorkspace.stdlib(),
|
||||
HostAdmissionContext.permissiveDefault(),
|
||||
assetSurfaceContextLoader.load(pipelineContext.resolvedWorkspace.mainProject().getRootPath()));
|
||||
final var semanticReadSurface = PBSFrontendPhaseService.semanticReadSurface(
|
||||
frontendPhaseContext,
|
||||
p.studio.compiler.source.diagnostics.DiagnosticSink.empty(),
|
||||
BuildingIssueSink.empty());
|
||||
final var ast = semanticReadSurface.astByFile().get(fileId);
|
||||
if (ast == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new EditorialDocument(
|
||||
text == null ? readCurrentText(pipelineContext, fileId) : text,
|
||||
ast,
|
||||
semanticReadSurface.supplementalTopDeclsByFile().getOrDefault(fileId, ReadOnlyList.empty()),
|
||||
frontendPhaseContext.feSurfaceContext()));
|
||||
}
|
||||
|
||||
private FileId findFileId(
|
||||
final BuilderPipelineContext pipelineContext,
|
||||
final Path documentPath) {
|
||||
for (int fileIndex = 0; fileIndex < pipelineContext.fileTable.size(); fileIndex += 1) {
|
||||
final FileId candidate = new FileId(fileIndex);
|
||||
final var sourceHandle = pipelineContext.fileTable.get(candidate);
|
||||
if (sourceHandle != null
|
||||
&& sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String readCurrentText(
|
||||
final BuilderPipelineContext pipelineContext,
|
||||
final FileId fileId) {
|
||||
final var sourceHandle = pipelineContext.fileTable.get(fileId);
|
||||
if (sourceHandle == null) {
|
||||
return "";
|
||||
}
|
||||
return sourceHandle.readUtf8().orElse("");
|
||||
}
|
||||
|
||||
private List<BaselineDocumentIssue> mapDiagnostics(
|
||||
final AnalysisSnapshot snapshot,
|
||||
final Path documentPath,
|
||||
@ -163,6 +303,14 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private BaselineCompletionItem mapCompletionItem(final PbsEditorialCompletionCandidate candidate) {
|
||||
return new BaselineCompletionItem(
|
||||
candidate.label(),
|
||||
mapCompletionKind(candidate.kind()),
|
||||
candidate.detail(),
|
||||
candidate.origin());
|
||||
}
|
||||
|
||||
private BaselineVisualTheme mapVisualTheme(final FrontendVisualThemeSpec theme) {
|
||||
return new BaselineVisualTheme(
|
||||
theme.themeId(),
|
||||
@ -210,4 +358,65 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return FrontendRegistryService.getFrontendSpec(context.languageId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("no frontend registered for languageId: " + context.languageId()));
|
||||
}
|
||||
|
||||
private BaselineCompletionItemKind mapCompletionKind(final PbsEditorialSymbolKind kind) {
|
||||
return switch (kind) {
|
||||
case KEYWORD -> BaselineCompletionItemKind.KEYWORD;
|
||||
case LOCAL, PARAMETER -> BaselineCompletionItemKind.VARIABLE;
|
||||
case FIELD -> BaselineCompletionItemKind.FIELD;
|
||||
case FUNCTION -> BaselineCompletionItemKind.FUNCTION;
|
||||
case METHOD -> BaselineCompletionItemKind.METHOD;
|
||||
case CONSTRUCTOR -> BaselineCompletionItemKind.CONSTRUCTOR;
|
||||
case STRUCT -> BaselineCompletionItemKind.STRUCT;
|
||||
case BUILTIN_TYPE -> BaselineCompletionItemKind.BUILTIN_TYPE;
|
||||
case SERVICE -> BaselineCompletionItemKind.SERVICE;
|
||||
case HOST -> BaselineCompletionItemKind.HOST;
|
||||
case CONTRACT -> BaselineCompletionItemKind.CONTRACT;
|
||||
case CALLBACK -> BaselineCompletionItemKind.CALLBACK;
|
||||
case ENUM -> BaselineCompletionItemKind.ENUM;
|
||||
case ERROR -> BaselineCompletionItemKind.ERROR;
|
||||
case GLOBAL -> BaselineCompletionItemKind.GLOBAL;
|
||||
case CONST -> BaselineCompletionItemKind.CONST;
|
||||
};
|
||||
}
|
||||
|
||||
private String formatHoverMarkdown(final PbsEditorialResolvedSymbol symbol) {
|
||||
final StringBuilder markdown = new StringBuilder();
|
||||
final List<PbsEditorialSignature> signatures = symbol.signatures();
|
||||
if (!signatures.isEmpty()) {
|
||||
markdown.append("```pbs\n");
|
||||
for (int index = 0; index < signatures.size(); index += 1) {
|
||||
if (index > 0) {
|
||||
markdown.append('\n');
|
||||
}
|
||||
markdown.append(signatures.get(index).label());
|
||||
}
|
||||
markdown.append("\n```");
|
||||
} else if (!symbol.detail().isBlank()) {
|
||||
markdown.append("```pbs\n").append(symbol.displayName());
|
||||
if (!symbol.detail().isBlank()) {
|
||||
markdown.append(" : ").append(symbol.detail());
|
||||
}
|
||||
markdown.append("\n```");
|
||||
}
|
||||
final String kindLabel = symbol.kind().name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
if (markdown.length() > 0) {
|
||||
markdown.append("\n\n");
|
||||
}
|
||||
markdown.append("**").append(kindLabel).append("**");
|
||||
if (!symbol.origin().isBlank()) {
|
||||
markdown.append(" from `").append(symbol.origin()).append('`');
|
||||
}
|
||||
if (!symbol.documentation().isBlank()) {
|
||||
markdown.append("\n\n").append(symbol.documentation());
|
||||
}
|
||||
return markdown.toString();
|
||||
}
|
||||
|
||||
private record EditorialDocument(
|
||||
String text,
|
||||
p.studio.compiler.pbs.ast.PbsAst.File ast,
|
||||
ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl> supplementalTopDecls,
|
||||
p.studio.compiler.messages.FESurfaceContext feSurfaceContext) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,40 @@ final class DocumentPositionMapper {
|
||||
return new DocumentPosition(line, character);
|
||||
}
|
||||
|
||||
int byteOffsetOf(
|
||||
final int line,
|
||||
final int character) {
|
||||
if (line <= 0 && character <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int currentLine = 0;
|
||||
int currentCharacter = 0;
|
||||
int utf8Offset = 0;
|
||||
for (int index = 0; index < text.length(); ) {
|
||||
if (currentLine == line && currentCharacter >= character) {
|
||||
return utf8Offset;
|
||||
}
|
||||
final int codePoint = text.codePointAt(index);
|
||||
final int utf16Width = Character.charCount(codePoint);
|
||||
final int utf8Width = utf8Length(codePoint);
|
||||
if (currentLine == line && currentCharacter + utf16Width > character) {
|
||||
return utf8Offset;
|
||||
}
|
||||
utf8Offset += utf8Width;
|
||||
if (codePoint == '\n') {
|
||||
if (currentLine == line) {
|
||||
return utf8Offset - utf8Width;
|
||||
}
|
||||
currentLine += 1;
|
||||
currentCharacter = 0;
|
||||
} else {
|
||||
currentCharacter += utf16Width;
|
||||
}
|
||||
index += utf16Width;
|
||||
}
|
||||
return utf8Offset;
|
||||
}
|
||||
|
||||
private int utf8Length(final int codePoint) {
|
||||
if (codePoint <= 0x7F) {
|
||||
return 1;
|
||||
@ -54,4 +88,4 @@ final class DocumentPositionMapper {
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
@ -11,6 +12,7 @@ import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.List;
|
||||
|
||||
public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
private final LspProjectContext project;
|
||||
@ -70,15 +72,44 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
languageServiceBridge.onSave(project, params.getTextDocument().getUri())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(final CompletionParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(Either.forRight(protocolMessageMapper.mapCompletion(
|
||||
languageServiceBridge.completion(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter()))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Hover> hover(final HoverParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapHover(languageServiceBridge.hover(
|
||||
project,
|
||||
params.getTextDocument().getUri(),
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SignatureHelp> signatureHelp(final SignatureHelpParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapSignatureHelp(
|
||||
languageServiceBridge.signatureHelp(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SemanticTokens> semanticTokensFull(final SemanticTokensParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
|
||||
@ -24,6 +24,18 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
|
||||
capabilities.setTextDocumentSync(syncOptions);
|
||||
capabilities.setHoverProvider(description.hoverSupported());
|
||||
if (description.completionSupported()) {
|
||||
final CompletionOptions completionOptions = new CompletionOptions();
|
||||
completionOptions.setResolveProvider(false);
|
||||
completionOptions.setTriggerCharacters(List.of("."));
|
||||
capabilities.setCompletionProvider(completionOptions);
|
||||
}
|
||||
if (description.signatureHelpSupported()) {
|
||||
final SignatureHelpOptions signatureHelpOptions = new SignatureHelpOptions();
|
||||
signatureHelpOptions.setTriggerCharacters(List.of("(", ","));
|
||||
signatureHelpOptions.setRetriggerCharacters(List.of(","));
|
||||
capabilities.setSignatureHelpProvider(signatureHelpOptions);
|
||||
}
|
||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||
semanticTokens.setFull(true);
|
||||
@ -71,6 +83,38 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
final CompletionList list = new CompletionList();
|
||||
list.setIsIncomplete(completion.incomplete());
|
||||
list.setItems(completion.items().stream().map(this::mapCompletionItem).toList());
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) {
|
||||
final SignatureHelp result = new SignatureHelp();
|
||||
result.setActiveSignature(signatureHelp.activeSignature());
|
||||
result.setActiveParameter(signatureHelp.activeParameter());
|
||||
result.setSignatures(signatureHelp.signatures().stream().map(signature -> {
|
||||
final SignatureInformation information = new SignatureInformation();
|
||||
information.setLabel(signature.label());
|
||||
if (!signature.documentation().isBlank()) {
|
||||
final MarkupContent content = new MarkupContent();
|
||||
content.setKind(MarkupKind.MARKDOWN);
|
||||
content.setValue(signature.documentation());
|
||||
information.setDocumentation(content);
|
||||
}
|
||||
information.setParameters(signature.parameters().stream().map(parameter -> {
|
||||
final ParameterInformation parameterInformation = new ParameterInformation();
|
||||
parameterInformation.setLabel(parameter);
|
||||
return parameterInformation;
|
||||
}).toList());
|
||||
return information;
|
||||
}).toList());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
|
||||
final List<BaselineSemanticToken> orderedTokens = new ArrayList<>(semanticTokens.tokens());
|
||||
@ -127,6 +171,39 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
private CompletionItem mapCompletionItem(final BaselineCompletionItem item) {
|
||||
final CompletionItem completionItem = new CompletionItem();
|
||||
completionItem.setLabel(item.label());
|
||||
completionItem.setKind(mapCompletionItemKind(item.kind()));
|
||||
if (!item.detail().isBlank()) {
|
||||
completionItem.setDetail(item.detail());
|
||||
}
|
||||
if (!item.documentation().isBlank()) {
|
||||
final MarkupContent content = new MarkupContent();
|
||||
content.setKind(MarkupKind.MARKDOWN);
|
||||
content.setValue(item.documentation());
|
||||
completionItem.setDocumentation(content);
|
||||
}
|
||||
return completionItem;
|
||||
}
|
||||
|
||||
private CompletionItemKind mapCompletionItemKind(final BaselineCompletionItemKind kind) {
|
||||
return switch (kind) {
|
||||
case KEYWORD -> CompletionItemKind.Keyword;
|
||||
case VARIABLE -> CompletionItemKind.Variable;
|
||||
case FIELD -> CompletionItemKind.Field;
|
||||
case FUNCTION -> CompletionItemKind.Function;
|
||||
case METHOD -> CompletionItemKind.Method;
|
||||
case CONSTRUCTOR -> CompletionItemKind.Constructor;
|
||||
case STRUCT, BUILTIN_TYPE -> CompletionItemKind.Struct;
|
||||
case SERVICE, HOST, CONTRACT -> CompletionItemKind.Interface;
|
||||
case CALLBACK -> CompletionItemKind.Function;
|
||||
case ENUM -> CompletionItemKind.Enum;
|
||||
case ERROR -> CompletionItemKind.EnumMember;
|
||||
case GLOBAL, CONST -> CompletionItemKind.Constant;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
|
||||
return switch (severity) {
|
||||
|
||||
@ -4,11 +4,15 @@ import org.eclipse.lsp4j.Hover;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.MessageParams;
|
||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineHover;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import org.eclipse.lsp4j.CompletionList;
|
||||
|
||||
public interface ProtocolMessageMapper {
|
||||
InitializeResult mapInitializeResult(BaselineServerDescription description);
|
||||
@ -19,6 +23,10 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
Hover mapHover(BaselineHover hover);
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
SemanticTokens mapSemanticTokens(BaselineSemanticTokens semanticTokens);
|
||||
|
||||
MessageParams mapInfoMessage(String message);
|
||||
|
||||
@ -98,6 +98,71 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() {
|
||||
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 overlay = """
|
||||
declare struct Vec() {
|
||||
fn blend(dx: int, dy: int) -> int { return dx; }
|
||||
}
|
||||
|
||||
fn mix(left: int, right: int) -> int { return left; }
|
||||
|
||||
fn frame(vec: Vec) -> void {
|
||||
mix(1, 2);
|
||||
vec.blend(1, 2);
|
||||
new Vec();
|
||||
}
|
||||
""";
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay);
|
||||
final var completionPosition = mapper.positionOf(overlay.indexOf("mix(1, 2);"));
|
||||
final var hoverPosition = mapper.positionOf(overlay.indexOf("blend(1, 2);"));
|
||||
final var functionSignaturePosition = mapper.positionOf(overlay.indexOf("2);"));
|
||||
final var constructorSignaturePosition = mapper.positionOf(overlay.indexOf("Vec();") + 4);
|
||||
|
||||
final var completion = bridge.completion(
|
||||
context,
|
||||
documentPath.toUri().toString(),
|
||||
overlay,
|
||||
completionPosition.line(),
|
||||
completionPosition.character());
|
||||
assertTrue(completion.items().stream().anyMatch(item -> item.label().equals("mix")));
|
||||
assertTrue(completion.items().stream().anyMatch(item -> item.label().equals("vec")));
|
||||
|
||||
final var hover = bridge.hover(
|
||||
context,
|
||||
documentPath.toUri().toString(),
|
||||
overlay,
|
||||
hoverPosition.line(),
|
||||
hoverPosition.character());
|
||||
assertTrue(hover.markdown().contains("blend(arg0: int, arg1: int) -> int"));
|
||||
assertTrue(hover.markdown().contains("**method**"));
|
||||
|
||||
final var functionSignatureHelp = bridge.signatureHelp(
|
||||
context,
|
||||
documentPath.toUri().toString(),
|
||||
overlay,
|
||||
functionSignaturePosition.line(),
|
||||
functionSignaturePosition.character());
|
||||
assertEquals(1, functionSignatureHelp.activeParameter());
|
||||
assertEquals("mix(left: int, right: int) -> int", functionSignatureHelp.signatures().getFirst().label());
|
||||
|
||||
final var constructorSignatureHelp = bridge.signatureHelp(
|
||||
context,
|
||||
documentPath.toUri().toString(),
|
||||
overlay,
|
||||
constructorSignaturePosition.line(),
|
||||
constructorSignaturePosition.character());
|
||||
assertEquals("Vec()", constructorSignatureHelp.signatures().getFirst().label());
|
||||
}
|
||||
|
||||
@Test
|
||||
void describeServerPublishesFrontendVisualThemes() {
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
@ -106,6 +171,8 @@ class CompilerLanguageServiceBridgeTest {
|
||||
|
||||
assertEquals("pbs", description.frontendLanguageId());
|
||||
assertEquals("pbs-default", description.activeVisualThemeId());
|
||||
assertTrue(description.completionSupported());
|
||||
assertTrue(description.signatureHelpSupported());
|
||||
assertEquals(1, description.semanticHostProjections().size());
|
||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||
|
||||
@ -6,16 +6,22 @@ import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.InitializedParams;
|
||||
import org.eclipse.lsp4j.MessageParams;
|
||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineCompletionItem;
|
||||
import p.studio.lsp.messages.BaselineCompletionItemKind;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineEditorPalette;
|
||||
import p.studio.lsp.messages.BaselineHover;
|
||||
import p.studio.lsp.messages.BaselineSemanticHostProjection;
|
||||
import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry;
|
||||
import p.studio.lsp.messages.BaselineSignature;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.BaselineTokenStyle;
|
||||
@ -90,6 +96,8 @@ class PrometeuLanguageServerTest {
|
||||
"1",
|
||||
"pbs",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of(),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -118,10 +126,35 @@ class PrometeuLanguageServerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineHover hover(final LspProjectContext project, final String documentUri, final int line, final int character) {
|
||||
public BaselineCompletion completion(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return new BaselineCompletion(false, List.of(new BaselineCompletionItem("demo", BaselineCompletionItemKind.FUNCTION, "", "")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineHover hover(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return new BaselineHover("hover");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return new BaselineSignatureHelp(List.of(new BaselineSignature("demo()", List.of(), "")), 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSemanticTokens semanticTokens(final LspProjectContext context, final String documentUri, final String text) {
|
||||
return new BaselineSemanticTokens(List.of(), List.of());
|
||||
@ -158,6 +191,16 @@ class PrometeuLanguageServerTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineCompletionItem;
|
||||
import p.studio.lsp.messages.BaselineCompletionItemKind;
|
||||
import p.studio.lsp.messages.BaselineEditorPalette;
|
||||
import p.studio.lsp.messages.BaselineSemanticHostProjection;
|
||||
import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry;
|
||||
import p.studio.lsp.messages.BaselineSignature;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.BaselineTokenStyle;
|
||||
import p.studio.lsp.messages.BaselineVisualTheme;
|
||||
@ -14,6 +20,7 @@ import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class Lsp4jProtocolMessageMapperTest {
|
||||
|
||||
@ -25,6 +32,8 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
"1",
|
||||
"pbs",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -43,6 +52,9 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
|
||||
final InitializeResult result = mapper.mapInitializeResult(description);
|
||||
|
||||
assertEquals(List.of("."), result.getCapabilities().getCompletionProvider().getTriggerCharacters());
|
||||
assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters());
|
||||
|
||||
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||
assertEquals("pbs", semanticPayload.get("frontendLanguageId"));
|
||||
@ -59,4 +71,33 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(List.of("declaration"), firstTokenProjection.get("hostTokenModifiers"));
|
||||
assertEquals("variable", firstTokenProjection.get("fallbackTokenType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionAndSignatureHelpMapToLspPayloads() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
|
||||
final var completion = mapper.mapCompletion(new BaselineCompletion(
|
||||
false,
|
||||
List.of(new BaselineCompletionItem(
|
||||
"blend",
|
||||
BaselineCompletionItemKind.METHOD,
|
||||
"blend(dx: int, dy: int) -> int",
|
||||
"method docs"))));
|
||||
assertEquals(1, completion.getItems().size());
|
||||
assertEquals("blend", completion.getItems().getFirst().getLabel());
|
||||
assertEquals("blend(dx: int, dy: int) -> int", completion.getItems().getFirst().getDetail());
|
||||
final var documentation = assertInstanceOf(Either.class, completion.getItems().getFirst().getDocumentation());
|
||||
assertTrue(documentation.isRight());
|
||||
|
||||
final var signatureHelp = mapper.mapSignatureHelp(new BaselineSignatureHelp(
|
||||
List.of(new BaselineSignature(
|
||||
"mix(left: int, right: int) -> int",
|
||||
List.of("left: int", "right: int"),
|
||||
"")),
|
||||
0,
|
||||
1));
|
||||
assertEquals(1, signatureHelp.getSignatures().size());
|
||||
assertEquals("mix(left: int, right: int) -> int", signatureHelp.getSignatures().getFirst().getLabel());
|
||||
assertEquals(1, signatureHelp.getActiveParameter());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user