1st semantic version deployed

This commit is contained in:
bQUARKz 2026-05-06 09:41:36 +01:00
parent 2994bc67a3
commit 0cafe84da0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
12 changed files with 347 additions and 5 deletions

View File

@ -6,5 +6,6 @@ dependencies {
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
implementation(libs.lsp4j)
}

View File

@ -0,0 +1,33 @@
package p.studio.lsp.messages;
import java.util.Objects;
public record BaselineSemanticToken(
int line,
int startCharacter,
int length,
String semanticKey) {
public BaselineSemanticToken {
if (line < 0) {
throw new IllegalArgumentException("line must be >= 0");
}
if (startCharacter < 0) {
throw new IllegalArgumentException("startCharacter must be >= 0");
}
if (length <= 0) {
throw new IllegalArgumentException("length must be > 0");
}
semanticKey = requireText(semanticKey, "semanticKey");
}
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,14 @@
package p.studio.lsp.messages;
import java.util.List;
import java.util.Objects;
public record BaselineSemanticTokens(
List<String> legend,
List<BaselineSemanticToken> tokens) {
public BaselineSemanticTokens {
legend = List.copyOf(Objects.requireNonNull(legend, "legend"));
tokens = List.copyOf(Objects.requireNonNull(tokens, "tokens"));
}
}

View File

@ -1,15 +1,18 @@
package p.studio.lsp.messages;
import java.util.Objects;
import java.util.List;
public record BaselineServerDescription(
String name,
String version,
boolean hoverSupported) {
boolean hoverSupported,
List<String> semanticTokenTypes) {
public BaselineServerDescription {
name = requireText(name, "name");
version = requireText(version, "version");
semanticTokenTypes = List.copyOf(Objects.requireNonNull(semanticTokenTypes, "semanticTokenTypes"));
}
private static String requireText(

View File

@ -2,6 +2,7 @@ package p.studio.lsp.services;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.LspProjectContext;
@ -12,5 +13,7 @@ public interface LanguageServiceBridge {
BaselineHover hover(LspProjectContext context, String documentUri, int line, int character);
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
String onSave(LspProjectContext context, String documentUri);
}

View File

@ -5,12 +5,18 @@ import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.AnalysisSnapshot;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.PBSDefinitions;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.utilities.SourceProviderFactory;
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;
@ -27,7 +33,8 @@ public record CompilerLanguageServiceBridge() implements LanguageServiceBridge {
return new BaselineServerDescription(
"Prometeu Studio LSP",
"0.1.0",
true);
true,
PBSDefinitions.PBS.getSemanticPresentation().semanticKeys());
}
@Override
@ -58,6 +65,37 @@ public record CompilerLanguageServiceBridge() implements LanguageServiceBridge {
""");
}
@Override
public BaselineSemanticTokens semanticTokens(
final LspProjectContext context,
final String documentUri,
final String text) {
Objects.requireNonNull(context, "context");
final String effectiveText = text == null ? "" : text;
final var legend = PBSDefinitions.PBS.getSemanticPresentation().semanticKeys();
final var tokens = PbsLexer.lex(effectiveText, FileId.none(), DiagnosticSink.empty());
final var positionMapper = new DocumentPositionMapper(effectiveText);
final var semanticTokens = new ArrayList<BaselineSemanticToken>();
for (int index = 0; index < tokens.size(); index += 1) {
final PbsToken token = tokens.get(index);
final PbsSemanticKind semanticKind = classifyToken(tokens, index);
if (semanticKind == null) {
continue;
}
final DocumentPosition start = positionMapper.positionOf(token.start());
final DocumentPosition end = positionMapper.positionOf(token.end());
final int length = Math.max(1, end.character() - start.character());
semanticTokens.add(new BaselineSemanticToken(
start.line(),
start.character(),
length,
semanticKind.semanticKey()));
}
return new BaselineSemanticTokens(legend, semanticTokens);
}
@Override
public String onSave(
final LspProjectContext context,
@ -121,4 +159,35 @@ public record CompilerLanguageServiceBridge() implements LanguageServiceBridge {
private Path normalizeDocumentPath(final String documentUri) {
return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize();
}
private PbsSemanticKind classifyToken(
final ReadOnlyList<PbsToken> tokens,
final int index) {
final PbsToken token = tokens.get(index);
final PbsSemanticKind lexicalKind = PbsSemanticKind.forToken(token);
if (lexicalKind != null) {
return lexicalKind;
}
if (token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER) {
if (isLifecycleToken(tokens, index, token)) {
return PbsSemanticKind.LIFECYCLE;
}
return PbsSemanticKind.IDENTIFIER;
}
return null;
}
private boolean isLifecycleToken(
final ReadOnlyList<PbsToken> tokens,
final int index,
final PbsToken token) {
if (!"Init".equals(token.lexeme()) && !"Frame".equals(token.lexeme())) {
return false;
}
if (index == 0 || index + 1 >= tokens.size()) {
return false;
}
return tokens.get(index - 1).kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.LEFT_BRACKET
&& tokens.get(index + 1).kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.RIGHT_BRACKET;
}
}

View File

@ -8,12 +8,15 @@ import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
public final class PrometeuTextDocumentService implements TextDocumentService {
private final LspProjectContext project;
private final LanguageServiceBridge languageServiceBridge;
private final ProtocolMessageMapper protocolMessageMapper;
private final ConcurrentMap<String, String> documentTextByUri = new ConcurrentHashMap<>();
private volatile LanguageClient client;
public PrometeuTextDocumentService(
@ -31,19 +34,25 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
@Override
public void didOpen(final DidOpenTextDocumentParams params) {
publishAnalysis(params.getTextDocument().getUri(), params.getTextDocument().getText());
final String uri = params.getTextDocument().getUri();
final String text = params.getTextDocument().getText();
documentTextByUri.put(uri, text);
publishAnalysis(uri, text);
}
@Override
public void didChange(final DidChangeTextDocumentParams params) {
final String uri = params.getTextDocument().getUri();
final String text = params.getContentChanges().isEmpty()
? ""
: params.getContentChanges().getFirst().getText();
publishAnalysis(params.getTextDocument().getUri(), text);
documentTextByUri.put(uri, text);
publishAnalysis(uri, text);
}
@Override
public void didClose(final DidCloseTextDocumentParams params) {
documentTextByUri.remove(params.getTextDocument().getUri());
final LanguageClient currentClient = client;
if (currentClient == null) {
return;
@ -70,6 +79,14 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
params.getPosition().getCharacter())));
}
@Override
public CompletableFuture<SemanticTokens> semanticTokensFull(final SemanticTokensParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.getOrDefault(uri, "");
return CompletableFuture.completedFuture(protocolMessageMapper.mapSemanticTokens(
languageServiceBridge.semanticTokens(project, uri, text)));
}
private void publishAnalysis(
final String uri,
final String text) {

View File

@ -3,6 +3,8 @@ package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.*;
import p.studio.lsp.messages.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
@ -20,6 +22,10 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
capabilities.setTextDocumentSync(syncOptions);
capabilities.setHoverProvider(description.hoverSupported());
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true);
capabilities.setSemanticTokensProvider(semanticTokens);
final InitializeResult result = new InitializeResult(capabilities);
final ServerInfo serverInfo = new ServerInfo();
@ -52,6 +58,36 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return result;
}
@Override
public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
final List<BaselineSemanticToken> orderedTokens = new ArrayList<>(semanticTokens.tokens());
orderedTokens.sort(Comparator
.comparingInt(BaselineSemanticToken::line)
.thenComparingInt(BaselineSemanticToken::startCharacter));
final List<Integer> data = new ArrayList<>();
int previousLine = 0;
int previousCharacter = 0;
for (final BaselineSemanticToken token : orderedTokens) {
final int deltaLine = token.line() - previousLine;
final int deltaStart = deltaLine == 0
? token.startCharacter() - previousCharacter
: token.startCharacter();
final int tokenType = semanticTokens.legend().indexOf(token.semanticKey());
if (tokenType < 0) {
continue;
}
data.add(deltaLine);
data.add(deltaStart);
data.add(token.length());
data.add(tokenType);
data.add(0);
previousLine = token.line();
previousCharacter = token.startCharacter();
}
return new SemanticTokens(data);
}
@Override
public MessageParams mapInfoMessage(final String message) {
return new MessageParams(MessageType.Info, message);

View File

@ -4,8 +4,10 @@ import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SemanticTokens;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineServerDescription;
public interface ProtocolMessageMapper {
@ -17,6 +19,8 @@ public interface ProtocolMessageMapper {
Hover mapHover(BaselineHover hover);
SemanticTokens mapSemanticTokens(BaselineSemanticTokens semanticTokens);
MessageParams mapInfoMessage(String message);
MessageParams mapErrorMessage(String message);
MessageParams mapWarningMessage(String message);

View File

@ -38,6 +38,23 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.message().contains("Compiler-backed baseline")));
}
@Test
void semanticTokensReturnsPbsLegendAndTokens() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final var semanticTokens = bridge.semanticTokens(
new LspProjectContext("main", "pbs", Path.of(".")),
Path.of("demo.pbs").toUri().toString(),
"""
fn main() -> void {
const answer = 42;
}
""");
assertTrue(semanticTokens.legend().contains("pbs-keyword"));
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-keyword")));
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-number")));
}
private Path findRepoRoot(final Path start) {
var current = start;
while (current != null) {

View File

@ -6,12 +6,14 @@ import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.services.LanguageServiceBridge;
@ -77,7 +79,7 @@ class PrometeuLanguageServerTest {
private static final class RecordingBridge implements LanguageServiceBridge {
private int describeServerCalls;
private final BaselineServerDescription serverDescription = new BaselineServerDescription("demo", "1", true);
private final BaselineServerDescription serverDescription = new BaselineServerDescription("demo", "1", true, List.of());
@Override
public BaselineServerDescription describeServer(final LspProjectContext project) {
@ -95,6 +97,11 @@ class PrometeuLanguageServerTest {
return new BaselineHover("hover");
}
@Override
public BaselineSemanticTokens semanticTokens(final LspProjectContext context, final String documentUri, final String text) {
return new BaselineSemanticTokens(List.of(), List.of());
}
@Override
public String onSave(final LspProjectContext project, final String documentUri) {
return "saved";
@ -126,6 +133,11 @@ class PrometeuLanguageServerTest {
throw new UnsupportedOperationException();
}
@Override
public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
throw new UnsupportedOperationException();
}
@Override
public MessageParams mapInfoMessage(final String message) {
final MessageParams params = new MessageParams();

View File

@ -52,6 +52,139 @@
"description": "Prometeu Studio LSP TCP port."
}
}
},
"semanticTokenTypes": [
{
"id": "pbs-comment",
"description": "PBS comment token"
},
{
"id": "pbs-string",
"description": "PBS string token"
},
{
"id": "pbs-number",
"description": "PBS number token"
},
{
"id": "pbs-literal",
"description": "PBS literal token"
},
{
"id": "pbs-lifecycle",
"description": "PBS lifecycle token"
},
{
"id": "pbs-keyword",
"description": "PBS keyword token"
},
{
"id": "pbs-operator",
"description": "PBS operator token"
},
{
"id": "pbs-punctuation",
"description": "PBS punctuation token"
},
{
"id": "pbs-function",
"description": "PBS function token"
},
{
"id": "pbs-method",
"description": "PBS method token"
},
{
"id": "pbs-constructor",
"description": "PBS constructor token"
},
{
"id": "pbs-struct",
"description": "PBS struct token"
},
{
"id": "pbs-contract",
"description": "PBS contract token"
},
{
"id": "pbs-host",
"description": "PBS host token"
},
{
"id": "pbs-builtin-type",
"description": "PBS builtin type token"
},
{
"id": "pbs-service",
"description": "PBS service token"
},
{
"id": "pbs-error",
"description": "PBS error token"
},
{
"id": "pbs-enum",
"description": "PBS enum token"
},
{
"id": "pbs-callback",
"description": "PBS callback token"
},
{
"id": "pbs-global",
"description": "PBS global token"
},
{
"id": "pbs-const",
"description": "PBS const token"
},
{
"id": "pbs-implements",
"description": "PBS implements token"
},
{
"id": "pbs-identifier",
"description": "PBS identifier token"
}
],
"configurationDefaults": {
"editor.semanticHighlighting.enabled": true,
"[pbs]": {
"editor.semanticTokenColorCustomizations": {
"enabled": true,
"rules": {
"pbs-keyword": "#569cd6",
"pbs-lifecycle": "#ef50c0",
"pbs-function": "#f2c14e",
"pbs-method": "#f2c14e",
"pbs-constructor": "#ecdcaa",
"pbs-struct": "#4ec9b0",
"pbs-contract": "#78dce8",
"pbs-host": "#b7a2fa",
"pbs-builtin-type": "#8be9fd",
"pbs-service": "#b7a2fa",
"pbs-error": "#ff5b5b",
"pbs-enum": "#56cfe1",
"pbs-callback": "#b7a2fa",
"pbs-global": {
"foreground": "#f790fc",
"italic": true
},
"pbs-const": "#f78c6c",
"pbs-implements": "#a1c181",
"pbs-string": "#00c088",
"pbs-number": "#ff90b0",
"pbs-comment": {
"foreground": "#c090b0",
"italic": true
},
"pbs-literal": "#4fc1ff",
"pbs-operator": "#d4d4d4",
"pbs-punctuation": "#d4d4d4",
"pbs-identifier": "#d4d4d4"
}
}
}
}
},
"scripts": {