From b64be2a9a1394aabd426bcbda120a26e5b864997 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 31 Mar 2026 17:19:08 +0100 Subject: [PATCH] implements PLN-0025 --- discussion/index.ndjson | 2 +- ...ement-fe-semantic-highlight-consumption.md | 4 +- .../lsp/dtos/PrometeuLspHighlightSpanDTO.java | 13 ++++ .../PrometeuLspAnalyzeDocumentResult.java | 3 + .../PrometeuLspSemanticReadPhase.java | 70 ++++++++++++++++++ .../editor/EditorDocumentHighlightOwner.java | 6 ++ .../EditorDocumentHighlightingResult.java | 16 +++++ .../EditorDocumentHighlightingRouter.java | 25 +++++++ .../EditorDocumentPresentationRegistry.java | 2 +- .../workspaces/editor/EditorWorkspace.java | 19 +++-- .../EditorDocumentSemanticHighlighting.java | 43 +++++++++++ .../themes/editor/presentations/fe.css | 61 ++++++++++++++++ .../EditorDocumentHighlightingRouterTest.java | 71 +++++++++++++++++++ 13 files changed, 325 insertions(+), 10 deletions(-) create mode 100644 prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/dtos/PrometeuLspHighlightSpanDTO.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java create mode 100644 prometeu-studio/src/main/resources/themes/editor/presentations/fe.css create mode 100644 prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 4462f138..2f63c113 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -11,4 +11,4 @@ {"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} {"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} {"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} -{"type":"discussion","id":"DSC-0013","status":"open","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[{"id":"AGD-0013","file":"AGD-0013-studio-editor-write-wave-supported-non-frontend-files.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"},{"id":"AGD-0014","file":"AGD-0014-studio-editor-frontend-edit-rights.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0010","file":"DEC-0010-studio-controlled-non-frontend-editor-write-wave.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0013"},{"id":"DEC-0011","file":"DEC-0011-studio-frontend-read-only-minimum-lsp-phase.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0014"}],"plans":[{"id":"PLN-0019","file":"PLN-0019-propagate-dec-0010-into-studio-and-vfs-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0020","file":"PLN-0020-build-dec-0010-vfs-access-policy-and-save-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0021","file":"PLN-0021-integrate-dec-0010-editor-write-ui-and-workflow.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0022","file":"PLN-0022-propagate-dec-0011-into-studio-vfs-and-lsp-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0023","file":"PLN-0023-scaffold-flat-packed-prometeu-lsp-api-and-session-seams.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0024","file":"PLN-0024-implement-read-only-fe-diagnostics-symbols-and-definition.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0025","file":"PLN-0025-implement-fe-semantic-highlight-consumption.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0013","status":"open","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[{"id":"AGD-0013","file":"AGD-0013-studio-editor-write-wave-supported-non-frontend-files.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"},{"id":"AGD-0014","file":"AGD-0014-studio-editor-frontend-edit-rights.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0010","file":"DEC-0010-studio-controlled-non-frontend-editor-write-wave.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0013"},{"id":"DEC-0011","file":"DEC-0011-studio-frontend-read-only-minimum-lsp-phase.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0014"}],"plans":[{"id":"PLN-0019","file":"PLN-0019-propagate-dec-0010-into-studio-and-vfs-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0020","file":"PLN-0020-build-dec-0010-vfs-access-policy-and-save-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0021","file":"PLN-0021-integrate-dec-0010-editor-write-ui-and-workflow.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0022","file":"PLN-0022-propagate-dec-0011-into-studio-vfs-and-lsp-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0023","file":"PLN-0023-scaffold-flat-packed-prometeu-lsp-api-and-session-seams.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0024","file":"PLN-0024-implement-read-only-fe-diagnostics-symbols-and-definition.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0025","file":"PLN-0025-implement-fe-semantic-highlight-consumption.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0025-implement-fe-semantic-highlight-consumption.md b/discussion/workflow/plans/PLN-0025-implement-fe-semantic-highlight-consumption.md index a9dfc6dd..ad976063 100644 --- a/discussion/workflow/plans/PLN-0025-implement-fe-semantic-highlight-consumption.md +++ b/discussion/workflow/plans/PLN-0025-implement-fe-semantic-highlight-consumption.md @@ -2,9 +2,9 @@ id: PLN-0025 ticket: studio-editor-write-wave-supported-non-frontend-files title: Implement FE semantic highlight and editor consumption for the read-only LSP phase -status: review +status: done created: 2026-03-31 -completed: +completed: 2026-03-31 tags: [studio, lsp, highlight, frontend, editor] --- diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/dtos/PrometeuLspHighlightSpanDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/dtos/PrometeuLspHighlightSpanDTO.java new file mode 100644 index 00000000..eb1557ef --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/dtos/PrometeuLspHighlightSpanDTO.java @@ -0,0 +1,13 @@ +package p.lsp.dtos; + +import java.util.Objects; + +public record PrometeuLspHighlightSpanDTO( + PrometeuLspRangeDTO range, + String semanticKey) { + + public PrometeuLspHighlightSpanDTO { + range = Objects.requireNonNull(range, "range"); + semanticKey = Objects.requireNonNull(semanticKey, "semanticKey"); + } +} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/messages/PrometeuLspAnalyzeDocumentResult.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/messages/PrometeuLspAnalyzeDocumentResult.java index 2f758917..cda690fa 100644 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/messages/PrometeuLspAnalyzeDocumentResult.java +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/lsp/messages/PrometeuLspAnalyzeDocumentResult.java @@ -1,6 +1,7 @@ package p.lsp.messages; import p.lsp.dtos.PrometeuLspDiagnosticDTO; +import p.lsp.dtos.PrometeuLspHighlightSpanDTO; import p.lsp.dtos.PrometeuLspSessionStateDTO; import p.lsp.dtos.PrometeuLspSymbolDTO; @@ -10,12 +11,14 @@ import java.util.Objects; public record PrometeuLspAnalyzeDocumentResult( PrometeuLspSessionStateDTO sessionState, List diagnostics, + List semanticHighlights, List documentSymbols, List workspaceSymbols) { public PrometeuLspAnalyzeDocumentResult { Objects.requireNonNull(sessionState, "sessionState"); diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); + semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights")); documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols")); workspaceSymbols = List.copyOf(Objects.requireNonNull(workspaceSymbols, "workspaceSymbols")); } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/lsp/v1/internal/PrometeuLspSemanticReadPhase.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/lsp/v1/internal/PrometeuLspSemanticReadPhase.java index d9c7ef61..da0d75d3 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/lsp/v1/internal/PrometeuLspSemanticReadPhase.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/lsp/v1/internal/PrometeuLspSemanticReadPhase.java @@ -50,6 +50,7 @@ final class PrometeuLspSemanticReadPhase { return new PrometeuLspAnalyzeDocumentResult( new PrometeuLspSessionStateDTO(true, List.of("diagnostics", "symbols", "definition", "highlight")), session.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), + session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), session.workspaceSymbols()); } @@ -142,6 +143,7 @@ final class PrometeuLspSemanticReadPhase { normalize(requestedDocumentPath), diagnosticsByDocument, Map.of(), + Map.of(), List.of(), Map.of(), Map.of()); @@ -160,6 +162,7 @@ final class PrometeuLspSemanticReadPhase { return new SemanticSession( normalize(requestedDocumentPath), diagnosticsByDocument, + semanticIndex.semanticHighlightsByDocument(), semanticIndex.documentSymbolsByDocument(), semanticIndex.workspaceSymbols(), semanticIndex.symbolsByName(), @@ -281,6 +284,7 @@ final class PrometeuLspSemanticReadPhase { private record SemanticSession( Path requestedDocumentPath, Map> diagnosticsByDocument, + Map> semanticHighlightsByDocument, Map> documentSymbolsByDocument, List workspaceSymbols, Map> symbolsByName, @@ -293,6 +297,7 @@ final class PrometeuLspSemanticReadPhase { } private static final class SemanticIndex { + private final Map> semanticHighlightsByDocument = new LinkedHashMap<>(); private final Map> documentSymbolsByDocument = new LinkedHashMap<>(); private final List workspaceSymbols = new ArrayList<>(); private final Map> symbolsByName = new LinkedHashMap<>(); @@ -317,9 +322,70 @@ final class PrometeuLspSemanticReadPhase { symbolsByName.computeIfAbsent(child.name(), ignored -> new ArrayList<>()).add(child); } } + semanticHighlightsByDocument.put( + normalizedDocumentPath, + buildSemanticHighlights(tokens, symbolsByName)); documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols)); } + private List buildSemanticHighlights( + final List tokens, + final Map> indexedSymbolsByName) { + final List highlights = new ArrayList<>(); + for (final PbsToken token : tokens) { + if (token.kind() == PbsTokenKind.EOF) { + continue; + } + final String semanticKey = semanticKey(token, indexedSymbolsByName); + if (semanticKey == null || semanticKey.isBlank()) { + continue; + } + highlights.add(new PrometeuLspHighlightSpanDTO( + new PrometeuLspRangeDTO(token.start(), token.end()), + semanticKey)); + } + return List.copyOf(highlights); + } + + private String semanticKey( + final PbsToken token, + final Map> 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; + }; + } + + private String semanticKeyForIdentifier( + final String lexeme, + final Map> indexedSymbolsByName) { + final List symbols = indexedSymbolsByName.getOrDefault(lexeme, List.of()); + if (symbols.isEmpty()) { + return "fe-identifier"; + } + final PrometeuLspSymbolKindDTO kind = symbols.get(0).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"; + }; + } + private PrometeuLspSymbolDTO symbolForTopDecl( final Path documentPath, final PbsAst.TopDecl topDecl) { @@ -407,6 +473,10 @@ final class PrometeuLspSemanticReadPhase { children); } + Map> semanticHighlightsByDocument() { + return Map.copyOf(semanticHighlightsByDocument); + } + Map> documentSymbolsByDocument() { return Map.copyOf(documentSymbolsByDocument); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java new file mode 100644 index 00000000..9e24dded --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java @@ -0,0 +1,6 @@ +package p.studio.workspaces.editor; + +enum EditorDocumentHighlightOwner { + LOCAL, + LSP +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java new file mode 100644 index 00000000..3dadd862 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java @@ -0,0 +1,16 @@ +package p.studio.workspaces.editor; + +import org.fxmisc.richtext.model.StyleSpans; + +import java.util.Collection; +import java.util.Objects; + +record EditorDocumentHighlightingResult( + EditorDocumentHighlightOwner owner, + StyleSpans> styleSpans) { + + EditorDocumentHighlightingResult { + owner = Objects.requireNonNull(owner, "owner"); + styleSpans = Objects.requireNonNull(styleSpans, "styleSpans"); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java new file mode 100644 index 00000000..4a1d56ae --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java @@ -0,0 +1,25 @@ +package p.studio.workspaces.editor; + +import p.lsp.messages.PrometeuLspAnalyzeDocumentResult; +import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSemanticHighlighting; + +final class EditorDocumentHighlightingRouter { + private EditorDocumentHighlightingRouter() { + } + + static EditorDocumentHighlightingResult route( + final EditorOpenFileBuffer fileBuffer, + final EditorDocumentPresentation presentation, + final PrometeuLspAnalyzeDocumentResult analysis) { + if (fileBuffer.frontendDocument() + && analysis != null + && !analysis.semanticHighlights().isEmpty()) { + return new EditorDocumentHighlightingResult( + EditorDocumentHighlightOwner.LSP, + EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())); + } + return new EditorDocumentHighlightingResult( + EditorDocumentHighlightOwner.LOCAL, + presentation.highlight(fileBuffer.content())); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java index ff288ee6..79c0c46b 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java @@ -21,7 +21,7 @@ final class EditorDocumentPresentationRegistry { EditorDocumentSyntaxHighlighting.bash()); private static final EditorDocumentPresentation FRONTEND_PRESENTATION = new EditorDocumentPresentation( "fe", - java.util.List.of(), + java.util.List.of(stylesheet("presentations/fe.css")), EditorDocumentSyntaxHighlighting.plainText()); EditorDocumentPresentation resolve(final String typeId) { diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java index 4ecc3e18..23e8c84f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java @@ -128,11 +128,18 @@ public final class EditorWorkspace extends Workspace { final var fileBuffer = activeFile.orElseThrow(); final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId()); + final PrometeuLspAnalyzeDocumentResult analysis = fileBuffer.frontendDocument() + ? prometeuLspService.analyzeDocument(new PrometeuLspAnalyzeDocumentRequest(fileBuffer.path())) + : null; + final EditorDocumentHighlightingResult highlighting = EditorDocumentHighlightingRouter.route( + fileBuffer, + presentation, + analysis); applyPresentationStylesheets(presentation); syncingEditor = true; try { codeArea.replaceText(fileBuffer.content()); - codeArea.setStyleSpans(0, presentation.highlight(fileBuffer.content())); + codeArea.setStyleSpans(0, highlighting.styleSpans()); } finally { syncingEditor = false; } @@ -140,7 +147,7 @@ public final class EditorWorkspace extends Workspace { EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); refreshCommandSurfaces(fileBuffer); statusBar.showFile(projectReference, fileBuffer, presentation); - refreshSemanticOutline(fileBuffer); + refreshSemanticOutline(fileBuffer, analysis); } private void revealActiveFileInNavigator() { @@ -317,13 +324,13 @@ public final class EditorWorkspace extends Workspace { textDocument.dirty()); } - private void refreshSemanticOutline(final EditorOpenFileBuffer fileBuffer) { - if (!fileBuffer.frontendDocument()) { + private void refreshSemanticOutline( + final EditorOpenFileBuffer fileBuffer, + final PrometeuLspAnalyzeDocumentResult analysis) { + if (!fileBuffer.frontendDocument() || analysis == null) { outlinePanel.showPlaceholder(); return; } - final PrometeuLspAnalyzeDocumentResult analysis = prometeuLspService.analyzeDocument( - new PrometeuLspAnalyzeDocumentRequest(fileBuffer.path())); outlinePanel.showSemanticReadResult( fileBuffer.path(), analysis.diagnostics(), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java new file mode 100644 index 00000000..08071202 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java @@ -0,0 +1,43 @@ +package p.studio.workspaces.editor.syntaxhighlight; + +import org.fxmisc.richtext.model.StyleSpans; +import org.fxmisc.richtext.model.StyleSpansBuilder; +import p.lsp.dtos.PrometeuLspHighlightSpanDTO; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public final class EditorDocumentSemanticHighlighting { + private EditorDocumentSemanticHighlighting() { + } + + public static StyleSpans> highlight( + final String content, + final List semanticHighlights) { + final StyleSpansBuilder> builder = new StyleSpansBuilder<>(); + final List orderedHighlights = semanticHighlights.stream() + .sorted(Comparator.comparingInt(highlight -> highlight.range().startOffset())) + .toList(); + int cursor = 0; + for (final PrometeuLspHighlightSpanDTO highlight : orderedHighlights) { + final int start = Math.max(cursor, highlight.range().startOffset()); + final int end = Math.min(content.length(), highlight.range().endOffset()); + if (start > cursor) { + builder.add(Collections.emptyList(), start - cursor); + } + if (end > start) { + builder.add(List.of("editor-syntax-" + highlight.semanticKey()), end - start); + cursor = end; + } + } + if (cursor < content.length()) { + builder.add(Collections.emptyList(), content.length() - cursor); + } + if (content.isEmpty()) { + builder.add(Collections.emptyList(), 0); + } + return builder.create(); + } +} diff --git a/prometeu-studio/src/main/resources/themes/editor/presentations/fe.css b/prometeu-studio/src/main/resources/themes/editor/presentations/fe.css new file mode 100644 index 00000000..b2ee77a3 --- /dev/null +++ b/prometeu-studio/src/main/resources/themes/editor/presentations/fe.css @@ -0,0 +1,61 @@ +.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; +} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java new file mode 100644 index 00000000..fdc9cc88 --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java @@ -0,0 +1,71 @@ +package p.studio.workspaces.editor; + +import org.junit.jupiter.api.Test; +import p.lsp.dtos.PrometeuLspHighlightSpanDTO; +import p.lsp.dtos.PrometeuLspRangeDTO; +import p.lsp.dtos.PrometeuLspSessionStateDTO; +import p.lsp.messages.PrometeuLspAnalyzeDocumentResult; +import p.studio.vfs.VfsDocumentAccessMode; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class EditorDocumentHighlightingRouterTest { + @Test + void frontendDocumentsUseLspOwnedHighlightsWhenSemanticSpansExist() { + 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 PrometeuLspAnalyzeDocumentResult analysis = new PrometeuLspAnalyzeDocumentResult( + new PrometeuLspSessionStateDTO(true, List.of("highlight")), + List.of(), + List.of(new PrometeuLspHighlightSpanDTO(new PrometeuLspRangeDTO(0, 2), "fe-keyword")), + List.of(), + List.of()); + + final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( + fileBuffer, + registry.resolve("pbs"), + analysis); + + assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); + } + + @Test + void nonFrontendDocumentsStayOnLocalHighlighting() { + final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); + final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( + Path.of("/tmp/example/prometeu.json"), + "prometeu.json", + "json", + "{\n \"name\": \"Example\"\n}\n", + "LF", + false, + VfsDocumentAccessMode.EDITABLE, + false); + + final PrometeuLspAnalyzeDocumentResult analysis = new PrometeuLspAnalyzeDocumentResult( + new PrometeuLspSessionStateDTO(true, List.of("highlight")), + List.of(), + List.of(new PrometeuLspHighlightSpanDTO(new PrometeuLspRangeDTO(0, 1), "fe-punctuation")), + List.of(), + List.of()); + + final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( + fileBuffer, + registry.resolve("json"), + analysis); + + assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); + } +}