Compare commits

...

8 Commits

Author SHA1 Message Date
ffd2975484 Merge pull request 'dev/multi-frontend-remove-pbs-branches' (#16) from dev/multi-frontend-remove-pbs-branches into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 61.73% (17588/28493) * Branch Coverage: 52.44% (6777/12924) * Lines of Code: 28493 * Cyclomatic Complexity: 11423 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 616
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #16
2026-07-15 10:47:09 +00:00
a62ddb0f74
LSP contract generalization
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/head Build started...
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 61.73% (17588/28493) * Branch Coverage: 52.44% (6777/12924) * Lines of Code: 28493 * Cyclomatic Complexity: 11423 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 616
2026-07-15 11:40:02 +01:00
79e4a64ff2
implements PLN-0116 2026-07-15 11:33:43 +01:00
3ca58dcc24
implements PLN-0115 2026-07-15 11:31:02 +01:00
bb8669bf46
implements PLN-0114 2026-07-15 11:29:02 +01:00
6205b82e0b
implements PLN-0113 2026-07-15 11:26:53 +01:00
088101449a
implements PLN-0112 2026-07-15 11:24:22 +01:00
b1e75973f9
implements PLN-0111 2026-07-15 11:22:38 +01:00
17 changed files with 671 additions and 138 deletions

View File

@ -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":[]}

View File

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

View File

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

View File

@ -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<FrontendEditorialContext> 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<PbsEditorialCompletionCandidate> 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<FrontendCompletionCandidate> 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<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
return completion(
request.documentText(),
ast,
typedSupplementalTopDecls,
feSurfaceContext,
offset).stream()
.map(this::toFrontendCompletionCandidate)
.toList();
}
public Optional<PbsEditorialResolvedSymbol> 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<FrontendHover> 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<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
return hover(
request.documentText(),
ast,
typedSupplementalTopDecls,
feSurfaceContext,
offset).map(this::toFrontendHover);
}
public Optional<PbsEditorialSignatureHelp> 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<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
return signatureHelp(
request.documentText(),
ast,
typedSupplementalTopDecls,
feSurfaceContext,
offset)
.map(this::toFrontendSignatureHelp)
.orElseGet(FrontendSignatureHelp::empty);
}
@Override
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
return semanticTokens(request == null ? "" : request.documentText());
}
public List<FrontendSemanticToken> 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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package p.studio.compiler.services;
public enum FrontendDocumentationFormat {
MARKDOWN,
PLAINTEXT
}

View File

@ -0,0 +1,7 @@
package p.studio.compiler.services;
public record FrontendEditorialContext(
Object syntaxTree,
Object supplementalDeclarations,
Object semanticContext) {
}

View File

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

View File

@ -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<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext,
final FileId fileId) {
return Optional.empty();
}
default List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
return List.of();
}
default List<FrontendCompletionCandidate> completion(
final FrontendDocumentRequest request,
final int offset) {
return List.of();
}
default List<FrontendCompletionCandidate> completion(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
return completion(request, offset);
}
default Optional<FrontendHover> hover(
final FrontendDocumentRequest request,
final int offset) {
return Optional.empty();
}
default Optional<FrontendHover> 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);
}
}

View File

@ -0,0 +1,28 @@
package p.studio.compiler.services;
import java.util.List;
import java.util.Objects;
public record FrontendSignature(
String label,
List<String> 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;
}
}

View File

@ -0,0 +1,23 @@
package p.studio.compiler.services;
import java.util.List;
public record FrontendSignatureHelp(
List<FrontendSignature> 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);
}
}

View File

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

View File

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

View File

@ -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<PBSFrontendLanguageService> 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<PbsEditorialCompletionCandidate> 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<FrontendCompletionCandidate> 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<PBSFrontendLanguageService> 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<PBSFrontendLanguageService> 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<FrontendSemanticToken> tokens = pbsLanguageService(context)
.map(languageService -> languageService.semanticTokens(effectiveText))
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
documentPath,
effectiveText);
final List<FrontendSemanticToken> tokens = provider.languageService()
.map(languageService -> languageService.semanticTokens(request))
.orElseGet(List::of);
final var positionMapper = new DocumentPositionMapper(effectiveText);
final var semanticTokens = new ArrayList<BaselineSemanticToken>();
@ -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<PBSFrontendLanguageService> 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<PbsEditorialSignature> signatures = symbol.signatures();
final List<FrontendSignature> 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<p.studio.compiler.pbs.ast.PbsAst.TopDecl> supplementalTopDecls,
p.studio.compiler.messages.FESurfaceContext feSurfaceContext) {
FrontendEditorialContext editorialContext) {
}
}