implements PLN-0063
This commit is contained in:
parent
cebef90118
commit
1f85be3721
@ -1,5 +1,5 @@
|
||||
{"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-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"}]}
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
id: PLN-0063
|
||||
ticket: studio-editor-workspace-cleanup
|
||||
title: Legacy LSP and VFS Module Removal with Runtime Preservation
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-05-05
|
||||
completed:
|
||||
completed: 2026-05-05
|
||||
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
|
||||
|
||||
- [ ] `prometeu-lsp` is no longer part of the build graph.
|
||||
- [ ] `prometeu-vfs` is no longer part of the build graph.
|
||||
- [ ] No production Studio class imports legacy `LSP` or `VFS` module types.
|
||||
- [ ] Compiler and shipper build flows still work.
|
||||
- [ ] Play/run/debug flows still work without the removed modules.
|
||||
- [x] `prometeu-lsp` is no longer part of the build graph.
|
||||
- [x] `prometeu-vfs` is no longer part of the build graph.
|
||||
- [x] No production Studio class imports legacy `LSP` or `VFS` module types.
|
||||
- [x] Compiler and shipper build flows still work.
|
||||
- [x] Play/run/debug flows still work without the removed modules.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
|
||||
|
||||
@ -5,10 +5,6 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
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-v1"))
|
||||
implementation(project(":prometeu-studio"))
|
||||
@ -23,4 +19,4 @@ application {
|
||||
javafx {
|
||||
version = libs.versions.javafx.get()
|
||||
modules("javafx.controls", "javafx.fxml")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
plugins {
|
||||
id("gradle.java-library-conventions")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package p.studio.lsp.events;
|
||||
|
||||
public enum LspEventKind {
|
||||
SESSION_READY
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package p.studio.lsp.events;
|
||||
|
||||
public interface LspEventSink {
|
||||
void publish(LspEvent event);
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public enum LspDiagnosticSeverity {
|
||||
ERROR,
|
||||
WARNING
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,8 +5,6 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
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-compiler:prometeu-compiler-core"))
|
||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||
|
||||
@ -2,7 +2,6 @@ package p.studio.projects;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import p.packer.messages.PackerProjectContext;
|
||||
import p.studio.vfs.messages.VfsProjectContext;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@ -36,8 +35,4 @@ public record ProjectReference(
|
||||
public PackerProjectContext toPackerProjectContext() {
|
||||
return new PackerProjectContext(name, absoluteRootPath());
|
||||
}
|
||||
|
||||
public VfsProjectContext toVfsProjectContext() {
|
||||
return new VfsProjectContext(name, languageId, absoluteRootPath());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
plugins {
|
||||
id("gradle.java-library-conventions")
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package p.studio.vfs;
|
||||
|
||||
import p.studio.vfs.messages.VfsProjectContext;
|
||||
|
||||
public interface ProjectDocumentVfsFactory {
|
||||
VfsProjectDocument open(VfsProjectContext projectContext);
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
package p.studio.vfs.messages;
|
||||
|
||||
public enum VfsDocumentAccessMode {
|
||||
READ_ONLY,
|
||||
EDITABLE
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package p.studio.vfs.messages;
|
||||
|
||||
public enum VfsDocumentSaveStatus {
|
||||
SAVED,
|
||||
NO_CHANGES,
|
||||
READ_ONLY
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,6 @@ plugins {
|
||||
rootProject.name = "prometeu-studio"
|
||||
|
||||
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-v1")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user