implements PLN-0063

This commit is contained in:
bQUARKz 2026-05-05 11:14:45 +01:00
parent cebef90118
commit 1f85be3721
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
59 changed files with 9 additions and 3046 deletions

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":32,"AGD":35,"DEC":32,"PLN":65,"LSN":47,"CLSN":1}} {"type":"meta","next_id":{"DSC":32,"AGD":35,"DEC":32,"PLN":65,"LSN":47,"CLSN":1}}
{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]}
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} {"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} {"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}

View File

@ -2,9 +2,9 @@
id: PLN-0063 id: PLN-0063
ticket: studio-editor-workspace-cleanup ticket: studio-editor-workspace-cleanup
title: Legacy LSP and VFS Module Removal with Runtime Preservation title: Legacy LSP and VFS Module Removal with Runtime Preservation
status: open status: done
created: 2026-05-05 created: 2026-05-05
completed: completed: 2026-05-05
tags: [studio, editor, cleanup, lsp, vfs, runtime] tags: [studio, editor, cleanup, lsp, vfs, runtime]
--- ---
@ -73,11 +73,11 @@ Remove the legacy `prometeu-lsp` and `prometeu-vfs` modules and all remaining co
## Acceptance Criteria ## Acceptance Criteria
- [ ] `prometeu-lsp` is no longer part of the build graph. - [x] `prometeu-lsp` is no longer part of the build graph.
- [ ] `prometeu-vfs` is no longer part of the build graph. - [x] `prometeu-vfs` is no longer part of the build graph.
- [ ] No production Studio class imports legacy `LSP` or `VFS` module types. - [x] No production Studio class imports legacy `LSP` or `VFS` module types.
- [ ] Compiler and shipper build flows still work. - [x] Compiler and shipper build flows still work.
- [ ] Play/run/debug flows still work without the removed modules. - [x] Play/run/debug flows still work without the removed modules.
## Dependencies ## Dependencies
@ -89,4 +89,3 @@ Remove the legacy `prometeu-lsp` and `prometeu-vfs` modules and all remaining co
- Hidden transitive dependencies may keep the build graph coupled to removed modules longer than expected. - Hidden transitive dependencies may keep the build graph coupled to removed modules longer than expected.
- Runtime or debug flows may have indirect event or session assumptions not obvious from imports alone. - Runtime or debug flows may have indirect event or session assumptions not obvious from imports alone.
- Deleting modules too early may obscure compile errors that should instead be fixed in surviving Studio code first. - Deleting modules too early may obscure compile errors that should instead be fixed in surviving Studio code first.

View File

@ -5,10 +5,6 @@ plugins {
dependencies { dependencies {
implementation(project(":prometeu-infra")) implementation(project(":prometeu-infra"))
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-lsp:prometeu-lsp-v1"))
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
implementation(project(":prometeu-vfs:prometeu-vfs-v1"))
implementation(project(":prometeu-packer:prometeu-packer-api")) implementation(project(":prometeu-packer:prometeu-packer-api"))
implementation(project(":prometeu-packer:prometeu-packer-v1")) implementation(project(":prometeu-packer:prometeu-packer-v1"))
implementation(project(":prometeu-studio")) implementation(project(":prometeu-studio"))
@ -23,4 +19,4 @@ application {
javafx { javafx {
version = libs.versions.javafx.get() version = libs.versions.javafx.get()
modules("javafx.controls", "javafx.fxml") modules("javafx.controls", "javafx.fxml")
} }

View File

@ -1,7 +0,0 @@
plugins {
id("gradle.java-library-conventions")
}
dependencies {
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
}

View File

@ -1,21 +0,0 @@
package p.studio.lsp;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.messages.*;
import p.studio.vfs.VfsProjectDocument;
public interface LspService extends AutoCloseable {
LspProjectContext projectContext();
VfsProjectDocument projectDocumentVfs();
LspSessionStateDTO snapshot();
LspAnalyzeDocumentResult analyzeDocument(LspAnalyzeDocumentRequest request);
LspDefinitionResult definition(LspDefinitionRequest request);
@Override
default void close() {
}
}

View File

@ -1,8 +0,0 @@
package p.studio.lsp;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.vfs.VfsProjectDocument;
public interface LspServiceFactory {
LspService open(LspProjectContext projectContext, VfsProjectDocument vfsProjectDocument);
}

View File

@ -1,16 +0,0 @@
package p.studio.lsp.dtos;
import java.nio.file.Path;
import java.util.Objects;
public record LspDefinitionTargetDTO(
String name,
Path documentPath,
LspRangeDTO range) {
public LspDefinitionTargetDTO {
name = Objects.requireNonNull(name, "name");
documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize();
range = Objects.requireNonNull(range, "range");
}
}

View File

@ -1,24 +0,0 @@
package p.studio.lsp.dtos;
import p.studio.lsp.messages.LspDiagnosticSeverity;
import java.nio.file.Path;
import java.util.Objects;
public record LspDiagnosticDTO(
Path documentPath,
LspRangeDTO range,
LspDiagnosticSeverity severity,
String phase,
String code,
String message) {
public LspDiagnosticDTO {
documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize();
range = Objects.requireNonNull(range, "range");
severity = Objects.requireNonNull(severity, "severity");
phase = phase == null ? "" : phase;
code = code == null ? "" : code;
message = Objects.requireNonNull(message, "message");
}
}

View File

@ -1,13 +0,0 @@
package p.studio.lsp.dtos;
import java.util.Objects;
public record LspHighlightSpanDTO(
LspRangeDTO range,
String semanticKey) {
public LspHighlightSpanDTO {
range = Objects.requireNonNull(range, "range");
semanticKey = Objects.requireNonNull(semanticKey, "semanticKey");
}
}

View File

@ -1,18 +0,0 @@
package p.studio.lsp.dtos;
import java.util.Objects;
public record LspInlineHintDTO(
LspRangeDTO anchor,
String label,
String category) {
public LspInlineHintDTO {
anchor = Objects.requireNonNull(anchor, "anchor");
label = Objects.requireNonNull(label, "label").trim();
category = Objects.requireNonNull(category, "category").trim();
if (label.isBlank()) {
throw new IllegalArgumentException("label cannot be blank");
}
}
}

View File

@ -1,15 +0,0 @@
package p.studio.lsp.dtos;
public record LspRangeDTO(
int startOffset,
int endOffset) {
public LspRangeDTO {
startOffset = Math.max(0, startOffset);
endOffset = Math.max(startOffset, endOffset);
}
public boolean contains(final int offset) {
return startOffset <= offset && offset < endOffset;
}
}

View File

@ -1,14 +0,0 @@
package p.studio.lsp.dtos;
import java.util.List;
import java.util.Objects;
public record LspSemanticPresentationDTO(
List<String> semanticKeys,
List<String> resources) {
public LspSemanticPresentationDTO {
semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys"));
resources = List.copyOf(Objects.requireNonNull(resources, "resources"));
}
}

View File

@ -1,13 +0,0 @@
package p.studio.lsp.dtos;
import java.util.List;
import java.util.Objects;
public record LspSessionStateDTO(
boolean semanticReadReady,
List<String> declaredCapabilities) {
public LspSessionStateDTO {
declaredCapabilities = List.copyOf(Objects.requireNonNull(declaredCapabilities, "declaredCapabilities"));
}
}

View File

@ -1,18 +0,0 @@
package p.studio.lsp.dtos;
import java.util.List;
import java.util.Objects;
public record LspStructuralAnchorDTO(
LspRangeDTO range,
LspRangeDTO startAnchor,
LspRangeDTO endAnchor,
List<LspStructuralAnchorDTO> children) {
public LspStructuralAnchorDTO {
range = Objects.requireNonNull(range, "range");
startAnchor = Objects.requireNonNull(startAnchor, "startAnchor");
endAnchor = Objects.requireNonNull(endAnchor, "endAnchor");
children = List.copyOf(Objects.requireNonNull(children, "children"));
}
}

View File

@ -1,23 +0,0 @@
package p.studio.lsp.dtos;
import p.studio.lsp.messages.LspSymbolKind;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public record LspSymbolDTO(
String name,
LspSymbolKind kind,
Path documentPath,
LspRangeDTO range,
List<LspSymbolDTO> children) {
public LspSymbolDTO {
name = Objects.requireNonNull(name, "name");
kind = Objects.requireNonNull(kind, "kind");
documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize();
range = Objects.requireNonNull(range, "range");
children = List.copyOf(Objects.requireNonNull(children, "children"));
}
}

View File

@ -1,13 +0,0 @@
package p.studio.lsp.events;
import java.util.Objects;
public record LspEvent(
LspEventKind kind,
String message) {
public LspEvent {
Objects.requireNonNull(kind, "kind");
Objects.requireNonNull(message, "message");
}
}

View File

@ -1,5 +0,0 @@
package p.studio.lsp.events;
public enum LspEventKind {
SESSION_READY
}

View File

@ -1,5 +0,0 @@
package p.studio.lsp.events;
public interface LspEventSink {
void publish(LspEvent event);
}

View File

@ -1,12 +0,0 @@
package p.studio.lsp.messages;
import java.nio.file.Path;
import java.util.Objects;
public record LspAnalyzeDocumentRequest(
Path documentPath) {
public LspAnalyzeDocumentRequest {
documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize();
}
}

View File

@ -1,34 +0,0 @@
package p.studio.lsp.messages;
import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspInlineHintDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import java.util.List;
import java.util.Objects;
public record LspAnalyzeDocumentResult(
LspSessionStateDTO sessionState,
LspSemanticPresentationDTO semanticPresentation,
List<LspDiagnosticDTO> diagnostics,
List<LspHighlightSpanDTO> semanticHighlights,
List<LspInlineHintDTO> inlineHints,
List<LspSymbolDTO> documentSymbols,
List<LspStructuralAnchorDTO> structuralAnchors,
List<LspSymbolDTO> workspaceSymbols) {
public LspAnalyzeDocumentResult {
Objects.requireNonNull(sessionState, "sessionState");
Objects.requireNonNull(semanticPresentation, "semanticPresentation");
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights"));
inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints"));
documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols"));
structuralAnchors = List.copyOf(Objects.requireNonNull(structuralAnchors, "structuralAnchors"));
workspaceSymbols = List.copyOf(Objects.requireNonNull(workspaceSymbols, "workspaceSymbols"));
}
}

View File

@ -1,14 +0,0 @@
package p.studio.lsp.messages;
import java.nio.file.Path;
import java.util.Objects;
public record LspDefinitionRequest(
Path documentPath,
int offset) {
public LspDefinitionRequest {
documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize();
offset = Math.max(0, offset);
}
}

View File

@ -1,19 +0,0 @@
package p.studio.lsp.messages;
import p.studio.lsp.dtos.LspDefinitionTargetDTO;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public record LspDefinitionResult(
Path documentPath,
int offset,
List<LspDefinitionTargetDTO> targets) {
public LspDefinitionResult {
documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize();
offset = Math.max(0, offset);
targets = List.copyOf(Objects.requireNonNull(targets, "targets"));
}
}

View File

@ -1,6 +0,0 @@
package p.studio.lsp.messages;
public enum LspDiagnosticSeverity {
ERROR,
WARNING
}

View File

@ -1,16 +0,0 @@
package p.studio.lsp.messages;
import java.nio.file.Path;
import java.util.Objects;
public record LspProjectContext(
String projectName,
String languageId,
Path rootPath) {
public LspProjectContext {
Objects.requireNonNull(projectName, "projectName");
Objects.requireNonNull(languageId, "languageId");
rootPath = Objects.requireNonNull(rootPath, "rootPath").toAbsolutePath().normalize();
}
}

View File

@ -1,24 +0,0 @@
package p.studio.lsp.messages;
public enum LspSymbolKind {
FUNCTION,
METHOD,
CONSTRUCTOR,
IF,
SWITCH,
FOR,
WHILE,
HANDLE,
GLOBAL,
CONST,
STRUCT,
CONTRACT,
HOST,
BUILTIN_TYPE,
SERVICE,
ERROR,
ENUM,
CALLBACK,
IMPLEMENTS,
UNKNOWN
}

View File

@ -1,22 +0,0 @@
package p.studio.lsp.dtos;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
final class LspInlineHintDTOTest {
@Test
void trimsLabelAndCategory() {
final var hint = new LspInlineHintDTO(new LspRangeDTO(3, 3), " int ", " type ");
assertEquals("int", hint.label());
assertEquals("type", hint.category());
}
@Test
void rejectsBlankLabels() {
assertThrows(IllegalArgumentException.class, () ->
new LspInlineHintDTO(new LspRangeDTO(1, 1), " ", "type"));
}
}

View File

@ -1,16 +0,0 @@
plugins {
id("gradle.java-library-conventions")
}
dependencies {
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-frontend-api"))
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
implementation(project(":prometeu-compiler:prometeu-deps"))
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
testImplementation(project(":prometeu-vfs:prometeu-vfs-v1"))
}

View File

@ -1,27 +0,0 @@
package p.studio.lsp;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
import p.studio.lsp.messages.SemanticSession;
import java.util.List;
import static p.studio.lsp.LspSemanticUtilities.normalize;
class LspSemanticAnalyseService {
LspAnalyzeDocumentResult analyze(
final SemanticSession session,
final LspAnalyzeDocumentRequest request) {
final var normalizedRequestedDocument = normalize(request.documentPath());
return new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("diagnostics", "symbols", "definition", "highlight", "structural-anchors")),
session.semanticPresentation(),
session.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.inlineHintsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.structuralAnchorsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.workspaceSymbols());
}
}

View File

@ -1,46 +0,0 @@
package p.studio.lsp;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.compiler.pbs.lexer.PbsTokenKind;
import p.studio.lsp.dtos.LspDefinitionTargetDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspDefinitionRequest;
import p.studio.lsp.messages.LspDefinitionResult;
import p.studio.lsp.messages.SemanticSession;
import java.nio.file.Path;
import java.util.List;
import static p.studio.lsp.LspSemanticUtilities.normalize;
import static p.studio.lsp.LspSemanticUtilities.tokenAt;
class LspSemanticDefinitionService {
LspDefinitionResult definition(
final SemanticSession session,
final LspDefinitionRequest request) {
final List<LspDefinitionTargetDTO> targets = resolveDefinitionTargets(session, request);
return new LspDefinitionResult(request.documentPath(), request.offset(), targets);
}
private List<LspDefinitionTargetDTO> resolveDefinitionTargets(
final SemanticSession session,
final LspDefinitionRequest request) {
final Path documentPath = normalize(request.documentPath());
final List<PbsToken> tokens = session.tokensByDocument().getOrDefault(documentPath, List.of());
final PbsToken activeToken = tokenAt(tokens, request.offset());
if (activeToken == null || activeToken.kind() != PbsTokenKind.IDENTIFIER) {
return List.of();
}
final String lexeme = activeToken.lexeme();
final List<LspSymbolDTO> sameDocumentMatches = session.symbolsByName().getOrDefault(lexeme, List.of()).stream()
.filter(symbol -> symbol.documentPath().equals(documentPath))
.toList();
final List<LspSymbolDTO> effectiveMatches = sameDocumentMatches.isEmpty()
? session.symbolsByName().getOrDefault(lexeme, List.of())
: sameDocumentMatches;
return effectiveMatches.stream()
.map(symbol -> new LspDefinitionTargetDTO(symbol.name(), symbol.documentPath(), symbol.range()))
.toList();
}
}

View File

@ -1,353 +0,0 @@
package p.studio.lsp;
import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.messages.*;
import p.studio.compiler.models.AnalysisSnapshot;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.FrontendSpec;
import p.studio.compiler.models.FrontendSemanticPresentationSpec;
import p.studio.compiler.models.SourceHandle;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.source.diagnostics.Diagnostic;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.services.PBSFrontendPhaseService;
import p.studio.compiler.services.PbsInlineHintSurface;
import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
import p.studio.compiler.workspaces.PipelineStage;
import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage;
import p.studio.compiler.workspaces.stages.ResolveDepsPipelineStage;
import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspInlineHintDTO;
import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.messages.*;
import p.studio.lsp.models.AnalysisRuntimeSnapshot;
import p.studio.lsp.models.SemanticIndex;
import p.studio.utilities.logs.LogAggregator;
import p.studio.vfs.VfsProjectDocument;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static p.studio.lsp.LspSemanticUtilities.*;
final class LspSemanticReadPhase {
private final LspSemanticAnalyseService analyseService;
private final LspSemanticDefinitionService definitionService;
LspSemanticReadPhase() {
this.analyseService = new LspSemanticAnalyseService();
this.definitionService = new LspSemanticDefinitionService();
}
public LspAnalyzeDocumentResult analyze(
final LspProjectContext projectContext,
final VfsProjectDocument vfsProjectDocument,
final LspAnalyzeDocumentRequest request) {
final var session = buildSession(projectContext, vfsProjectDocument, request.documentPath());
return analyseService.analyze(session, request);
}
public LspDefinitionResult definition(
final LspProjectContext projectContext,
final VfsProjectDocument vfsProjectDocument,
final LspDefinitionRequest request) {
final var session = buildSession(projectContext, vfsProjectDocument, request.documentPath());
return definitionService.definition(session, request);
}
public static SemanticSession buildSession(
final LspProjectContext projectContext,
final VfsProjectDocument vfsProjectDocument,
final Path requestedDocumentPath) {
final BuilderPipelineConfig config = new BuilderPipelineConfig(
false,
projectContext.rootPath().toString(),
"core-v1",
new OverlaySourceProviderFactoryImpl(vfsProjectDocument, requestedDocumentPath));
final BuilderPipelineContext context = BuilderPipelineContext.fromConfig(config);
final AnalysisRuntimeSnapshot snapshot = runAnalysisStages(context);
return index(snapshot, context, requestedDocumentPath);
}
private static AnalysisRuntimeSnapshot runAnalysisStages(final BuilderPipelineContext context) {
final LogAggregator logs = LogAggregator.empty();
final List<BuildingIssue> diagnostics = new ArrayList<>();
final List<PipelineStage> stages = List.of(
new ResolveDepsPipelineStage(),
new LoadSourcesPipelineStage());
for (final PipelineStage stage : stages) {
final BuildingIssueSink stageIssues = stage.run(context, logs);
diagnostics.addAll(stageIssues.asCollection());
if (stageIssues.hasErrors()) {
break;
}
}
final DiagnosticSink frontendDiagnostics = DiagnosticSink.empty();
FrontendSpec frontendSpec = FrontendRegistryService.getDefaultFrontendSpec();
if (context.resolvedWorkspace != null && context.fileTable != null) {
final BuildingIssueSink frontendIssues = runFrontendPhase(context, logs, frontendDiagnostics);
diagnostics.addAll(frontendIssues.asCollection());
frontendSpec = context.resolvedWorkspace.frontendSpec();
}
return new AnalysisRuntimeSnapshot(
new AnalysisSnapshot(
frontendSpec,
diagnostics,
context.resolvedWorkspace,
context.fileTable,
context.irBackend),
List.copyOf(frontendDiagnostics.asCollection()));
}
private static BuildingIssueSink runFrontendPhase(
final BuilderPipelineContext context,
final LogAggregator logs,
final DiagnosticSink frontendDiagnostics) {
final var frontendSpec = context.resolvedWorkspace.frontendSpec();
final var service = FrontendRegistryService.getFrontendPhaseService(frontendSpec.getLanguageId());
if (service.isEmpty()) {
return BuildingIssueSink.empty().report(builder -> builder
.error(true)
.message("[BUILD]: unable to find a service for frontend phase: " + frontendSpec.getLanguageId()));
}
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 BuildingIssueSink issues = BuildingIssueSink.empty();
context.irBackend = service.get().compile(frontendPhaseContext, frontendDiagnostics, logs, issues);
return issues;
}
private static SemanticSession index(
final AnalysisRuntimeSnapshot runtimeSnapshot,
final BuilderPipelineContext context,
final Path requestedDocumentPath) {
final AnalysisSnapshot snapshot = runtimeSnapshot.analysisSnapshot();
final Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument = diagnosticsByDocument(
snapshot.diagnostics(),
snapshot,
runtimeSnapshot.frontendDiagnostics());
final SemanticIndex semanticIndex = new SemanticIndex();
if (snapshot.fileTable() == null) {
return new SemanticSession(
normalize(requestedDocumentPath),
semanticPresentation(snapshot.frontendSpec()),
diagnosticsByDocument,
Map.of(),
Map.of(),
Map.of(),
Map.of(),
List.of(),
Map.of(),
Map.of());
}
final Map<FileId, List<PbsAst.TopDecl>> importedSupplementalTopDeclsByFile = importedSupplementalTopDeclsByFile(
snapshot.frontendSpec(),
context);
final Map<Path, List<LspInlineHintDTO>> inlineHintsByDocument = inlineHintsByDocument(
snapshot,
snapshot.frontendSpec(),
context);
final List<IndexedDocument> indexedDocuments = new ArrayList<>();
for (final FileId fileId : snapshot.fileTable()) {
final SourceHandle sourceHandle = snapshot.fileTable().get(fileId);
if (!isSourceRelated(snapshot.frontendSpec(), sourceHandle)) {
continue;
}
final String source = sourceHandle.readUtf8().orElse("");
final DiagnosticSink diagnostics = DiagnosticSink.empty();
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.ORDINARY);
final Path documentPath = sourceHandle.getCanonPath();
semanticIndex.registerDocument(documentPath, ast, tokens.asList());
indexedDocuments.add(new IndexedDocument(
fileId,
documentPath,
importedSupplementalTopDeclsByFile.getOrDefault(fileId, List.of())));
}
for (final IndexedDocument indexedDocument : indexedDocuments) {
semanticIndex.buildHighlights(
indexedDocument.documentPath(),
indexedDocument.visibleImportedTopDecls());
}
return new SemanticSession(
normalize(requestedDocumentPath),
semanticPresentation(snapshot.frontendSpec()),
diagnosticsByDocument,
semanticIndex.semanticHighlightsByDocument(),
inlineHintsByDocument,
semanticIndex.documentSymbolsByDocument(),
semanticIndex.structuralAnchorsByDocument(),
semanticIndex.workspaceSymbols(),
semanticIndex.symbolsByName(),
semanticIndex.tokensByDocument());
}
private static Map<FileId, List<PbsAst.TopDecl>> importedSupplementalTopDeclsByFile(
final FrontendSpec frontendSpec,
final BuilderPipelineContext context) {
final var surface = semanticReadSurface(frontendSpec, context);
final Map<FileId, List<PbsAst.TopDecl>> byFile = new LinkedHashMap<>();
for (final var entry : surface.supplementalTopDeclsByFile().entrySet()) {
byFile.put(entry.getKey(), entry.getValue().asList());
}
return Map.copyOf(byFile);
}
private static Map<Path, List<LspInlineHintDTO>> inlineHintsByDocument(
final AnalysisSnapshot snapshot,
final FrontendSpec frontendSpec,
final BuilderPipelineContext context) {
if (snapshot.fileTable() == null) {
return Map.of();
}
final var surface = semanticReadSurface(frontendSpec, context);
final Map<Path, List<LspInlineHintDTO>> byDocument = new LinkedHashMap<>();
for (final var entry : surface.inlineHintsByFile().entrySet()) {
final SourceHandle sourceHandle = snapshot.fileTable().get(entry.getKey());
final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize();
final var hints = entry.getValue().asList().stream()
.map(LspSemanticReadPhase::toInlineHintDTO)
.toList();
if (!hints.isEmpty()) {
byDocument.put(documentPath, hints);
}
}
return Map.copyOf(byDocument);
}
private static PBSFrontendPhaseService.PbsSemanticReadSurface semanticReadSurface(
final FrontendSpec frontendSpec,
final BuilderPipelineContext context) {
if (context.resolvedWorkspace == null || context.fileTable == null || !"pbs".equals(frontendSpec.getLanguageId())) {
return new PBSFrontendPhaseService.PbsSemanticReadSurface(Map.of(), 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();
return PBSFrontendPhaseService.semanticReadSurface(
frontendPhaseContext,
diagnostics,
issues);
}
private static LspInlineHintDTO toInlineHintDTO(final PbsInlineHintSurface hint) {
return new LspInlineHintDTO(
new LspRangeDTO(
(int) hint.anchorSpan().getStart(),
(int) hint.anchorSpan().getEnd()),
hint.label(),
hint.category());
}
private record IndexedDocument(
FileId fileId,
Path documentPath,
List<PbsAst.TopDecl> visibleImportedTopDecls) {
}
private static boolean isSourceRelated(final FrontendSpec frontendSpec, final SourceHandle sourceHandle) {
return frontendSpec.getAllowedExtensions().contains(sourceHandle.getExtension());
}
private static LspSemanticPresentationDTO semanticPresentation(final FrontendSpec frontendSpec) {
final FrontendSemanticPresentationSpec presentation = frontendSpec.getSemanticPresentation();
return new LspSemanticPresentationDTO(presentation.semanticKeys(), presentation.resources());
}
private static Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument(
final List<BuildingIssue> issues,
final AnalysisSnapshot snapshot,
final List<Diagnostic> frontendDiagnostics) {
final Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument = new LinkedHashMap<>();
diagnosticIssues(issues, snapshot, diagnosticsByDocument);
diagnosticFrontend(frontendDiagnostics, snapshot, diagnosticsByDocument);
return freezeMapOfLists(diagnosticsByDocument);
}
private static boolean isValidIssueDiagnostic(
final AnalysisSnapshot snapshot,
final BuildingIssue issue) {
if (snapshot.fileTable() == null || issue.getFileId() == null || issue.getFileId() < 0) {
return false;
}
return issue.getFileId() < snapshot.fileTable().size();
}
private static void diagnosticIssues(
final List<BuildingIssue> issues,
final AnalysisSnapshot snapshot,
final Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument) {
for (final var issue : issues) {
if (!isValidIssueDiagnostic(snapshot, issue)) {
continue;
}
final SourceHandle sourceHandle = snapshot.fileTable().get(new FileId(issue.getFileId()));
final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize();
diagnosticsByDocument
.computeIfAbsent(documentPath, ignored -> new ArrayList<>())
.add(new LspDiagnosticDTO(
documentPath,
new LspRangeDTO(
safeOffset(issue.getStart()),
safeEnd(issue.getStart(), issue.getEnd())),
issue.isError()
? LspDiagnosticSeverity.ERROR
: LspDiagnosticSeverity.WARNING,
issue.getPhase(),
issue.getCode(),
issue.getMessage()));
}
}
private static boolean isValidFEDiagnostic(
final AnalysisSnapshot snapshot,
final Diagnostic diagnostic) {
return snapshot.fileTable() != null && !diagnostic.getSpan().getFileId().isNone();
}
private static void diagnosticFrontend(
final List<Diagnostic> feDiagnostics,
final AnalysisSnapshot snapshot,
final Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument) {
for (final var feDiagnostic : feDiagnostics) {
if (!isValidFEDiagnostic(snapshot, feDiagnostic)) {
continue;
}
final SourceHandle sourceHandle = snapshot.fileTable().get(feDiagnostic.getSpan().getFileId());
final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize();
diagnosticsByDocument
.computeIfAbsent(documentPath, ignored -> new ArrayList<>())
.add(new LspDiagnosticDTO(
documentPath,
new LspRangeDTO(
(int) feDiagnostic.getSpan().getStart(),
(int) feDiagnostic.getSpan().getEnd()),
feDiagnostic.getSeverity().isError()
? LspDiagnosticSeverity.ERROR
: LspDiagnosticSeverity.WARNING,
feDiagnostic.getPhase().name(),
feDiagnostic.getCode(),
feDiagnostic.getMessage()));
}
}
}

View File

@ -1,52 +0,0 @@
package p.studio.lsp;
import lombok.experimental.UtilityClass;
import p.studio.compiler.pbs.lexer.PbsToken;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@UtilityClass
public class LspSemanticUtilities {
public static PbsToken tokenAt(final List<PbsToken> tokens, final int offset) {
for (final PbsToken token : tokens) {
if (token.start() <= offset && offset < token.end()) {
return token;
}
}
return null;
}
public static Path normalize(final Path path) {
final Path normalized = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
try {
return Files.exists(normalized) ? normalized.toRealPath() : normalized;
} catch (IOException ignored) {
return normalized;
}
}
public static <T> Map<Path, List<T>> freezeMapOfLists(final Map<Path, List<T>> mutable) {
final Map<Path, List<T>> frozen = new LinkedHashMap<>();
for (final var entry : mutable.entrySet()) {
frozen.put(entry.getKey(), List.copyOf(entry.getValue()));
}
return Map.copyOf(frozen);
}
public static int safeOffset(final Integer value) {
return value == null ? 0 : Math.max(0, value);
}
public static int safeEnd(final Integer start, final Integer end) {
final int safeStart = safeOffset(start);
final int safeEnd = safeOffset(end);
return Math.max(safeStart, safeEnd);
}
}

View File

@ -1,17 +0,0 @@
package p.studio.lsp;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.vfs.VfsProjectDocument;
import java.util.Objects;
public final class LspServiceFactoryImpl implements LspServiceFactory {
@Override
public LspService open(
final LspProjectContext projectContext,
final VfsProjectDocument vfsProjectDocument) {
return new LspServiceImpl(
Objects.requireNonNull(projectContext, "projectContext"),
Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"));
}
}

View File

@ -1,50 +0,0 @@
package p.studio.lsp;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.messages.*;
import p.studio.vfs.VfsProjectDocument;
import java.util.List;
import java.util.Objects;
public final class LspServiceImpl implements LspService {
private final LspProjectContext projectContext;
private final VfsProjectDocument vfsProjectDocument;
private final LspSemanticReadPhase semanticReadPhase;
public LspServiceImpl(
final LspProjectContext projectContext,
final VfsProjectDocument vfsProjectDocument) {
this.projectContext = Objects.requireNonNull(projectContext, "projectContext");
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
this.semanticReadPhase = new LspSemanticReadPhase();
}
@Override
public LspProjectContext projectContext() {
return projectContext;
}
@Override
public VfsProjectDocument projectDocumentVfs() {
return vfsProjectDocument;
}
@Override
public LspSessionStateDTO snapshot() {
return new LspSessionStateDTO(
true,
List.of("diagnostics", "symbols", "definition", "highlight"));
}
@Override
public LspAnalyzeDocumentResult analyzeDocument(final LspAnalyzeDocumentRequest request) {
return semanticReadPhase.analyze(projectContext, vfsProjectDocument, request);
}
@Override
public LspDefinitionResult definition(final LspDefinitionRequest request) {
return semanticReadPhase.definition(projectContext, vfsProjectDocument, request);
}
}

View File

@ -1,51 +0,0 @@
package p.studio.lsp;
import p.studio.compiler.utilities.SourceProvider;
import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.vfs.VfsDocumentOpenResult;
import p.studio.vfs.VfsProjectDocument;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
final class OverlaySourceProviderFactoryImpl implements SourceProviderFactory {
private final VfsProjectDocument vfsProjectDocument;
private final Path openedDocumentPath;
OverlaySourceProviderFactoryImpl(
final VfsProjectDocument vfsProjectDocument,
final Path openedDocumentPath) {
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
this.openedDocumentPath = openedDocumentPath == null
? null
: normalize(openedDocumentPath);
}
@Override
public SourceProvider create(final Path path) {
final Path normalizedPath = normalize(path);
return () -> read(normalizedPath);
}
private byte[] read(final Path path) throws IOException {
if (openedDocumentPath != null && openedDocumentPath.equals(path)) {
final VfsDocumentOpenResult result = vfsProjectDocument.openDocument(path);
if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) {
return textDocument.content().getBytes(StandardCharsets.UTF_8);
}
}
return Files.readAllBytes(path);
}
private Path normalize(final Path path) {
final Path normalized = path.toAbsolutePath().normalize();
try {
return Files.exists(normalized) ? normalized.toRealPath() : normalized;
} catch (IOException ignored) {
return normalized;
}
}
}

View File

@ -1,26 +0,0 @@
package p.studio.lsp.messages;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspInlineHintDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public record SemanticSession(
Path requestedDocumentPath,
LspSemanticPresentationDTO semanticPresentation,
Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument,
Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument,
Map<Path, List<LspInlineHintDTO>> inlineHintsByDocument,
Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument,
Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument,
List<LspSymbolDTO> workspaceSymbols,
Map<String, List<LspSymbolDTO>> symbolsByName,
Map<Path, List<PbsToken>> tokensByDocument) {
}

View File

@ -1,11 +0,0 @@
package p.studio.lsp.models;
import p.studio.compiler.models.AnalysisSnapshot;
import p.studio.compiler.source.diagnostics.Diagnostic;
import java.util.List;
public record AnalysisRuntimeSnapshot(
AnalysisSnapshot analysisSnapshot,
List<Diagnostic> frontendDiagnostics) {
}

View File

@ -1,635 +0,0 @@
package p.studio.lsp.models;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.compiler.pbs.lexer.PbsTokenKind;
import p.studio.compiler.source.Span;
import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspSymbolKind;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static p.studio.lsp.LspSemanticUtilities.normalize;
public final class SemanticIndex {
private final Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument = new LinkedHashMap<>();
private final Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument = new LinkedHashMap<>();
private final Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument = new LinkedHashMap<>();
private final List<LspSymbolDTO> workspaceSymbols = new ArrayList<>();
private final Map<String, List<LspSymbolDTO>> symbolsByName = new LinkedHashMap<>();
private final Map<Path, List<PbsToken>> tokensByDocument = new LinkedHashMap<>();
public void index(
final Path documentPath,
final PbsAst.File ast,
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);
tokensByDocument.put(normalizedDocumentPath, List.copyOf(tokens));
final List<LspSymbolDTO> documentSymbols = new ArrayList<>();
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
final LspSymbolDTO symbol = symbolForTopDecl(normalizedDocumentPath, topDecl);
if (symbol == null) {
continue;
}
documentSymbols.add(symbol);
workspaceSymbols.add(symbol);
symbolsByName.computeIfAbsent(symbol.name(), ignored -> new ArrayList<>()).add(symbol);
for (final LspSymbolDTO child : symbol.children()) {
symbolsByName.computeIfAbsent(child.name(), ignored -> new ArrayList<>()).add(child);
}
}
documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols));
structuralAnchorsByDocument.put(
normalizedDocumentPath,
structuralAnchors(documentSymbols, tokens));
}
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(
normalizedDocumentPath,
buildSemanticHighlights(tokens, visibleKindsByName));
}
private List<LspHighlightSpanDTO> buildSemanticHighlights(
final List<PbsToken> tokens,
final Map<String, LspSymbolKind> visibleKindsByName) {
final List<LspHighlightSpanDTO> highlights = new ArrayList<>();
for (int index = 0; index < tokens.size(); index++) {
final PbsToken token = tokens.get(index);
if (token.kind() == PbsTokenKind.EOF) {
continue;
}
final String semanticKey = semanticKey(tokens, index, visibleKindsByName);
if (semanticKey == null || semanticKey.isBlank()) {
continue;
}
highlights.add(new LspHighlightSpanDTO(
new LspRangeDTO(token.start(), token.end()),
semanticKey));
}
return List.copyOf(highlights);
}
private String semanticKey(
final List<PbsToken> tokens,
final int tokenIndex,
final Map<String, LspSymbolKind> visibleKindsByName) {
final PbsToken token = tokens.get(tokenIndex);
if (isLifecycleAttributeToken(tokens, tokenIndex)) {
return PbsSemanticKind.LIFECYCLE.semanticKey();
}
final PbsSemanticKind semanticKind = token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER
? semanticKindForIdentifier(token.lexeme(), visibleKindsByName)
: PbsSemanticKind.forToken(token);
return semanticKind == null ? null : semanticKind.semanticKey();
}
private boolean isLifecycleAttributeToken(final List<PbsToken> tokens, final int tokenIndex) {
if (tokenIndex < 0 || tokenIndex >= tokens.size()) {
return false;
}
final PbsToken token = tokens.get(tokenIndex);
return switch (token.kind()) {
case LEFT_BRACKET -> hasLifecycleAttributeWindow(tokens, tokenIndex, tokenIndex + 1, tokenIndex + 2);
case IDENTIFIER -> hasLifecycleAttributeWindow(tokens, tokenIndex - 1, tokenIndex, tokenIndex + 1);
case RIGHT_BRACKET -> hasLifecycleAttributeWindow(tokens, tokenIndex - 2, tokenIndex - 1, tokenIndex);
default -> false;
};
}
private boolean hasLifecycleAttributeWindow(
final List<PbsToken> tokens,
final int leftBracketIndex,
final int identifierIndex,
final int rightBracketIndex) {
if (leftBracketIndex < 0 || rightBracketIndex >= tokens.size()) {
return false;
}
return tokens.get(leftBracketIndex).kind() == PbsTokenKind.LEFT_BRACKET
&& tokens.get(identifierIndex).kind() == PbsTokenKind.IDENTIFIER
&& isLifecycleAttributeName(tokens.get(identifierIndex).lexeme())
&& tokens.get(rightBracketIndex).kind() == PbsTokenKind.RIGHT_BRACKET;
}
private boolean isLifecycleAttributeName(final String lexeme) {
return "Init".equals(lexeme) || "Frame".equals(lexeme);
}
private PbsSemanticKind semanticKindForIdentifier(
final String lexeme,
final Map<String, LspSymbolKind> visibleKindsByName) {
final LspSymbolKind kind = visibleKindsByName.get(lexeme);
if (kind == null) {
return PbsSemanticKind.IDENTIFIER;
}
return switch (kind) {
case FUNCTION -> PbsSemanticKind.FUNCTION;
case METHOD -> PbsSemanticKind.METHOD;
case CONSTRUCTOR -> PbsSemanticKind.CONSTRUCTOR;
case STRUCT -> PbsSemanticKind.STRUCT;
case CONTRACT -> PbsSemanticKind.CONTRACT;
case HOST -> PbsSemanticKind.HOST;
case BUILTIN_TYPE -> PbsSemanticKind.BUILTIN_TYPE;
case SERVICE -> PbsSemanticKind.SERVICE;
case ERROR -> PbsSemanticKind.ERROR;
case ENUM -> PbsSemanticKind.ENUM;
case CALLBACK -> PbsSemanticKind.CALLBACK;
case GLOBAL -> PbsSemanticKind.GLOBAL;
case CONST -> PbsSemanticKind.CONST;
case IMPLEMENTS -> PbsSemanticKind.IMPLEMENTS;
default -> PbsSemanticKind.IDENTIFIER;
};
}
private String topDeclName(final PbsAst.TopDecl topDecl) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
return functionDecl.name();
}
if (topDecl instanceof PbsAst.StructDecl structDecl) {
return structDecl.name();
}
if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
return contractDecl.name();
}
if (topDecl instanceof PbsAst.HostDecl hostDecl) {
return hostDecl.name();
}
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
return builtinTypeDecl.name();
}
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
return serviceDecl.name();
}
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
return errorDecl.name();
}
if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
return enumDecl.name();
}
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
return callbackDecl.name();
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
return globalDecl.name();
}
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
return constDecl.name();
}
if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) {
return implementsDecl.binderName();
}
return null;
}
private LspSymbolKind symbolKindForTopDecl(final PbsAst.TopDecl topDecl) {
if (topDecl instanceof PbsAst.FunctionDecl) {
return LspSymbolKind.FUNCTION;
}
if (topDecl instanceof PbsAst.StructDecl) {
return LspSymbolKind.STRUCT;
}
if (topDecl instanceof PbsAst.ContractDecl) {
return LspSymbolKind.CONTRACT;
}
if (topDecl instanceof PbsAst.HostDecl) {
return LspSymbolKind.HOST;
}
if (topDecl instanceof PbsAst.BuiltinTypeDecl) {
return LspSymbolKind.BUILTIN_TYPE;
}
if (topDecl instanceof PbsAst.ServiceDecl) {
return LspSymbolKind.SERVICE;
}
if (topDecl instanceof PbsAst.ErrorDecl) {
return LspSymbolKind.ERROR;
}
if (topDecl instanceof PbsAst.EnumDecl) {
return LspSymbolKind.ENUM;
}
if (topDecl instanceof PbsAst.CallbackDecl) {
return LspSymbolKind.CALLBACK;
}
if (topDecl instanceof PbsAst.GlobalDecl) {
return LspSymbolKind.GLOBAL;
}
if (topDecl instanceof PbsAst.ConstDecl) {
return LspSymbolKind.CONST;
}
if (topDecl instanceof PbsAst.ImplementsDecl) {
return LspSymbolKind.IMPLEMENTS;
}
return null;
}
private LspSymbolDTO symbolForTopDecl(
final Path documentPath,
final PbsAst.TopDecl topDecl) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
return symbol(
documentPath,
functionDecl.name(),
LspSymbolKind.FUNCTION,
functionDecl.span(),
localSymbolsInBlock(documentPath, functionDecl.body()));
}
if (topDecl instanceof PbsAst.StructDecl structDecl) {
return symbol(documentPath, structDecl.name(), LspSymbolKind.STRUCT, structDecl.span(), structChildren(documentPath, structDecl));
}
if (topDecl instanceof PbsAst.ContractDecl(
String name, p.studio.utilities.structures.ReadOnlyList<PbsAst.FunctionSignature> signatures, Span span
)) {
return symbol(documentPath, name, LspSymbolKind.CONTRACT, span, signatureChildren(documentPath, signatures.asList()));
}
if (topDecl instanceof PbsAst.HostDecl(
String name, p.studio.utilities.structures.ReadOnlyList<PbsAst.FunctionSignature> signatures, Span span
)) {
return symbol(documentPath, name, LspSymbolKind.HOST, span, signatureChildren(documentPath, signatures.asList()));
}
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
return symbol(documentPath, builtinTypeDecl.name(), LspSymbolKind.BUILTIN_TYPE, builtinTypeDecl.span(), signatureChildren(documentPath, builtinTypeDecl.signatures().asList()));
}
if (topDecl instanceof PbsAst.ServiceDecl(
String name, p.studio.utilities.structures.ReadOnlyList<PbsAst.FunctionDecl> methods, Span span
)) {
return symbol(documentPath, name, LspSymbolKind.SERVICE, span, functionChildren(documentPath, methods.asList()));
}
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
return symbol(documentPath, errorDecl.name(), LspSymbolKind.ERROR, errorDecl.span(), List.of());
}
if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
return symbol(documentPath, enumDecl.name(), LspSymbolKind.ENUM, enumDecl.span(), List.of());
}
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
return symbol(documentPath, callbackDecl.name(), LspSymbolKind.CALLBACK, callbackDecl.span(), List.of());
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
return symbol(documentPath, globalDecl.name(), LspSymbolKind.GLOBAL, globalDecl.span(), List.of());
}
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
return symbol(documentPath, constDecl.name(), LspSymbolKind.CONST, constDecl.span(), List.of());
}
if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) {
return symbol(documentPath, implementsDecl.binderName(), LspSymbolKind.IMPLEMENTS, implementsDecl.span(), functionChildren(documentPath, implementsDecl.methods().asList()));
}
return null;
}
private List<LspSymbolDTO> structChildren(
final Path documentPath,
final PbsAst.StructDecl structDecl) {
final List<LspSymbolDTO> children = new ArrayList<>(functionChildren(documentPath, structDecl.methods().asList()));
for (final PbsAst.CtorDecl ctorDecl : structDecl.ctors()) {
children.add(symbol(
documentPath,
ctorDecl.name(),
LspSymbolKind.CONSTRUCTOR,
ctorDecl.span(),
localSymbolsInBlock(documentPath, ctorDecl.body())));
}
return List.copyOf(children);
}
private List<LspSymbolDTO> functionChildren(
final Path documentPath,
final List<PbsAst.FunctionDecl> functions) {
final List<LspSymbolDTO> children = new ArrayList<>();
for (final PbsAst.FunctionDecl functionDecl : functions) {
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);
}
private List<LspSymbolDTO> signatureChildren(
final Path documentPath,
final List<PbsAst.FunctionSignature> signatures) {
final List<LspSymbolDTO> children = new ArrayList<>();
for (final PbsAst.FunctionSignature signature : signatures) {
children.add(symbol(documentPath, signature.name(), LspSymbolKind.METHOD, signature.span(), List.of()));
}
return List.copyOf(children);
}
private LspSymbolDTO symbol(
final Path documentPath,
final String name,
final LspSymbolKind kind,
final Span span,
final List<LspSymbolDTO> children) {
return new LspSymbolDTO(
Objects.requireNonNullElse(name, kind.name().toLowerCase()),
kind,
documentPath,
new LspRangeDTO((int) span.getStart(), (int) span.getEnd()),
children);
}
private List<LspStructuralAnchorDTO> structuralAnchors(
final List<LspSymbolDTO> documentSymbols,
final List<PbsToken> tokens) {
final List<LspStructuralAnchorDTO> anchors = new ArrayList<>();
for (final LspSymbolDTO symbol : documentSymbols) {
final LspStructuralAnchorDTO anchor = structuralAnchor(symbol, tokens);
if (anchor != null) {
anchors.add(anchor);
}
}
return List.copyOf(anchors);
}
private LspStructuralAnchorDTO structuralAnchor(
final LspSymbolDTO symbol,
final List<PbsToken> tokens) {
final List<LspStructuralAnchorDTO> children = new ArrayList<>();
for (final LspSymbolDTO child : symbol.children()) {
final LspStructuralAnchorDTO anchorChild = structuralAnchor(child, tokens);
if (anchorChild != null) {
children.add(anchorChild);
}
}
final AnchorPair anchorPair = anchorPair(symbol.range(), tokens);
if (anchorPair == null) {
if (children.isEmpty()) {
return null;
}
return new LspStructuralAnchorDTO(symbol.range(), symbol.range(), symbol.range(), children);
}
return new LspStructuralAnchorDTO(symbol.range(), anchorPair.start(), anchorPair.end(), children);
}
private AnchorPair anchorPair(
final LspRangeDTO range,
final List<PbsToken> tokens) {
PbsToken firstLeftBrace = null;
PbsToken lastRightBrace = null;
for (final PbsToken token : tokens) {
if (token.kind() == PbsTokenKind.EOF) {
continue;
}
if (token.start() < range.startOffset() || token.end() > range.endOffset()) {
continue;
}
if (token.kind() == PbsTokenKind.LEFT_BRACE && firstLeftBrace == null) {
firstLeftBrace = token;
}
if (token.kind() == PbsTokenKind.RIGHT_BRACE) {
lastRightBrace = token;
}
}
if (firstLeftBrace == null || lastRightBrace == null || lastRightBrace.start() < firstLeftBrace.start()) {
return null;
}
return new AnchorPair(
new LspRangeDTO(firstLeftBrace.start(), firstLeftBrace.end()),
new LspRangeDTO(lastRightBrace.start(), lastRightBrace.end()));
}
public Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument() {
return Map.copyOf(semanticHighlightsByDocument);
}
public Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument() {
return Map.copyOf(documentSymbolsByDocument);
}
public Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument() {
return Map.copyOf(structuralAnchorsByDocument);
}
public List<LspSymbolDTO> workspaceSymbols() {
return List.copyOf(workspaceSymbols);
}
public Map<String, List<LspSymbolDTO>> symbolsByName() {
final Map<String, List<LspSymbolDTO>> frozen = new LinkedHashMap<>();
for (final var entry : symbolsByName.entrySet()) {
frozen.put(entry.getKey(), List.copyOf(entry.getValue()));
}
return Map.copyOf(frozen);
}
public Map<Path, List<PbsToken>> tokensByDocument() {
return Map.copyOf(tokensByDocument);
}
private record AnchorPair(
LspRangeDTO start,
LspRangeDTO end) {
}
}

View File

@ -1,427 +0,0 @@
package p.studio.lsp;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.dtos.LspDefinitionTargetDTO;
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
import p.studio.lsp.messages.LspDefinitionRequest;
import p.studio.vfs.*;
import p.studio.vfs.messages.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class LspServiceImplTest {
@TempDir
Path tempDir;
private static final String OVERLAY_SOURCE = """
fn helper_call() -> void
{
if true {
helper();
}
while false {
helper();
}
let value = switch true {
true: { 1 },
default: { 2 }
};
let recovered = handle helper_result() {
_: {
ok(0)
}
};
}
""";
private static final String SERVICE_SOURCE = """
declare service Game {
fn tick(x: int) -> int { return x; }
}
fn main() -> void {
Game.tick(1);
}
""";
private static final String SDK_IMPORT_SOURCE = """
import { Composer } from @sdk:composer;
fn main() -> void {
Composer.emit_sprite(1, 0, 0, 0, 0, 0, false, false, 0);
}
""";
private static final String PROJECT_IMPORT_SOURCE = """
import { Color } from @app:graphics;
fn main(color: Color) -> Color {
return color;
}
""";
@Test
void analyzeDocumentUsesVfsOverlayForRequestedDocumentAndFilesystemFallbackForClosedFiles() throws Exception {
final Path projectRoot = createProject();
final Path mainFile = projectRoot.resolve("src/main.pbs");
final Path helperFile = projectRoot.resolve("src/helper.pbs");
Files.writeString(mainFile, "fn broken( -> void {}\n");
Files.writeString(helperFile, "fn helper() -> void {}\n");
final VfsProjectDocument delegate = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot));
final LspService service = new LspServiceImpl(
new LspProjectContext("Example", "pbs", projectRoot),
new OverlayVfsProjectDocument(delegate, mainFile, OVERLAY_SOURCE));
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper_call"));
assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper"));
assertEquals(List.of("/themes/pbs/semantic-highlighting.css"), analysis.semanticPresentation().resources());
assertTrue(analysis.semanticPresentation().semanticKeys().contains("pbs-function"));
assertFalse(analysis.inlineHints().isEmpty(), analysis.toString());
assertTrue(
analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")),
analysis.toString());
assertTrue(
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.IF),
analysis.toString());
assertTrue(
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.WHILE),
analysis.toString());
assertTrue(
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SWITCH),
analysis.toString());
assertTrue(
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.HANDLE),
analysis.toString());
assertFalse(analysis.structuralAnchors().isEmpty(), analysis.toString());
assertEquals(OVERLAY_SOURCE.indexOf("{"), analysis.structuralAnchors().getFirst().startAnchor().startOffset());
assertTrue(
flattenAnchors(analysis.structuralAnchors()).stream().anyMatch(anchor ->
anchor.startAnchor().startOffset() == OVERLAY_SOURCE.indexOf("if true {") + "if true ".length()),
analysis.toString());
assertTrue(
analysis.workspaceSymbols().stream().anyMatch(symbol ->
symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))),
analysis.toString());
final int offset = OVERLAY_SOURCE.indexOf("helper();");
final var definition = service.definition(new LspDefinitionRequest(mainFile, offset));
final List<LspDefinitionTargetDTO> targets = definition.targets();
assertEquals(2, targets.size());
assertEquals(normalize(helperFile), targets.getFirst().documentPath());
assertEquals("helper", targets.getFirst().name());
}
@Test
void analyzeDocumentSurfacesDiagnosticsWithoutAbortingSemanticRead() throws Exception {
final Path projectRoot = createProject();
final Path mainFile = projectRoot.resolve("src/main.pbs");
Files.writeString(mainFile, """
fn main( -> void
{
helper();
}
""");
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));
assertFalse(analysis.diagnostics().isEmpty(), analysis.toString());
assertTrue(analysis.diagnostics().stream().allMatch(diagnostic ->
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, "Composer"));
}
@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"));
}
@Test
void analyzeDocumentReturnsDedicatedInlineHintTransportSurface() throws Exception {
final Path projectRoot = createProject();
final Path mainFile = projectRoot.resolve("src/main.pbs");
final String source = "fn main() -> void { let value = 1; }\n";
Files.writeString(mainFile, 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(1, analysis.inlineHints().size(), analysis.toString());
assertEquals("int", analysis.inlineHints().getFirst().label());
assertEquals("type", analysis.inlineHints().getFirst().category());
assertEquals("value", sourceSlice(source, analysis.inlineHints().getFirst().anchor()));
}
@Test
void analyzeDocumentPreservesValidInlineHintsUnderPartialDegradation() throws Exception {
final Path projectRoot = createProject();
final Path mainFile = projectRoot.resolve("src/main.pbs");
final String source = """
fn ok() -> void {
let value = 1;
return;
}
fn broken( -> void {
let missing = 2;
}
""";
Files.writeString(mainFile, 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));
assertFalse(analysis.diagnostics().isEmpty(), analysis.toString());
assertEquals(1, analysis.inlineHints().size(), analysis.toString());
assertEquals("int", analysis.inlineHints().getFirst().label());
assertEquals("value", sourceSlice(source, analysis.inlineHints().getFirst().anchor()));
}
private Path createProject() throws Exception {
final Path src = Files.createDirectories(tempDir.resolve("src"));
Files.writeString(tempDir.resolve("prometeu.json"), """
{
"name": "Example",
"version": "1.0.0",
"language": "pbs",
"stdlib": "1",
"dependencies": []
}
""");
Files.writeString(src.resolve("mod.barrel"), """
pub fn helper() -> void;
pub fn helper_result() -> result<MyError> int;
pub error MyError;
pub service Log;
""");
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);
}
""");
Files.writeString(src.resolve("log.pbs"), """
declare service Log {
fn failure(message: string) -> void;
}
""");
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 static List<LspStructuralAnchorDTO> flattenAnchors(
final List<LspStructuralAnchorDTO> anchors) {
final List<LspStructuralAnchorDTO> flattened = new java.util.ArrayList<>();
for (final var anchor : anchors) {
flattened.add(anchor);
flattened.addAll(flattenAnchors(anchor.children()));
}
return flattened;
}
private VfsProjectContext projectContext(final Path projectRoot) {
return new VfsProjectContext("Example", "pbs", projectRoot);
}
private static Path normalize(final Path path) {
try {
return path.toAbsolutePath().normalize().toRealPath();
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}
private static String semanticKeyForLexeme(
final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis,
final String source,
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()
.filter(highlight -> lexeme.equals(spanContent(source, highlight.range().startOffset(), highlight.range().endOffset())))
.map(p.studio.lsp.dtos.LspHighlightSpanDTO::semanticKey)
.toList();
}
private static String spanContent(
final String source,
final int start,
final int end) {
if (start < 0 || end > source.length() || start >= end) {
return "";
}
return source.substring(start, end);
}
private static String sourceSlice(
final String source,
final p.studio.lsp.dtos.LspRangeDTO range) {
return spanContent(source, range.startOffset(), range.endOffset());
}
private static final class OverlayVfsProjectDocument implements VfsProjectDocument {
private final VfsProjectDocument delegate;
private final Path overlayPath;
private final String overlayContent;
private OverlayVfsProjectDocument(
final VfsProjectDocument delegate,
final Path overlayPath,
final String overlayContent) {
this.delegate = delegate;
this.overlayPath = normalize(overlayPath);
this.overlayContent = overlayContent;
}
@Override
public VfsProjectContext projectContext() {
return delegate.projectContext();
}
@Override
public VfsProjectSnapshot snapshot() {
return delegate.snapshot();
}
@Override
public VfsProjectSnapshot refresh() {
return delegate.refresh();
}
@Override
public VfsProjectSnapshot refresh(final VfsRefreshRequest request) {
return delegate.refresh(request);
}
@Override
public VfsDocumentOpenResult openDocument(final Path path) {
final Path normalizedPath = normalize(path);
if (!overlayPath.equals(normalizedPath)) {
return delegate.openDocument(normalizedPath);
}
final VfsDocumentAccessContext accessContext = new VfsDocumentAccessContext(
normalizedPath,
"pbs",
true,
VfsDocumentAccessMode.READ_ONLY,
Map.of());
return new VfsDocumentOpenResult.VfsTextDocument(
normalizedPath,
normalizedPath.getFileName().toString(),
"pbs",
overlayContent,
"LF",
false,
accessContext);
}
@Override
public void close() {
delegate.close();
}
private static Path normalize(final Path path) {
final Path normalized = path.toAbsolutePath().normalize();
try {
return Files.exists(normalized) ? normalized.toRealPath() : normalized;
} catch (Exception ignored) {
return normalized;
}
}
}
}

View File

@ -5,8 +5,6 @@ plugins {
dependencies { dependencies {
implementation(project(":prometeu-infra")) implementation(project(":prometeu-infra"))
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
implementation(project(":prometeu-packer:prometeu-packer-api")) implementation(project(":prometeu-packer:prometeu-packer-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core")) implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-build-pipeline")) implementation(project(":prometeu-compiler:prometeu-build-pipeline"))

View File

@ -2,7 +2,6 @@ package p.studio.projects;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import p.packer.messages.PackerProjectContext; import p.packer.messages.PackerProjectContext;
import p.studio.vfs.messages.VfsProjectContext;
import java.nio.file.Path; import java.nio.file.Path;
@ -36,8 +35,4 @@ public record ProjectReference(
public PackerProjectContext toPackerProjectContext() { public PackerProjectContext toPackerProjectContext() {
return new PackerProjectContext(name, absoluteRootPath()); return new PackerProjectContext(name, absoluteRootPath());
} }
public VfsProjectContext toVfsProjectContext() {
return new VfsProjectContext(name, languageId, absoluteRootPath());
}
} }

View File

@ -1,3 +0,0 @@
plugins {
id("gradle.java-library-conventions")
}

View File

@ -1,7 +0,0 @@
package p.studio.vfs;
import p.studio.vfs.messages.VfsProjectContext;
public interface ProjectDocumentVfsFactory {
VfsProjectDocument open(VfsProjectContext projectContext);
}

View File

@ -1,40 +0,0 @@
package p.studio.vfs;
import p.studio.vfs.messages.VfsDocumentAccessContext;
import p.studio.vfs.messages.VfsUnsupportedReason;
import java.nio.file.Path;
import java.util.Objects;
public sealed interface VfsDocumentOpenResult permits VfsDocumentOpenResult.VfsUnsupportedDocument, VfsDocumentOpenResult.VfsTextDocument {
Path path();
record VfsUnsupportedDocument(
Path path,
VfsUnsupportedReason reason) implements VfsDocumentOpenResult {
public VfsUnsupportedDocument {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
Objects.requireNonNull(reason, "reason");
}
}
record VfsTextDocument(
Path path,
String documentName,
String typeId,
String content,
String lineSeparator,
boolean dirty,
VfsDocumentAccessContext accessContext) implements VfsDocumentOpenResult {
public VfsTextDocument {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
Objects.requireNonNull(documentName, "documentName");
Objects.requireNonNull(typeId, "typeId");
Objects.requireNonNull(content, "content");
Objects.requireNonNull(lineSeparator, "lineSeparator");
Objects.requireNonNull(accessContext, "accessContext");
}
}
}

View File

@ -1,47 +0,0 @@
package p.studio.vfs;
import p.studio.vfs.messages.*;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public interface VfsProjectDocument extends AutoCloseable {
VfsProjectContext projectContext();
VfsProjectSnapshot snapshot();
VfsProjectSnapshot refresh();
VfsProjectSnapshot refresh(VfsRefreshRequest request);
VfsDocumentOpenResult openDocument(Path path);
default VfsDocumentAccessContext accessContext(final Path path) {
throw new UnsupportedOperationException("Document access context is not supported by this VFS implementation.");
}
default VfsDocumentAccessContext updateAccessContext(final Path path, final Map<String, String> attributes) {
throw new UnsupportedOperationException("Document access context updates are not supported by this VFS implementation.");
}
default VfsDocumentOpenResult.VfsTextDocument updateDocument(final Path path, final String content) {
throw new UnsupportedOperationException("Document mutation is not supported by this VFS implementation.");
}
default VfsDocumentSaveResult saveDocument(final Path path) {
throw new UnsupportedOperationException("Document save is not supported by this VFS implementation.");
}
default VfsDocumentOpenResult.VfsTextDocument discardDocument(final Path path) {
throw new UnsupportedOperationException("Document discard is not supported by this VFS implementation.");
}
default List<VfsDocumentSaveResult> saveAllDocuments() {
return List.of();
}
@Override
default void close() {
}
}

View File

@ -1,20 +0,0 @@
package p.studio.vfs.messages;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
public record VfsDocumentAccessContext(
Path path,
String typeId,
boolean frontendDocument,
VfsDocumentAccessMode accessMode,
Map<String, String> attributes) {
public VfsDocumentAccessContext {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
Objects.requireNonNull(typeId, "typeId");
Objects.requireNonNull(accessMode, "accessMode");
attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes"));
}
}

View File

@ -1,6 +0,0 @@
package p.studio.vfs.messages;
public enum VfsDocumentAccessMode {
READ_ONLY,
EDITABLE
}

View File

@ -1,16 +0,0 @@
package p.studio.vfs.messages;
import java.nio.file.Path;
import java.util.Objects;
public record VfsDocumentSaveResult(
Path path,
String typeId,
VfsDocumentSaveStatus status) {
public VfsDocumentSaveResult {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
Objects.requireNonNull(typeId, "typeId");
Objects.requireNonNull(status, "status");
}
}

View File

@ -1,7 +0,0 @@
package p.studio.vfs.messages;
public enum VfsDocumentSaveStatus {
SAVED,
NO_CHANGES,
READ_ONLY
}

View File

@ -1,11 +0,0 @@
package p.studio.vfs.messages;
public final class VfsDocumentTypeIds {
public static final String TEXT = "text";
public static final String JSON = "json";
public static final String NDJSON = "ndjson";
public static final String BASH = "bash";
private VfsDocumentTypeIds() {
}
}

View File

@ -1,16 +0,0 @@
package p.studio.vfs.messages;
import java.nio.file.Path;
import java.util.Objects;
public record VfsProjectContext(
String projectName,
String languageId,
Path rootPath) {
public VfsProjectContext {
Objects.requireNonNull(projectName, "projectName");
Objects.requireNonNull(languageId, "languageId");
rootPath = Objects.requireNonNull(rootPath, "rootPath").toAbsolutePath().normalize();
}
}

View File

@ -1,19 +0,0 @@
package p.studio.vfs.messages;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public record VfsProjectNode(
Path path,
String displayName,
boolean directory,
boolean taggedSourceRoot,
List<VfsProjectNode> children) {
public VfsProjectNode {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
Objects.requireNonNull(displayName, "displayName");
children = List.copyOf(Objects.requireNonNull(children, "children"));
}
}

View File

@ -1,14 +0,0 @@
package p.studio.vfs.messages;
import java.nio.file.Path;
import java.util.Objects;
public record VfsProjectSnapshot(
Path projectRoot,
VfsProjectNode rootNode) {
public VfsProjectSnapshot {
projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize();
Objects.requireNonNull(rootNode, "rootNode");
}
}

View File

@ -1,10 +0,0 @@
package p.studio.vfs.messages;
import java.nio.file.Path;
import java.util.Objects;
public record VfsRefreshRequest(Path targetPath) {
public VfsRefreshRequest {
targetPath = Objects.requireNonNull(targetPath, "targetPath").toAbsolutePath().normalize();
}
}

View File

@ -1,10 +0,0 @@
package p.studio.vfs.messages;
public enum VfsUnsupportedReason {
OUTSIDE_PROJECT,
NOT_FOUND,
NOT_A_FILE,
NO_HANDLER,
BINARY_CONTENT,
INVALID_UTF8
}

View File

@ -1,9 +0,0 @@
plugins {
id("gradle.java-library-conventions")
}
dependencies {
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
}

View File

@ -1,12 +0,0 @@
package p.studio.vfs;
import p.studio.vfs.messages.VfsProjectContext;
import java.util.Objects;
public final class FilesystemProjectDocumentVfsFactory implements ProjectDocumentVfsFactory {
@Override
public VfsProjectDocument open(final VfsProjectContext projectContext) {
return new FilesystemVfsProjectDocument(Objects.requireNonNull(projectContext, "projectContext"));
}
}

View File

@ -1,466 +0,0 @@
package p.studio.vfs;
import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.models.FrontendSpec;
import p.studio.vfs.messages.*;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
final class FilesystemVfsProjectDocument implements VfsProjectDocument {
private final VfsProjectContext projectContext;
private final Map<Path, EditableDocumentSnapshot> editableSnapshots = new LinkedHashMap<>();
private final Map<Path, Map<String, String>> accessContextAttributes = new HashMap<>();
private VfsProjectSnapshot snapshot;
FilesystemVfsProjectDocument(final VfsProjectContext projectContext) {
this.projectContext = Objects.requireNonNull(projectContext, "projectContext");
this.snapshot = buildSnapshot();
}
@Override
public VfsProjectContext projectContext() {
return projectContext;
}
@Override
public VfsProjectSnapshot snapshot() {
return snapshot;
}
@Override
public VfsProjectSnapshot refresh() {
snapshot = buildSnapshot();
return snapshot;
}
@Override
public VfsProjectSnapshot refresh(final VfsRefreshRequest request) {
final Path refreshTarget = refreshRootFor(Objects.requireNonNull(request, "request").targetPath());
if (refreshTarget.equals(projectContext.rootPath())) {
return refresh();
}
final Set<Path> taggedRoots = taggedSourceRoots();
final String displayName = refreshTarget.getFileName() == null
? projectContext.projectName()
: refreshTarget.getFileName().toString();
final VfsProjectNode refreshedNode = buildNode(refreshTarget, displayName, taggedRoots);
snapshot = new VfsProjectSnapshot(
projectContext.rootPath(),
replaceNode(snapshot.rootNode(), refreshedNode));
return snapshot;
}
@Override
public VfsDocumentOpenResult openDocument(final Path path) {
try {
final var supportedDocument = requireSupportedDocument(path);
return openSupportedDocument(supportedDocument);
} catch (UnsupportedDocumentException unsupportedDocumentException) {
return new VfsDocumentOpenResult.VfsUnsupportedDocument(
unsupportedDocumentException.path(),
unsupportedDocumentException.reason());
}
}
@Override
public VfsDocumentAccessContext accessContext(final Path path) {
return accessContextFor(requireSupportedDocument(path));
}
@Override
public VfsDocumentAccessContext updateAccessContext(final Path path, final Map<String, String> attributes) {
final var supportedDocument = requireSupportedDocument(path);
accessContextAttributes.put(supportedDocument.path(), Map.copyOf(Objects.requireNonNull(attributes, "attributes")));
return accessContextFor(supportedDocument);
}
@Override
public VfsDocumentOpenResult.VfsTextDocument updateDocument(final Path path, final String content) {
final var supportedDocument = requireSupportedDocument(path);
if (supportedDocument.accessMode() != VfsDocumentAccessMode.EDITABLE) {
throw new IllegalStateException("Document is hard read-only: " + supportedDocument.path());
}
final var normalizedPath = supportedDocument.path();
final var snapshot = editableSnapshots.computeIfAbsent(
normalizedPath,
ignored -> new EditableDocumentSnapshot(
supportedDocument.content(),
supportedDocument.content(),
supportedDocument.lineSeparator()));
snapshot.content = Objects.requireNonNull(content, "content");
snapshot.lineSeparator = lineSeparator(snapshot.content);
return toVfsTextDocument(supportedDocument.withContent(snapshot.content, snapshot.lineSeparator), snapshot.isDirty(), accessContextFor(supportedDocument));
}
@Override
public VfsDocumentSaveResult saveDocument(final Path path) {
final var supportedDocument = requireSupportedDocument(path);
final var accessContext = accessContextFor(supportedDocument);
if (accessContext.accessMode() != VfsDocumentAccessMode.EDITABLE) {
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.READ_ONLY);
}
final var snapshot = editableSnapshots.computeIfAbsent(
supportedDocument.path(),
ignored -> new EditableDocumentSnapshot(
supportedDocument.content(),
supportedDocument.content(),
supportedDocument.lineSeparator()));
if (!snapshot.isDirty()) {
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.NO_CHANGES);
}
writeUtf8(supportedDocument.path(), snapshot.content);
snapshot.savedContent = snapshot.content;
snapshot.lineSeparator = lineSeparator(snapshot.content);
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.SAVED);
}
@Override
public VfsDocumentOpenResult.VfsTextDocument discardDocument(final Path path) {
final var supportedDocument = requireSupportedDocument(path);
editableSnapshots.remove(supportedDocument.path());
return toVfsTextDocument(supportedDocument, false, accessContextFor(supportedDocument));
}
@Override
public List<VfsDocumentSaveResult> saveAllDocuments() {
return editableSnapshots.keySet().stream()
.map(this::saveDocument)
.toList();
}
private SupportedDocument requireSupportedDocument(final Path path) {
final Path normalizedPath = normalize(path);
if (!normalizedPath.startsWith(projectContext.rootPath())) {
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.OUTSIDE_PROJECT);
}
if (!Files.exists(normalizedPath)) {
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.NOT_FOUND);
}
if (!Files.isRegularFile(normalizedPath)) {
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.NOT_A_FILE);
}
final byte[] bytes = readAllBytes(normalizedPath);
if (containsNulByte(bytes)) {
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.BINARY_CONTENT);
}
final String content;
try {
content = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString();
} catch (CharacterCodingException codingException) {
throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.INVALID_UTF8, codingException);
}
final DocumentKind documentKind = documentKind(normalizedPath, content);
return new SupportedDocument(
normalizedPath,
normalizedPath.getFileName().toString(),
documentKind.typeId(),
documentKind.frontendDocument(),
documentKind.accessMode(),
content,
lineSeparator(content));
}
private VfsDocumentOpenResult openSupportedDocument(final SupportedDocument supportedDocument) {
final var accessContext = accessContextFor(supportedDocument);
if (supportedDocument.accessMode() == VfsDocumentAccessMode.EDITABLE) {
final var snapshot = editableSnapshots.computeIfAbsent(
supportedDocument.path(),
ignored -> new EditableDocumentSnapshot(
supportedDocument.content(),
supportedDocument.content(),
supportedDocument.lineSeparator()));
return toVfsTextDocument(
supportedDocument.withContent(snapshot.content, snapshot.lineSeparator),
snapshot.isDirty(),
accessContext);
}
return toVfsTextDocument(supportedDocument, false, accessContext);
}
private VfsDocumentOpenResult.VfsTextDocument toVfsTextDocument(
final SupportedDocument supportedDocument,
final boolean dirty,
final VfsDocumentAccessContext accessContext) {
return new VfsDocumentOpenResult.VfsTextDocument(
supportedDocument.path(),
supportedDocument.documentName(),
supportedDocument.typeId(),
supportedDocument.content(),
supportedDocument.lineSeparator(),
dirty,
accessContext);
}
private VfsProjectSnapshot buildSnapshot() {
return new VfsProjectSnapshot(
projectContext.rootPath(),
buildNode(projectContext.rootPath(), projectContext.projectName(), taggedSourceRoots()));
}
private Set<Path> taggedSourceRoots() {
return FrontendRegistryService.getFrontendSpec(projectContext.languageId())
.stream()
.flatMap(frontendSpec -> frontendSpec.getSourceRoots().stream())
.map(projectContext.rootPath()::resolve)
.map(Path::toAbsolutePath)
.map(Path::normalize)
.collect(Collectors.toSet());
}
private DocumentKind documentKind(final Path path, final String content) {
final String extension = extensionOf(path);
if ("json".equals(extension)) {
return new DocumentKind(VfsDocumentTypeIds.JSON, false, VfsDocumentAccessMode.EDITABLE);
}
if ("ndjson".equals(extension)) {
return new DocumentKind(VfsDocumentTypeIds.NDJSON, false, VfsDocumentAccessMode.EDITABLE);
}
if (isBashDocument(path, extension, content)) {
return new DocumentKind(VfsDocumentTypeIds.BASH, false, VfsDocumentAccessMode.EDITABLE);
}
if (isFrontendSourceDocument(extension)) {
return new DocumentKind(projectContext.languageId(), true, VfsDocumentAccessMode.EDITABLE);
}
return new DocumentKind(VfsDocumentTypeIds.TEXT, false, VfsDocumentAccessMode.EDITABLE);
}
private VfsProjectNode buildNode(
final Path path,
final String displayName,
final Set<Path> taggedRoots) {
if (!Files.isDirectory(path)) {
return new VfsProjectNode(path, displayName, false, taggedRoots.contains(path), List.of());
}
final List<VfsProjectNode> children;
try (Stream<Path> stream = Files.list(path)) {
children = stream
.sorted(nodeComparator())
.map(child -> buildNode(child, child.getFileName().toString(), taggedRoots))
.toList();
} catch (IOException ioException) {
throw new UncheckedIOException(ioException);
}
return new VfsProjectNode(path, displayName, true, taggedRoots.contains(path), children);
}
private VfsProjectNode replaceNode(final VfsProjectNode currentNode, final VfsProjectNode replacementNode) {
if (currentNode.path().equals(replacementNode.path())) {
return replacementNode;
}
if (!replacementNode.path().startsWith(currentNode.path())) {
return currentNode;
}
if (!currentNode.directory()) {
return currentNode;
}
final List<VfsProjectNode> replacedChildren = currentNode.children()
.stream()
.map(child -> replaceNode(child, replacementNode))
.sorted(projectNodeComparator())
.toList();
return new VfsProjectNode(
currentNode.path(),
currentNode.displayName(),
true,
currentNode.taggedSourceRoot(),
replacedChildren);
}
private Path refreshRootFor(final Path targetPath) {
final Path normalizedTarget = normalize(targetPath);
if (!normalizedTarget.startsWith(projectContext.rootPath())) {
throw new IllegalArgumentException("Refresh target must stay within the project root.");
}
if (Files.exists(normalizedTarget)) {
return normalizedTarget;
}
final Path parent = normalizedTarget.getParent();
if (parent == null || !parent.startsWith(projectContext.rootPath())) {
return projectContext.rootPath();
}
return parent;
}
private Comparator<Path> nodeComparator() {
return Comparator
.comparing((Path path) -> !Files.isDirectory(path))
.thenComparing(path -> path.getFileName().toString(), String.CASE_INSENSITIVE_ORDER);
}
private Comparator<VfsProjectNode> projectNodeComparator() {
return Comparator
.comparing((VfsProjectNode node) -> !node.directory())
.thenComparing(VfsProjectNode::displayName, String.CASE_INSENSITIVE_ORDER);
}
private Path normalize(final Path path) {
return Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
}
private boolean isFrontendSourceDocument(final String extension) {
if (extension.isBlank()) {
return false;
}
return FrontendRegistryService.getFrontendSpec(projectContext.languageId())
.map(FrontendSpec::getAllowedExtensions)
.stream()
.flatMap(allowedExtensions -> allowedExtensions.stream())
.anyMatch(allowedExtension -> allowedExtension.equalsIgnoreCase(extension));
}
private boolean isBashDocument(final Path path, final String extension, final String content) {
if (Set.of("sh", "bash", "bashrc", "bash_profile").contains(extension)) {
return true;
}
final String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
if (".bashrc".equals(fileName) || ".bash_profile".equals(fileName)) {
return true;
}
final int firstLineBreak = content.indexOf('\n');
final String firstLine = firstLineBreak >= 0 ? content.substring(0, firstLineBreak) : content;
return firstLine.startsWith("#!") && (firstLine.contains("bash") || firstLine.contains("/sh"));
}
private String extensionOf(final Path path) {
final String fileName = path.getFileName().toString();
final int dot = fileName.lastIndexOf('.');
if (dot < 0 || dot == fileName.length() - 1) {
return "";
}
return fileName.substring(dot + 1).toLowerCase(Locale.ROOT);
}
private boolean containsNulByte(final byte[] bytes) {
for (final byte value : bytes) {
if (value == 0) {
return true;
}
}
return false;
}
private String lineSeparator(final String content) {
return content.contains("\r\n") ? "CRLF" : "LF";
}
private byte[] readAllBytes(final Path path) {
try {
return Files.readAllBytes(path);
} catch (IOException ioException) {
throw new UncheckedIOException(ioException);
}
}
private void writeUtf8(final Path path, final String content) {
try {
Files.writeString(path, content, StandardCharsets.UTF_8);
} catch (IOException ioException) {
throw new UncheckedIOException(ioException);
}
}
private VfsDocumentAccessContext accessContextFor(final SupportedDocument supportedDocument) {
final var attributes = accessContextAttributes.computeIfAbsent(supportedDocument.path(), ignored -> Map.of());
return new VfsDocumentAccessContext(
supportedDocument.path(),
supportedDocument.typeId(),
supportedDocument.frontendDocument(),
supportedDocument.accessMode(),
attributes);
}
private record DocumentKind(
String typeId,
boolean frontendDocument,
VfsDocumentAccessMode accessMode) {
}
private record SupportedDocument(
Path path,
String documentName,
String typeId,
boolean frontendDocument,
VfsDocumentAccessMode accessMode,
String content,
String lineSeparator) {
private SupportedDocument withContent(final String updatedContent, final String updatedLineSeparator) {
return new SupportedDocument(
path,
documentName,
typeId,
frontendDocument,
accessMode,
updatedContent,
updatedLineSeparator);
}
}
private static final class EditableDocumentSnapshot {
private String savedContent;
private String content;
private String lineSeparator;
private EditableDocumentSnapshot(
final String savedContent,
final String content,
final String lineSeparator) {
this.savedContent = savedContent;
this.content = content;
this.lineSeparator = lineSeparator;
}
private boolean isDirty() {
return !Objects.equals(savedContent, content);
}
}
private static final class UnsupportedDocumentException extends IllegalArgumentException {
private final Path path;
private final VfsUnsupportedReason reason;
private UnsupportedDocumentException(final Path path, final VfsUnsupportedReason reason) {
super(reason.name() + ": " + path);
this.path = path;
this.reason = reason;
}
private UnsupportedDocumentException(
final Path path,
final VfsUnsupportedReason reason,
final Throwable cause) {
super(reason.name() + ": " + path, cause);
this.path = path;
this.reason = reason;
}
private Path path() {
return path;
}
private VfsUnsupportedReason reason() {
return reason;
}
}
}

View File

@ -1,237 +0,0 @@
package p.studio.vfs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.vfs.messages.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
final class FilesystemVfsProjectDocumentTest {
@TempDir
Path tempDir;
@Test
void snapshotIncludesHiddenFilesOrdersFoldersFirstAndTagsSourceRoots() throws Exception {
Files.createDirectories(tempDir.resolve("src"));
Files.createDirectories(tempDir.resolve("assets"));
Files.writeString(tempDir.resolve(".env"), "TOKEN=1\n");
Files.writeString(tempDir.resolve("README.md"), "# project\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsProjectSnapshot snapshot = vfs.snapshot();
assertEquals("Example", snapshot.rootNode().displayName());
assertEquals("assets", snapshot.rootNode().children().get(0).displayName());
assertEquals("src", snapshot.rootNode().children().get(1).displayName());
assertEquals(".env", snapshot.rootNode().children().get(2).displayName());
assertTrue(snapshot.rootNode().children().get(1).taggedSourceRoot());
}
@Test
void openDocumentReturnsTextDocumentForUtf8TextFile() throws Exception {
final Path file = tempDir.resolve("main.pbs");
Files.writeString(file, "fn main(): void\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentOpenResult result = vfs.openDocument(file);
final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result);
assertEquals("main.pbs", document.documentName());
assertEquals("pbs", document.typeId());
assertEquals("LF", document.lineSeparator());
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
assertTrue(document.accessContext().frontendDocument());
assertTrue(document.content().contains("fn main()"));
}
@Test
void openDocumentClassifiesJsonFilesWithJsonTypeIdentifier() throws Exception {
final Path file = tempDir.resolve("prometeu.json");
Files.writeString(file, "{\n \"name\": \"Example\"\n}\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentOpenResult result = vfs.openDocument(file);
final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result);
assertEquals(VfsDocumentTypeIds.JSON, document.typeId());
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
}
@Test
void openDocumentClassifiesNdjsonFilesWithJsonTypeIdentifier() throws Exception {
final Path file = tempDir.resolve("events.ndjson");
Files.writeString(file, "{\"event\":\"start\"}\n{\"event\":\"done\"}\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentOpenResult result = vfs.openDocument(file);
final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result);
assertEquals(VfsDocumentTypeIds.NDJSON, document.typeId());
}
@Test
void openDocumentClassifiesBashScriptsWithBashTypeIdentifier() throws Exception {
final Path file = tempDir.resolve("script.sh");
Files.writeString(file, "#!/usr/bin/env bash\nprintf \"hi\"\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentOpenResult result = vfs.openDocument(file);
final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result);
assertEquals(VfsDocumentTypeIds.BASH, document.typeId());
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
}
@Test
void frontendDocumentsUseEditableInMemorySnapshotsUntilSavePersistsThem() throws Exception {
final Path file = tempDir.resolve("main.pbs");
Files.writeString(file, "fn main(): void\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final var updated = vfs.updateDocument(file, "fn main(): int\n");
assertTrue(updated.dirty());
assertEquals("fn main(): int\n", updated.content());
assertEquals("fn main(): void\n", Files.readString(file));
assertEquals("fn main(): int\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content());
final VfsDocumentSaveResult saveResult = vfs.saveDocument(file);
assertEquals(VfsDocumentSaveStatus.SAVED, saveResult.status());
assertEquals("fn main(): int\n", Files.readString(file));
assertFalse(assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).dirty());
}
@Test
void editableDocumentsUseInMemorySnapshotsUntilSavePersistsThem() throws Exception {
final Path file = tempDir.resolve("notes.txt");
Files.writeString(file, "alpha\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final var updated = vfs.updateDocument(file, "beta\n");
assertTrue(updated.dirty());
assertEquals("beta\n", updated.content());
assertEquals("alpha\n", Files.readString(file));
assertEquals("beta\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content());
final VfsDocumentSaveResult saveResult = vfs.saveDocument(file);
assertEquals(VfsDocumentSaveStatus.SAVED, saveResult.status());
assertEquals("beta\n", Files.readString(file));
assertFalse(assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).dirty());
}
@Test
void discardDocumentDropsDirtySnapshotAndRestoresFilesystemState() throws Exception {
final Path file = tempDir.resolve("notes.txt");
Files.writeString(file, "alpha\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
vfs.updateDocument(file, "beta\n");
final var discarded = vfs.discardDocument(file);
assertFalse(discarded.dirty());
assertEquals("alpha\n", discarded.content());
assertEquals("alpha\n", Files.readString(file));
assertEquals("alpha\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content());
}
@Test
void saveAllDocumentsPersistsEditableDirtySnapshotsIncludingFrontendDocuments() throws Exception {
final Path editable = tempDir.resolve("notes.txt");
final Path frontend = tempDir.resolve("main.pbs");
Files.writeString(editable, "alpha\n");
Files.writeString(frontend, "fn main(): void\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
vfs.updateDocument(editable, "beta\n");
vfs.updateDocument(frontend, "fn main(): int\n");
final var results = vfs.saveAllDocuments();
assertEquals(2, results.size());
assertEquals(editable.toAbsolutePath().normalize(), results.get(0).path());
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(0).status());
assertEquals(frontend.toAbsolutePath().normalize(), results.get(1).path());
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(1).status());
assertEquals("beta\n", Files.readString(editable));
assertEquals("fn main(): int\n", Files.readString(frontend));
}
@Test
void accessContextCanBeReadAndMutatedWithoutChangingCanonicalPolicy() throws Exception {
final Path file = tempDir.resolve("prometeu.json");
Files.writeString(file, "{\n \"name\": \"Example\"\n}\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentAccessContext initialContext = vfs.accessContext(file);
final VfsDocumentAccessContext updatedContext = vfs.updateAccessContext(file, Map.of("statusBar", "future-toggle"));
assertEquals(VfsDocumentAccessMode.EDITABLE, initialContext.accessMode());
assertEquals(VfsDocumentTypeIds.JSON, initialContext.typeId());
assertEquals(Map.of("statusBar", "future-toggle"), updatedContext.attributes());
assertEquals(initialContext.accessMode(), updatedContext.accessMode());
assertEquals(initialContext.typeId(), updatedContext.typeId());
}
@Test
void openDocumentRejectsBinaryLikeFiles() throws Exception {
final Path file = tempDir.resolve("sprite.bin");
Files.write(file, new byte[]{0x01, 0x00, 0x02});
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentOpenResult result = vfs.openDocument(file);
final var unsupported = assertInstanceOf(VfsDocumentOpenResult.VfsUnsupportedDocument.class, result);
assertEquals(VfsUnsupportedReason.BINARY_CONTENT, unsupported.reason());
}
@Test
void openDocumentRejectsPathsOutsideProjectScope() throws Exception {
final Path outsideFile = tempDir.getParent().resolve("outside.pbs");
Files.writeString(outsideFile, "fn stray(): void\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
final VfsDocumentOpenResult result = vfs.openDocument(outsideFile);
final var unsupported = assertInstanceOf(VfsDocumentOpenResult.VfsUnsupportedDocument.class, result);
assertEquals(VfsUnsupportedReason.OUTSIDE_PROJECT, unsupported.reason());
}
@Test
void targetedRefreshUpdatesOnlyTheRequestedSubtreeInTheSnapshot() throws Exception {
final Path src = Files.createDirectories(tempDir.resolve("src"));
Files.createDirectories(tempDir.resolve("assets"));
Files.writeString(src.resolve("main.pbs"), "fn main(): void\n");
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
Files.writeString(src.resolve("later.pbs"), "fn later(): void\n");
final VfsProjectSnapshot refreshedSnapshot = vfs.refresh(new VfsRefreshRequest(src));
final VfsProjectNode srcNode = refreshedSnapshot.rootNode().children().get(1);
assertEquals("src", srcNode.displayName());
assertEquals(2, srcNode.children().size());
assertEquals("later.pbs", srcNode.children().get(0).displayName());
assertEquals("main.pbs", srcNode.children().get(1).displayName());
}
private VfsProjectContext projectContext() {
return new VfsProjectContext("Example", "pbs", tempDir);
}
}

View File

@ -5,10 +5,6 @@ plugins {
rootProject.name = "prometeu-studio" rootProject.name = "prometeu-studio"
include("prometeu-infra") include("prometeu-infra")
include("prometeu-vfs:prometeu-vfs-api")
include("prometeu-vfs:prometeu-vfs-v1")
include("prometeu-lsp:prometeu-lsp-api")
include("prometeu-lsp:prometeu-lsp-v1")
include("prometeu-packer:prometeu-packer-api") include("prometeu-packer:prometeu-packer-api")
include("prometeu-packer:prometeu-packer-v1") include("prometeu-packer:prometeu-packer-v1")