diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 91087884..0e5a5b24 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":42,"PLN":111,"LSN":58,"CLSN":1}} +{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":43,"PLN":117,"LSN":59,"CLSN":1}} {"type":"discussion","id":"DSC-0065","status":"open","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[{"id":"AGD-0068","file":"AGD-0068-multi-frontend-avoid-premature-abstractions.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0064","status":"open","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[{"id":"AGD-0067","file":"AGD-0067-multi-frontend-architectural-tests.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0063","status":"open","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[{"id":"AGD-0066","file":"AGD-0066-multi-frontend-synthetic-test-frontend.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} @@ -8,7 +8,7 @@ {"type":"discussion","id":"DSC-0059","status":"open","ticket":"multi-frontend-common-lifecycle","title":"Extrair lifecycle comum das responsabilidades do frontend PBS","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","lifecycle","backend","multi-frontend"],"agendas":[{"id":"AGD-0062","file":"AGD-0062-multi-frontend-common-lifecycle.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0058","status":"open","ticket":"multi-frontend-serializable-ir","title":"Manter a IR comum serializavel por design","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","ir","backend","serialization","multi-frontend"],"agendas":[{"id":"AGD-0061","file":"AGD-0061-multi-frontend-serializable-ir.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0057","status":"open","ticket":"multi-frontend-frontend-backend-contract","title":"Estabilizar contrato entre frontend e backend comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","ir","backend","multi-frontend"],"agendas":[{"id":"AGD-0060","file":"AGD-0060-multi-frontend-frontend-backend-contract.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0056","status":"open","ticket":"multi-frontend-remove-pbs-branches","title":"Remover verificacoes explicitas de PBS do codigo comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[{"id":"AGD-0059","file":"AGD-0059-multi-frontend-remove-pbs-branches.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0055","status":"done","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0056","file":"discussion/lessons/DSC-0055-multi-frontend-compiler-vs-language-services/LSN-0056-compile-first-frontends-with-optional-editorial-capabilities.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} diff --git a/discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md b/discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md new file mode 100644 index 00000000..61fa3142 --- /dev/null +++ b/discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md @@ -0,0 +1,92 @@ +--- +id: LSN-0058 +ticket: multi-frontend-remove-pbs-branches +title: Generic Frontend Editorial Contract for LSP +created: 2026-07-15 +tags: [compiler, compiler-general, compiler-pbs, studio, frontend, lsp, coupling, multi-frontend] +--- + +## Original Problem + +Prometeu was preparing for multiple compiler frontends while the LSP bridge still knew too much about PBS internals. The first framing was broad: remove explicit PBS checks from common code. After inspection, that was too blunt. + +PBS was already mostly centered in `prometeu-frontend-pbs`. Defaults, templates, `.pbs` fixtures, VS Code language registration, semantic keys, and composition-root bootstrap were legitimate PBS references. The real architectural problem was narrower: common LSP code imported and called PBS-specific editorial types and services for semantic tokens, completion, hover, signature help, and semantic read preparation. + +## Consolidated Decision + +The common LSP/editorial path must consume frontend-provided editorial capabilities through generic contracts exposed by `prometeu-frontend-api`. + +The provider contract remains stable: + +1. `FrontendProvider.compiler()` is mandatory. +2. `FrontendProvider.languageService()` is optional. +3. Compile-only frontends are valid. +4. Unknown `languageId` values fail explicitly. +5. PBS may be the product default only when the user has not chosen a language. + +PBS is not the shared editor model. PBS implements the generic contract from inside its frontend boundary. + +## Final Implementation Shape + +The implementation introduced generic frontend editorial DTOs and default methods in `FrontendLanguageService`: + +1. `FrontendDocumentRequest` +2. `FrontendEditorialContext` +3. `FrontendCompletionCandidate` +4. `FrontendHover` +5. `FrontendSignatureHelp` +6. `FrontendSignature` +7. `FrontendSymbolKind` +8. `FrontendDocumentation` + +`CompilerLanguageServiceBridge` now resolves the selected provider and calls generic `FrontendLanguageService` methods for semantic tokens, completion, hover, and signature help. + +PBS-specific analysis setup moved behind `PBSFrontendLanguageService.prepareEditorialContext(...)`. The bridge no longer imports `p.studio.compiler.pbs.*`, no longer knows `PbsAst.File`, and no longer calls `PBSFrontendPhaseService.semanticReadSurface(...)` directly. + +An architectural test now protects the common LSP code from reintroducing PBS implementation imports. + +## Examples + +A frontend with no language service remains valid: + +```java +public final class CompileOnlyFrontendProvider implements FrontendProvider { + @Override + public FrontendSpec specification() { + return SPEC; + } + + @Override + public FrontendPhaseService compiler() { + return compiler; + } +} +``` + +An LSP consumer should resolve the selected provider, ask for `languageService()`, and fall back deterministically when editorial capability is absent. + +PBS can still keep rich internal models such as `PbsEditorialResolvedSymbol`, but those models must be mapped to generic `FrontendHover`, `FrontendCompletionCandidate`, or `FrontendSignatureHelp` before common LSP consumes them. + +## Pitfalls + +Do not remove legitimate PBS defaults just because they mention PBS. PBS remains the product default when the user has not chosen another language. + +Do not use PBS as a fallback for unknown `languageId`. Unknown language identity is a configuration error and must fail explicitly. + +Do not make editor assistance mandatory. Compilation and editor support are separate frontend capabilities. + +Do not move frontend semantic ownership into LSP or Studio. The LSP bridge transports frontend-owned editorial results; it does not author language semantics. + +Do not reintroduce PBS imports in common LSP code for convenience. If a capability needs PBS-specific state, create or extend a frontend-owned generic boundary and keep the PBS mapping inside `prometeu-frontend-pbs`. + +## References + +- `DEC-0042` locked the generic frontend editorial language-service contract. +- `PLN-0111` introduced generic frontend editorial DTOs and defaults. +- `PLN-0112` migrated semantic tokens. +- `PLN-0113` migrated completion. +- `PLN-0114` migrated hover and documentation. +- `PLN-0115` migrated signature help. +- `PLN-0116` removed PBS imports from common LSP and added boundary coverage. +- Related lesson: `LSN-0055` Static frontend providers before plugin architecture. +- Related lesson: `LSN-0056` Compile-first frontends with optional editorial capabilities. diff --git a/discussion/workflow/agendas/AGD-0059-multi-frontend-remove-pbs-branches.md b/discussion/workflow/agendas/AGD-0059-multi-frontend-remove-pbs-branches.md deleted file mode 100644 index 966bf7e9..00000000 --- a/discussion/workflow/agendas/AGD-0059-multi-frontend-remove-pbs-branches.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: AGD-0059 -ticket: multi-frontend-remove-pbs-branches -title: Remover verificacoes explicitas de PBS do codigo comum -status: open -created: 2026-07-15 -resolved: -decision: -tags: [compiler, compiler-general, compiler-pbs, studio, frontend, coupling, multi-frontend] ---- - -# Agenda - Remover verificacoes explicitas de PBS do codigo comum - -## Objetivo - -Domain owner: `compiler/general`, com impacto em `studio` e `compiler/pbs`. - -Definir uma auditoria e remocao incremental de branches, instanciacoes e chamadas estaticas PBS em codigo comum. - -## Contexto atual - -O documento de alinhamento aponta ocorrencias como `"pbs"`, `PBSDefinitions`, `PBSFrontendPhaseService`, `PBSFrontend`, `PbsParser`, `PbsSemantic` e `languageId`. A primeira busca no repo confirma ocorrencias espalhadas em docs, testes, frontend PBS, build pipeline e Studio. - -## Escopo - -- Classificar ocorrencias como internas ao PBS, composition root, testes PBS ou referencias indevidas. -- Substituir referencias indevidas por provider, spec, capability ou servico registrado. -- Manter excecoes explicitas documentadas. - -## Fora de escopo - -- Remover constantes PBS internas do proprio frontend. -- Alterar templates explicitamente PBS. -- Renomear packages. - -## Arquivos e componentes a inspecionar - -- `prometeu-app/src/main/java/p/studio/AppContainer.java` -- `prometeu-studio/src/main/java/p/studio/window/NewProjectWizard.java` -- `prometeu-studio/src/main/java/p/studio/projects/...` -- `prometeu-compiler/prometeu-build-pipeline/...` -- `prometeu-compiler/frontends/prometeu-frontend-pbs/...` -- testes que importam `p.studio.compiler.pbs...` - -## Alteracoes propostas - -Opcao A: auditoria textual primeiro, seguida de PRs pequenos por area. - -Opcao B: criar teste arquitetural antes e usar falhas para guiar remocao. - -Recomendacao inicial: fazer auditoria textual e classificar em tabela curta antes de alterar; em seguida adicionar teste arquitetural para impedir regressao. - -## Estrategia de implementacao - -Usar `rg` para localizar termos, confirmar nomes reais, registrar classificacao no plano derivado e migrar apenas uma fronteira por vez. - -## Testes necessarios - -- Teste de dependencia impedindo imports PBS em packages comuns. -- Testes de build existentes do PBS. -- Testes de Studio/project wizard para garantir templates PBS ainda funcionam. - -## Criterios de aceitacao - -- Codigo comum nao decide comportamento por `"pbs"`. -- Excecoes ficam restritas a composition root, testes PBS, templates PBS e frontend PBS. - -## Riscos - -- Remover uma referencia valida do PBS por classificacao apressada. -- Quebrar setup de projeto existente ao mover extensoes e templates para provider. - -## Decisoes que devem ser registradas - -- Lista de excecoes permitidas. -- Criterio de ownership para cada pacote comum. -- Ordem de migracao por area. - diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index e56ceadd..f91fbc08 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -1,13 +1,27 @@ package p.studio.compiler; import p.studio.compiler.messages.FESurfaceContext; +import p.studio.compiler.messages.BuildingIssueSink; +import p.studio.compiler.messages.FrontendPhaseContext; import p.studio.compiler.models.FrontendSemanticToken; import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate; import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol; import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp; +import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind; import p.studio.compiler.pbs.semantics.PbsEditorialSupportService; +import p.studio.compiler.services.FrontendCompletionCandidate; +import p.studio.compiler.services.FrontendDocumentRequest; +import p.studio.compiler.services.FrontendDocumentation; +import p.studio.compiler.services.FrontendEditorialContext; +import p.studio.compiler.services.FrontendHover; import p.studio.compiler.services.FrontendLanguageService; +import p.studio.compiler.services.FrontendSignature; +import p.studio.compiler.services.FrontendSignatureHelp; +import p.studio.compiler.services.FrontendSymbolKind; +import p.studio.compiler.services.PBSFrontendPhaseService; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; import p.studio.utilities.structures.ReadOnlyList; import java.util.List; @@ -29,6 +43,27 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService this.semanticTokenProvider = Objects.requireNonNull(semanticTokenProvider, "semanticTokenProvider"); } + @Override + public Optional prepareEditorialContext( + final FrontendPhaseContext phaseContext, + final FileId fileId) { + if (phaseContext == null || fileId == null) { + return Optional.empty(); + } + final var semanticReadSurface = PBSFrontendPhaseService.semanticReadSurface( + phaseContext, + DiagnosticSink.empty(), + BuildingIssueSink.empty()); + final var ast = semanticReadSurface.astByFile().get(fileId); + if (ast == null) { + return Optional.empty(); + } + return Optional.of(new FrontendEditorialContext( + ast, + semanticReadSurface.supplementalTopDeclsByFile().getOrDefault(fileId, ReadOnlyList.empty()), + phaseContext.feSurfaceContext())); + } + public List completion( final String sourceText, final PbsAst.File ast, @@ -38,6 +73,32 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return editorialSupportService.completion(sourceText, ast, supplementalTopDecls, feSurfaceContext, offset); } + @Override + public List completion( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + if (request == null + || editorialContext == null + || !(editorialContext.syntaxTree() instanceof PbsAst.File ast) + || !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList supplementalTopDecls) + || !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)) { + return List.of(); + } + + @SuppressWarnings("unchecked") + final ReadOnlyList typedSupplementalTopDecls = + (ReadOnlyList) supplementalTopDecls; + return completion( + request.documentText(), + ast, + typedSupplementalTopDecls, + feSurfaceContext, + offset).stream() + .map(this::toFrontendCompletionCandidate) + .toList(); + } + public Optional hover( final String sourceText, final PbsAst.File ast, @@ -47,6 +108,30 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return editorialSupportService.hover(sourceText, ast, supplementalTopDecls, feSurfaceContext, offset); } + @Override + public Optional hover( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + if (request == null + || editorialContext == null + || !(editorialContext.syntaxTree() instanceof PbsAst.File ast) + || !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList supplementalTopDecls) + || !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)) { + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + final ReadOnlyList typedSupplementalTopDecls = + (ReadOnlyList) supplementalTopDecls; + return hover( + request.documentText(), + ast, + typedSupplementalTopDecls, + feSurfaceContext, + offset).map(this::toFrontendHover); + } + public Optional signatureHelp( final String sourceText, final PbsAst.File ast, @@ -56,7 +141,76 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return editorialSupportService.signatureHelp(sourceText, ast, supplementalTopDecls, feSurfaceContext, offset); } + @Override + public FrontendSignatureHelp signatureHelp( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + if (request == null + || editorialContext == null + || !(editorialContext.syntaxTree() instanceof PbsAst.File ast) + || !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList supplementalTopDecls) + || !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)) { + return FrontendSignatureHelp.empty(); + } + + @SuppressWarnings("unchecked") + final ReadOnlyList typedSupplementalTopDecls = + (ReadOnlyList) supplementalTopDecls; + return signatureHelp( + request.documentText(), + ast, + typedSupplementalTopDecls, + feSurfaceContext, + offset) + .map(this::toFrontendSignatureHelp) + .orElseGet(FrontendSignatureHelp::empty); + } + + @Override + public List semanticTokens(final FrontendDocumentRequest request) { + return semanticTokens(request == null ? "" : request.documentText()); + } + public List semanticTokens(final String documentText) { return semanticTokenProvider.tokenize(documentText == null ? "" : documentText); } + + private FrontendCompletionCandidate toFrontendCompletionCandidate(final PbsEditorialCompletionCandidate candidate) { + return new FrontendCompletionCandidate( + candidate.label(), + toFrontendSymbolKind(candidate.kind()), + candidate.detail(), + candidate.documentation(), + candidate.origin()); + } + + private FrontendHover toFrontendHover(final PbsEditorialResolvedSymbol symbol) { + return new FrontendHover( + symbol.displayName(), + toFrontendSymbolKind(symbol.kind()), + symbol.detail(), + symbol.origin(), + symbol.signatures().stream() + .map(this::toFrontendSignature) + .toList(), + FrontendDocumentation.markdown(symbol.documentation())); + } + + private FrontendSignatureHelp toFrontendSignatureHelp(final PbsEditorialSignatureHelp signatureHelp) { + return new FrontendSignatureHelp( + signatureHelp.signatures().stream() + .map(this::toFrontendSignature) + .toList(), + signatureHelp.activeSignature(), + signatureHelp.activeParameter()); + } + + private FrontendSignature toFrontendSignature(final p.studio.compiler.pbs.semantics.PbsEditorialSignature signature) { + return new FrontendSignature(signature.label(), signature.parameterLabels(), ""); + } + + private FrontendSymbolKind toFrontendSymbolKind(final PbsEditorialSymbolKind kind) { + return FrontendSymbolKind.valueOf(kind.name()); + } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/FrontendProviderBoundaryTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/FrontendProviderBoundaryTest.java index 6ea8fef5..03428814 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/FrontendProviderBoundaryTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/FrontendProviderBoundaryTest.java @@ -75,6 +75,29 @@ class FrontendProviderBoundaryTest { } } + @Test + void commonLspMustNotImportPbsImplementationPackages() throws IOException { + final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()); + final var sourceRoot = repoRoot.resolve("prometeu-lsp/prometeu-lsp-v1/src/main/java"); + final var prohibitedNeedles = List.of( + "p.studio.compiler.PBSFrontendLanguageService", + "p.studio.compiler.services.PBSFrontendPhaseService", + "p.studio.compiler.pbs", + "PbsAst", + "PbsEditorial"); + + try (final var files = Files.walk(sourceRoot)) { + final var violations = files + .filter(path -> path.toString().endsWith(".java")) + .filter(path -> prohibitedNeedles.stream().anyMatch(needle -> contains(path, needle))) + .toList(); + if (!violations.isEmpty()) { + fail("common LSP code must consume generic frontend language-service contracts instead of PBS implementation packages: " + + violations); + } + } + } + @Test void compileOnlyFrontendProviderMayOmitLanguageService() { final FrontendProvider provider = new CompileOnlyFrontendProvider(); diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCompletionCandidate.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCompletionCandidate.java new file mode 100644 index 00000000..221a3764 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCompletionCandidate.java @@ -0,0 +1,33 @@ +package p.studio.compiler.services; + +import java.util.Objects; + +public record FrontendCompletionCandidate( + String label, + FrontendSymbolKind kind, + String detail, + String documentation, + String origin) { + + public FrontendCompletionCandidate { + label = requireText(label, "label"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + documentation = normalize(documentation); + origin = normalize(origin); + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentRequest.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentRequest.java new file mode 100644 index 00000000..5c3b20d5 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentRequest.java @@ -0,0 +1,22 @@ +package p.studio.compiler.services; + +import java.nio.file.Path; +import java.util.Objects; + +public record FrontendDocumentRequest( + Path projectRoot, + Path documentPath, + String documentText) { + + public FrontendDocumentRequest { + projectRoot = normalizePath(projectRoot, "projectRoot"); + documentPath = normalizePath(documentPath, "documentPath"); + documentText = documentText == null ? "" : documentText; + } + + private static Path normalizePath( + final Path path, + final String field) { + return Objects.requireNonNull(path, field).toAbsolutePath().normalize(); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentation.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentation.java new file mode 100644 index 00000000..af21850f --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentation.java @@ -0,0 +1,21 @@ +package p.studio.compiler.services; + +import java.util.Objects; + +public record FrontendDocumentation( + FrontendDocumentationFormat format, + String text) { + + public FrontendDocumentation { + format = Objects.requireNonNull(format, "format"); + text = text == null ? "" : text; + } + + public boolean isBlank() { + return text.isBlank(); + } + + public static FrontendDocumentation markdown(final String text) { + return new FrontendDocumentation(FrontendDocumentationFormat.MARKDOWN, text); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentationFormat.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentationFormat.java new file mode 100644 index 00000000..ffb13aac --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentationFormat.java @@ -0,0 +1,6 @@ +package p.studio.compiler.services; + +public enum FrontendDocumentationFormat { + MARKDOWN, + PLAINTEXT +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java new file mode 100644 index 00000000..783d7616 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java @@ -0,0 +1,7 @@ +package p.studio.compiler.services; + +public record FrontendEditorialContext( + Object syntaxTree, + Object supplementalDeclarations, + Object semanticContext) { +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendHover.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendHover.java new file mode 100644 index 00000000..071526ff --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendHover.java @@ -0,0 +1,38 @@ +package p.studio.compiler.services; + +import java.util.List; +import java.util.Objects; + +public record FrontendHover( + String displayName, + FrontendSymbolKind kind, + String detail, + String origin, + List signatures, + FrontendDocumentation documentation) { + + public FrontendHover { + displayName = requireText(displayName, "displayName"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + origin = normalize(origin); + signatures = signatures == null ? List.of() : List.copyOf(signatures); + documentation = documentation == null + ? FrontendDocumentation.markdown("") + : documentation; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 529d8cac..cad07baa 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -1,7 +1,62 @@ package p.studio.compiler.services; +import p.studio.compiler.models.FrontendSemanticToken; +import p.studio.compiler.messages.FrontendPhaseContext; +import p.studio.compiler.source.identifiers.FileId; + +import java.util.List; +import java.util.Optional; + /** - * Marker contract for optional frontend-owned editor services. + * Optional frontend-owned editor services. */ public interface FrontendLanguageService { + default Optional prepareEditorialContext( + final FrontendPhaseContext phaseContext, + final FileId fileId) { + return Optional.empty(); + } + + default List semanticTokens(final FrontendDocumentRequest request) { + return List.of(); + } + + default List completion( + final FrontendDocumentRequest request, + final int offset) { + return List.of(); + } + + default List completion( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + return completion(request, offset); + } + + default Optional hover( + final FrontendDocumentRequest request, + final int offset) { + return Optional.empty(); + } + + default Optional hover( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + return hover(request, offset); + } + + default FrontendSignatureHelp signatureHelp( + final FrontendDocumentRequest request, + final int offset) { + return FrontendSignatureHelp.empty(); + } + + default FrontendSignatureHelp signatureHelp( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + return signatureHelp(request, offset); + } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSignature.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSignature.java new file mode 100644 index 00000000..40dfc86f --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSignature.java @@ -0,0 +1,28 @@ +package p.studio.compiler.services; + +import java.util.List; +import java.util.Objects; + +public record FrontendSignature( + String label, + List parameterLabels, + String documentation) { + + public FrontendSignature { + label = requireText(label, "label"); + parameterLabels = parameterLabels == null ? List.of() : parameterLabels.stream() + .map(parameterLabel -> requireText(parameterLabel, "parameterLabel")) + .toList(); + documentation = documentation == null ? "" : documentation.trim(); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSignatureHelp.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSignatureHelp.java new file mode 100644 index 00000000..b052eb95 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSignatureHelp.java @@ -0,0 +1,23 @@ +package p.studio.compiler.services; + +import java.util.List; + +public record FrontendSignatureHelp( + List signatures, + int activeSignature, + int activeParameter) { + + public FrontendSignatureHelp { + signatures = signatures == null ? List.of() : List.copyOf(signatures); + if (activeSignature < 0) { + throw new IllegalArgumentException("activeSignature must be >= 0"); + } + if (activeParameter < 0) { + throw new IllegalArgumentException("activeParameter must be >= 0"); + } + } + + public static FrontendSignatureHelp empty() { + return new FrontendSignatureHelp(List.of(), 0, 0); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java new file mode 100644 index 00000000..cfec8cda --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java @@ -0,0 +1,21 @@ +package p.studio.compiler.services; + +public enum FrontendSymbolKind { + KEYWORD, + LOCAL, + PARAMETER, + FIELD, + FUNCTION, + METHOD, + CONSTRUCTOR, + STRUCT, + BUILTIN_TYPE, + SERVICE, + HOST, + CONTRACT, + CALLBACK, + ENUM, + ERROR, + GLOBAL, + CONST +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java new file mode 100644 index 00000000..25c3dc25 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -0,0 +1,63 @@ +package p.studio.compiler.services; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FrontendLanguageServiceTest { + + @Test + void defaultLanguageServiceCapabilitiesAreEmpty() { + final FrontendLanguageService service = new FrontendLanguageService() { + }; + final FrontendDocumentRequest request = new FrontendDocumentRequest( + Path.of("."), + Path.of("src/main.pbs"), + "fun main() {}"); + + assertTrue(service.semanticTokens(request).isEmpty()); + assertTrue(service.completion(request, 0).isEmpty()); + assertTrue(service.hover(request, 0).isEmpty()); + assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0)); + } + + @Test + void completionCandidateNormalizesOptionalText() { + final var candidate = new FrontendCompletionCandidate( + " draw ", + FrontendSymbolKind.FUNCTION, + " callable ", + " docs ", + " stdlib "); + + assertEquals("draw", candidate.label()); + assertEquals("callable", candidate.detail()); + assertEquals("docs", candidate.documentation()); + assertEquals("stdlib", candidate.origin()); + } + + @Test + void hoverDefaultsToEmptyMarkdownDocumentation() { + final var hover = new FrontendHover( + "draw", + FrontendSymbolKind.FUNCTION, + null, + null, + List.of(new FrontendSignature("draw(x)", List.of("x"), null)), + null); + + assertEquals("draw", hover.displayName()); + assertTrue(hover.documentation().isBlank()); + assertEquals(FrontendDocumentationFormat.MARKDOWN, hover.documentation().format()); + assertEquals(1, hover.signatures().size()); + } + + @Test + void signatureHelpRejectsNegativeActiveIndexes() { + assertThrows(IllegalArgumentException.class, () -> new FrontendSignatureHelp(List.of(), -1, 0)); + assertThrows(IllegalArgumentException.class, () -> new FrontendSignatureHelp(List.of(), 0, -1)); + } +} 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 37e27159..1129b92d 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 @@ -2,12 +2,16 @@ package p.studio.lsp.services.compiler; import org.apache.commons.lang3.StringUtils; import p.studio.compiler.FrontendRegistryService; -import p.studio.compiler.PBSFrontendLanguageService; import p.studio.compiler.messages.*; import p.studio.compiler.models.*; -import p.studio.compiler.pbs.semantics.*; +import p.studio.compiler.services.FrontendCompletionCandidate; +import p.studio.compiler.services.FrontendDocumentRequest; +import p.studio.compiler.services.FrontendEditorialContext; +import p.studio.compiler.services.FrontendHover; import p.studio.compiler.services.FrontendProvider; -import p.studio.compiler.services.PBSFrontendPhaseService; +import p.studio.compiler.services.FrontendSignature; +import p.studio.compiler.services.FrontendSignatureHelp; +import p.studio.compiler.services.FrontendSymbolKind; import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.utilities.SourceProviderFactory; import p.studio.compiler.workspaces.AssetSurfaceContextLoader; @@ -15,7 +19,6 @@ import p.studio.compiler.workspaces.BuilderPipelineService; 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; @@ -72,18 +75,25 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final int line, final int character) { Objects.requireNonNull(context, "context"); - final Optional languageService = pbsLanguageService(context); + final var provider = frontendProvider(context); + final var languageService = provider.languageService(); if (languageService.isEmpty()) { return new BaselineCompletion(false, List.of()); } return editorialDocument(context, documentUri, text) .map(document -> { final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); - final List candidates = languageService.orElseThrow().completion( - document.text(), - document.ast(), - document.supplementalTopDecls(), - document.feSurfaceContext(), + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + normalizeDocumentPath(documentUri), + document.text()); + final FrontendEditorialContext editorialContext = new FrontendEditorialContext( + document.editorialContext().syntaxTree(), + document.editorialContext().supplementalDeclarations(), + document.editorialContext().semanticContext()); + final List candidates = languageService.orElseThrow().completion( + request, + editorialContext, offset); return new BaselineCompletion( false, @@ -100,18 +110,25 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final int line, final int character) { Objects.requireNonNull(context, "context"); - final Optional languageService = pbsLanguageService(context); + final var provider = frontendProvider(context); + final var languageService = provider.languageService(); if (languageService.isEmpty()) { return new BaselineHover("No symbol information."); } return editorialDocument(context, documentUri, text) .flatMap(document -> { final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + normalizeDocumentPath(documentUri), + document.text()); + final FrontendEditorialContext editorialContext = new FrontendEditorialContext( + document.editorialContext().syntaxTree(), + document.editorialContext().supplementalDeclarations(), + document.editorialContext().semanticContext()); return languageService.orElseThrow().hover( - document.text(), - document.ast(), - document.supplementalTopDecls(), - document.feSurfaceContext(), + request, + editorialContext, offset); }) .map(symbol -> new BaselineHover(formatHoverMarkdown(symbol))) @@ -126,26 +143,28 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final int line, final int character) { Objects.requireNonNull(context, "context"); - final Optional languageService = pbsLanguageService(context); + final var provider = frontendProvider(context); + final var languageService = provider.languageService(); if (languageService.isEmpty()) { return new BaselineSignatureHelp(List.of(), 0, 0); } return editorialDocument(context, documentUri, text) - .flatMap(document -> { + .map(document -> { final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + normalizeDocumentPath(documentUri), + document.text()); + final FrontendEditorialContext editorialContext = new FrontendEditorialContext( + document.editorialContext().syntaxTree(), + document.editorialContext().supplementalDeclarations(), + document.editorialContext().semanticContext()); return languageService.orElseThrow().signatureHelp( - document.text(), - document.ast(), - document.supplementalTopDecls(), - document.feSurfaceContext(), + request, + editorialContext, offset); }) - .map(signatureHelp -> new BaselineSignatureHelp( - signatureHelp.signatures().stream() - .map(signature -> new BaselineSignature(signature.label(), signature.parameterLabels(), "")) - .toList(), - signatureHelp.activeSignature(), - signatureHelp.activeParameter())) + .map(this::mapSignatureHelp) .orElseGet(() -> new BaselineSignatureHelp(List.of(), 0, 0)); } @@ -156,10 +175,16 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final String text) { Objects.requireNonNull(context, "context"); final String effectiveText = text == null ? "" : text; - final var frontend = frontendProvider(context).specification(); + final Path documentPath = normalizeDocumentPath(documentUri); + final var provider = frontendProvider(context); + final var frontend = provider.specification(); final FrontendSemanticPresentationSpec presentation = frontend.getSemanticPresentation(); - final List tokens = pbsLanguageService(context) - .map(languageService -> languageService.semanticTokens(effectiveText)) + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + documentPath, + effectiveText); + final List tokens = provider.languageService() + .map(languageService -> languageService.semanticTokens(request)) .orElseGet(List::of); final var positionMapper = new DocumentPositionMapper(effectiveText); final var semanticTokens = new ArrayList(); @@ -202,6 +227,10 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final LspProjectContext context, final String documentUri, final String text) { + final var languageService = frontendProvider(context).languageService(); + if (languageService.isEmpty()) { + return Optional.empty(); + } final Path documentPath = normalizeDocumentPath(documentUri); final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig( new BuilderPipelineConfig( @@ -222,19 +251,11 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg pipelineContext.resolvedWorkspace.target(), 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())); + return languageService.orElseThrow() + .prepareEditorialContext(frontendPhaseContext, fileId) + .map(editorialContext -> new EditorialDocument( + text == null ? readCurrentText(pipelineContext, fileId) : text, + editorialContext)); } private FileId findFileId( @@ -305,7 +326,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize(); } - private BaselineCompletionItem mapCompletionItem(final PbsEditorialCompletionCandidate candidate) { + private BaselineCompletionItem mapCompletionItem(final FrontendCompletionCandidate candidate) { return new BaselineCompletionItem( candidate.label(), mapCompletionKind(candidate.kind()), @@ -356,6 +377,18 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg tokenStyle.underline()); } + private BaselineSignatureHelp mapSignatureHelp(final FrontendSignatureHelp signatureHelp) { + return new BaselineSignatureHelp( + signatureHelp.signatures().stream() + .map(signature -> new BaselineSignature( + signature.label(), + signature.parameterLabels(), + signature.documentation())) + .toList(), + signatureHelp.activeSignature(), + signatureHelp.activeParameter()); + } + private p.studio.compiler.models.FrontendSpec frontendSpec(final LspProjectContext context) { return frontendProvider(context).specification(); } @@ -364,13 +397,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg return frontendProviderResolver.apply(context.languageId()); } - private Optional pbsLanguageService(final LspProjectContext context) { - return frontendProvider(context).languageService() - .filter(PBSFrontendLanguageService.class::isInstance) - .map(PBSFrontendLanguageService.class::cast); - } - - private BaselineCompletionItemKind mapCompletionKind(final PbsEditorialSymbolKind kind) { + private BaselineCompletionItemKind mapCompletionKind(final FrontendSymbolKind kind) { return switch (kind) { case KEYWORD -> BaselineCompletionItemKind.KEYWORD; case LOCAL, PARAMETER -> BaselineCompletionItemKind.VARIABLE; @@ -391,9 +418,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg }; } - private String formatHoverMarkdown(final PbsEditorialResolvedSymbol symbol) { + private String formatHoverMarkdown(final FrontendHover symbol) { final StringBuilder markdown = new StringBuilder(); - final List signatures = symbol.signatures(); + final List signatures = symbol.signatures(); if (!signatures.isEmpty()) { markdown.append("```pbs\n"); for (int index = 0; index < signatures.size(); index += 1) { @@ -419,15 +446,13 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg markdown.append(" from `").append(symbol.origin()).append('`'); } if (!symbol.documentation().isBlank()) { - markdown.append("\n\n").append(symbol.documentation()); + markdown.append("\n\n").append(symbol.documentation().text()); } return markdown.toString(); } private record EditorialDocument( String text, - p.studio.compiler.pbs.ast.PbsAst.File ast, - ReadOnlyList supplementalTopDecls, - p.studio.compiler.messages.FESurfaceContext feSurfaceContext) { + FrontendEditorialContext editorialContext) { } }