diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 6e4ee852..bd780fbb 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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":[]} diff --git a/discussion/workflow/plans/PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md b/discussion/workflow/plans/PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md index b82f7705..cadf8be4 100644 --- a/discussion/workflow/plans/PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md +++ b/discussion/workflow/plans/PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md @@ -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 diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspStructuralAnchorDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspStructuralAnchorDTO.java new file mode 100644 index 00000000..334f6f8b --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspStructuralAnchorDTO.java @@ -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 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")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java index d144f0a7..fa68b695 100644 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java @@ -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 diagnostics, List semanticHighlights, List documentSymbols, + List structuralAnchors, List 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")); } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java index b9886740..38ab8116 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java @@ -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()); } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java index 4300141a..c7dd9d06 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java @@ -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()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java index 5c4721fd..12048f7f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java @@ -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> diagnosticsByDocument, Map> semanticHighlightsByDocument, Map> documentSymbolsByDocument, + Map> structuralAnchorsByDocument, List workspaceSymbols, Map> symbolsByName, Map> tokensByDocument) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java index ad543f3f..50fa3a81 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java @@ -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> semanticHighlightsByDocument = new LinkedHashMap<>(); private final Map> documentSymbolsByDocument = new LinkedHashMap<>(); + private final Map> structuralAnchorsByDocument = new LinkedHashMap<>(); private final List workspaceSymbols = new ArrayList<>(); private final Map> symbolsByName = new LinkedHashMap<>(); private final Map> 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 structuralAnchors( + final List documentSymbols, + final List tokens) { + final List 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 tokens) { + final List 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 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> semanticHighlightsByDocument() { return Map.copyOf(semanticHighlightsByDocument); } @@ -506,6 +571,10 @@ public final class SemanticIndex { return Map.copyOf(documentSymbolsByDocument); } + public Map> structuralAnchorsByDocument() { + return Map.copyOf(structuralAnchorsByDocument); + } + public List workspaceSymbols() { return List.copyOf(workspaceSymbols); } @@ -521,4 +590,9 @@ public final class SemanticIndex { public Map> tokensByDocument() { return Map.copyOf(tokensByDocument); } + + private record AnchorPair( + LspRangeDTO start, + LspRangeDTO end) { + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java index 6789a060..175ef67f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java @@ -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 flattenAnchors( + final List anchors) { + final List 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); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java index e7b2512d..b38a6706 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java @@ -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 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 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 EditorDocumentScopeGuideModel fromRanges( + final String content, + final List roots, + final int initialDepth, + final Integer initialParentId, + final RangeFactory rangeFactory, + final ChildAccessor childAccessor) { final List lineStarts = lineStarts(content); final List 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 void appendRangeNode( final List ranges, final List lineStarts, final int contentLength, - final LspSymbolDTO symbol, + final T node, final int depth, final Integer parentId, - final int[] nextId) { + final int[] nextId, + final RangeFactory rangeFactory, + final ChildAccessor 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 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 { + List children(T node); + } + + @FunctionalInterface + private interface RangeFactory { + GuideRange create( + T node, + int depth, + Integer parentId, + int[] nextId, + List lineStarts, + int contentLength); + } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java index 58526e9f..68941f43 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java @@ -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()); } diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java index a17b9959..92ecc737 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java @@ -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( diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java index dd1be621..b5efe311 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java @@ -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,