diff --git a/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts b/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts index e73cc24e..a7ccc7b3 100644 --- a/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts +++ b/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts @@ -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) } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticToken.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticToken.java new file mode 100644 index 00000000..f7efb7e5 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticToken.java @@ -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; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticTokens.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticTokens.java new file mode 100644 index 00000000..b20f4d35 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticTokens.java @@ -0,0 +1,14 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSemanticTokens( + List legend, + List tokens) { + + public BaselineSemanticTokens { + legend = List.copyOf(Objects.requireNonNull(legend, "legend")); + tokens = List.copyOf(Objects.requireNonNull(tokens, "tokens")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 2fc747a9..af31b775 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -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 semanticTokenTypes) { public BaselineServerDescription { name = requireText(name, "name"); version = requireText(version, "version"); + semanticTokenTypes = List.copyOf(Objects.requireNonNull(semanticTokenTypes, "semanticTokenTypes")); } private static String requireText( diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index 3b7f982b..aa1ea97d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -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); } 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 1ff405ee..c3a7c27f 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 @@ -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(); + + 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 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 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; + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index 903bab5f..184f4054 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -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 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 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) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 29079505..28d8cf8d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -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 orderedTokens = new ArrayList<>(semanticTokens.tokens()); + orderedTokens.sort(Comparator + .comparingInt(BaselineSemanticToken::line) + .thenComparingInt(BaselineSemanticToken::startCharacter)); + + final List 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); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index ad69e8c0..18172125 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -4,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); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 1aa1be51..c95d6c36 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -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) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 3f9dcb85..33ae0009 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -6,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(); diff --git a/tools/vscode-extension/package.json b/tools/vscode-extension/package.json index 8621202e..159d05a2 100644 --- a/tools/vscode-extension/package.json +++ b/tools/vscode-extension/package.json @@ -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": {