From ce2e3e4cd3c227413d7ca265ecdc5f3cef5cffbd Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 08:45:31 +0100 Subject: [PATCH] implements PLN-0137 (3/7) folding-and-selection-ranges Advertise folding and selection only when the frontend supports them. Folding uses recovered spans for the listed declarations, their bodies, parameter lists, and Doc text blocks. A brace, parenthesis, or text block the tree does not cover still folds, including an unmatched opener through the end of the text. Selection grows outward from the identifier to the argument or parameter list, the block, and the declaration. --- ...ler Pipeline Entry Points Specification.md | 4 + .../compiler/PBSFrontendLanguageService.java | 32 ++ .../compiler/pbs/PbsStructureRanges.java | 380 ++++++++++++++++++ .../compiler/pbs/PbsStructureRangesTest.java | 73 ++++ .../services/FrontendFoldingRange.java | 15 + .../services/FrontendLanguageService.java | 31 ++ .../services/FrontendSelectionRange.java | 16 + .../services/FrontendLanguageServiceTest.java | 6 + .../lsp/messages/BaselineFoldingRange.java | 14 + .../lsp/messages/BaselineFoldingRanges.java | 13 + .../lsp/messages/BaselineSelectionRange.java | 15 + .../lsp/messages/BaselineSelectionRanges.java | 13 + .../messages/BaselineServerDescription.java | 2 + .../lsp/services/LanguageServiceBridge.java | 15 + .../CompilerLanguageServiceBridge.java | 124 ++++++ .../protocol/PrometeuTextDocumentService.java | 26 ++ .../mapping/Lsp4jProtocolMessageMapper.java | 47 +++ .../mapping/ProtocolMessageMapper.java | 12 + .../CompilerLanguageServiceBridgeTest.java | 6 + .../protocol/PrometeuLanguageServerTest.java | 2 + .../Lsp4jProtocolMessageMapperTest.java | 8 + 21 files changed, 854 insertions(+) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index 7e2850c5..ab924de8 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -290,6 +290,10 @@ When tooling publishes an editor diagnostic, `source` MUST be the `languageId` o When a frontend exposes document links, each link MUST cover the span of a module reference whose resolved destination is a compiler-known regular file. A module reference that does not resolve to a regular file MUST produce no link. Tooling MUST NOT target virtual, untitled, or synthetic URIs, including virtual stdlib documents. Document links MUST NOT replace definition. A missing document-link capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error. +When a frontend exposes folding ranges, the ranges MUST come from recovered syntax spans for top-level `fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, and `host` declarations, their bodies, and `Doc` text blocks. A parameter list MUST fold from the parenthesis pair. A brace, parenthesis, or text-block delimiter that the recovered tree does not cover MUST still produce a token range, including an unmatched opener through the end of the text. Folding ranges MUST NOT be document symbols. + +When a frontend exposes selection ranges, the chain at a cursor MUST run from the inside outward in this order: identifier, argument or parameter list, block or `Doc` text block, declaration. A layer that does not contain the cursor MUST be omitted. A missing folding or selection capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error. + `FrontendSpec` remains the source of static frontend-owned presentation metadata such as semantic vocabularies, host projections, and visual themes. Producing semantic tokens for a live document is an optional editor-facing capability; the existence of static presentation metadata MUST NOT imply that every frontend can provide live semantic-token results. The frontend registry MUST resolve providers by `languageId`. A lookup for an unknown `languageId` MUST fail explicitly with a diagnostic-friendly error. Unknown languages MUST NOT silently fall back to PBS or to any other frontend. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index 8333ede3..14439297 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -16,12 +16,15 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.semantics.PbsEditorialRename; import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol; +import p.studio.compiler.pbs.PbsStructureRanges; import p.studio.compiler.pbs.semantics.PbsQuickFix; import p.studio.compiler.pbs.semantics.PbsQuickFixCollector; import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendDocumentLink; +import p.studio.compiler.services.FrontendFoldingRange; +import p.studio.compiler.services.FrontendSelectionRange; import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentSymbol; @@ -443,6 +446,35 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return List.copyOf(links); } + @Override + public boolean foldingRangesSupported() { + return true; + } + + @Override + public List foldingRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext) { + return PbsStructureRanges.foldingRanges(request == null || request.documentText() == null + ? "" + : request.documentText()); + } + + @Override + public boolean selectionRangesSupported() { + return true; + } + + @Override + public List selectionRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final List offsets) { + return PbsStructureRanges.selectionRanges( + request == null || request.documentText() == null ? "" : request.documentText(), + offsets); + } + @Override public boolean codeActionsSupported() { return true; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java new file mode 100644 index 00000000..e5d6c127 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java @@ -0,0 +1,380 @@ +package p.studio.compiler.pbs; + +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.services.FrontendFoldingRange; +import p.studio.compiler.services.FrontendSelectionRange; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; + +public final class PbsStructureRanges { + private PbsStructureRanges() { + } + + public static List foldingRanges(final String documentText) { + final String text = documentText == null ? "" : documentText; + final ReadOnlyList tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty()); + final PbsAst.File ast = PbsParser.parse(tokens, FileId.none(), DiagnosticSink.empty()); + return foldingRanges(text, ast, tokens); + } + + public static List selectionRanges( + final String documentText, + final List offsets) { + final String text = documentText == null ? "" : documentText; + final ReadOnlyList tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty()); + final PbsAst.File ast = PbsParser.parse(tokens, FileId.none(), DiagnosticSink.empty()); + if (offsets == null || offsets.isEmpty()) { + return List.of(); + } + final Collected collected = collect(ast); + final List delimiters = delimiters(tokens, textEnd(text)); + final ArrayList ranges = new ArrayList<>(); + for (final Integer offset : offsets) { + if (offset == null || offset < 0) { + continue; + } + final FrontendSelectionRange range = selectionAt(offset, tokens, collected, delimiters); + if (range != null) { + ranges.add(range); + } + } + return List.copyOf(ranges); + } + + private static List foldingRanges( + final String text, + final PbsAst.File ast, + final ReadOnlyList tokens) { + final Collected collected = collect(ast); + final LinkedHashSet emitted = new LinkedHashSet<>(); + final ArrayList ranges = new ArrayList<>(); + for (final SpanPair span : collected.folding()) { + addRange(ranges, emitted, span); + } + for (final SpanPair delimiter : delimiters(tokens, textEnd(text))) { + if (collected.coveredKeys().contains(delimiter.key())) { + continue; + } + addRange(ranges, emitted, delimiter); + } + ranges.sort(Comparator.comparingInt(SpanPair::start).thenComparingInt(SpanPair::end)); + return ranges.stream() + .map(span -> new FrontendFoldingRange(span.start(), span.end())) + .toList(); + } + + private static FrontendSelectionRange selectionAt( + final int offset, + final ReadOnlyList tokens, + final Collected collected, + final List delimiters) { + final SpanPair identifier = smallest(identifierSpans(tokens), offset); + final SpanPair parameters = smallest(delimiters, offset); + final SpanPair block = smallest(collected.blocks(), offset); + final SpanPair declaration = smallest(collected.declarations(), offset); + FrontendSelectionRange node = null; + SpanPair previous = null; + for (final SpanPair layer : new SpanPair[] {declaration, block, parameters, identifier}) { + if (layer == null) { + continue; + } + if (previous != null && previous.start() == layer.start() && previous.end() == layer.end()) { + continue; + } + node = new FrontendSelectionRange(layer.start(), layer.end(), node); + previous = layer; + } + return node; + } + + private static Collected collect(final PbsAst.File ast) { + final Collected collected = new Collected(); + if (ast == null || ast.topDecls() == null) { + return collected; + } + for (final PbsAst.TopDecl topDecl : ast.topDecls()) { + visitTop(topDecl, true, collected); + } + return collected; + } + + private static void visitTop( + final PbsAst.TopDecl topDecl, + final boolean topLevel, + final Collected collected) { + if (topDecl == null) { + return; + } + if (topLevel && isListedDeclaration(topDecl)) { + addSpan(collected.folding(), topDecl.span()); + } + if (isListedDeclaration(topDecl) || topDecl instanceof PbsAst.FunctionDecl) { + addSpan(collected.declarations(), topDecl.span()); + } + switch (topDecl) { + case PbsAst.FunctionDecl functionDecl -> visitFunction(functionDecl, collected); + case PbsAst.StructDecl structDecl -> { + visitAttributes(structDecl.attributes(), collected); + if (structDecl.methods() != null) { + for (final PbsAst.FunctionDecl method : structDecl.methods()) { + visitTop(method, false, collected); + } + } + if (structDecl.ctors() != null) { + for (final PbsAst.CtorDecl ctor : structDecl.ctors()) { + visitCtor(ctor, collected); + } + } + } + case PbsAst.ServiceDecl serviceDecl -> { + visitAttributes(serviceDecl.attributes(), collected); + if (serviceDecl.methods() != null) { + for (final PbsAst.FunctionDecl method : serviceDecl.methods()) { + visitTop(method, false, collected); + } + } + } + case PbsAst.ContractDecl contractDecl -> visitAttributes(contractDecl.attributes(), collected); + case PbsAst.HostDecl hostDecl -> visitAttributes(hostDecl.attributes(), collected); + case PbsAst.EnumDecl enumDecl -> visitAttributes(enumDecl.attributes(), collected); + case PbsAst.ErrorDecl errorDecl -> visitAttributes(errorDecl.attributes(), collected); + case PbsAst.CallbackDecl callbackDecl -> visitAttributes(callbackDecl.attributes(), collected); + default -> { + } + } + } + + private static void visitFunction( + final PbsAst.FunctionDecl functionDecl, + final Collected collected) { + visitAttributes(functionDecl.attributes(), collected); + coverBlock(functionDecl.body(), true, collected); + } + + private static void visitCtor( + final PbsAst.CtorDecl ctor, + final Collected collected) { + if (ctor == null) { + return; + } + addSpan(collected.declarations(), ctor.span()); + coverBlock(ctor.body(), true, collected); + } + + private static void visitAttributes( + final ReadOnlyList attributes, + final Collected collected) { + if (attributes == null) { + return; + } + for (final PbsAst.Attribute attribute : attributes) { + if (attribute == null || attribute.arguments() == null) { + continue; + } + for (final PbsAst.AttributeArgument argument : attribute.arguments()) { + if (argument != null && argument.value() instanceof PbsAst.AttributeDocTextBlockValue doc) { + addSpan(collected.folding(), doc.span()); + addSpan(collected.blocks(), doc.span()); + collected.cover(doc.span()); + } + } + } + } + + private static void coverBlock( + final PbsAst.Block block, + final boolean emit, + final Collected collected) { + if (block == null) { + return; + } + if (emit) { + addSpan(collected.folding(), block.span()); + } + addSpan(collected.blocks(), block.span()); + collected.cover(block.span()); + if (block.statements() == null) { + return; + } + for (final PbsAst.Statement statement : block.statements()) { + coverStatement(statement, collected); + } + } + + private static void coverStatement( + final PbsAst.Statement statement, + final Collected collected) { + switch (statement) { + case null -> { + } + case PbsAst.IfStatement ifStatement -> { + coverBlock(ifStatement.thenBlock(), false, collected); + coverBlock(ifStatement.elseBlock(), false, collected); + coverStatement(ifStatement.elseIf(), collected); + } + case PbsAst.ForStatement forStatement -> coverBlock(forStatement.body(), false, collected); + case PbsAst.WhileStatement whileStatement -> coverBlock(whileStatement.body(), false, collected); + default -> { + } + } + } + + private static boolean isListedDeclaration(final PbsAst.TopDecl topDecl) { + return topDecl instanceof PbsAst.FunctionDecl + || topDecl instanceof PbsAst.StructDecl + || topDecl instanceof PbsAst.ServiceDecl + || topDecl instanceof PbsAst.ContractDecl + || topDecl instanceof PbsAst.HostDecl + || topDecl instanceof PbsAst.EnumDecl + || topDecl instanceof PbsAst.ErrorDecl + || topDecl instanceof PbsAst.CallbackDecl; + } + + private static List delimiters( + final ReadOnlyList tokens, + final int textEnd) { + final ArrayList stack = new ArrayList<>(); + final ArrayList pairs = new ArrayList<>(); + if (tokens == null) { + return pairs; + } + for (final PbsToken token : tokens) { + if (token == null) { + continue; + } + if (token.kind() == PbsTokenKind.LEFT_BRACE || token.kind() == PbsTokenKind.LEFT_PAREN) { + stack.add(token); + continue; + } + if (token.kind() == PbsTokenKind.DOC_TEXT_BLOCK) { + addPair(pairs, token.start(), token.end()); + continue; + } + if (token.kind() == PbsTokenKind.RIGHT_BRACE || token.kind() == PbsTokenKind.RIGHT_PAREN) { + final PbsTokenKind openKind = token.kind() == PbsTokenKind.RIGHT_BRACE + ? PbsTokenKind.LEFT_BRACE + : PbsTokenKind.LEFT_PAREN; + if (!stack.isEmpty() && stack.getLast().kind() == openKind) { + final PbsToken open = stack.removeLast(); + addPair(pairs, open.start(), token.end()); + } + } + } + for (final PbsToken open : stack) { + addPair(pairs, open.start(), textEnd); + } + return pairs; + } + + private static List identifierSpans(final ReadOnlyList tokens) { + final ArrayList spans = new ArrayList<>(); + if (tokens == null) { + return spans; + } + for (final PbsToken token : tokens) { + if (token != null && token.kind() == PbsTokenKind.IDENTIFIER) { + addPair(spans, token.start(), token.end()); + } + } + return spans; + } + + private static SpanPair smallest(final List spans, final int offset) { + SpanPair best = null; + for (final SpanPair span : spans) { + if (span.start() > offset || offset >= span.end()) { + continue; + } + if (best == null + || span.end() - span.start() < best.end() - best.start() + || (span.end() - span.start() == best.end() - best.start() && span.start() > best.start())) { + best = span; + } + } + return best; + } + + private static void addSpan(final List spans, final Span span) { + if (span == null) { + return; + } + addPair(spans, toOffset(span.getStart()), toOffset(span.getEnd())); + } + + private static void addPair(final List spans, final int start, final int end) { + if (start < 0 || end <= start) { + return; + } + spans.add(new SpanPair(start, end)); + } + + private static void addRange( + final List ranges, + final LinkedHashSet emitted, + final SpanPair span) { + if (span == null || !emitted.add(span.key())) { + return; + } + ranges.add(span); + } + + private static int textEnd(final String text) { + return text.getBytes(StandardCharsets.UTF_8).length; + } + + private static int toOffset(final long value) { + if (value <= 0L) { + return 0; + } + return (int) Math.min(Integer.MAX_VALUE, value); + } + + private record SpanPair(int start, int end) { + private String key() { + return start + ":" + end; + } + } + + private static final class Collected { + private final ArrayList folding = new ArrayList<>(); + private final ArrayList covered = new ArrayList<>(); + private final ArrayList blocks = new ArrayList<>(); + private final ArrayList declarations = new ArrayList<>(); + + private List folding() { + return folding; + } + + private void cover(final Span span) { + addSpan(covered, span); + } + + private LinkedHashSet coveredKeys() { + final LinkedHashSet keys = new LinkedHashSet<>(); + for (final SpanPair span : covered) { + keys.add(span.key()); + } + return keys; + } + + private List blocks() { + return blocks; + } + + private List declarations() { + return declarations; + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java new file mode 100644 index 00000000..7f2737ec --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java @@ -0,0 +1,73 @@ +package p.studio.compiler.pbs; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.services.FrontendFoldingRange; +import p.studio.compiler.services.FrontendSelectionRange; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PbsStructureRangesTest { + private static final String SOURCE = """ + [Doc(markdown = \"\"\" + hello + \"\"\")] + fn length(value: int) -> int { + return value; + } + """; + + @Test + void foldingUsesDeclarationBodyParameterListAndDocText() { + final var ranges = PbsStructureRanges.foldingRanges(SOURCE); + final var slices = ranges.stream().map(range -> slice(SOURCE, range)).toList(); + + assertTrue(slices.stream().anyMatch(slice -> slice.contains("hello")), slices.toString()); + assertTrue(slices.stream().anyMatch(slice -> slice.equals("(value: int)")), slices.toString()); + assertTrue(slices.stream().anyMatch(slice -> slice.startsWith("{")), slices.toString()); + assertTrue(slices.stream().anyMatch(slice -> slice.contains("fn length")), slices.toString()); + } + + @Test + void selectionGrowsFromIdentifierToBlockToDeclaration() { + final int marker = SOURCE.indexOf("return value"); + assertTrue(marker >= 0, SOURCE); + final int offset = marker + "return ".length(); + final var selections = PbsStructureRanges.selectionRanges(SOURCE, List.of(offset)); + assertEquals(1, selections.size(), "offset " + offset); + final FrontendSelectionRange selection = selections.getFirst(); + + assertEquals("value", slice(SOURCE, selection.startOffset(), selection.endOffset())); + assertNotNull(selection.parent()); + assertTrue(slice(SOURCE, selection.parent().startOffset(), selection.parent().endOffset()).startsWith("{")); + assertNotNull(selection.parent().parent()); + assertTrue(slice(SOURCE, selection.parent().parent().startOffset(), selection.parent().parent().endOffset()) + .contains("fn length")); + } + + @Test + void unmatchedBraceFoldsThroughTheEndOfTheText() { + final String broken = """ + fn broken() -> void { + let x: int = 1; + """; + final int textEnd = broken.getBytes(StandardCharsets.UTF_8).length; + final var ranges = PbsStructureRanges.foldingRanges(broken); + + assertTrue(ranges.stream().anyMatch(range -> + range.endOffset() == textEnd && slice(broken, range).startsWith("{"))); + } + + private static String slice(final String text, final FrontendFoldingRange range) { + return slice(text, range.startOffset(), range.endOffset()); + } + + private static String slice(final String text, final int start, final int end) { + final byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + return new String(bytes, start, end - start, StandardCharsets.UTF_8); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java new file mode 100644 index 00000000..1e0d977f --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java @@ -0,0 +1,15 @@ +package p.studio.compiler.services; + +public record FrontendFoldingRange( + int startOffset, + int endOffset) { + + public FrontendFoldingRange { + if (startOffset < 0) { + throw new IllegalArgumentException("startOffset must not be negative"); + } + if (endOffset < startOffset) { + throw new IllegalArgumentException("endOffset must not be before startOffset"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 5934fd1e..48a33e00 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -153,6 +153,37 @@ public interface FrontendLanguageService { return documentLinks(request); } + default boolean foldingRangesSupported() { + return false; + } + + default List foldingRanges(final FrontendDocumentRequest request) { + return List.of(); + } + + default List foldingRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext) { + return foldingRanges(request); + } + + default boolean selectionRangesSupported() { + return false; + } + + default List selectionRanges( + final FrontendDocumentRequest request, + final List offsets) { + return List.of(); + } + + default List selectionRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final List offsets) { + return selectionRanges(request, offsets); + } + default boolean codeActionsSupported() { return false; } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java new file mode 100644 index 00000000..15750bc9 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java @@ -0,0 +1,16 @@ +package p.studio.compiler.services; + +public record FrontendSelectionRange( + int startOffset, + int endOffset, + FrontendSelectionRange parent) { + + public FrontendSelectionRange { + if (startOffset < 0) { + throw new IllegalArgumentException("startOffset must not be negative"); + } + if (endOffset < startOffset) { + throw new IllegalArgumentException("endOffset must not be before startOffset"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index a5c5a7d5..dfb2d63d 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -33,6 +33,12 @@ class FrontendLanguageServiceTest { assertFalse(service.documentLinksSupported()); assertTrue(service.documentLinks(request).isEmpty()); assertTrue(service.documentLinks(request, null).isEmpty()); + assertFalse(service.foldingRangesSupported()); + assertTrue(service.foldingRanges(request).isEmpty()); + assertTrue(service.foldingRanges(request, null).isEmpty()); + assertFalse(service.selectionRangesSupported()); + assertTrue(service.selectionRanges(request, List.of(0)).isEmpty()); + assertTrue(service.selectionRanges(request, null, List.of(0)).isEmpty()); assertFalse(service.codeActionsSupported()); assertTrue(service.codeActions(request).isEmpty()); assertTrue(service.codeActions(request, null).isEmpty()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java new file mode 100644 index 00000000..877122e5 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java @@ -0,0 +1,14 @@ +package p.studio.lsp.messages; + +public record BaselineFoldingRange( + int startLine, + int startCharacter, + int endLine, + int endCharacter) { + + public BaselineFoldingRange { + if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) { + throw new IllegalArgumentException("folding range coordinates must not be negative"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java new file mode 100644 index 00000000..0d759b9a --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java @@ -0,0 +1,13 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineFoldingRanges(List ranges) { + public BaselineFoldingRanges { + ranges = ranges == null ? List.of() : List.copyOf(ranges); + } + + public static BaselineFoldingRanges empty() { + return new BaselineFoldingRanges(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java new file mode 100644 index 00000000..09aac25c --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java @@ -0,0 +1,15 @@ +package p.studio.lsp.messages; + +public record BaselineSelectionRange( + int startLine, + int startCharacter, + int endLine, + int endCharacter, + BaselineSelectionRange parent) { + + public BaselineSelectionRange { + if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) { + throw new IllegalArgumentException("selection range coordinates must not be negative"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java new file mode 100644 index 00000000..94cc6abc --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java @@ -0,0 +1,13 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineSelectionRanges(List ranges) { + public BaselineSelectionRanges { + ranges = ranges == null ? List.of() : List.copyOf(ranges); + } + + public static BaselineSelectionRanges empty() { + return new BaselineSelectionRanges(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 85e3c003..ef9eba20 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -17,6 +17,8 @@ public record BaselineServerDescription( boolean renameSupported, boolean codeActionsSupported, boolean documentLinksSupported, + boolean foldingRangesSupported, + boolean selectionRangesSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index 838c073a..f704f8df 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -10,6 +10,9 @@ import p.studio.lsp.messages.BaselineWorkspaceSymbols; import p.studio.lsp.messages.BaselineCodeActionDiagnostic; import p.studio.lsp.messages.BaselineCodeActions; import p.studio.lsp.messages.BaselineDocumentLinks; +import p.studio.lsp.messages.BaselineFoldingRanges; +import p.studio.lsp.messages.BaselineSelectionRanges; +import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.BaselinePrepareRename; import p.studio.lsp.messages.BaselineRename; import p.studio.lsp.messages.BaselineSemanticTokens; @@ -65,6 +68,18 @@ public interface LanguageServiceBridge { return BaselineDocumentLinks.empty(); } + default BaselineFoldingRanges foldingRanges(LspProjectContext context, String documentUri, String text) { + return BaselineFoldingRanges.empty(); + } + + default BaselineSelectionRanges selectionRanges( + LspProjectContext context, + String documentUri, + String text, + List positions) { + return BaselineSelectionRanges.empty(); + } + BaselineCodeActions codeActions( LspProjectContext context, String documentUri, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index f34faec2..cf85b739 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -7,6 +7,8 @@ import p.studio.compiler.models.*; import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendDocumentLink; +import p.studio.compiler.services.FrontendFoldingRange; +import p.studio.compiler.services.FrontendSelectionRange; import p.studio.compiler.services.FrontendLanguageService; import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; @@ -61,6 +63,12 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final boolean documentLinksSupported = languageService .map(FrontendLanguageService::documentLinksSupported) .orElse(false); + final boolean foldingRangesSupported = languageService + .map(FrontendLanguageService::foldingRangesSupported) + .orElse(false); + final boolean selectionRangesSupported = languageService + .map(FrontendLanguageService::selectionRangesSupported) + .orElse(false); return new BaselineServerDescription( "Prometeu Studio LSP", "0.1.0", @@ -75,6 +83,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg languageServicePresent, codeActionsSupported, documentLinksSupported, + foldingRangesSupported, + selectionRangesSupported, presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -393,6 +403,120 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(BaselineDocumentLinks::empty); } + @Override + public BaselineFoldingRanges foldingRanges( + final LspProjectContext context, + final String documentUri, + final String text) { + return structureRanges(context, documentUri, text, true, List.of()).folding(); + } + + @Override + public BaselineSelectionRanges selectionRanges( + final LspProjectContext context, + final String documentUri, + final String text, + final List positions) { + return structureRanges(context, documentUri, text, false, positions == null ? List.of() : positions).selection(); + } + + private StructureRanges structureRanges( + final LspProjectContext context, + final String documentUri, + final String text, + final boolean folding, + final List positions) { + Objects.requireNonNull(context, "context"); + final var languageService = frontendProvider(context).languageService(); + if (languageService.isEmpty()) { + return StructureRanges.empty(); + } + final boolean supported = folding + ? languageService.orElseThrow().foldingRangesSupported() + : languageService.orElseThrow().selectionRangesSupported(); + if (!supported) { + return StructureRanges.empty(); + } + final String effectiveText = text == null ? "" : text; + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + normalizeDocumentPath(documentUri), + effectiveText); + final DocumentPositionMapper mapper = new DocumentPositionMapper(effectiveText); + if (folding) { + return new StructureRanges(new BaselineFoldingRanges(mapFoldingRanges( + languageService.orElseThrow().foldingRanges(request, null), + mapper)), BaselineSelectionRanges.empty()); + } + final ArrayList offsets = new ArrayList<>(); + for (final DocumentPosition position : positions) { + if (position == null) { + offsets.add(0); + continue; + } + offsets.add(mapper.byteOffsetOf(position.line(), position.character())); + } + return new StructureRanges( + BaselineFoldingRanges.empty(), + new BaselineSelectionRanges(mapSelectionRanges( + languageService.orElseThrow().selectionRanges(request, null, offsets), + mapper))); + } + + private List mapFoldingRanges( + final List ranges, + final DocumentPositionMapper mapper) { + if (ranges == null || ranges.isEmpty()) { + return List.of(); + } + final ArrayList mapped = new ArrayList<>(); + for (final FrontendFoldingRange range : ranges) { + final DocumentPosition start = mapper.positionOf(range.startOffset()); + final DocumentPosition end = mapper.positionOf(range.endOffset()); + mapped.add(new BaselineFoldingRange( + start.line(), + start.character(), + end.line(), + end.character())); + } + return List.copyOf(mapped); + } + + private List mapSelectionRanges( + final List ranges, + final DocumentPositionMapper mapper) { + if (ranges == null || ranges.isEmpty()) { + return List.of(); + } + final ArrayList mapped = new ArrayList<>(); + for (final FrontendSelectionRange range : ranges) { + mapped.add(mapSelectionRange(range, mapper)); + } + return List.copyOf(mapped); + } + + private BaselineSelectionRange mapSelectionRange( + final FrontendSelectionRange range, + final DocumentPositionMapper mapper) { + if (range == null) { + return null; + } + final DocumentPosition start = mapper.positionOf(range.startOffset()); + final DocumentPosition end = mapper.positionOf(range.endOffset()); + return new BaselineSelectionRange( + start.line(), + start.character(), + end.line(), + end.character(), + mapSelectionRange(range.parent(), mapper)); + } + + private record StructureRanges(BaselineFoldingRanges folding, BaselineSelectionRanges selection) { + private static StructureRanges empty() { + return new StructureRanges(BaselineFoldingRanges.empty(), BaselineSelectionRanges.empty()); + } + } + @Override public BaselineCodeActions codeActions( final LspProjectContext context, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index 4d4cad03..a8b8ea89 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -9,6 +9,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import p.studio.lsp.messages.BaselineCodeActionDiagnostic; +import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.services.LanguageServiceBridge; import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; @@ -168,6 +169,31 @@ public final class PrometeuTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); } + @Override + public CompletableFuture> foldingRange(final FoldingRangeRequestParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + return CompletableFuture.completedFuture(protocolMessageMapper.mapFoldingRanges( + languageServiceBridge.foldingRanges(project, uri, text))); + } + + @Override + public CompletableFuture> selectionRange(final SelectionRangeParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + final ArrayList positions = new ArrayList<>(); + if (params.getPositions() != null) { + for (final Position position : params.getPositions()) { + if (position == null) { + continue; + } + positions.add(new DocumentPosition(position.getLine(), position.getCharacter())); + } + } + return CompletableFuture.completedFuture(protocolMessageMapper.mapSelectionRanges( + languageServiceBridge.selectionRanges(project, uri, text, positions))); + } + @Override public CompletableFuture> documentLink(final DocumentLinkParams params) { final String uri = params.getTextDocument().getUri(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index b3608e2e..e1bdf41c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -58,6 +58,12 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { documentLinkOptions.setResolveProvider(false); capabilities.setDocumentLinkProvider(documentLinkOptions); } + if (description.foldingRangesSupported()) { + capabilities.setFoldingRangeProvider(true); + } + if (description.selectionRangesSupported()) { + capabilities.setSelectionRangeProvider(true); + } final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -181,6 +187,47 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return List.copyOf(mapped); } + @Override + public List mapFoldingRanges(final BaselineFoldingRanges ranges) { + if (ranges == null || ranges.ranges().isEmpty()) { + return List.of(); + } + final ArrayList mapped = new ArrayList<>(); + for (final BaselineFoldingRange range : ranges.ranges()) { + final FoldingRange foldingRange = new FoldingRange(); + foldingRange.setStartLine(range.startLine()); + foldingRange.setStartCharacter(range.startCharacter()); + foldingRange.setEndLine(range.endLine()); + foldingRange.setEndCharacter(range.endCharacter()); + mapped.add(foldingRange); + } + return List.copyOf(mapped); + } + + @Override + public List mapSelectionRanges(final BaselineSelectionRanges ranges) { + if (ranges == null || ranges.ranges().isEmpty()) { + return List.of(); + } + final ArrayList mapped = new ArrayList<>(); + for (final BaselineSelectionRange range : ranges.ranges()) { + mapped.add(mapSelectionRange(range)); + } + return List.copyOf(mapped); + } + + private SelectionRange mapSelectionRange(final BaselineSelectionRange range) { + if (range == null) { + return null; + } + final SelectionRange selectionRange = new SelectionRange(); + selectionRange.setRange(new Range( + new Position(range.startLine(), range.startCharacter()), + new Position(range.endLine(), range.endCharacter()))); + selectionRange.setParent(mapSelectionRange(range.parent())); + return selectionRange; + } + @Override public List> mapCodeActions( final BaselineCodeActions actions, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index 51bb2f84..b7de3e24 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -11,6 +11,8 @@ import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; @@ -21,6 +23,8 @@ import org.eclipse.lsp4j.PrepareRenameDefaultBehavior; import org.eclipse.lsp4j.PrepareRenameResult; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentLinks; +import p.studio.lsp.messages.BaselineFoldingRanges; +import p.studio.lsp.messages.BaselineSelectionRanges; import p.studio.lsp.messages.BaselineDocumentSymbols; import p.studio.lsp.messages.BaselineWorkspaceSymbols; import p.studio.lsp.messages.BaselineCodeActions; @@ -65,6 +69,14 @@ public interface ProtocolMessageMapper { return List.of(); } + default List mapFoldingRanges(BaselineFoldingRanges ranges) { + return List.of(); + } + + default List mapSelectionRanges(BaselineSelectionRanges ranges) { + return List.of(); + } + CompletionList mapCompletion(BaselineCompletion completion); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 3f00e44b..a9983601 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -11,6 +11,7 @@ import p.studio.compiler.services.FrontendProvider; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.lsp.messages.BaselineCodeActionDiagnostic; import p.studio.lsp.messages.BaselineIssueSeverity; +import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.LspProjectContext; import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.structures.ReadOnlySet; @@ -77,6 +78,9 @@ class CompilerLanguageServiceBridgeTest { new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1))) .actions().isEmpty()); assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().isEmpty()); + assertTrue(bridge.foldingRanges(context, documentUri, "fn main() {}").ranges().isEmpty()); + assertTrue(bridge.selectionRanges(context, documentUri, "fn main() {}", List.of(new DocumentPosition(0, 0))) + .ranges().isEmpty()); } @Test @@ -772,6 +776,8 @@ class CompilerLanguageServiceBridgeTest { assertTrue(description.renameSupported()); assertTrue(description.codeActionsSupported()); assertTrue(description.documentLinksSupported()); + assertTrue(description.foldingRangesSupported()); + assertTrue(description.selectionRangesSupported()); assertEquals(1, description.semanticHostProjections().size()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 0f93d292..97d4aaee 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -106,6 +106,8 @@ class PrometeuLanguageServerTest { true, false, false, + false, + false, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index 85ce742d..284cee4b 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -58,6 +58,8 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -86,6 +88,8 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds()); assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider()); assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().getResolveProvider()); + assertEquals(Boolean.TRUE, result.getCapabilities().getFoldingRangeProvider().getLeft()); + assertEquals(Boolean.TRUE, result.getCapabilities().getSelectionRangeProvider().getLeft()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -308,6 +312,8 @@ final class Lsp4jProtocolMessageMapperTest { final var mapper = new Lsp4jProtocolMessageMapper(); final var unsupported = mapper.mapInitializeResult(serverDescription(false)); assertNull(unsupported.getCapabilities().getDocumentLinkProvider()); + assertNull(unsupported.getCapabilities().getFoldingRangeProvider()); + assertNull(unsupported.getCapabilities().getSelectionRangeProvider()); final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of( new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25)))); @@ -335,6 +341,8 @@ final class Lsp4jProtocolMessageMapperTest { true, true, documentLinksSupported, + documentLinksSupported, + documentLinksSupported, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode",