implements PLN-0076

This commit is contained in:
bQUARKz 2026-05-08 10:13:38 +01:00
parent 7f40541e96
commit 7cbe61f5eb
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
16 changed files with 643 additions and 12 deletions

View File

@ -5,7 +5,10 @@ plugins {
dependencies { dependencies {
implementation(project(":prometeu-lsp:prometeu-lsp-api")) implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core")) 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:prometeu-build-pipeline"))
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
implementation(project(":prometeu-compiler:prometeu-frontend-registry")) implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
implementation(project(":prometeu-compiler:prometeu-deps"))
implementation(libs.lsp4j) implementation(libs.lsp4j)
} }

View File

@ -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"));
}
}

View File

@ -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();
}
}

View File

@ -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
}

View File

@ -8,6 +8,8 @@ public record BaselineServerDescription(
String version, String version,
String frontendLanguageId, String frontendLanguageId,
boolean hoverSupported, boolean hoverSupported,
boolean completionSupported,
boolean signatureHelpSupported,
List<String> semanticTokenTypes, List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections, List<BaselineSemanticHostProjection> semanticHostProjections,
List<BaselineVisualTheme> visualThemes, List<BaselineVisualTheme> visualThemes,

View File

@ -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();
}
}

View File

@ -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");
}
}
}

View File

@ -1,8 +1,10 @@
package p.studio.lsp.services; package p.studio.lsp.services;
import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp;
import p.studio.lsp.messages.BaselineServerDescription; import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
@ -11,7 +13,11 @@ public interface LanguageServiceBridge {
BaselineDocumentAnalysis analyzeDocument(LspProjectContext context, String documentUri, String text); 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); BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);

View File

@ -3,7 +3,10 @@ package p.studio.lsp.services.compiler;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import p.studio.compiler.FrontendRegistryService; import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.messages.BuilderPipelineConfig; 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.BuildingIssue;
import p.studio.compiler.messages.HostAdmissionContext;
import p.studio.compiler.models.AnalysisSnapshot; import p.studio.compiler.models.AnalysisSnapshot;
import p.studio.compiler.models.BuilderPipelineContext; import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.FrontendEditorPaletteSpec; 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.FrontendTokenStyleSpec;
import p.studio.compiler.models.FrontendVisualThemeSpec; import p.studio.compiler.models.FrontendVisualThemeSpec;
import p.studio.compiler.source.identifiers.FileId; 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.utilities.SourceProviderFactory;
import p.studio.compiler.workspaces.BuilderPipelineService; 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.messages.*;
import p.studio.lsp.services.LanguageServiceBridge; import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
import java.net.URI; import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
public final class CompilerLanguageServiceBridge implements LanguageServiceBridge { public final class CompilerLanguageServiceBridge implements LanguageServiceBridge {
private final AssetSurfaceContextLoader assetSurfaceContextLoader = new AssetSurfaceContextLoader();
private final PbsEditorialSupportService editorialSupportService = new PbsEditorialSupportService();
@Override @Override
public BaselineServerDescription describeServer(final LspProjectContext context) { public BaselineServerDescription describeServer(final LspProjectContext context) {
Objects.requireNonNull(context, "context"); Objects.requireNonNull(context, "context");
@ -38,6 +54,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
"0.1.0", "0.1.0",
context.languageId(), context.languageId(),
true, true,
true,
true,
presentation.semanticKeys(), presentation.semanticKeys(),
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
presentation.themes().stream().map(this::mapVisualTheme).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(),
@ -57,19 +75,76 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
} }
@Override @Override
public BaselineHover hover( public BaselineCompletion completion(
final LspProjectContext context, final LspProjectContext context,
final String documentUri, final String documentUri,
final String text,
final int line, final int line,
final int character) { final int character) {
Objects.requireNonNull(context, "context"); Objects.requireNonNull(context, "context");
return new BaselineHover(""" return editorialDocument(context, documentUri, text)
**Prometeu Studio LSP** .map(document -> {
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
Compiler-backed baseline. final List<PbsEditorialCompletionCandidate> candidates = editorialSupportService.completion(
document.text(),
Semantic enrichment will be layered later. 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 @Override
@ -119,6 +194,71 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty()); 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( private List<BaselineDocumentIssue> mapDiagnostics(
final AnalysisSnapshot snapshot, final AnalysisSnapshot snapshot,
final Path documentPath, final Path documentPath,
@ -163,6 +303,14 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize(); 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) { private BaselineVisualTheme mapVisualTheme(final FrontendVisualThemeSpec theme) {
return new BaselineVisualTheme( return new BaselineVisualTheme(
theme.themeId(), theme.themeId(),
@ -210,4 +358,65 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return FrontendRegistryService.getFrontendSpec(context.languageId()) return FrontendRegistryService.getFrontendSpec(context.languageId())
.orElseThrow(() -> new IllegalArgumentException("no frontend registered for languageId: " + 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) {
}
} }

View File

@ -42,6 +42,40 @@ final class DocumentPositionMapper {
return new DocumentPosition(line, character); 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) { private int utf8Length(final int codePoint) {
if (codePoint <= 0x7F) { if (codePoint <= 0x7F) {
return 1; return 1;
@ -54,4 +88,4 @@ final class DocumentPositionMapper {
} }
return 4; return 4;
} }
} }

View File

@ -1,6 +1,7 @@
package p.studio.lsp.services.protocol; package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.TextDocumentService;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
@ -11,6 +12,7 @@ import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.List;
public final class PrometeuTextDocumentService implements TextDocumentService { public final class PrometeuTextDocumentService implements TextDocumentService {
private final LspProjectContext project; private final LspProjectContext project;
@ -70,15 +72,44 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
languageServiceBridge.onSave(project, params.getTextDocument().getUri()))); 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 @Override
public CompletableFuture<Hover> hover(final HoverParams params) { 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( return CompletableFuture.completedFuture(protocolMessageMapper.mapHover(languageServiceBridge.hover(
project, project,
params.getTextDocument().getUri(), uri,
text,
params.getPosition().getLine(), params.getPosition().getLine(),
params.getPosition().getCharacter()))); 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 @Override
public CompletableFuture<SemanticTokens> semanticTokensFull(final SemanticTokensParams params) { public CompletableFuture<SemanticTokens> semanticTokensFull(final SemanticTokensParams params) {
final String uri = params.getTextDocument().getUri(); final String uri = params.getTextDocument().getUri();

View File

@ -24,6 +24,18 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
capabilities.setTextDocumentSync(syncOptions); capabilities.setTextDocumentSync(syncOptions);
capabilities.setHoverProvider(description.hoverSupported()); 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(); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true); semanticTokens.setFull(true);
@ -71,6 +83,38 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return result; 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 @Override
public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) { public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
final List<BaselineSemanticToken> orderedTokens = new ArrayList<>(semanticTokens.tokens()); final List<BaselineSemanticToken> orderedTokens = new ArrayList<>(semanticTokens.tokens());
@ -127,6 +171,39 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return diagnostic; 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) { private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
return switch (severity) { return switch (severity) {

View File

@ -4,11 +4,15 @@ import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.SemanticTokens;
import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp;
import p.studio.lsp.messages.BaselineServerDescription; import p.studio.lsp.messages.BaselineServerDescription;
import org.eclipse.lsp4j.CompletionList;
public interface ProtocolMessageMapper { public interface ProtocolMessageMapper {
InitializeResult mapInitializeResult(BaselineServerDescription description); InitializeResult mapInitializeResult(BaselineServerDescription description);
@ -19,6 +23,10 @@ public interface ProtocolMessageMapper {
Hover mapHover(BaselineHover hover); Hover mapHover(BaselineHover hover);
CompletionList mapCompletion(BaselineCompletion completion);
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
SemanticTokens mapSemanticTokens(BaselineSemanticTokens semanticTokens); SemanticTokens mapSemanticTokens(BaselineSemanticTokens semanticTokens);
MessageParams mapInfoMessage(String message); MessageParams mapInfoMessage(String message);

View File

@ -98,6 +98,71 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type"))); 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 @Test
void describeServerPublishesFrontendVisualThemes() { void describeServerPublishesFrontendVisualThemes() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
@ -106,6 +171,8 @@ class CompilerLanguageServiceBridgeTest {
assertEquals("pbs", description.frontendLanguageId()); assertEquals("pbs", description.frontendLanguageId());
assertEquals("pbs-default", description.activeVisualThemeId()); assertEquals("pbs-default", description.activeVisualThemeId());
assertTrue(description.completionSupported());
assertTrue(description.signatureHelpSupported());
assertEquals(1, description.semanticHostProjections().size()); assertEquals(1, description.semanticHostProjections().size());
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()

View File

@ -6,16 +6,22 @@ import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams; import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.TextDocumentService;
import org.junit.jupiter.api.Test; 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.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineEditorPalette; import p.studio.lsp.messages.BaselineEditorPalette;
import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticHostProjection; import p.studio.lsp.messages.BaselineSemanticHostProjection;
import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; 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.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineServerDescription; import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.BaselineTokenStyle; import p.studio.lsp.messages.BaselineTokenStyle;
@ -90,6 +96,8 @@ class PrometeuLanguageServerTest {
"1", "1",
"pbs", "pbs",
true, true,
true,
true,
List.of(), List.of(),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -118,10 +126,35 @@ class PrometeuLanguageServerTest {
} }
@Override @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"); 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 @Override
public BaselineSemanticTokens semanticTokens(final LspProjectContext context, final String documentUri, final String text) { public BaselineSemanticTokens semanticTokens(final LspProjectContext context, final String documentUri, final String text) {
return new BaselineSemanticTokens(List.of(), List.of()); return new BaselineSemanticTokens(List.of(), List.of());
@ -158,6 +191,16 @@ class PrometeuLanguageServerTest {
throw new UnsupportedOperationException(); 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 @Override
public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) { public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -1,10 +1,16 @@
package p.studio.lsp.services.protocol.mapping; package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.jupiter.api.Test; 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.BaselineEditorPalette;
import p.studio.lsp.messages.BaselineSemanticHostProjection; import p.studio.lsp.messages.BaselineSemanticHostProjection;
import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; 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.BaselineServerDescription;
import p.studio.lsp.messages.BaselineTokenStyle; import p.studio.lsp.messages.BaselineTokenStyle;
import p.studio.lsp.messages.BaselineVisualTheme; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class Lsp4jProtocolMessageMapperTest { final class Lsp4jProtocolMessageMapperTest {
@ -25,6 +32,8 @@ final class Lsp4jProtocolMessageMapperTest {
"1", "1",
"pbs", "pbs",
true, true,
true,
true,
List.of("demo-keyword"), List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -43,6 +52,9 @@ final class Lsp4jProtocolMessageMapperTest {
final InitializeResult result = mapper.mapInitializeResult(description); 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 experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
assertEquals("pbs", semanticPayload.get("frontendLanguageId")); assertEquals("pbs", semanticPayload.get("frontendLanguageId"));
@ -59,4 +71,33 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(List.of("declaration"), firstTokenProjection.get("hostTokenModifiers")); assertEquals(List.of("declaration"), firstTokenProjection.get("hostTokenModifiers"));
assertEquals("variable", firstTokenProjection.get("fallbackTokenType")); 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());
}
} }