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 {
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)
}

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 frontendLanguageId,
boolean hoverSupported,
boolean completionSupported,
boolean signatureHelpSupported,
List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections,
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;
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);

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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