diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java index d2f677a2..5322be97 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java @@ -14,8 +14,19 @@ public enum PbsSemanticKind { OPERATOR("pbs-operator"), PUNCTUATION("pbs-punctuation"), FUNCTION("pbs-function"), - TYPE("pbs-type"), - BINDING("pbs-binding"), + METHOD("pbs-method"), + CONSTRUCTOR("pbs-constructor"), + STRUCT("pbs-struct"), + CONTRACT("pbs-contract"), + HOST("pbs-host"), + BUILTIN_TYPE("pbs-builtin-type"), + SERVICE("pbs-service"), + ERROR("pbs-error"), + ENUM("pbs-enum"), + CALLBACK("pbs-callback"), + GLOBAL("pbs-global"), + CONST("pbs-const"), + IMPLEMENTS("pbs-implements"), IDENTIFIER("pbs-identifier"); private final String semanticKey; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 00543560..68225aa3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -108,6 +108,22 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { return irBackend; } + public static Map> importedSupplementalTopDeclsByFile( + final FrontendPhaseContext ctx, + final DiagnosticSink diagnostics, + final BuildingIssueSink issues) { + final var service = new PBSFrontendPhaseService(); + final var assembly = service.moduleAssemblyService.assemble(ctx, ctx.nameTable(), diagnostics, issues); + final var importedSemanticContexts = service.importedSemanticContextService.build( + assembly.parsedSourceFiles(), + assembly.moduleTable()); + final Map> supplementalTopDeclsByFile = new LinkedHashMap<>(); + for (final var entry : importedSemanticContexts.entrySet()) { + supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls()); + } + return Map.copyOf(supplementalTopDeclsByFile); + } + private IRBackend mergeCompiledSources( final ArrayList compiledSourceFiles, final Set failedModuleIds, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css index 9b06ab54..a55119bb 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css @@ -18,12 +18,56 @@ -fx-fill: #dcdcaa; } -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-type { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-method { + -fx-fill: #d7d787; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-constructor { + -fx-fill: #f2c14e; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-struct { -fx-fill: #4ec9b0; } -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-binding { - -fx-fill: #c586c0; +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-contract { + -fx-fill: #78dce8; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-host { + -fx-fill: #ffb86c; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-builtin-type { + -fx-fill: #8be9fd; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-service { + -fx-fill: #e06c75; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-error { + -fx-fill: #ff6b6b; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-enum { + -fx-fill: #56cfe1; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-callback { + -fx-fill: #c792ea; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-global { + -fx-fill: #f78c6c; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-const { + -fx-fill: #ffcb6b; +} + +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-implements { + -fx-fill: #a1c181; } .editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-string { diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java index 580af5c3..3f65c287 100644 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java @@ -4,6 +4,11 @@ public enum LspSymbolKind { FUNCTION, METHOD, CONSTRUCTOR, + IF, + SWITCH, + FOR, + WHILE, + HANDLE, GLOBAL, CONST, STRUCT, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java index f2a9a09a..4300141a 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java @@ -13,6 +13,7 @@ import p.studio.compiler.pbs.parser.PbsParser; import p.studio.compiler.source.diagnostics.Diagnostic; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; +import p.studio.compiler.services.PBSFrontendPhaseService; import p.studio.compiler.workspaces.AssetSurfaceContextLoader; import p.studio.compiler.workspaces.PipelineStage; import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage; @@ -70,7 +71,7 @@ final class LspSemanticReadPhase { new OverlaySourceProviderFactoryImpl(vfsProjectDocument, requestedDocumentPath)); final BuilderPipelineContext context = BuilderPipelineContext.fromConfig(config); final AnalysisRuntimeSnapshot snapshot = runAnalysisStages(context); - return index(snapshot, requestedDocumentPath); + return index(snapshot, context, requestedDocumentPath); } private static AnalysisRuntimeSnapshot runAnalysisStages(final BuilderPipelineContext context) { @@ -129,6 +130,7 @@ final class LspSemanticReadPhase { private static SemanticSession index( final AnalysisRuntimeSnapshot runtimeSnapshot, + final BuilderPipelineContext context, final Path requestedDocumentPath) { final AnalysisSnapshot snapshot = runtimeSnapshot.analysisSnapshot(); final Map> diagnosticsByDocument = diagnosticsByDocument( @@ -147,6 +149,10 @@ final class LspSemanticReadPhase { Map.of(), Map.of()); } + final Map> importedSupplementalTopDeclsByFile = importedSupplementalTopDeclsByFile( + snapshot.frontendSpec(), + context); + final List indexedDocuments = new ArrayList<>(); for (final FileId fileId : snapshot.fileTable()) { final SourceHandle sourceHandle = snapshot.fileTable().get(fileId); if (!isSourceRelated(snapshot.frontendSpec(), sourceHandle)) { @@ -156,7 +162,17 @@ final class LspSemanticReadPhase { final DiagnosticSink diagnostics = DiagnosticSink.empty(); final var tokens = PbsLexer.lex(source, fileId, diagnostics); final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.ORDINARY); - semanticIndex.index(sourceHandle.getCanonPath(), ast, tokens.asList()); + final Path documentPath = sourceHandle.getCanonPath(); + semanticIndex.registerDocument(documentPath, ast, tokens.asList()); + indexedDocuments.add(new IndexedDocument( + fileId, + documentPath, + importedSupplementalTopDeclsByFile.getOrDefault(fileId, List.of()))); + } + for (final IndexedDocument indexedDocument : indexedDocuments) { + semanticIndex.buildHighlights( + indexedDocument.documentPath(), + indexedDocument.visibleImportedTopDecls()); } return new SemanticSession( normalize(requestedDocumentPath), @@ -169,6 +185,39 @@ final class LspSemanticReadPhase { semanticIndex.tokensByDocument()); } + private static Map> importedSupplementalTopDeclsByFile( + final FrontendSpec frontendSpec, + final BuilderPipelineContext context) { + if (context.resolvedWorkspace == null || context.fileTable == null || !"pbs".equals(frontendSpec.getLanguageId())) { + return Map.of(); + } + final FESurfaceContext feSurfaceContext = new AssetSurfaceContextLoader().load(context.resolvedWorkspace.mainProject().getRootPath()); + final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext( + context.resolvedWorkspace.graph().projectTable(), + context.fileTable, + context.resolvedWorkspace.stack(), + context.resolvedWorkspace.stdlib(), + HostAdmissionContext.permissiveDefault(), + feSurfaceContext); + final var diagnostics = DiagnosticSink.empty(); + final var issues = BuildingIssueSink.empty(); + final var supplementalTopDecls = PBSFrontendPhaseService.importedSupplementalTopDeclsByFile( + frontendPhaseContext, + diagnostics, + issues); + final Map> byFile = new LinkedHashMap<>(); + for (final var entry : supplementalTopDecls.entrySet()) { + byFile.put(entry.getKey(), entry.getValue().asList()); + } + return Map.copyOf(byFile); + } + + private record IndexedDocument( + FileId fileId, + Path documentPath, + List visibleImportedTopDecls) { + } + private static boolean isSourceRelated(final FrontendSpec frontendSpec, final SourceHandle sourceHandle) { return frontendSpec.getAllowedExtensions().contains(sourceHandle.getExtension()); } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java index 8addf7fa..ad543f3f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static p.studio.lsp.LspSemanticUtilities.normalize; @@ -29,6 +30,22 @@ public final class SemanticIndex { final Path documentPath, final PbsAst.File ast, final List tokens) { + index(documentPath, ast, tokens, List.of()); + } + + public void index( + final Path documentPath, + final PbsAst.File ast, + final List tokens, + final List visibleImportedTopDecls) { + registerDocument(documentPath, ast, tokens); + buildHighlights(documentPath, visibleImportedTopDecls); + } + + public void registerDocument( + final Path documentPath, + final PbsAst.File ast, + final List tokens) { final Path normalizedDocumentPath = normalize(documentPath); tokensByDocument.put(normalizedDocumentPath, List.copyOf(tokens)); final List documentSymbols = new ArrayList<>(); @@ -44,21 +61,49 @@ public final class SemanticIndex { symbolsByName.computeIfAbsent(child.name(), ignored -> new ArrayList<>()).add(child); } } + documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols)); + } + + public void buildHighlights( + final Path documentPath, + final List visibleImportedTopDecls) { + final Path normalizedDocumentPath = normalize(documentPath); + final List tokens = tokensByDocument.getOrDefault(normalizedDocumentPath, List.of()); + final Map visibleKindsByName = new LinkedHashMap<>(); + for (final var entry : symbolsByName.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + visibleKindsByName.putIfAbsent(entry.getKey(), entry.getValue().getFirst().kind()); + } + for (final LspSymbolDTO symbol : documentSymbolsByDocument.getOrDefault(normalizedDocumentPath, List.of())) { + visibleKindsByName.putIfAbsent(symbol.name(), symbol.kind()); + for (final LspSymbolDTO child : symbol.children()) { + visibleKindsByName.putIfAbsent(child.name(), child.kind()); + } + } + for (final PbsAst.TopDecl topDecl : visibleImportedTopDecls) { + final LspSymbolKind visibleKind = symbolKindForTopDecl(topDecl); + final String visibleName = topDeclName(topDecl); + if (visibleKind == null || visibleName == null || visibleName.isBlank()) { + continue; + } + visibleKindsByName.putIfAbsent(visibleName, visibleKind); + } semanticHighlightsByDocument.put( normalizedDocumentPath, - buildSemanticHighlights(tokens, symbolsByName)); - documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols)); + buildSemanticHighlights(tokens, visibleKindsByName)); } private List buildSemanticHighlights( final List tokens, - final Map> indexedSymbolsByName) { + final Map visibleKindsByName) { final List highlights = new ArrayList<>(); for (final PbsToken token : tokens) { if (token.kind() == PbsTokenKind.EOF) { continue; } - final String semanticKey = semanticKey(token, indexedSymbolsByName); + final String semanticKey = semanticKey(token, visibleKindsByName); if (semanticKey == null || semanticKey.isBlank()) { continue; } @@ -71,34 +116,129 @@ public final class SemanticIndex { private String semanticKey( final PbsToken token, - final Map> indexedSymbolsByName) { + final Map visibleKindsByName) { final PbsSemanticKind semanticKind = token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER - ? semanticKindForIdentifier(token.lexeme(), indexedSymbolsByName) + ? semanticKindForIdentifier(token.lexeme(), visibleKindsByName) : PbsSemanticKind.forToken(token); return semanticKind == null ? null : semanticKind.semanticKey(); } private PbsSemanticKind semanticKindForIdentifier( final String lexeme, - final Map> indexedSymbolsByName) { - final List symbols = indexedSymbolsByName.getOrDefault(lexeme, List.of()); - if (symbols.isEmpty()) { + final Map visibleKindsByName) { + final LspSymbolKind kind = visibleKindsByName.get(lexeme); + if (kind == null) { return PbsSemanticKind.IDENTIFIER; } - final LspSymbolKind kind = symbols.getFirst().kind(); return switch (kind) { - case FUNCTION, METHOD, CALLBACK -> PbsSemanticKind.FUNCTION; - case STRUCT, CONTRACT, HOST, BUILTIN_TYPE, SERVICE, ERROR, ENUM -> PbsSemanticKind.TYPE; - case GLOBAL, CONST -> PbsSemanticKind.BINDING; + case FUNCTION -> PbsSemanticKind.FUNCTION; + case METHOD -> PbsSemanticKind.METHOD; + case CONSTRUCTOR -> PbsSemanticKind.CONSTRUCTOR; + case STRUCT -> PbsSemanticKind.STRUCT; + case CONTRACT -> PbsSemanticKind.CONTRACT; + case HOST -> PbsSemanticKind.HOST; + case BUILTIN_TYPE -> PbsSemanticKind.BUILTIN_TYPE; + case SERVICE -> PbsSemanticKind.SERVICE; + case ERROR -> PbsSemanticKind.ERROR; + case ENUM -> PbsSemanticKind.ENUM; + case CALLBACK -> PbsSemanticKind.CALLBACK; + case GLOBAL -> PbsSemanticKind.GLOBAL; + case CONST -> PbsSemanticKind.CONST; + case IMPLEMENTS -> PbsSemanticKind.IMPLEMENTS; default -> PbsSemanticKind.IDENTIFIER; }; } + private String topDeclName(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return functionDecl.name(); + } + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return structDecl.name(); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return contractDecl.name(); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return hostDecl.name(); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return builtinTypeDecl.name(); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return serviceDecl.name(); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return errorDecl.name(); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return enumDecl.name(); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return callbackDecl.name(); + } + if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { + return globalDecl.name(); + } + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + return constDecl.name(); + } + if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) { + return implementsDecl.binderName(); + } + return null; + } + + private LspSymbolKind symbolKindForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl) { + return LspSymbolKind.FUNCTION; + } + if (topDecl instanceof PbsAst.StructDecl) { + return LspSymbolKind.STRUCT; + } + if (topDecl instanceof PbsAst.ContractDecl) { + return LspSymbolKind.CONTRACT; + } + if (topDecl instanceof PbsAst.HostDecl) { + return LspSymbolKind.HOST; + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl) { + return LspSymbolKind.BUILTIN_TYPE; + } + if (topDecl instanceof PbsAst.ServiceDecl) { + return LspSymbolKind.SERVICE; + } + if (topDecl instanceof PbsAst.ErrorDecl) { + return LspSymbolKind.ERROR; + } + if (topDecl instanceof PbsAst.EnumDecl) { + return LspSymbolKind.ENUM; + } + if (topDecl instanceof PbsAst.CallbackDecl) { + return LspSymbolKind.CALLBACK; + } + if (topDecl instanceof PbsAst.GlobalDecl) { + return LspSymbolKind.GLOBAL; + } + if (topDecl instanceof PbsAst.ConstDecl) { + return LspSymbolKind.CONST; + } + if (topDecl instanceof PbsAst.ImplementsDecl) { + return LspSymbolKind.IMPLEMENTS; + } + return null; + } + private LspSymbolDTO symbolForTopDecl( final Path documentPath, final PbsAst.TopDecl topDecl) { if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { - return symbol(documentPath, functionDecl.name(), LspSymbolKind.FUNCTION, functionDecl.span(), List.of()); + return symbol( + documentPath, + functionDecl.name(), + LspSymbolKind.FUNCTION, + functionDecl.span(), + localSymbolsInBlock(documentPath, functionDecl.body())); } if (topDecl instanceof PbsAst.StructDecl structDecl) { return symbol(documentPath, structDecl.name(), LspSymbolKind.STRUCT, structDecl.span(), structChildren(documentPath, structDecl)); @@ -147,7 +287,12 @@ public final class SemanticIndex { final PbsAst.StructDecl structDecl) { final List children = new ArrayList<>(functionChildren(documentPath, structDecl.methods().asList())); for (final PbsAst.CtorDecl ctorDecl : structDecl.ctors()) { - children.add(symbol(documentPath, ctorDecl.name(), LspSymbolKind.CONSTRUCTOR, ctorDecl.span(), List.of())); + children.add(symbol( + documentPath, + ctorDecl.name(), + LspSymbolKind.CONSTRUCTOR, + ctorDecl.span(), + localSymbolsInBlock(documentPath, ctorDecl.body()))); } return List.copyOf(children); } @@ -157,7 +302,174 @@ public final class SemanticIndex { final List functions) { final List children = new ArrayList<>(); for (final PbsAst.FunctionDecl functionDecl : functions) { - children.add(symbol(documentPath, functionDecl.name(), LspSymbolKind.METHOD, functionDecl.span(), List.of())); + children.add(symbol( + documentPath, + functionDecl.name(), + LspSymbolKind.METHOD, + functionDecl.span(), + localSymbolsInBlock(documentPath, functionDecl.body()))); + } + return List.copyOf(children); + } + + private List localSymbolsInBlock( + final Path documentPath, + final PbsAst.Block block) { + final List children = new ArrayList<>(); + for (final PbsAst.Statement statement : block.statements().asList()) { + children.addAll(localSymbolsInStatement(documentPath, statement)); + } + if (block.tailExpression() != null) { + children.addAll(localSymbolsInExpression(documentPath, block.tailExpression())); + } + return List.copyOf(children); + } + + private List localSymbolsInStatement( + final Path documentPath, + final PbsAst.Statement statement) { + return switch (statement) { + case PbsAst.LetStatement letStatement -> localSymbolsInExpression(documentPath, letStatement.initializer()); + case PbsAst.AssignStatement assignStatement -> localSymbolsInExpression(documentPath, assignStatement.value()); + case PbsAst.ReturnStatement returnStatement -> localSymbolsInNullableExpression(documentPath, returnStatement.value()); + case PbsAst.IfStatement ifStatement -> List.of(symbol( + documentPath, + "if", + LspSymbolKind.IF, + ifStatement.span(), + children( + localSymbolsInBlock(documentPath, ifStatement.thenBlock()), + ifStatement.elseIf() == null ? List.of() : localSymbolsInStatement(documentPath, ifStatement.elseIf()), + ifStatement.elseBlock() == null ? List.of() : localSymbolsInBlock(documentPath, ifStatement.elseBlock())))); + case PbsAst.ForStatement forStatement -> List.of(symbol( + documentPath, + "for " + forStatement.iteratorName(), + LspSymbolKind.FOR, + forStatement.span(), + localSymbolsInBlock(documentPath, forStatement.body()))); + case PbsAst.WhileStatement whileStatement -> List.of(symbol( + documentPath, + "while", + LspSymbolKind.WHILE, + whileStatement.span(), + localSymbolsInBlock(documentPath, whileStatement.body()))); + case PbsAst.ExpressionStatement expressionStatement -> localSymbolsInExpression(documentPath, expressionStatement.expression()); + case PbsAst.BreakStatement ignored -> List.of(); + case PbsAst.ContinueStatement ignored -> List.of(); + }; + } + + private List localSymbolsInExpression( + final Path documentPath, + final PbsAst.Expression expression) { + return switch (expression) { + case PbsAst.IfExpr ifExpr -> List.of(symbol( + documentPath, + "if", + LspSymbolKind.IF, + ifExpr.span(), + children( + localSymbolsInBlock(documentPath, ifExpr.thenBlock()), + localSymbolsInExpression(documentPath, ifExpr.elseExpression())))); + case PbsAst.SwitchExpr switchExpr -> List.of(symbol( + documentPath, + "switch", + LspSymbolKind.SWITCH, + switchExpr.span(), + childrenFromSwitchArms(documentPath, switchExpr.arms().asList()))); + case PbsAst.HandleExpr handleExpr -> List.of(symbol( + documentPath, + "handle", + LspSymbolKind.HANDLE, + handleExpr.span(), + childrenFromHandleArms(documentPath, handleExpr.arms().asList()))); + case PbsAst.BlockExpr blockExpr -> localSymbolsInBlock(documentPath, blockExpr.block()); + case PbsAst.ElseExpr elseExpr -> children( + localSymbolsInExpression(documentPath, elseExpr.optionalExpression()), + localSymbolsInExpression(documentPath, elseExpr.fallbackExpression())); + case PbsAst.UnaryExpr unaryExpr -> localSymbolsInExpression(documentPath, unaryExpr.expression()); + case PbsAst.BinaryExpr binaryExpr -> children( + localSymbolsInExpression(documentPath, binaryExpr.left()), + localSymbolsInExpression(documentPath, binaryExpr.right())); + case PbsAst.ApplyExpr applyExpr -> children( + localSymbolsInExpression(documentPath, applyExpr.callee()), + localSymbolsInExpression(documentPath, applyExpr.argument())); + case PbsAst.AsExpr asExpr -> localSymbolsInExpression(documentPath, asExpr.expression()); + case PbsAst.CallExpr callExpr -> { + final List> nested = new ArrayList<>(); + nested.add(localSymbolsInExpression(documentPath, callExpr.callee())); + for (final PbsAst.Expression argument : callExpr.arguments().asList()) { + nested.add(localSymbolsInExpression(documentPath, argument)); + } + yield children(nested.toArray(List[]::new)); + } + case PbsAst.MemberExpr memberExpr -> localSymbolsInExpression(documentPath, memberExpr.receiver()); + case PbsAst.PropagateExpr propagateExpr -> localSymbolsInExpression(documentPath, propagateExpr.expression()); + case PbsAst.GroupExpr groupExpr -> localSymbolsInExpression(documentPath, groupExpr.expression()); + case PbsAst.NewExpr newExpr -> { + final List> nested = new ArrayList<>(); + for (final PbsAst.Expression argument : newExpr.arguments().asList()) { + nested.add(localSymbolsInExpression(documentPath, argument)); + } + yield children(nested.toArray(List[]::new)); + } + case PbsAst.BindExpr bindExpr -> localSymbolsInExpression(documentPath, bindExpr.contextExpression()); + case PbsAst.SomeExpr someExpr -> localSymbolsInExpression(documentPath, someExpr.value()); + case PbsAst.OkExpr okExpr -> localSymbolsInExpression(documentPath, okExpr.value()); + case PbsAst.TupleExpr tupleExpr -> { + final List> nested = new ArrayList<>(); + for (final PbsAst.TupleItem item : tupleExpr.items().asList()) { + nested.add(localSymbolsInExpression(documentPath, item.expression())); + } + yield children(nested.toArray(List[]::new)); + } + case PbsAst.IdentifierExpr ignored -> List.of(); + case PbsAst.IntLiteralExpr ignored -> List.of(); + case PbsAst.FloatLiteralExpr ignored -> List.of(); + case PbsAst.BoundedLiteralExpr ignored -> List.of(); + case PbsAst.StringLiteralExpr ignored -> List.of(); + case PbsAst.BoolLiteralExpr ignored -> List.of(); + case PbsAst.ThisExpr ignored -> List.of(); + case PbsAst.NoneExpr ignored -> List.of(); + case PbsAst.ErrExpr ignored -> List.of(); + case PbsAst.UnitExpr ignored -> List.of(); + }; + } + + private List childrenFromSwitchArms( + final Path documentPath, + final List arms) { + final List children = new ArrayList<>(); + for (final PbsAst.SwitchArm arm : arms) { + children.addAll(localSymbolsInBlock(documentPath, arm.block())); + } + return List.copyOf(children); + } + + private List childrenFromHandleArms( + final Path documentPath, + final List arms) { + final List children = new ArrayList<>(); + for (final PbsAst.HandleArm arm : arms) { + children.addAll(localSymbolsInBlock(documentPath, arm.block())); + } + return List.copyOf(children); + } + + private List localSymbolsInNullableExpression( + final Path documentPath, + final PbsAst.Expression expression) { + return expression == null ? List.of() : localSymbolsInExpression(documentPath, expression); + } + + @SafeVarargs + private static List children(final List... parts) { + final List children = new ArrayList<>(); + for (final List part : parts) { + if (part == null || part.isEmpty()) { + continue; + } + children.addAll(part); } return List.copyOf(children); } @@ -179,7 +491,7 @@ public final class SemanticIndex { final Span span, final List children) { return new LspSymbolDTO( - name, + Objects.requireNonNullElse(name, kind.name().toLowerCase()), kind, documentPath, new LspRangeDTO((int) span.getStart(), (int) span.getEnd()), diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java index e158c20e..6789a060 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java @@ -25,7 +25,47 @@ final class LspServiceImplTest { private static final String OVERLAY_SOURCE = """ fn helper_call() -> void { - helper(); + if true { + helper(); + } + while false { + helper(); + } + let value = switch true { + true: { 1 }, + default: { 2 } + }; + let recovered = handle helper_result() { + _: { + ok(0) + } + }; + } + """; + + private static final String SERVICE_SOURCE = """ + declare service Game { + fn tick(x: int) -> int { return x; } + } + + fn main() -> void { + Game.tick(1); + } + """; + + private static final String SDK_IMPORT_SOURCE = """ + import { Gfx } from @sdk:gfx; + + fn main() -> void { + Gfx.clear(); + } + """; + + private static final String PROJECT_IMPORT_SOURCE = """ + import { Color } from @app:graphics; + + fn main(color: Color) -> Color { + return color; } """; @@ -52,6 +92,18 @@ final class LspServiceImplTest { assertTrue( analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")), analysis.toString()); + assertTrue( + flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.IF), + analysis.toString()); + assertTrue( + flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.WHILE), + analysis.toString()); + assertTrue( + flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SWITCH), + analysis.toString()); + assertTrue( + flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.HANDLE), + analysis.toString()); assertTrue( analysis.workspaceSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))), @@ -89,6 +141,62 @@ final class LspServiceImplTest { diagnostic.documentPath().equals(normalize(mainFile)))); } + @Test + void analyzeDocumentHighlightsServiceIdentifiersWithServiceSemanticKey() throws Exception { + final Path projectRoot = createProject(); + final Path mainFile = projectRoot.resolve("src/main.pbs"); + Files.writeString(mainFile, SERVICE_SOURCE); + + final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); + final LspService service = new LspServiceImpl( + new LspProjectContext("Example", "pbs", projectRoot), + vfs); + + final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); + + assertEquals("pbs-service", semanticKeyForLexeme(analysis, SERVICE_SOURCE, "Game")); + assertTrue(flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> + symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SERVICE && symbol.name().equals("Game"))); + } + + @Test + void analyzeDocumentHighlightsSdkImportedHostIdentifiersWithHostSemanticKey() throws Exception { + final Path projectRoot = createProject(); + final Path mainFile = projectRoot.resolve("src/main.pbs"); + Files.writeString(mainFile, SDK_IMPORT_SOURCE); + + final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); + final LspService service = new LspServiceImpl( + new LspProjectContext("Example", "pbs", projectRoot), + vfs); + + final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); + + assertEquals(List.of("pbs-service", "pbs-service"), semanticKeysForLexeme(analysis, SDK_IMPORT_SOURCE, "Gfx")); + } + + @Test + void analyzeDocumentHighlightsProjectImportedStructIdentifiersWithStructSemanticKey() throws Exception { + final Path projectRoot = createProject(); + final Path mainFile = projectRoot.resolve("src/main.pbs"); + final Path graphicsDir = projectRoot.resolve("src/graphics"); + final Path graphicsFile = graphicsDir.resolve("types.pbs"); + final Path graphicsBarrel = graphicsDir.resolve("mod.barrel"); + Files.createDirectories(graphicsDir); + Files.writeString(mainFile, PROJECT_IMPORT_SOURCE); + Files.writeString(graphicsFile, "declare struct Color(raw: int);\n"); + Files.writeString(graphicsBarrel, "pub struct Color;\n"); + + final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); + final LspService service = new LspServiceImpl( + new LspProjectContext("Example", "pbs", projectRoot), + vfs); + + final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); + + assertEquals(List.of("pbs-struct", "pbs-struct", "pbs-struct"), semanticKeysForLexeme(analysis, PROJECT_IMPORT_SOURCE, "Color")); + } + private Path createProject() throws Exception { final Path src = Files.createDirectories(tempDir.resolve("src")); Files.writeString(tempDir.resolve("prometeu.json"), """ @@ -100,10 +208,34 @@ final class LspServiceImplTest { "dependencies": [] } """); - Files.writeString(src.resolve("mod.barrel"), "pub fn helper() -> void;\n"); + Files.writeString(src.resolve("mod.barrel"), """ + pub fn helper() -> void; + pub fn helper_result() -> result int; + pub error MyError; + """); + Files.writeString(src.resolve("helper.pbs"), "fn helper() -> void {}\n"); + Files.writeString(src.resolve("helper_result.pbs"), """ + declare error MyError { + Failed + } + + fn helper_result() -> result int { + return ok(1); + } + """); return tempDir; } + private static List flatten( + final List symbols) { + final List flattened = new java.util.ArrayList<>(); + for (final var symbol : symbols) { + flattened.add(symbol); + flattened.addAll(flatten(symbol.children())); + } + return flattened; + } + private VfsProjectContext projectContext(final Path projectRoot) { return new VfsProjectContext("Example", "pbs", projectRoot); } @@ -120,11 +252,17 @@ final class LspServiceImplTest { final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis, final String source, final String lexeme) { + return semanticKeysForLexeme(analysis, source, lexeme).stream().findFirst().orElseThrow(); + } + + private static List semanticKeysForLexeme( + 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(); + .toList(); } private static String spanContent( diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java new file mode 100644 index 00000000..03f6f3c8 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java @@ -0,0 +1,93 @@ +package p.studio.workspaces.editor; + +import javafx.scene.Node; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; + +final class EditorDocumentScopeGuideGraphicFactory { + private static final double COLUMN_WIDTH = 10.0; + private static final double CANVAS_PADDING = 6.0; + private static final Color GUIDE_COLOR = Color.web("#384657"); + private static final Color GUIDE_CAP_COLOR = Color.web("#6fa8dc"); + + private EditorDocumentScopeGuideGraphicFactory() { + } + + static Node create( + final Node lineNumberNode, + final int paragraphIndex, + final EditorDocumentScopeGuideModel model) { + final var container = new HBox(lineNumberNode); + final int maxDepth = model.maxDepth(); + if (maxDepth > 0) { + container.getChildren().add(new ScopeGuideCanvas(model.segmentsForLine(paragraphIndex), maxDepth)); + } + return container; + } + + private static final class ScopeGuideCanvas extends Region { + private final Canvas canvas = new Canvas(); + private final java.util.List segments; + private final int maxDepth; + + private ScopeGuideCanvas( + final java.util.List segments, + final int maxDepth) { + this.segments = segments; + this.maxDepth = maxDepth; + getStyleClass().add("editor-workspace-scope-guide-gutter"); + getChildren().add(canvas); + setMinWidth(computeGuideWidth(maxDepth)); + setPrefWidth(computeGuideWidth(maxDepth)); + setMaxWidth(computeGuideWidth(maxDepth)); + } + + @Override + protected double computePrefWidth(final double height) { + return computeGuideWidth(maxDepth); + } + + @Override + protected void layoutChildren() { + final double width = snapSizeX(getWidth()); + final double height = Math.max(1.0, snapSizeY(getHeight())); + canvas.setWidth(width); + canvas.setHeight(height); + draw(width, height); + } + + private void draw(final double width, final double height) { + final GraphicsContext graphics = canvas.getGraphicsContext2D(); + graphics.clearRect(0, 0, width, height); + if (segments.isEmpty()) { + return; + } + final double centerY = Math.floor(height / 2.0); + for (final var segment : segments) { + final double x = CANVAS_PADDING + (segment.depth() * COLUMN_WIDTH) + (COLUMN_WIDTH / 2.0); + graphics.setLineWidth(1.2); + graphics.setStroke(GUIDE_COLOR); + switch (segment.kind()) { + case START -> { + graphics.strokeLine(x, centerY, x, height); + graphics.setStroke(GUIDE_CAP_COLOR); + graphics.strokeLine(x, centerY, x + 4.0, centerY); + } + case CONTINUE -> graphics.strokeLine(x, 0, x, height); + case END -> { + graphics.strokeLine(x, 0, x, centerY); + graphics.setStroke(GUIDE_CAP_COLOR); + graphics.strokeLine(x, centerY, x + 4.0, centerY); + } + } + } + } + + private static double computeGuideWidth(final int maxDepth) { + return maxDepth <= 0 ? 0.0 : CANVAS_PADDING * 2.0 + (maxDepth * COLUMN_WIDTH); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java new file mode 100644 index 00000000..2fe8db7b --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java @@ -0,0 +1,163 @@ +package p.studio.workspaces.editor; + +import p.studio.lsp.dtos.LspRangeDTO; +import p.studio.lsp.dtos.LspSymbolDTO; +import p.studio.lsp.messages.LspSymbolKind; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class EditorDocumentScopeGuideModel { + private static final EditorDocumentScopeGuideModel EMPTY = new EditorDocumentScopeGuideModel(List.of(), 0); + + private final List> lines; + private final int maxDepth; + + private EditorDocumentScopeGuideModel( + final List> lines, + final int maxDepth) { + this.lines = lines; + this.maxDepth = maxDepth; + } + + static EditorDocumentScopeGuideModel empty() { + return EMPTY; + } + + static EditorDocumentScopeGuideModel from( + final String content, + final List symbols) { + final int totalLines = totalLines(content); + final List> lines = new ArrayList<>(totalLines); + for (int line = 0; line < totalLines; line++) { + lines.add(new ArrayList<>()); + } + + final List lineStarts = lineStarts(content); + final int[] maxDepth = new int[] {0}; + for (final LspSymbolDTO symbol : symbols) { + appendSymbol(lines, lineStarts, content.length(), symbol, 0, maxDepth); + } + final List> frozen = lines.stream() + .map(List::copyOf) + .toList(); + return new EditorDocumentScopeGuideModel(frozen, maxDepth[0]); + } + + List segmentsForLine(final int lineIndex) { + if (lineIndex < 0 || lineIndex >= lines.size()) { + return List.of(); + } + return lines.get(lineIndex); + } + + int maxDepth() { + return maxDepth; + } + + private static void appendSymbol( + final List> lines, + final List lineStarts, + final int contentLength, + final LspSymbolDTO symbol, + final int depth, + final int[] maxDepth) { + if (symbol.kind() != LspSymbolKind.UNKNOWN) { + appendRange(lines, lineStarts, contentLength, symbol.range(), depth, maxDepth); + } + for (final LspSymbolDTO child : symbol.children()) { + appendSymbol(lines, lineStarts, contentLength, child, depth + 1, maxDepth); + } + } + + private static void appendRange( + final List> lines, + final List lineStarts, + final int contentLength, + final LspRangeDTO range, + final int depth, + final int[] maxDepth) { + if (lines.isEmpty()) { + return; + } + final int startLine = lineForOffset(lineStarts, clamp(range.startOffset(), 0, contentLength)); + final int inclusiveEndOffset = Math.max(range.startOffset(), Math.min(contentLength, range.endOffset()) - 1); + final int endLine = lineForOffset(lineStarts, clamp(inclusiveEndOffset, 0, contentLength)); + if (endLine <= startLine) { + return; + } + maxDepth[0] = Math.max(maxDepth[0], depth + 1); + for (int line = startLine; line <= endLine; line++) { + final GuideSegmentKind kind; + if (line == startLine) { + kind = GuideSegmentKind.START; + } else if (line == endLine) { + kind = GuideSegmentKind.END; + } else { + kind = GuideSegmentKind.CONTINUE; + } + lines.get(line).add(new GuideSegment(depth, kind)); + } + } + + private static int totalLines(final String content) { + if (content.isEmpty()) { + return 1; + } + int total = 1; + for (int index = 0; index < content.length(); index++) { + if (content.charAt(index) == '\n') { + total++; + } + } + return total; + } + + private static List lineStarts(final String content) { + if (content.isEmpty()) { + return List.of(0); + } + final List starts = new ArrayList<>(); + starts.add(0); + for (int index = 0; index < content.length(); index++) { + if (content.charAt(index) == '\n') { + starts.add(index + 1); + } + } + return Collections.unmodifiableList(starts); + } + + private static int lineForOffset(final List lineStarts, final int offset) { + int low = 0; + int high = lineStarts.size() - 1; + while (low <= high) { + final int mid = (low + high) >>> 1; + final int start = lineStarts.get(mid); + final int nextStart = mid + 1 < lineStarts.size() ? lineStarts.get(mid + 1) : Integer.MAX_VALUE; + if (offset < start) { + high = mid - 1; + continue; + } + if (offset >= nextStart) { + low = mid + 1; + continue; + } + return mid; + } + return Math.max(0, Math.min(lineStarts.size() - 1, low)); + } + + private static int clamp(final int value, final int minimum, final int maximum) { + return Math.max(minimum, Math.min(maximum, value)); + } + + enum GuideSegmentKind { + START, + CONTINUE, + END + } + + record GuideSegment(int depth, GuideSegmentKind kind) { + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java index 042188b1..2432a6d6 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java @@ -5,6 +5,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; import p.studio.lsp.dtos.LspSymbolDTO; +import p.studio.lsp.messages.LspSymbolKind; import p.studio.Container; import p.studio.controls.WorkspaceDockPane; import p.studio.utilities.i18n.I18n; @@ -64,24 +65,38 @@ public final class EditorOutlinePanel extends WorkspaceDockPane { private void rebuildSymbols(final List symbols) { symbolsBox.getChildren().clear(); - if (symbols.isEmpty()) { - symbolsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); - return; - } + int visible = 0; for (final LspSymbolDTO symbol : symbols) { - appendSymbol(symbol, 0); + visible += appendSymbol(symbol, 0); + } + if (visible == 0) { + symbolsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); } } - private void appendSymbol(final LspSymbolDTO symbol, final int depth) { - final var label = new Label(symbol.name() + " • " + symbol.kind().name().toLowerCase()); - label.setWrapText(true); - label.setPadding(new Insets(0, 0, 0, depth * 12)); - label.getStyleClass().add("editor-workspace-outline-item"); - symbolsBox.getChildren().add(label); - for (final LspSymbolDTO child : symbol.children()) { - appendSymbol(child, depth + 1); + private int appendSymbol(final LspSymbolDTO symbol, final int depth) { + int visible = 0; + int childDepth = depth; + if (showInOutline(symbol.kind())) { + final var label = new Label(symbol.name() + " • " + symbol.kind().name().toLowerCase()); + label.setWrapText(true); + label.setPadding(new Insets(0, 0, 0, depth * 12)); + label.getStyleClass().add("editor-workspace-outline-item"); + symbolsBox.getChildren().add(label); + visible++; + childDepth++; } + for (final LspSymbolDTO child : symbol.children()) { + visible += appendSymbol(child, childDepth); + } + return visible; + } + + private boolean showInOutline(final LspSymbolKind kind) { + return switch (kind) { + case IF, SWITCH, FOR, WHILE, HANDLE -> false; + default -> true; + }; } private Label sectionTitle(final I18n key) { 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 a4d54c2d..3bf3beef 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 @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.IntFunction; public final class EditorWorkspace extends Workspace { private final BorderPane root = new BorderPane(); @@ -42,6 +43,8 @@ public final class EditorWorkspace extends Workspace { private final VfsProjectDocument vfsProjectDocument; private final EditorOpenFileSession openFileSession = new EditorOpenFileSession(); private final List activePresentationStylesheets = new ArrayList<>(); + private final IntFunction lineNumberFactory = LineNumberFactory.get(codeArea); + private EditorDocumentScopeGuideModel scopeGuideModel = EditorDocumentScopeGuideModel.empty(); private boolean syncingEditor; public EditorWorkspace( @@ -52,7 +55,7 @@ public final class EditorWorkspace extends Workspace { this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService"); root.getStyleClass().add("editor-workspace"); - codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea)); + refreshParagraphGraphics(); codeArea.setEditable(false); codeArea.setWrapText(false); codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current)); @@ -134,6 +137,8 @@ public final class EditorWorkspace extends Workspace { fileBuffer, presentation, analysis); + scopeGuideModel = guidesFor(fileBuffer, analysis); + refreshParagraphGraphics(); applyPresentationStylesheets(presentation); syncingEditor = true; try { @@ -178,6 +183,8 @@ public final class EditorWorkspace extends Workspace { private void showEditorPlaceholder() { final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); + scopeGuideModel = EditorDocumentScopeGuideModel.empty(); + refreshParagraphGraphics(); applyPresentationStylesheets(presentation); syncingEditor = true; try { @@ -209,6 +216,13 @@ public final class EditorWorkspace extends Workspace { root.getStylesheets().addAll(activePresentationStylesheets); } + private void refreshParagraphGraphics() { + codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create( + lineNumberFactory.apply(paragraphIndex), + paragraphIndex, + scopeGuideModel)); + } + private VBox buildLayout() { final var content = new SplitPane(buildLeftColumn(), buildRightColumn()); content.setDividerPositions(0.30); @@ -354,4 +368,13 @@ public final class EditorWorkspace extends Workspace { outlinePanel.showSemanticReadResult(fileBuffer.path(), analysis.documentSymbols()); helperPanel.showSemanticReadResult(fileBuffer.path(), analysis.diagnostics()); } + + private EditorDocumentScopeGuideModel guidesFor( + final EditorOpenFileBuffer fileBuffer, + final LspAnalyzeDocumentResult analysis) { + if (!fileBuffer.frontendDocument() || analysis == null) { + return EditorDocumentScopeGuideModel.empty(); + } + return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols()); + } } 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 index 3a48a7e2..a17b9959 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java @@ -9,6 +9,7 @@ import p.studio.lsp.messages.LspAnalyzeDocumentResult; import p.studio.vfs.messages.VfsDocumentAccessMode; import java.nio.file.Path; +import java.util.Collection; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -107,4 +108,47 @@ final class EditorDocumentHighlightingRouterTest { assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); assertTrue(result.styleSpans().getStyleSpan(0).getStyle().isEmpty()); } + + @Test + void frontendServiceHighlightsProjectToDedicatedServiceCssClass() { + final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); + final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( + Path.of("/tmp/example/src/main.pbs"), + "main.pbs", + "pbs", + "declare service Game {}", + "LF", + true, + VfsDocumentAccessMode.READ_ONLY, + false); + + final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( + new LspSessionStateDTO(true, List.of("highlight")), + new LspSemanticPresentationDTO( + List.of("pbs-service"), + List.of("/themes/pbs/semantic-highlighting.css")), + List.of(), + List.of(new LspHighlightSpanDTO(new LspRangeDTO(16, 20), "pbs-service")), + List.of(), + List.of()); + + final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( + fileBuffer, + registry.resolve("pbs", analysis.semanticPresentation()), + analysis); + + assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); + } + + private boolean containsStyle( + final org.fxmisc.richtext.model.StyleSpans> styleSpans, + final String styleClass) { + for (int index = 0; index < styleSpans.getSpanCount(); index++) { + if (styleSpans.getStyleSpan(index).getStyle().contains(styleClass)) { + return true; + } + } + return false; + } } diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java index ab5d78e4..2f88576a 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java @@ -14,11 +14,11 @@ final class EditorDocumentPresentationRegistryTest { final EditorDocumentPresentation presentation = registry.resolve( "pbs", new LspSemanticPresentationDTO( - java.util.List.of("pbs-keyword"), + java.util.List.of("pbs-keyword", "pbs-service"), java.util.List.of("/themes/pbs/semantic-highlighting.css"))); assertEquals("pbs", presentation.styleKey()); - assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); + assertEquals(java.util.List.of("pbs-keyword", "pbs-service"), presentation.semanticKeys()); assertEquals(1, presentation.stylesheetUrls().size()); assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css")); } diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java new file mode 100644 index 00000000..adc8b899 --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java @@ -0,0 +1,94 @@ +package p.studio.workspaces.editor; + +import org.junit.jupiter.api.Test; +import p.studio.lsp.dtos.LspRangeDTO; +import p.studio.lsp.dtos.LspSymbolDTO; +import p.studio.lsp.messages.LspSymbolKind; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class EditorDocumentScopeGuideModelTest { + @Test + void buildsNestedGuidesForMultilineStructuralSymbols() { + final String content = """ + struct Hero { + fn attack() { + let value = 1 + } + } + + enum Kind { + Unit + } + """; + final LspSymbolDTO attack = symbol(content, "fn attack()", "}", LspSymbolKind.METHOD, List.of()); + final LspSymbolDTO hero = new LspSymbolDTO( + "struct Hero", + LspSymbolKind.STRUCT, + Path.of("/tmp/example/main.pbs"), + new LspRangeDTO( + content.indexOf("struct Hero"), + content.indexOf("\n\nenum Kind")), + List.of(attack)); + final LspSymbolDTO kind = symbol(content, "enum Kind", "}\n", LspSymbolKind.ENUM, List.of()); + + final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(hero, kind)); + + assertEquals(2, model.maxDepth()); + assertKinds(model, 0, "START"); + assertKinds(model, 1, "CONTINUE", "START"); + assertKinds(model, 2, "CONTINUE", "CONTINUE"); + assertKinds(model, 3, "CONTINUE", "END"); + assertKinds(model, 4, "END"); + assertKinds(model, 5); + assertKinds(model, 6, "START"); + assertKinds(model, 7, "CONTINUE"); + assertKinds(model, 8, "END"); + } + + @Test + void ignoresSingleLineSymbols() { + final String content = "const VALUE = 1\n"; + final LspSymbolDTO symbol = new LspSymbolDTO( + "VALUE", + LspSymbolKind.CONST, + Path.of("/tmp/example/main.pbs"), + new LspRangeDTO(0, content.indexOf('\n')), + List.of()); + + final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(symbol)); + + assertEquals(0, model.maxDepth()); + assertKinds(model, 0); + } + + private static LspSymbolDTO symbol( + final String content, + final String startMarker, + final String endMarker, + final LspSymbolKind kind, + final List children) { + final int start = content.indexOf(startMarker); + final int end = content.indexOf(endMarker, start) + endMarker.length(); + return new LspSymbolDTO( + startMarker, + kind, + Path.of("/tmp/example/main.pbs"), + new LspRangeDTO(start, end), + children); + } + + private static void assertKinds( + final EditorDocumentScopeGuideModel model, + final int line, + final String... expectedKinds) { + assertEquals( + List.of(expectedKinds), + model.segmentsForLine(line).stream() + .map(segment -> segment.kind().name()) + .toList()); + } +}