implements PLN-0028 studio frontend-owned semantic presentation consumption

This commit is contained in:
bQUARKz 2026-04-02 15:14:19 +01:00
parent de9782c16e
commit d6ffd9eb62
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 140 additions and 93 deletions

View File

@ -10,47 +10,47 @@
-fx-text-fill: #71859a;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-keyword {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-keyword {
-fx-fill: #8dc7ff;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-function {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-function {
-fx-fill: #f0cb79;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-type {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-type {
-fx-fill: #9ddba8;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-binding {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-binding {
-fx-fill: #ffb1c8;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-string {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-string {
-fx-fill: #e2c48c;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-number {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-number {
-fx-fill: #c4e58a;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-comment {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-comment {
-fx-fill: #6f8192;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-literal {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-literal {
-fx-fill: #c8a2ff;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-operator {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-operator {
-fx-fill: #dbe6f1;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-punctuation {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-punctuation {
-fx-fill: #adc1d4;
}
.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-identifier {
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-identifier {
-fx-fill: #edf4fb;
}

View File

@ -13,6 +13,7 @@ final class EditorDocumentHighlightingRouter {
final LspAnalyzeDocumentResult analysis) {
if (fileBuffer.frontendDocument()
&& analysis != null
&& presentation.supportsSemanticHighlighting()
&& !analysis.semanticHighlights().isEmpty()) {
return new EditorDocumentHighlightingResult(
EditorDocumentHighlightOwner.LSP,

View File

@ -10,15 +10,21 @@ import java.util.Objects;
record EditorDocumentPresentation(
String styleKey,
List<String> stylesheetUrls,
List<String> semanticKeys,
EditorDocumentSyntaxHighlighting syntaxHighlighting) {
EditorDocumentPresentation {
styleKey = Objects.requireNonNull(styleKey, "styleKey");
stylesheetUrls = List.copyOf(Objects.requireNonNull(stylesheetUrls, "stylesheetUrls"));
semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys"));
syntaxHighlighting = Objects.requireNonNull(syntaxHighlighting, "syntaxHighlighting");
}
StyleSpans<Collection<String>> highlight(final String content) {
return syntaxHighlighting.highlight(content);
}
boolean supportsSemanticHighlighting() {
return !semanticKeys.isEmpty() && !stylesheetUrls.isEmpty();
}
}

View File

@ -1,36 +1,47 @@
package p.studio.workspaces.editor;
import p.studio.compiler.FrontendRegistryService;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.vfs.messages.VfsDocumentTypeIds;
import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting;
import java.net.URL;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger;
final class EditorDocumentPresentationRegistry {
private static final Logger LOGGER = Logger.getLogger(EditorDocumentPresentationRegistry.class.getName());
private static final EditorDocumentPresentation TEXT_PRESENTATION = new EditorDocumentPresentation(
"text",
java.util.List.of(),
java.util.List.of(),
EditorDocumentSyntaxHighlighting.plainText());
private static final EditorDocumentPresentation JSON_PRESENTATION = new EditorDocumentPresentation(
"json",
java.util.List.of(stylesheet("presentations/json.css")),
java.util.List.of(),
EditorDocumentSyntaxHighlighting.json());
private static final EditorDocumentPresentation BASH_PRESENTATION = new EditorDocumentPresentation(
"bash",
java.util.List.of(stylesheet("presentations/bash.css")),
java.util.List.of(),
EditorDocumentSyntaxHighlighting.bash());
private static final EditorDocumentPresentation FRONTEND_PRESENTATION = new EditorDocumentPresentation(
"fe",
java.util.List.of(stylesheet("presentations/fe.css")),
EditorDocumentSyntaxHighlighting.plainText());
EditorDocumentPresentation resolve(final String typeId) {
return resolve(typeId, null);
}
EditorDocumentPresentation resolve(
final String typeId,
final LspSemanticPresentationDTO semanticPresentation) {
final String normalizedTypeId = normalize(typeId);
if (normalizedTypeId.isBlank()) {
return TEXT_PRESENTATION;
}
if (FrontendRegistryService.getFrontendSpec(normalizedTypeId).isPresent()) {
return FRONTEND_PRESENTATION;
return frontendPresentation(normalizedTypeId, semanticPresentation);
}
if (VfsDocumentTypeIds.JSON.equals(normalizedTypeId)) {
return JSON_PRESENTATION;
@ -41,6 +52,26 @@ final class EditorDocumentPresentationRegistry {
return TEXT_PRESENTATION;
}
private EditorDocumentPresentation frontendPresentation(
final String normalizedTypeId,
final LspSemanticPresentationDTO semanticPresentation) {
if (semanticPresentation == null) {
return new EditorDocumentPresentation(
normalizedTypeId,
java.util.List.of(),
java.util.List.of(),
EditorDocumentSyntaxHighlighting.plainText());
}
return new EditorDocumentPresentation(
normalizedTypeId,
semanticPresentation.resources().stream()
.map(EditorDocumentPresentationRegistry::resourceStylesheet)
.flatMap(Optional::stream)
.toList(),
semanticPresentation.semanticKeys(),
EditorDocumentSyntaxHighlighting.plainText());
}
private String normalize(final String typeId) {
return typeId == null ? "" : typeId.trim().toLowerCase(Locale.ROOT);
}
@ -51,4 +82,22 @@ final class EditorDocumentPresentationRegistry {
"missing editor presentation stylesheet: " + relativePath)
.toExternalForm();
}
private static Optional<String> resourceStylesheet(final String resourcePath) {
final String normalizedPath = normalizeResourcePath(resourcePath);
final URL resource = EditorDocumentPresentationRegistry.class.getResource(normalizedPath);
if (resource == null) {
LOGGER.fine("missing frontend semantic presentation resource: " + resourcePath);
return Optional.empty();
}
return Optional.of(resource.toExternalForm());
}
private static String normalizeResourcePath(final String resourcePath) {
final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim();
if (normalized.isEmpty()) {
throw new IllegalArgumentException("resourcePath cannot be blank");
}
return normalized.startsWith("/") ? normalized : "/" + normalized;
}
}

View File

@ -124,10 +124,12 @@ public final class EditorWorkspace extends Workspace {
}
final var fileBuffer = activeFile.orElseThrow();
final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId());
final LspAnalyzeDocumentResult analysis = fileBuffer.frontendDocument()
? prometeuLspService.analyzeDocument(new LspAnalyzeDocumentRequest(fileBuffer.path()))
: null;
final EditorDocumentPresentation presentation = presentationRegistry.resolve(
fileBuffer.typeId(),
analysis == null ? null : analysis.semanticPresentation());
final EditorDocumentHighlightingResult highlighting = EditorDocumentHighlightingRouter.route(
fileBuffer,
presentation,

View File

@ -28,7 +28,7 @@ public final class EditorDocumentSemanticHighlighting {
builder.add(Collections.emptyList(), start - cursor);
}
if (end > start) {
builder.add(List.of("editor-syntax-" + highlight.semanticKey()), end - start);
builder.add(List.of("editor-semantic-" + highlight.semanticKey()), end - start);
cursor = end;
}
}

View File

@ -708,10 +708,6 @@
-fx-fill: #eef3f8;
}
.editor-workspace-code-area-type-fe .text {
-fx-fill: #f2f6fb;
}
.editor-workspace-command-bar {
-fx-padding: 10 12 0 12;
-fx-alignment: center-left;
@ -880,12 +876,6 @@
-fx-text-fill: #c5d2de;
}
.editor-workspace-status-chip-type-fe {
-fx-background-color: #271a35;
-fx-border-color: #8d67c7;
-fx-text-fill: #f3ecff;
}
.editor-workspace-status-chip-position {
-fx-min-width: 44;
-fx-pref-width: 44;

View File

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

View File

@ -12,6 +12,7 @@ import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class EditorDocumentHighlightingRouterTest {
@Test
@ -29,18 +30,21 @@ final class EditorDocumentHighlightingRouterTest {
final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("highlight")),
new LspSemanticPresentationDTO(List.of(), List.of()),
new LspSemanticPresentationDTO(
List.of("pbs-keyword"),
List.of("/themes/pbs/semantic-highlighting.css")),
List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "fe-keyword")),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")),
List.of(),
List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
fileBuffer,
registry.resolve("pbs"),
registry.resolve("pbs", analysis.semanticPresentation()),
analysis);
assertEquals(EditorDocumentHighlightOwner.LSP, result.owner());
assertEquals(List.of("editor-semantic-pbs-keyword"), List.copyOf(result.styleSpans().getStyleSpan(0).getStyle()));
}
@Test
@ -71,4 +75,36 @@ final class EditorDocumentHighlightingRouterTest {
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner());
}
@Test
void frontendDocumentsDegradeToLocalWhenSemanticPresentationResourcesAreUnavailable() {
final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer(
Path.of("/tmp/example/src/main.pbs"),
"main.pbs",
"pbs",
"fn main() -> void {}",
"LF",
true,
VfsDocumentAccessMode.READ_ONLY,
false);
final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("highlight")),
new LspSemanticPresentationDTO(
List.of("pbs-keyword"),
List.of("/themes/pbs/missing.css")),
List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")),
List.of(),
List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
fileBuffer,
registry.resolve("pbs", analysis.semanticPresentation()),
analysis);
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner());
assertTrue(result.styleSpans().getStyleSpan(0).getStyle().isEmpty());
}
}

View File

@ -1,15 +1,39 @@
package p.studio.workspaces.editor;
import org.junit.jupiter.api.Test;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class EditorDocumentPresentationRegistryTest {
private final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
@Test
void resolvesFrontendTypeIdsToFrontendPresentation() {
assertEquals("fe", registry.resolve("pbs").styleKey());
final EditorDocumentPresentation presentation = registry.resolve(
"pbs",
new LspSemanticPresentationDTO(
java.util.List.of("pbs-keyword"),
java.util.List.of("/themes/pbs/semantic-highlighting.css")));
assertEquals("pbs", presentation.styleKey());
assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys());
assertEquals(1, presentation.stylesheetUrls().size());
assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css"));
}
@Test
void missingFrontendResourcesDegradeToPlainFrontendPresentation() {
final EditorDocumentPresentation presentation = registry.resolve(
"pbs",
new LspSemanticPresentationDTO(
java.util.List.of("pbs-keyword"),
java.util.List.of("/themes/pbs/missing.css")));
assertEquals("pbs", presentation.styleKey());
assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys());
assertTrue(presentation.stylesheetUrls().isEmpty());
}
@Test