diff --git a/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts b/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts index addf13e3..c2b12b6f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts +++ b/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts @@ -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) } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletion.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletion.java new file mode 100644 index 00000000..335191cd --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletion.java @@ -0,0 +1,12 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineCompletion( + boolean incomplete, + List items) { + public BaselineCompletion { + items = List.copyOf(Objects.requireNonNull(items, "items")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItem.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItem.java new file mode 100644 index 00000000..6dc80510 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItem.java @@ -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(); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java new file mode 100644 index 00000000..28da0724 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java @@ -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 +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 6af55458..96a5461c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -8,6 +8,8 @@ public record BaselineServerDescription( String version, String frontendLanguageId, boolean hoverSupported, + boolean completionSupported, + boolean signatureHelpSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignature.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignature.java new file mode 100644 index 00000000..6ba774de --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignature.java @@ -0,0 +1,29 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSignature( + String label, + List 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(); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignatureHelp.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignatureHelp.java new file mode 100644 index 00000000..12bb6a4e --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignatureHelp.java @@ -0,0 +1,19 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSignatureHelp( + List 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"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index aa1ea97d..f7e7d543 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -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); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 8152a91d..4e971e0c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -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 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( + 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 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 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 supplementalTopDecls, + p.studio.compiler.messages.FESurfaceContext feSurfaceContext) { + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java index 7c791003..449a7482 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java @@ -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; } -} \ No newline at end of file +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index 184f4054..ef4b139d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -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, 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(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(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 semanticTokensFull(final SemanticTokensParams params) { final String uri = params.getTextDocument().getUri(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 48caa92a..7b3d548c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -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 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) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index 18172125..7897adae 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -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); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 87f49fbb..319dc6ef 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -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() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 5b4fc14d..ae67bbb9 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -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(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index bf6a9b24..78516e5b 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -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()); + } }