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":"meta","next_id":{"DSC":32,"AGD":35,"DEC":32,"PLN":65,"LSN":47,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]}
|
{"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]}
|
||||||
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||||
{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}
|
{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
id: PLN-0063
|
id: PLN-0063
|
||||||
ticket: studio-editor-workspace-cleanup
|
ticket: studio-editor-workspace-cleanup
|
||||||
title: Legacy LSP and VFS Module Removal with Runtime Preservation
|
title: Legacy LSP and VFS Module Removal with Runtime Preservation
|
||||||
status: open
|
status: done
|
||||||
created: 2026-05-05
|
created: 2026-05-05
|
||||||
completed:
|
completed: 2026-05-05
|
||||||
tags: [studio, editor, cleanup, lsp, vfs, runtime]
|
tags: [studio, editor, cleanup, lsp, vfs, runtime]
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -73,11 +73,11 @@ Remove the legacy `prometeu-lsp` and `prometeu-vfs` modules and all remaining co
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- [ ] `prometeu-lsp` is no longer part of the build graph.
|
- [x] `prometeu-lsp` is no longer part of the build graph.
|
||||||
- [ ] `prometeu-vfs` is no longer part of the build graph.
|
- [x] `prometeu-vfs` is no longer part of the build graph.
|
||||||
- [ ] No production Studio class imports legacy `LSP` or `VFS` module types.
|
- [x] No production Studio class imports legacy `LSP` or `VFS` module types.
|
||||||
- [ ] Compiler and shipper build flows still work.
|
- [x] Compiler and shipper build flows still work.
|
||||||
- [ ] Play/run/debug flows still work without the removed modules.
|
- [x] Play/run/debug flows still work without the removed modules.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@ -89,4 +89,3 @@ Remove the legacy `prometeu-lsp` and `prometeu-vfs` modules and all remaining co
|
|||||||
- Hidden transitive dependencies may keep the build graph coupled to removed modules longer than expected.
|
- Hidden transitive dependencies may keep the build graph coupled to removed modules longer than expected.
|
||||||
- Runtime or debug flows may have indirect event or session assumptions not obvious from imports alone.
|
- Runtime or debug flows may have indirect event or session assumptions not obvious from imports alone.
|
||||||
- Deleting modules too early may obscure compile errors that should instead be fixed in surviving Studio code first.
|
- Deleting modules too early may obscure compile errors that should instead be fixed in surviving Studio code first.
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,6 @@ plugins {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":prometeu-infra"))
|
implementation(project(":prometeu-infra"))
|
||||||
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
|
||||||
implementation(project(":prometeu-lsp:prometeu-lsp-v1"))
|
|
||||||
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
|
|
||||||
implementation(project(":prometeu-vfs:prometeu-vfs-v1"))
|
|
||||||
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
||||||
implementation(project(":prometeu-packer:prometeu-packer-v1"))
|
implementation(project(":prometeu-packer:prometeu-packer-v1"))
|
||||||
implementation(project(":prometeu-studio"))
|
implementation(project(":prometeu-studio"))
|
||||||
@ -23,4 +19,4 @@ application {
|
|||||||
javafx {
|
javafx {
|
||||||
version = libs.versions.javafx.get()
|
version = libs.versions.javafx.get()
|
||||||
modules("javafx.controls", "javafx.fxml")
|
modules("javafx.controls", "javafx.fxml")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
dependencies {
|
||||||
implementation(project(":prometeu-infra"))
|
implementation(project(":prometeu-infra"))
|
||||||
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
|
||||||
implementation(project(":prometeu-vfs:prometeu-vfs-api"))
|
|
||||||
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package p.studio.projects;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import p.packer.messages.PackerProjectContext;
|
import p.packer.messages.PackerProjectContext;
|
||||||
import p.studio.vfs.messages.VfsProjectContext;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@ -36,8 +35,4 @@ public record ProjectReference(
|
|||||||
public PackerProjectContext toPackerProjectContext() {
|
public PackerProjectContext toPackerProjectContext() {
|
||||||
return new PackerProjectContext(name, absoluteRootPath());
|
return new PackerProjectContext(name, absoluteRootPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public VfsProjectContext toVfsProjectContext() {
|
|
||||||
return new VfsProjectContext(name, languageId, absoluteRootPath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
rootProject.name = "prometeu-studio"
|
||||||
|
|
||||||
include("prometeu-infra")
|
include("prometeu-infra")
|
||||||
include("prometeu-vfs:prometeu-vfs-api")
|
|
||||||
include("prometeu-vfs:prometeu-vfs-v1")
|
|
||||||
include("prometeu-lsp:prometeu-lsp-api")
|
|
||||||
include("prometeu-lsp:prometeu-lsp-v1")
|
|
||||||
include("prometeu-packer:prometeu-packer-api")
|
include("prometeu-packer:prometeu-packer-api")
|
||||||
include("prometeu-packer:prometeu-packer-v1")
|
include("prometeu-packer:prometeu-packer-v1")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user