implements PLN-0027 frontend semantic presentation contract and lsp descriptor

This commit is contained in:
bQUARKz 2026-04-02 15:11:40 +01:00
parent 78758c1023
commit de9782c16e
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
14 changed files with 253 additions and 32 deletions

View File

@ -1,6 +1,8 @@
package p.studio.compiler;
import p.studio.compiler.models.FrontendSpec;
import p.studio.compiler.models.FrontendSemanticPresentationSpec;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.utilities.structures.ReadOnlySet;
import java.util.List;
@ -12,5 +14,8 @@ public class PBSDefinitions {
.allowedExtensions(ReadOnlySet.from("pbs", "barrel"))
.sourceRoots(ReadOnlySet.from("src"))
.stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1)))
.semanticPresentation(new FrontendSemanticPresentationSpec(
PbsSemanticKind.semanticKeys(),
List.of("/themes/pbs/semantic-highlighting.css")))
.build();
}

View File

@ -0,0 +1,56 @@
package p.studio.compiler.pbs;
import p.studio.compiler.pbs.lexer.PbsToken;
import java.util.Arrays;
import java.util.List;
public enum PbsSemanticKind {
COMMENT("pbs-comment"),
STRING("pbs-string"),
NUMBER("pbs-number"),
LITERAL("pbs-literal"),
KEYWORD("pbs-keyword"),
OPERATOR("pbs-operator"),
PUNCTUATION("pbs-punctuation"),
FUNCTION("pbs-function"),
TYPE("pbs-type"),
BINDING("pbs-binding"),
IDENTIFIER("pbs-identifier");
private final String semanticKey;
PbsSemanticKind(final String semanticKey) {
this.semanticKey = semanticKey;
}
public String semanticKey() {
return semanticKey;
}
public static List<String> semanticKeys() {
return Arrays.stream(values())
.map(PbsSemanticKind::semanticKey)
.toList();
}
public static PbsSemanticKind forToken(final PbsToken token) {
return switch (token.kind()) {
case COMMENT -> COMMENT;
case STRING_LITERAL -> STRING;
case INT_LITERAL, FLOAT_LITERAL, BOUNDED_LITERAL -> NUMBER;
case TRUE, FALSE, NONE -> LITERAL;
case IMPORT, FROM, AS, SERVICE, HOST, FN, APPLY, BIND, NEW, IMPLEMENTS, USING, CTOR,
DECLARE, LET, CONST, GLOBAL, STRUCT, CONTRACT, ERROR, ENUM, CALLBACK, BUILTIN,
SELF, THIS, PUB, MUT, MOD, TYPE, IF, ELSE, SWITCH, DEFAULT, FOR, UNTIL, STEP,
WHILE, BREAK, CONTINUE, RETURN, VOID, OPTIONAL, RESULT, SOME, OK, ERR, HANDLE,
AND, OR, NOT, SPAWN, YIELD, SLEEP, MATCH -> KEYWORD;
case PLUS, MINUS, STAR, SLASH, PERCENT, BANG, PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL,
SLASH_EQUAL, PERCENT_EQUAL, BANG_EQUAL, EQUAL, EQUAL_EQUAL, LESS, LESS_EQUAL,
GREATER, GREATER_EQUAL, AND_AND, OR_OR, ARROW, QUESTION -> OPERATOR;
case LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, LEFT_BRACKET, RIGHT_BRACKET,
COMMA, COLON, SEMICOLON, AT, DOT, DOT_DOT -> PUNCTUATION;
case IDENTIFIER, EOF -> null;
};
}
}

View File

@ -0,0 +1,61 @@
.editor-workspace-code-area-type-pbs {
-fx-highlight-fill: #1b3244;
}
.editor-workspace-code-area-type-pbs .text {
-fx-fill: #edf4fb;
}
.editor-workspace-code-area-type-pbs .lineno {
-fx-text-fill: #71859a;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-keyword {
-fx-fill: #8dc7ff;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-function {
-fx-fill: #f0cb79;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-type {
-fx-fill: #9ddba8;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-binding {
-fx-fill: #ffb1c8;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-string {
-fx-fill: #e2c48c;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-number {
-fx-fill: #c4e58a;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-comment {
-fx-fill: #6f8192;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-literal {
-fx-fill: #c8a2ff;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-operator {
-fx-fill: #dbe6f1;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-punctuation {
-fx-fill: #adc1d4;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-identifier {
-fx-fill: #edf4fb;
}
.editor-workspace-status-chip-type-pbs {
-fx-background-color: #152432;
-fx-border-color: #4d8db9;
-fx-text-fill: #e8f5ff;
}

View File

@ -0,0 +1,33 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.PBSDefinitions;
import java.net.URL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class PbsSemanticPresentationContractTest {
@Test
void shouldPublishSemanticPresentationContractThroughFrontendSpec() {
final var presentation = PBSDefinitions.PBS.getSemanticPresentation();
assertFalse(presentation.semanticKeys().isEmpty());
assertEquals(PbsSemanticKind.semanticKeys(), presentation.semanticKeys());
assertEquals(1, presentation.resources().size());
assertEquals("/themes/pbs/semantic-highlighting.css", presentation.resources().getFirst());
}
@Test
void shouldPublishResolvableSemanticPresentationResources() {
final var resourcePath = PBSDefinitions.PBS.getSemanticPresentation().resources().getFirst();
final URL resource = PBSDefinitions.class.getResource(resourcePath);
assertNotNull(resource, resourcePath);
assertTrue(resource.toExternalForm().endsWith("themes/pbs/semantic-highlighting.css"));
}
}

View File

@ -0,0 +1,18 @@
package p.studio.compiler.models;
import java.util.List;
import java.util.Objects;
public record FrontendSemanticPresentationSpec(
List<String> semanticKeys,
List<String> resources) {
public FrontendSemanticPresentationSpec {
semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys"));
resources = List.copyOf(Objects.requireNonNull(resources, "resources"));
}
public static FrontendSemanticPresentationSpec empty() {
return new FrontendSemanticPresentationSpec(List.of(), List.of());
}
}

View File

@ -15,6 +15,8 @@ public class FrontendSpec {
private final boolean caseSensitive;
@Builder.Default
private final List<Stdlib> stdlibVersions = List.of();
@Builder.Default
private final FrontendSemanticPresentationSpec semanticPresentation = FrontendSemanticPresentationSpec.empty();
public String toString() {
return String.format("FrontendSpec(language=%s)", languageId);

View File

@ -0,0 +1,14 @@
package p.studio.lsp.dtos;
import java.util.List;
import java.util.Objects;
public record LspSemanticPresentationDTO(
List<String> semanticKeys,
List<String> resources) {
public LspSemanticPresentationDTO {
semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys"));
resources = List.copyOf(Objects.requireNonNull(resources, "resources"));
}
}

View File

@ -2,6 +2,7 @@ package p.studio.lsp.messages;
import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
@ -10,6 +11,7 @@ import java.util.Objects;
public record LspAnalyzeDocumentResult(
LspSessionStateDTO sessionState,
LspSemanticPresentationDTO semanticPresentation,
List<LspDiagnosticDTO> diagnostics,
List<LspHighlightSpanDTO> semanticHighlights,
List<LspSymbolDTO> documentSymbols,
@ -17,6 +19,7 @@ public record LspAnalyzeDocumentResult(
public LspAnalyzeDocumentResult {
Objects.requireNonNull(sessionState, "sessionState");
Objects.requireNonNull(semanticPresentation, "semanticPresentation");
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights"));
documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols"));

View File

@ -16,6 +16,7 @@ class LspSemanticAnalyseService {
final var normalizedRequestedDocument = normalize(request.documentPath());
return new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("diagnostics", "symbols", "definition", "highlight")),
session.semanticPresentation(),
session.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),

View File

@ -5,6 +5,7 @@ import p.studio.compiler.messages.*;
import p.studio.compiler.models.AnalysisSnapshot;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.FrontendSpec;
import p.studio.compiler.models.FrontendSemanticPresentationSpec;
import p.studio.compiler.models.SourceHandle;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
@ -18,6 +19,7 @@ import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage;
import p.studio.compiler.workspaces.stages.ResolveDepsPipelineStage;
import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.messages.*;
import p.studio.lsp.models.AnalysisRuntimeSnapshot;
import p.studio.lsp.models.SemanticIndex;
@ -137,6 +139,7 @@ final class LspSemanticReadPhase {
if (snapshot.fileTable() == null) {
return new SemanticSession(
normalize(requestedDocumentPath),
semanticPresentation(snapshot.frontendSpec()),
diagnosticsByDocument,
Map.of(),
Map.of(),
@ -157,6 +160,7 @@ final class LspSemanticReadPhase {
}
return new SemanticSession(
normalize(requestedDocumentPath),
semanticPresentation(snapshot.frontendSpec()),
diagnosticsByDocument,
semanticIndex.semanticHighlightsByDocument(),
semanticIndex.documentSymbolsByDocument(),
@ -169,6 +173,11 @@ final class LspSemanticReadPhase {
return frontendSpec.getAllowedExtensions().contains(sourceHandle.getExtension());
}
private static LspSemanticPresentationDTO semanticPresentation(final FrontendSpec frontendSpec) {
final FrontendSemanticPresentationSpec presentation = frontendSpec.getSemanticPresentation();
return new LspSemanticPresentationDTO(presentation.semanticKeys(), presentation.resources());
}
private static Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument(
final List<BuildingIssue> issues,
final AnalysisSnapshot snapshot,

View File

@ -3,6 +3,7 @@ package p.studio.lsp.messages;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import java.nio.file.Path;
@ -11,6 +12,7 @@ import java.util.Map;
public record SemanticSession(
Path requestedDocumentPath,
LspSemanticPresentationDTO semanticPresentation,
Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument,
Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument,
Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument,

View File

@ -1,5 +1,6 @@
package p.studio.lsp.models;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.compiler.pbs.lexer.PbsTokenKind;
@ -71,39 +72,25 @@ public final class SemanticIndex {
private String semanticKey(
final PbsToken token,
final Map<String, List<LspSymbolDTO>> indexedSymbolsByName) {
return switch (token.kind()) {
case COMMENT -> "fe-comment";
case STRING_LITERAL -> "fe-string";
case INT_LITERAL, FLOAT_LITERAL, BOUNDED_LITERAL -> "fe-number";
case TRUE, FALSE, NONE -> "fe-literal";
case IDENTIFIER -> semanticKeyForIdentifier(token.lexeme(), indexedSymbolsByName);
case IMPORT, FROM, AS, SERVICE, HOST, FN, APPLY, BIND, NEW, IMPLEMENTS, USING, CTOR,
DECLARE, LET, CONST, GLOBAL, STRUCT, CONTRACT, ERROR, ENUM, CALLBACK, BUILTIN,
SELF, THIS, PUB, MUT, MOD, TYPE, IF, ELSE, SWITCH, DEFAULT, FOR, UNTIL, STEP,
WHILE, BREAK, CONTINUE, RETURN, VOID, OPTIONAL, RESULT, SOME, OK, ERR, HANDLE,
AND, OR, NOT -> "fe-keyword";
case PLUS, MINUS, STAR, SLASH, PERCENT, BANG, PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL,
SLASH_EQUAL, PERCENT_EQUAL, BANG_EQUAL, EQUAL, EQUAL_EQUAL, LESS, LESS_EQUAL,
GREATER, GREATER_EQUAL, AND_AND, OR_OR, ARROW, QUESTION -> "fe-operator";
case LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, LEFT_BRACKET, RIGHT_BRACKET,
COMMA, COLON, SEMICOLON, AT, DOT, DOT_DOT -> "fe-punctuation";
default -> null;
};
final PbsSemanticKind semanticKind = token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER
? semanticKindForIdentifier(token.lexeme(), indexedSymbolsByName)
: PbsSemanticKind.forToken(token);
return semanticKind == null ? null : semanticKind.semanticKey();
}
private String semanticKeyForIdentifier(
private PbsSemanticKind semanticKindForIdentifier(
final String lexeme,
final Map<String, List<LspSymbolDTO>> indexedSymbolsByName) {
final List<LspSymbolDTO> symbols = indexedSymbolsByName.getOrDefault(lexeme, List.of());
if (symbols.isEmpty()) {
return "fe-identifier";
return PbsSemanticKind.IDENTIFIER;
}
final LspSymbolKind kind = symbols.getFirst().kind();
return switch (kind) {
case FUNCTION, METHOD, CALLBACK -> "fe-callable";
case STRUCT, CONTRACT, HOST, BUILTIN_TYPE, SERVICE, ERROR, ENUM -> "fe-type";
case GLOBAL, CONST -> "fe-binding";
default -> "fe-identifier";
case FUNCTION, METHOD, CALLBACK -> PbsSemanticKind.FUNCTION;
case STRUCT, CONTRACT, HOST, BUILTIN_TYPE, SERVICE, ERROR, ENUM -> PbsSemanticKind.TYPE;
case GLOBAL, CONST -> PbsSemanticKind.BINDING;
default -> PbsSemanticKind.IDENTIFIER;
};
}

View File

@ -22,6 +22,13 @@ final class LspServiceImplTest {
@TempDir
Path tempDir;
private static final String OVERLAY_SOURCE = """
fn helper_call() -> void
{
helper();
}
""";
@Test
void analyzeDocumentUsesVfsOverlayForRequestedDocumentAndFilesystemFallbackForClosedFiles() throws Exception {
final Path projectRoot = createProject();
@ -31,18 +38,17 @@ final class LspServiceImplTest {
Files.writeString(helperFile, "fn helper() -> void {}\n");
final VfsProjectDocument delegate = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
final String overlaySource = """
fn helper_call() -> void
{
helper();
}
""";
final LspService service = new LspServiceImpl(
new LspProjectContext("Example", "pbs", projectRoot),
new OverlayVfsProjectDocument(delegate, mainFile, overlaySource));
new OverlayVfsProjectDocument(delegate, mainFile, OVERLAY_SOURCE));
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper_call"));
assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper"));
assertEquals(List.of("/themes/pbs/semantic-highlighting.css"), analysis.semanticPresentation().resources());
assertTrue(analysis.semanticPresentation().semanticKeys().contains("pbs-function"));
assertTrue(
analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")),
analysis.toString());
@ -51,7 +57,7 @@ final class LspServiceImplTest {
symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))),
analysis.toString());
final int offset = overlaySource.indexOf("helper();");
final int offset = OVERLAY_SOURCE.indexOf("helper();");
final var definition = service.definition(new LspDefinitionRequest(mainFile, offset));
final List<LspDefinitionTargetDTO> targets = definition.targets();
@ -110,6 +116,27 @@ final class LspServiceImplTest {
}
}
private static String semanticKeyForLexeme(
final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis,
final String source,
final String lexeme) {
return analysis.semanticHighlights().stream()
.filter(highlight -> lexeme.equals(spanContent(source, highlight.range().startOffset(), highlight.range().endOffset())))
.map(p.studio.lsp.dtos.LspHighlightSpanDTO::semanticKey)
.findFirst()
.orElseThrow();
}
private static String spanContent(
final String source,
final int start,
final int end) {
if (start < 0 || end > source.length() || start >= end) {
return "";
}
return source.substring(start, end);
}
private static final class OverlayVfsProjectDocument implements VfsProjectDocument {
private final VfsProjectDocument delegate;
private final Path overlayPath;

View File

@ -2,6 +2,7 @@ package p.studio.workspaces.editor;
import org.junit.jupiter.api.Test;
import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
@ -28,6 +29,7 @@ final class EditorDocumentHighlightingRouterTest {
final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("highlight")),
new LspSemanticPresentationDTO(List.of(), List.of()),
List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "fe-keyword")),
List.of(),
@ -56,6 +58,7 @@ final class EditorDocumentHighlightingRouterTest {
final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("highlight")),
new LspSemanticPresentationDTO(List.of(), List.of()),
List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 1), "fe-punctuation")),
List.of(),