implement PLN-0032 structural anchor payloads and tests

This commit is contained in:
bQUARKz 2026-04-03 10:02:48 +01:00
parent 20851c0958
commit d93fe98bcd
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 264 additions and 25 deletions

View File

@ -14,4 +14,4 @@
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-04-02","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} {"type":"discussion","id":"DSC-0013","status":"done","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-04-02","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]}
{"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]}
{"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]} {"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]}
{"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"review","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]} {"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]}

View File

@ -2,9 +2,9 @@
id: PLN-0032 id: PLN-0032
ticket: studio-editor-scope-guides-and-brace-anchoring ticket: studio-editor-scope-guides-and-brace-anchoring
title: Frontend structural-anchor payload propagation and anchor-aware test coverage title: Frontend structural-anchor payload propagation and anchor-aware test coverage
status: review status: done
created: 2026-04-03 created: 2026-04-03
completed: completed: 2026-04-03
tags: tags:
- studio - studio
- frontend - frontend
@ -108,10 +108,10 @@ The implementation must preserve the DEC-0014 rule that structural anchors are t
## Acceptance Criteria ## Acceptance Criteria
- [ ] Frontend semantic-read payloads expose structural anchors through a dedicated semantic surface. - [x] Frontend semantic-read payloads expose structural anchors through a dedicated semantic surface.
- [ ] Studio consumes structural anchors without overloading `documentSymbols`. - [x] Studio consumes structural anchors without overloading `documentSymbols`.
- [ ] Exact guide positioning uses structural-anchor metadata when available. - [x] Exact guide positioning uses structural-anchor metadata when available.
- [ ] Tests cover payload transport, anchor interpretation, and anchor-aware rendering. - [x] Tests cover payload transport, anchor interpretation, and anchor-aware rendering.
## Dependencies ## Dependencies

View File

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

View File

@ -4,6 +4,7 @@ import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspHighlightSpanDTO; import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspSessionStateDTO; import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.lsp.dtos.LspSymbolDTO;
import java.util.List; import java.util.List;
@ -15,6 +16,7 @@ public record LspAnalyzeDocumentResult(
List<LspDiagnosticDTO> diagnostics, List<LspDiagnosticDTO> diagnostics,
List<LspHighlightSpanDTO> semanticHighlights, List<LspHighlightSpanDTO> semanticHighlights,
List<LspSymbolDTO> documentSymbols, List<LspSymbolDTO> documentSymbols,
List<LspStructuralAnchorDTO> structuralAnchors,
List<LspSymbolDTO> workspaceSymbols) { List<LspSymbolDTO> workspaceSymbols) {
public LspAnalyzeDocumentResult { public LspAnalyzeDocumentResult {
@ -23,6 +25,7 @@ public record LspAnalyzeDocumentResult(
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights")); semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights"));
documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols")); documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols"));
structuralAnchors = List.copyOf(Objects.requireNonNull(structuralAnchors, "structuralAnchors"));
workspaceSymbols = List.copyOf(Objects.requireNonNull(workspaceSymbols, "workspaceSymbols")); workspaceSymbols = List.copyOf(Objects.requireNonNull(workspaceSymbols, "workspaceSymbols"));
} }
} }

View File

@ -15,11 +15,12 @@ class LspSemanticAnalyseService {
final LspAnalyzeDocumentRequest request) { final LspAnalyzeDocumentRequest request) {
final var normalizedRequestedDocument = normalize(request.documentPath()); final var normalizedRequestedDocument = normalize(request.documentPath());
return new LspAnalyzeDocumentResult( return new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("diagnostics", "symbols", "definition", "highlight")), new LspSessionStateDTO(true, List.of("diagnostics", "symbols", "definition", "highlight", "structural-anchors")),
session.semanticPresentation(), session.semanticPresentation(),
session.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), session.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.structuralAnchorsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.workspaceSymbols()); session.workspaceSymbols());
} }
} }

View File

@ -145,6 +145,7 @@ final class LspSemanticReadPhase {
diagnosticsByDocument, diagnosticsByDocument,
Map.of(), Map.of(),
Map.of(), Map.of(),
Map.of(),
List.of(), List.of(),
Map.of(), Map.of(),
Map.of()); Map.of());
@ -180,6 +181,7 @@ final class LspSemanticReadPhase {
diagnosticsByDocument, diagnosticsByDocument,
semanticIndex.semanticHighlightsByDocument(), semanticIndex.semanticHighlightsByDocument(),
semanticIndex.documentSymbolsByDocument(), semanticIndex.documentSymbolsByDocument(),
semanticIndex.structuralAnchorsByDocument(),
semanticIndex.workspaceSymbols(), semanticIndex.workspaceSymbols(),
semanticIndex.symbolsByName(), semanticIndex.symbolsByName(),
semanticIndex.tokensByDocument()); semanticIndex.tokensByDocument());

View File

@ -4,6 +4,7 @@ import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.lsp.dtos.LspDiagnosticDTO; import p.studio.lsp.dtos.LspDiagnosticDTO;
import p.studio.lsp.dtos.LspHighlightSpanDTO; import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.lsp.dtos.LspSymbolDTO;
import java.nio.file.Path; import java.nio.file.Path;
@ -16,6 +17,7 @@ public record SemanticSession(
Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument, Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument,
Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument, Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument,
Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument, Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument,
Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument,
List<LspSymbolDTO> workspaceSymbols, List<LspSymbolDTO> workspaceSymbols,
Map<String, List<LspSymbolDTO>> symbolsByName, Map<String, List<LspSymbolDTO>> symbolsByName,
Map<Path, List<PbsToken>> tokensByDocument) { Map<Path, List<PbsToken>> tokensByDocument) {

View File

@ -7,6 +7,7 @@ import p.studio.compiler.pbs.lexer.PbsTokenKind;
import p.studio.compiler.source.Span; import p.studio.compiler.source.Span;
import p.studio.lsp.dtos.LspHighlightSpanDTO; import p.studio.lsp.dtos.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspRangeDTO; import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspSymbolKind; import p.studio.lsp.messages.LspSymbolKind;
@ -22,6 +23,7 @@ import static p.studio.lsp.LspSemanticUtilities.normalize;
public final class SemanticIndex { public final class SemanticIndex {
private final Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument = new LinkedHashMap<>(); private final Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument = new LinkedHashMap<>();
private final Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument = 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 List<LspSymbolDTO> workspaceSymbols = new ArrayList<>();
private final Map<String, List<LspSymbolDTO>> symbolsByName = new LinkedHashMap<>(); private final Map<String, List<LspSymbolDTO>> symbolsByName = new LinkedHashMap<>();
private final Map<Path, List<PbsToken>> tokensByDocument = new LinkedHashMap<>(); private final Map<Path, List<PbsToken>> tokensByDocument = new LinkedHashMap<>();
@ -62,6 +64,9 @@ public final class SemanticIndex {
} }
} }
documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols)); documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols));
structuralAnchorsByDocument.put(
normalizedDocumentPath,
structuralAnchors(documentSymbols, tokens));
} }
public void buildHighlights( public void buildHighlights(
@ -498,6 +503,66 @@ public final class SemanticIndex {
children); 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() { public Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument() {
return Map.copyOf(semanticHighlightsByDocument); return Map.copyOf(semanticHighlightsByDocument);
} }
@ -506,6 +571,10 @@ public final class SemanticIndex {
return Map.copyOf(documentSymbolsByDocument); return Map.copyOf(documentSymbolsByDocument);
} }
public Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument() {
return Map.copyOf(structuralAnchorsByDocument);
}
public List<LspSymbolDTO> workspaceSymbols() { public List<LspSymbolDTO> workspaceSymbols() {
return List.copyOf(workspaceSymbols); return List.copyOf(workspaceSymbols);
} }
@ -521,4 +590,9 @@ public final class SemanticIndex {
public Map<Path, List<PbsToken>> tokensByDocument() { public Map<Path, List<PbsToken>> tokensByDocument() {
return Map.copyOf(tokensByDocument); return Map.copyOf(tokensByDocument);
} }
private record AnchorPair(
LspRangeDTO start,
LspRangeDTO end) {
}
} }

View File

@ -2,6 +2,7 @@ package p.studio.lsp;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.dtos.LspDefinitionTargetDTO; import p.studio.lsp.dtos.LspDefinitionTargetDTO;
import p.studio.lsp.messages.LspAnalyzeDocumentRequest; import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
@ -104,6 +105,12 @@ final class LspServiceImplTest {
assertTrue( assertTrue(
flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.HANDLE), flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.HANDLE),
analysis.toString()); 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( assertTrue(
analysis.workspaceSymbols().stream().anyMatch(symbol -> analysis.workspaceSymbols().stream().anyMatch(symbol ->
symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))), symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))),
@ -236,6 +243,16 @@ final class LspServiceImplTest {
return flattened; 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) { private VfsProjectContext projectContext(final Path projectRoot) {
return new VfsProjectContext("Example", "pbs", projectRoot); return new VfsProjectContext("Example", "pbs", projectRoot);
} }

View File

@ -1,6 +1,7 @@
package p.studio.workspaces.editor; package p.studio.workspaces.editor;
import p.studio.lsp.dtos.LspRangeDTO; import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspSymbolKind; import p.studio.lsp.messages.LspSymbolKind;
@ -34,11 +35,70 @@ final class EditorDocumentScopeGuideModel {
static EditorDocumentScopeGuideModel from( static EditorDocumentScopeGuideModel from(
final String content, final String content,
final List<LspSymbolDTO> symbols) { final List<LspSymbolDTO> symbols) {
return fromRanges(
content,
symbols,
0,
null,
(symbol, depth, parentId, nextId, lineStarts, contentLength) -> {
if (symbol.kind() == LspSymbolKind.UNKNOWN) {
return null;
}
final LspRangeDTO range = symbol.range();
return toGuideRange(
lineStarts,
contentLength,
range,
new LspRangeDTO(range.startOffset(), Math.min(range.endOffset(), range.startOffset() + 1)),
new LspRangeDTO(Math.max(range.startOffset(), range.endOffset() - 1), range.endOffset()),
depth,
parentId,
nextId[0]);
},
LspSymbolDTO::children);
}
static EditorDocumentScopeGuideModel fromStructuralAnchors(
final String content,
final List<LspStructuralAnchorDTO> structuralAnchors) {
return fromRanges(
content,
structuralAnchors,
0,
null,
(anchor, depth, parentId, nextId, lineStarts, contentLength) -> toGuideRange(
lineStarts,
contentLength,
anchor.range(),
anchor.startAnchor(),
anchor.endAnchor(),
depth,
parentId,
nextId[0]),
LspStructuralAnchorDTO::children);
}
private static <T> EditorDocumentScopeGuideModel fromRanges(
final String content,
final List<T> roots,
final int initialDepth,
final Integer initialParentId,
final RangeFactory<T> rangeFactory,
final ChildAccessor<T> childAccessor) {
final List<Integer> lineStarts = lineStarts(content); final List<Integer> lineStarts = lineStarts(content);
final List<GuideRange> ranges = new ArrayList<>(); final List<GuideRange> ranges = new ArrayList<>();
final int[] nextId = new int[] {1}; final int[] nextId = new int[] {1};
for (final LspSymbolDTO symbol : symbols) { for (final T root : roots) {
appendSymbol(ranges, lineStarts, content.length(), symbol, 0, null, nextId); appendRangeNode(
ranges,
lineStarts,
content.length(),
root,
initialDepth,
initialParentId,
nextId,
rangeFactory,
childAccessor);
} }
ranges.sort(Comparator ranges.sort(Comparator
.comparingInt(GuideRange::startOffset) .comparingInt(GuideRange::startOffset)
@ -88,25 +148,25 @@ final class EditorDocumentScopeGuideModel {
return lineStarts.size(); return lineStarts.size();
} }
private static void appendSymbol( private static <T> void appendRangeNode(
final List<GuideRange> ranges, final List<GuideRange> ranges,
final List<Integer> lineStarts, final List<Integer> lineStarts,
final int contentLength, final int contentLength,
final LspSymbolDTO symbol, final T node,
final int depth, final int depth,
final Integer parentId, final Integer parentId,
final int[] nextId) { final int[] nextId,
final RangeFactory<T> rangeFactory,
final ChildAccessor<T> childAccessor) {
Integer currentParentId = parentId; Integer currentParentId = parentId;
if (symbol.kind() != LspSymbolKind.UNKNOWN) { final GuideRange range = rangeFactory.create(node, depth, parentId, nextId, lineStarts, contentLength);
final GuideRange range = toGuideRange(lineStarts, contentLength, symbol.range(), depth, parentId, nextId[0]); if (range != null) {
if (range != null) { ranges.add(range);
ranges.add(range); currentParentId = range.id();
currentParentId = range.id(); nextId[0]++;
nextId[0]++;
}
} }
for (final LspSymbolDTO child : symbol.children()) { for (final T child : childAccessor.children(node)) {
appendSymbol(ranges, lineStarts, contentLength, child, depth + 1, currentParentId, nextId); appendRangeNode(ranges, lineStarts, contentLength, child, depth + 1, currentParentId, nextId, rangeFactory, childAccessor);
} }
} }
@ -114,18 +174,25 @@ final class EditorDocumentScopeGuideModel {
final List<Integer> lineStarts, final List<Integer> lineStarts,
final int contentLength, final int contentLength,
final LspRangeDTO range, final LspRangeDTO range,
final LspRangeDTO startAnchor,
final LspRangeDTO endAnchor,
final int depth, final int depth,
final Integer parentId, final Integer parentId,
final int id) { final int id) {
final int startOffset = clamp(range.startOffset(), 0, contentLength); final int startOffset = clamp(range.startOffset(), 0, contentLength);
final int endOffset = Math.max(startOffset, clamp(range.endOffset(), 0, contentLength)); final int endOffset = Math.max(startOffset, clamp(range.endOffset(), 0, contentLength));
final int startLine = lineForOffset(lineStarts, startOffset); final int startAnchorOffset = clamp(startAnchor.startOffset(), startOffset, endOffset);
final int endAnchorOffset = clamp(Math.max(startAnchorOffset, endAnchor.startOffset()), startOffset, endOffset);
final int startLine = lineForOffset(lineStarts, startAnchorOffset);
final int inclusiveEndOffset = Math.max(startOffset, Math.max(startOffset, endOffset) - 1); final int inclusiveEndOffset = Math.max(startOffset, Math.max(startOffset, endOffset) - 1);
final int endLine = lineForOffset(lineStarts, clamp(inclusiveEndOffset, 0, contentLength)); final int inclusiveEndAnchorOffset = Math.max(
startAnchorOffset,
Math.min(Math.max(startOffset, endOffset) - 1, Math.max(endAnchorOffset, startAnchorOffset)));
final int endLine = lineForOffset(lineStarts, clamp(inclusiveEndAnchorOffset, 0, contentLength));
if (endLine <= startLine) { if (endLine <= startLine) {
return null; return null;
} }
return new GuideRange(id, depth, startOffset, endOffset, startLine, endLine, parentId); return new GuideRange(id, depth, startOffset, endOffset, startAnchorOffset, endAnchorOffset, startLine, endLine, parentId);
} }
private GuideRange findById(final int id) { private GuideRange findById(final int id) {
@ -225,6 +292,8 @@ final class EditorDocumentScopeGuideModel {
int depth, int depth,
int startOffset, int startOffset,
int endOffset, int endOffset,
int startAnchorOffset,
int endAnchorOffset,
int startLine, int startLine,
int endLine, int endLine,
Integer parentId) { Integer parentId) {
@ -252,4 +321,20 @@ final class EditorDocumentScopeGuideModel {
Objects.requireNonNull(kind, "kind"); Objects.requireNonNull(kind, "kind");
} }
} }
@FunctionalInterface
private interface ChildAccessor<T> {
List<T> children(T node);
}
@FunctionalInterface
private interface RangeFactory<T> {
GuideRange create(
T node,
int depth,
Integer parentId,
int[] nextId,
List<Integer> lineStarts,
int contentLength);
}
} }

View File

@ -381,6 +381,11 @@ public final class EditorWorkspace extends Workspace {
if (!fileBuffer.frontendDocument() || analysis == null) { if (!fileBuffer.frontendDocument() || analysis == null) {
return EditorDocumentScopeGuideModel.empty(); return EditorDocumentScopeGuideModel.empty();
} }
if (!analysis.structuralAnchors().isEmpty()) {
return EditorDocumentScopeGuideModel.fromStructuralAnchors(
fileBuffer.content(),
analysis.structuralAnchors());
}
return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols()); return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols());
} }

View File

@ -37,6 +37,7 @@ final class EditorDocumentHighlightingRouterTest {
List.of(), List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")), List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")),
List.of(), List.of(),
List.of(),
List.of()); List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
@ -67,6 +68,7 @@ final class EditorDocumentHighlightingRouterTest {
List.of(), List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 1), "fe-punctuation")), List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 1), "fe-punctuation")),
List.of(), List.of(),
List.of(),
List.of()); List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
@ -98,6 +100,7 @@ final class EditorDocumentHighlightingRouterTest {
List.of(), List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")), List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")),
List.of(), List.of(),
List.of(),
List.of()); List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
@ -130,6 +133,7 @@ final class EditorDocumentHighlightingRouterTest {
List.of(), List.of(),
List.of(new LspHighlightSpanDTO(new LspRangeDTO(16, 20), "pbs-service")), List.of(new LspHighlightSpanDTO(new LspRangeDTO(16, 20), "pbs-service")),
List.of(), List.of(),
List.of(),
List.of()); List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(

View File

@ -2,6 +2,7 @@ package p.studio.workspaces.editor;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import p.studio.lsp.dtos.LspRangeDTO; import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO; import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspSymbolKind; import p.studio.lsp.messages.LspSymbolKind;
@ -108,6 +109,33 @@ final class EditorDocumentScopeGuideModelTest {
assertEquals(EditorDocumentScopeGuideModel.ActiveGuides.empty(), model.resolveActiveGuides(content.indexOf("value"))); assertEquals(EditorDocumentScopeGuideModel.ActiveGuides.empty(), model.resolveActiveGuides(content.indexOf("value")));
} }
@Test
void structuralAnchorsDriveGuideStartAndEndLines() {
final String content = """
fn main(
value: int
) -> void
{
helper();
}
""";
final LspStructuralAnchorDTO anchor = new LspStructuralAnchorDTO(
new LspRangeDTO(content.indexOf("fn main"), content.length()),
new LspRangeDTO(content.indexOf("{"), content.indexOf("{") + 1),
new LspRangeDTO(content.indexOf("}"), content.indexOf("}") + 1),
List.of());
final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.fromStructuralAnchors(content, List.of(anchor));
final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("helper"));
assertSegments(model, 0, guides);
assertSegments(model, 1, guides);
assertSegments(model, 2, guides);
assertSegments(model, 3, guides, "ACTIVE_SCOPE:START");
assertSegments(model, 4, guides, "ACTIVE_SCOPE:CONTINUE");
assertSegments(model, 5, guides, "ACTIVE_SCOPE:END");
}
private static LspSymbolDTO symbol( private static LspSymbolDTO symbol(
final String content, final String content,
final String startMarker, final String startMarker,