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-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-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
ticket: studio-editor-scope-guides-and-brace-anchoring
title: Frontend structural-anchor payload propagation and anchor-aware test coverage
status: review
status: done
created: 2026-04-03
completed:
completed: 2026-04-03
tags:
- studio
- frontend
@ -108,10 +108,10 @@ The implementation must preserve the DEC-0014 rule that structural anchors are t
## Acceptance Criteria
- [ ] Frontend semantic-read payloads expose structural anchors through a dedicated semantic surface.
- [ ] Studio consumes structural anchors without overloading `documentSymbols`.
- [ ] Exact guide positioning uses structural-anchor metadata when available.
- [ ] Tests cover payload transport, anchor interpretation, and anchor-aware rendering.
- [x] Frontend semantic-read payloads expose structural anchors through a dedicated semantic surface.
- [x] Studio consumes structural anchors without overloading `documentSymbols`.
- [x] Exact guide positioning uses structural-anchor metadata when available.
- [x] Tests cover payload transport, anchor interpretation, and anchor-aware rendering.
## 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.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspSessionStateDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import java.util.List;
@ -15,6 +16,7 @@ public record LspAnalyzeDocumentResult(
List<LspDiagnosticDTO> diagnostics,
List<LspHighlightSpanDTO> semanticHighlights,
List<LspSymbolDTO> documentSymbols,
List<LspStructuralAnchorDTO> structuralAnchors,
List<LspSymbolDTO> workspaceSymbols) {
public LspAnalyzeDocumentResult {
@ -23,6 +25,7 @@ public record LspAnalyzeDocumentResult(
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights"));
documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols"));
structuralAnchors = List.copyOf(Objects.requireNonNull(structuralAnchors, "structuralAnchors"));
workspaceSymbols = List.copyOf(Objects.requireNonNull(workspaceSymbols, "workspaceSymbols"));
}
}

View File

@ -15,11 +15,12 @@ class LspSemanticAnalyseService {
final LspAnalyzeDocumentRequest request) {
final var normalizedRequestedDocument = normalize(request.documentPath());
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.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.structuralAnchorsByDocument().getOrDefault(normalizedRequestedDocument, List.of()),
session.workspaceSymbols());
}
}

View File

@ -145,6 +145,7 @@ final class LspSemanticReadPhase {
diagnosticsByDocument,
Map.of(),
Map.of(),
Map.of(),
List.of(),
Map.of(),
Map.of());
@ -180,6 +181,7 @@ final class LspSemanticReadPhase {
diagnosticsByDocument,
semanticIndex.semanticHighlightsByDocument(),
semanticIndex.documentSymbolsByDocument(),
semanticIndex.structuralAnchorsByDocument(),
semanticIndex.workspaceSymbols(),
semanticIndex.symbolsByName(),
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.LspHighlightSpanDTO;
import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import java.nio.file.Path;
@ -16,6 +17,7 @@ public record SemanticSession(
Map<Path, List<LspDiagnosticDTO>> diagnosticsByDocument,
Map<Path, List<LspHighlightSpanDTO>> semanticHighlightsByDocument,
Map<Path, List<LspSymbolDTO>> documentSymbolsByDocument,
Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument,
List<LspSymbolDTO> workspaceSymbols,
Map<String, List<LspSymbolDTO>> symbolsByName,
Map<Path, List<PbsToken>> tokensByDocument) {

View File

@ -7,6 +7,7 @@ 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;
@ -22,6 +23,7 @@ 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<>();
@ -62,6 +64,9 @@ public final class SemanticIndex {
}
}
documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols));
structuralAnchorsByDocument.put(
normalizedDocumentPath,
structuralAnchors(documentSymbols, tokens));
}
public void buildHighlights(
@ -498,6 +503,66 @@ public final class SemanticIndex {
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);
}
@ -506,6 +571,10 @@ public final class SemanticIndex {
return Map.copyOf(documentSymbolsByDocument);
}
public Map<Path, List<LspStructuralAnchorDTO>> structuralAnchorsByDocument() {
return Map.copyOf(structuralAnchorsByDocument);
}
public List<LspSymbolDTO> workspaceSymbols() {
return List.copyOf(workspaceSymbols);
}
@ -521,4 +590,9 @@ public final class SemanticIndex {
public Map<Path, List<PbsToken>> 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.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;
@ -104,6 +105,12 @@ final class LspServiceImplTest {
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))),
@ -236,6 +243,16 @@ final class LspServiceImplTest {
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);
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package p.studio.workspaces.editor;
import org.junit.jupiter.api.Test;
import p.studio.lsp.dtos.LspRangeDTO;
import p.studio.lsp.dtos.LspStructuralAnchorDTO;
import p.studio.lsp.dtos.LspSymbolDTO;
import p.studio.lsp.messages.LspSymbolKind;
@ -108,6 +109,33 @@ final class EditorDocumentScopeGuideModelTest {
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(
final String content,
final String startMarker,