better semantic style for PBS

This commit is contained in:
bQUARKz 2026-04-02 18:35:58 +01:00
parent 275a7aa408
commit 4c1a5317dd
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
14 changed files with 1051 additions and 44 deletions

View File

@ -14,8 +14,19 @@ public enum PbsSemanticKind {
OPERATOR("pbs-operator"), OPERATOR("pbs-operator"),
PUNCTUATION("pbs-punctuation"), PUNCTUATION("pbs-punctuation"),
FUNCTION("pbs-function"), FUNCTION("pbs-function"),
TYPE("pbs-type"), METHOD("pbs-method"),
BINDING("pbs-binding"), 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"); IDENTIFIER("pbs-identifier");
private final String semanticKey; private final String semanticKey;

View File

@ -108,6 +108,22 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
return irBackend; return irBackend;
} }
public static Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> 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<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
for (final var entry : importedSemanticContexts.entrySet()) {
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
}
return Map.copyOf(supplementalTopDeclsByFile);
}
private IRBackend mergeCompiledSources( private IRBackend mergeCompiledSources(
final ArrayList<CompiledSourceFile> compiledSourceFiles, final ArrayList<CompiledSourceFile> compiledSourceFiles,
final Set<ModuleId> failedModuleIds, final Set<ModuleId> failedModuleIds,

View File

@ -18,12 +18,56 @@
-fx-fill: #dcdcaa; -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; -fx-fill: #4ec9b0;
} }
.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-binding { .editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-contract {
-fx-fill: #c586c0; -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 { .editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-string {

View File

@ -4,6 +4,11 @@ public enum LspSymbolKind {
FUNCTION, FUNCTION,
METHOD, METHOD,
CONSTRUCTOR, CONSTRUCTOR,
IF,
SWITCH,
FOR,
WHILE,
HANDLE,
GLOBAL, GLOBAL,
CONST, CONST,
STRUCT, STRUCT,

View File

@ -13,6 +13,7 @@ import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.source.diagnostics.Diagnostic; import p.studio.compiler.source.diagnostics.Diagnostic;
import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.services.PBSFrontendPhaseService;
import p.studio.compiler.workspaces.AssetSurfaceContextLoader; import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
import p.studio.compiler.workspaces.PipelineStage; import p.studio.compiler.workspaces.PipelineStage;
import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage; import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage;
@ -70,7 +71,7 @@ final class LspSemanticReadPhase {
new OverlaySourceProviderFactoryImpl(vfsProjectDocument, requestedDocumentPath)); new OverlaySourceProviderFactoryImpl(vfsProjectDocument, requestedDocumentPath));
final BuilderPipelineContext context = BuilderPipelineContext.fromConfig(config); final BuilderPipelineContext context = BuilderPipelineContext.fromConfig(config);
final AnalysisRuntimeSnapshot snapshot = runAnalysisStages(context); final AnalysisRuntimeSnapshot snapshot = runAnalysisStages(context);
return index(snapshot, requestedDocumentPath); return index(snapshot, context, requestedDocumentPath);
} }
private static AnalysisRuntimeSnapshot runAnalysisStages(final BuilderPipelineContext context) { private static AnalysisRuntimeSnapshot runAnalysisStages(final BuilderPipelineContext context) {
@ -129,6 +130,7 @@ final class LspSemanticReadPhase {
private static SemanticSession index( private static SemanticSession index(
final AnalysisRuntimeSnapshot runtimeSnapshot, final AnalysisRuntimeSnapshot runtimeSnapshot,
final BuilderPipelineContext context,
final Path requestedDocumentPath) { final Path requestedDocumentPath) {
final AnalysisSnapshot snapshot = runtimeSnapshot.analysisSnapshot(); final AnalysisSnapshot snapshot = runtimeSnapshot.analysisSnapshot();
final Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument = diagnosticsByDocument( final Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument = diagnosticsByDocument(
@ -147,6 +149,10 @@ final class LspSemanticReadPhase {
Map.of(), Map.of(),
Map.of()); Map.of());
} }
final Map<FileId, List<PbsAst.TopDecl>> importedSupplementalTopDeclsByFile = importedSupplementalTopDeclsByFile(
snapshot.frontendSpec(),
context);
final List<IndexedDocument> indexedDocuments = new ArrayList<>();
for (final FileId fileId : snapshot.fileTable()) { for (final FileId fileId : snapshot.fileTable()) {
final SourceHandle sourceHandle = snapshot.fileTable().get(fileId); final SourceHandle sourceHandle = snapshot.fileTable().get(fileId);
if (!isSourceRelated(snapshot.frontendSpec(), sourceHandle)) { if (!isSourceRelated(snapshot.frontendSpec(), sourceHandle)) {
@ -156,7 +162,17 @@ final class LspSemanticReadPhase {
final DiagnosticSink diagnostics = DiagnosticSink.empty(); final DiagnosticSink diagnostics = DiagnosticSink.empty();
final var tokens = PbsLexer.lex(source, fileId, diagnostics); final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.ORDINARY); 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( return new SemanticSession(
normalize(requestedDocumentPath), normalize(requestedDocumentPath),
@ -169,6 +185,39 @@ final class LspSemanticReadPhase {
semanticIndex.tokensByDocument()); semanticIndex.tokensByDocument());
} }
private static Map<FileId, List<PbsAst.TopDecl>> 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<FileId, List<PbsAst.TopDecl>> 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<PbsAst.TopDecl> visibleImportedTopDecls) {
}
private static boolean isSourceRelated(final FrontendSpec frontendSpec, final SourceHandle sourceHandle) { private static boolean isSourceRelated(final FrontendSpec frontendSpec, final SourceHandle sourceHandle) {
return frontendSpec.getAllowedExtensions().contains(sourceHandle.getExtension()); return frontendSpec.getAllowedExtensions().contains(sourceHandle.getExtension());
} }

View File

@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import static p.studio.lsp.LspSemanticUtilities.normalize; import static p.studio.lsp.LspSemanticUtilities.normalize;
@ -29,6 +30,22 @@ public final class SemanticIndex {
final Path documentPath, final Path documentPath,
final PbsAst.File ast, final PbsAst.File ast,
final List<PbsToken> tokens) { final List<PbsToken> tokens) {
index(documentPath, ast, tokens, List.of());
}
public void index(
final Path documentPath,
final PbsAst.File ast,
final List<PbsToken> tokens,
final List<PbsAst.TopDecl> visibleImportedTopDecls) {
registerDocument(documentPath, ast, tokens);
buildHighlights(documentPath, visibleImportedTopDecls);
}
public void registerDocument(
final Path documentPath,
final PbsAst.File ast,
final List<PbsToken> tokens) {
final Path normalizedDocumentPath = normalize(documentPath); final Path normalizedDocumentPath = normalize(documentPath);
tokensByDocument.put(normalizedDocumentPath, List.copyOf(tokens)); tokensByDocument.put(normalizedDocumentPath, List.copyOf(tokens));
final List<LspSymbolDTO> documentSymbols = new ArrayList<>(); final List<LspSymbolDTO> documentSymbols = new ArrayList<>();
@ -44,21 +61,49 @@ public final class SemanticIndex {
symbolsByName.computeIfAbsent(child.name(), ignored -> new ArrayList<>()).add(child); symbolsByName.computeIfAbsent(child.name(), ignored -> new ArrayList<>()).add(child);
} }
} }
documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols));
}
public void buildHighlights(
final Path documentPath,
final List<PbsAst.TopDecl> visibleImportedTopDecls) {
final Path normalizedDocumentPath = normalize(documentPath);
final List<PbsToken> tokens = tokensByDocument.getOrDefault(normalizedDocumentPath, List.of());
final Map<String, LspSymbolKind> 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( semanticHighlightsByDocument.put(
normalizedDocumentPath, normalizedDocumentPath,
buildSemanticHighlights(tokens, symbolsByName)); buildSemanticHighlights(tokens, visibleKindsByName));
documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols));
} }
private List<LspHighlightSpanDTO> buildSemanticHighlights( private List<LspHighlightSpanDTO> buildSemanticHighlights(
final List<PbsToken> tokens, final List<PbsToken> tokens,
final Map<String, List<LspSymbolDTO>> indexedSymbolsByName) { final Map<String, LspSymbolKind> visibleKindsByName) {
final List<LspHighlightSpanDTO> highlights = new ArrayList<>(); final List<LspHighlightSpanDTO> highlights = new ArrayList<>();
for (final PbsToken token : tokens) { for (final PbsToken token : tokens) {
if (token.kind() == PbsTokenKind.EOF) { if (token.kind() == PbsTokenKind.EOF) {
continue; continue;
} }
final String semanticKey = semanticKey(token, indexedSymbolsByName); final String semanticKey = semanticKey(token, visibleKindsByName);
if (semanticKey == null || semanticKey.isBlank()) { if (semanticKey == null || semanticKey.isBlank()) {
continue; continue;
} }
@ -71,34 +116,129 @@ public final class SemanticIndex {
private String semanticKey( private String semanticKey(
final PbsToken token, final PbsToken token,
final Map<String, List<LspSymbolDTO>> indexedSymbolsByName) { final Map<String, LspSymbolKind> visibleKindsByName) {
final PbsSemanticKind semanticKind = token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER final PbsSemanticKind semanticKind = token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER
? semanticKindForIdentifier(token.lexeme(), indexedSymbolsByName) ? semanticKindForIdentifier(token.lexeme(), visibleKindsByName)
: PbsSemanticKind.forToken(token); : PbsSemanticKind.forToken(token);
return semanticKind == null ? null : semanticKind.semanticKey(); return semanticKind == null ? null : semanticKind.semanticKey();
} }
private PbsSemanticKind semanticKindForIdentifier( private PbsSemanticKind semanticKindForIdentifier(
final String lexeme, final String lexeme,
final Map<String, List<LspSymbolDTO>> indexedSymbolsByName) { final Map<String, LspSymbolKind> visibleKindsByName) {
final List<LspSymbolDTO> symbols = indexedSymbolsByName.getOrDefault(lexeme, List.of()); final LspSymbolKind kind = visibleKindsByName.get(lexeme);
if (symbols.isEmpty()) { if (kind == null) {
return PbsSemanticKind.IDENTIFIER; return PbsSemanticKind.IDENTIFIER;
} }
final LspSymbolKind kind = symbols.getFirst().kind();
return switch (kind) { return switch (kind) {
case FUNCTION, METHOD, CALLBACK -> PbsSemanticKind.FUNCTION; case FUNCTION -> PbsSemanticKind.FUNCTION;
case STRUCT, CONTRACT, HOST, BUILTIN_TYPE, SERVICE, ERROR, ENUM -> PbsSemanticKind.TYPE; case METHOD -> PbsSemanticKind.METHOD;
case GLOBAL, CONST -> PbsSemanticKind.BINDING; 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; 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( private LspSymbolDTO symbolForTopDecl(
final Path documentPath, final Path documentPath,
final PbsAst.TopDecl topDecl) { final PbsAst.TopDecl topDecl) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { 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) { if (topDecl instanceof PbsAst.StructDecl structDecl) {
return symbol(documentPath, structDecl.name(), LspSymbolKind.STRUCT, structDecl.span(), structChildren(documentPath, 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 PbsAst.StructDecl structDecl) {
final List<LspSymbolDTO> children = new ArrayList<>(functionChildren(documentPath, structDecl.methods().asList())); final List<LspSymbolDTO> children = new ArrayList<>(functionChildren(documentPath, structDecl.methods().asList()));
for (final PbsAst.CtorDecl ctorDecl : structDecl.ctors()) { 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); return List.copyOf(children);
} }
@ -157,7 +302,174 @@ public final class SemanticIndex {
final List<PbsAst.FunctionDecl> functions) { final List<PbsAst.FunctionDecl> functions) {
final List<LspSymbolDTO> children = new ArrayList<>(); final List<LspSymbolDTO> children = new ArrayList<>();
for (final PbsAst.FunctionDecl functionDecl : functions) { 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<LspSymbolDTO> localSymbolsInBlock(
final Path documentPath,
final PbsAst.Block block) {
final List<LspSymbolDTO> 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<LspSymbolDTO> 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<LspSymbolDTO> 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<List<LspSymbolDTO>> 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<List<LspSymbolDTO>> 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<List<LspSymbolDTO>> 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<LspSymbolDTO> childrenFromSwitchArms(
final Path documentPath,
final List<PbsAst.SwitchArm> arms) {
final List<LspSymbolDTO> children = new ArrayList<>();
for (final PbsAst.SwitchArm arm : arms) {
children.addAll(localSymbolsInBlock(documentPath, arm.block()));
}
return List.copyOf(children);
}
private List<LspSymbolDTO> childrenFromHandleArms(
final Path documentPath,
final List<PbsAst.HandleArm> arms) {
final List<LspSymbolDTO> children = new ArrayList<>();
for (final PbsAst.HandleArm arm : arms) {
children.addAll(localSymbolsInBlock(documentPath, arm.block()));
}
return List.copyOf(children);
}
private List<LspSymbolDTO> localSymbolsInNullableExpression(
final Path documentPath,
final PbsAst.Expression expression) {
return expression == null ? List.of() : localSymbolsInExpression(documentPath, expression);
}
@SafeVarargs
private static List<LspSymbolDTO> children(final List<LspSymbolDTO>... parts) {
final List<LspSymbolDTO> children = new ArrayList<>();
for (final List<LspSymbolDTO> part : parts) {
if (part == null || part.isEmpty()) {
continue;
}
children.addAll(part);
} }
return List.copyOf(children); return List.copyOf(children);
} }
@ -179,7 +491,7 @@ public final class SemanticIndex {
final Span span, final Span span,
final List<LspSymbolDTO> children) { final List<LspSymbolDTO> children) {
return new LspSymbolDTO( return new LspSymbolDTO(
name, Objects.requireNonNullElse(name, kind.name().toLowerCase()),
kind, kind,
documentPath, documentPath,
new LspRangeDTO((int) span.getStart(), (int) span.getEnd()), new LspRangeDTO((int) span.getStart(), (int) span.getEnd()),

View File

@ -25,8 +25,48 @@ final class LspServiceImplTest {
private static final String OVERLAY_SOURCE = """ private static final String OVERLAY_SOURCE = """
fn helper_call() -> void fn helper_call() -> void
{ {
if true {
helper(); 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;
}
"""; """;
@Test @Test
@ -52,6 +92,18 @@ final class LspServiceImplTest {
assertTrue( assertTrue(
analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")), analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")),
analysis.toString()); 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( assertTrue(
analysis.workspaceSymbols().stream().anyMatch(symbol -> analysis.workspaceSymbols().stream().anyMatch(symbol ->
symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))), symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))),
@ -89,6 +141,62 @@ final class LspServiceImplTest {
diagnostic.documentPath().equals(normalize(mainFile)))); 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 { private Path createProject() throws Exception {
final Path src = Files.createDirectories(tempDir.resolve("src")); final Path src = Files.createDirectories(tempDir.resolve("src"));
Files.writeString(tempDir.resolve("prometeu.json"), """ Files.writeString(tempDir.resolve("prometeu.json"), """
@ -100,10 +208,34 @@ final class LspServiceImplTest {
"dependencies": [] "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<MyError> 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<MyError> int {
return ok(1);
}
""");
return tempDir; return tempDir;
} }
private static List<p.studio.lsp.dtos.LspSymbolDTO> flatten(
final List<p.studio.lsp.dtos.LspSymbolDTO> symbols) {
final List<p.studio.lsp.dtos.LspSymbolDTO> 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) { private VfsProjectContext projectContext(final Path projectRoot) {
return new VfsProjectContext("Example", "pbs", projectRoot); return new VfsProjectContext("Example", "pbs", projectRoot);
} }
@ -120,11 +252,17 @@ final class LspServiceImplTest {
final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis, final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis,
final String source, final String source,
final String lexeme) { final String lexeme) {
return semanticKeysForLexeme(analysis, source, lexeme).stream().findFirst().orElseThrow();
}
private static List<String> semanticKeysForLexeme(
final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis,
final String source,
final String lexeme) {
return analysis.semanticHighlights().stream() return analysis.semanticHighlights().stream()
.filter(highlight -> lexeme.equals(spanContent(source, highlight.range().startOffset(), highlight.range().endOffset()))) .filter(highlight -> lexeme.equals(spanContent(source, highlight.range().startOffset(), highlight.range().endOffset())))
.map(p.studio.lsp.dtos.LspHighlightSpanDTO::semanticKey) .map(p.studio.lsp.dtos.LspHighlightSpanDTO::semanticKey)
.findFirst() .toList();
.orElseThrow();
} }
private static String spanContent( private static String spanContent(

View File

@ -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<EditorDocumentScopeGuideModel.GuideSegment> segments;
private final int maxDepth;
private ScopeGuideCanvas(
final java.util.List<EditorDocumentScopeGuideModel.GuideSegment> 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);
}
}
}

View File

@ -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<List<GuideSegment>> lines;
private final int maxDepth;
private EditorDocumentScopeGuideModel(
final List<List<GuideSegment>> lines,
final int maxDepth) {
this.lines = lines;
this.maxDepth = maxDepth;
}
static EditorDocumentScopeGuideModel empty() {
return EMPTY;
}
static EditorDocumentScopeGuideModel from(
final String content,
final List<LspSymbolDTO> symbols) {
final int totalLines = totalLines(content);
final List<List<GuideSegment>> lines = new ArrayList<>(totalLines);
for (int line = 0; line < totalLines; line++) {
lines.add(new ArrayList<>());
}
final List<Integer> lineStarts = lineStarts(content);
final int[] maxDepth = new int[] {0};
for (final LspSymbolDTO symbol : symbols) {
appendSymbol(lines, lineStarts, content.length(), symbol, 0, maxDepth);
}
final List<List<GuideSegment>> frozen = lines.stream()
.map(List::copyOf)
.toList();
return new EditorDocumentScopeGuideModel(frozen, maxDepth[0]);
}
List<GuideSegment> 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<List<GuideSegment>> lines,
final List<Integer> 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<List<GuideSegment>> lines,
final List<Integer> 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<Integer> lineStarts(final String content) {
if (content.isEmpty()) {
return List.of(0);
}
final List<Integer> 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<Integer> 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) {
}
}

View File

@ -5,6 +5,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspSymbolKind;
import p.studio.Container; import p.studio.Container;
import p.studio.controls.WorkspaceDockPane; import p.studio.controls.WorkspaceDockPane;
import p.studio.utilities.i18n.I18n; import p.studio.utilities.i18n.I18n;
@ -64,24 +65,38 @@ public final class EditorOutlinePanel extends WorkspaceDockPane {
private void rebuildSymbols(final List<LspSymbolDTO> symbols) { private void rebuildSymbols(final List<LspSymbolDTO> symbols) {
symbolsBox.getChildren().clear(); symbolsBox.getChildren().clear();
if (symbols.isEmpty()) { int visible = 0;
symbolsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS));
return;
}
for (final LspSymbolDTO symbol : symbols) { 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) { 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()); final var label = new Label(symbol.name() + "" + symbol.kind().name().toLowerCase());
label.setWrapText(true); label.setWrapText(true);
label.setPadding(new Insets(0, 0, 0, depth * 12)); label.setPadding(new Insets(0, 0, 0, depth * 12));
label.getStyleClass().add("editor-workspace-outline-item"); label.getStyleClass().add("editor-workspace-outline-item");
symbolsBox.getChildren().add(label); symbolsBox.getChildren().add(label);
for (final LspSymbolDTO child : symbol.children()) { visible++;
appendSymbol(child, depth + 1); 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) { private Label sectionTitle(final I18n key) {

View File

@ -24,6 +24,7 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.IntFunction;
public final class EditorWorkspace extends Workspace { public final class EditorWorkspace extends Workspace {
private final BorderPane root = new BorderPane(); private final BorderPane root = new BorderPane();
@ -42,6 +43,8 @@ public final class EditorWorkspace extends Workspace {
private final VfsProjectDocument vfsProjectDocument; private final VfsProjectDocument vfsProjectDocument;
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession(); private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
private final List<String> activePresentationStylesheets = new ArrayList<>(); private final List<String> activePresentationStylesheets = new ArrayList<>();
private final IntFunction<Node> lineNumberFactory = LineNumberFactory.get(codeArea);
private EditorDocumentScopeGuideModel scopeGuideModel = EditorDocumentScopeGuideModel.empty();
private boolean syncingEditor; private boolean syncingEditor;
public EditorWorkspace( public EditorWorkspace(
@ -52,7 +55,7 @@ public final class EditorWorkspace extends Workspace {
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService"); this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
root.getStyleClass().add("editor-workspace"); root.getStyleClass().add("editor-workspace");
codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea)); refreshParagraphGraphics();
codeArea.setEditable(false); codeArea.setEditable(false);
codeArea.setWrapText(false); codeArea.setWrapText(false);
codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current)); codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current));
@ -134,6 +137,8 @@ public final class EditorWorkspace extends Workspace {
fileBuffer, fileBuffer,
presentation, presentation,
analysis); analysis);
scopeGuideModel = guidesFor(fileBuffer, analysis);
refreshParagraphGraphics();
applyPresentationStylesheets(presentation); applyPresentationStylesheets(presentation);
syncingEditor = true; syncingEditor = true;
try { try {
@ -178,6 +183,8 @@ public final class EditorWorkspace extends Workspace {
private void showEditorPlaceholder() { private void showEditorPlaceholder() {
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
scopeGuideModel = EditorDocumentScopeGuideModel.empty();
refreshParagraphGraphics();
applyPresentationStylesheets(presentation); applyPresentationStylesheets(presentation);
syncingEditor = true; syncingEditor = true;
try { try {
@ -209,6 +216,13 @@ public final class EditorWorkspace extends Workspace {
root.getStylesheets().addAll(activePresentationStylesheets); root.getStylesheets().addAll(activePresentationStylesheets);
} }
private void refreshParagraphGraphics() {
codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create(
lineNumberFactory.apply(paragraphIndex),
paragraphIndex,
scopeGuideModel));
}
private VBox buildLayout() { private VBox buildLayout() {
final var content = new SplitPane(buildLeftColumn(), buildRightColumn()); final var content = new SplitPane(buildLeftColumn(), buildRightColumn());
content.setDividerPositions(0.30); content.setDividerPositions(0.30);
@ -354,4 +368,13 @@ public final class EditorWorkspace extends Workspace {
outlinePanel.showSemanticReadResult(fileBuffer.path(), analysis.documentSymbols()); outlinePanel.showSemanticReadResult(fileBuffer.path(), analysis.documentSymbols());
helperPanel.showSemanticReadResult(fileBuffer.path(), analysis.diagnostics()); 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());
}
} }

View File

@ -9,6 +9,7 @@ import p.studio.lsp.messages.LspAnalyzeDocumentResult;
import p.studio.vfs.messages.VfsDocumentAccessMode; import p.studio.vfs.messages.VfsDocumentAccessMode;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -107,4 +108,47 @@ final class EditorDocumentHighlightingRouterTest {
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner());
assertTrue(result.styleSpans().getStyleSpan(0).getStyle().isEmpty()); 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<Collection<String>> styleSpans,
final String styleClass) {
for (int index = 0; index < styleSpans.getSpanCount(); index++) {
if (styleSpans.getStyleSpan(index).getStyle().contains(styleClass)) {
return true;
}
}
return false;
}
} }

View File

@ -14,11 +14,11 @@ final class EditorDocumentPresentationRegistryTest {
final EditorDocumentPresentation presentation = registry.resolve( final EditorDocumentPresentation presentation = registry.resolve(
"pbs", "pbs",
new LspSemanticPresentationDTO( 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"))); java.util.List.of("/themes/pbs/semantic-highlighting.css")));
assertEquals("pbs", presentation.styleKey()); 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()); assertEquals(1, presentation.stylesheetUrls().size());
assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css")); assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css"));
} }

View File

@ -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<LspSymbolDTO> 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());
}
}