better semantic style for PBS
This commit is contained in:
parent
275a7aa408
commit
4c1a5317dd
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -4,6 +4,11 @@ public enum LspSymbolKind {
|
|||||||
FUNCTION,
|
FUNCTION,
|
||||||
METHOD,
|
METHOD,
|
||||||
CONSTRUCTOR,
|
CONSTRUCTOR,
|
||||||
|
IF,
|
||||||
|
SWITCH,
|
||||||
|
FOR,
|
||||||
|
WHILE,
|
||||||
|
HANDLE,
|
||||||
GLOBAL,
|
GLOBAL,
|
||||||
CONST,
|
CONST,
|
||||||
STRUCT,
|
STRUCT,
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()),
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user