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.
This commit is contained in:
parent
3320f222bb
commit
ce2e3e4cd3
@ -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 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.
|
`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.
|
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.
|
||||||
|
|||||||
@ -16,12 +16,15 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum
|
|||||||
import p.studio.compiler.models.SourceKind;
|
import p.studio.compiler.models.SourceKind;
|
||||||
import p.studio.compiler.pbs.semantics.PbsEditorialRename;
|
import p.studio.compiler.pbs.semantics.PbsEditorialRename;
|
||||||
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
|
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.PbsQuickFix;
|
||||||
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
|
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
|
||||||
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
||||||
import p.studio.compiler.services.FrontendCodeAction;
|
import p.studio.compiler.services.FrontendCodeAction;
|
||||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||||
import p.studio.compiler.services.FrontendDocumentLink;
|
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.FrontendDefinitionLocation;
|
||||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||||
@ -443,6 +446,35 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
|||||||
return List.copyOf(links);
|
return List.copyOf(links);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean foldingRangesSupported() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FrontendFoldingRange> 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<FrontendSelectionRange> selectionRanges(
|
||||||
|
final FrontendDocumentRequest request,
|
||||||
|
final FrontendEditorialContext editorialContext,
|
||||||
|
final List<Integer> offsets) {
|
||||||
|
return PbsStructureRanges.selectionRanges(
|
||||||
|
request == null || request.documentText() == null ? "" : request.documentText(),
|
||||||
|
offsets);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean codeActionsSupported() {
|
public boolean codeActionsSupported() {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -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<FrontendFoldingRange> foldingRanges(final String documentText) {
|
||||||
|
final String text = documentText == null ? "" : documentText;
|
||||||
|
final ReadOnlyList<PbsToken> 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<FrontendSelectionRange> selectionRanges(
|
||||||
|
final String documentText,
|
||||||
|
final List<Integer> offsets) {
|
||||||
|
final String text = documentText == null ? "" : documentText;
|
||||||
|
final ReadOnlyList<PbsToken> 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<SpanPair> delimiters = delimiters(tokens, textEnd(text));
|
||||||
|
final ArrayList<FrontendSelectionRange> 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<FrontendFoldingRange> foldingRanges(
|
||||||
|
final String text,
|
||||||
|
final PbsAst.File ast,
|
||||||
|
final ReadOnlyList<PbsToken> tokens) {
|
||||||
|
final Collected collected = collect(ast);
|
||||||
|
final LinkedHashSet<String> emitted = new LinkedHashSet<>();
|
||||||
|
final ArrayList<SpanPair> 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<PbsToken> tokens,
|
||||||
|
final Collected collected,
|
||||||
|
final List<SpanPair> 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<PbsAst.Attribute> 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<SpanPair> delimiters(
|
||||||
|
final ReadOnlyList<PbsToken> tokens,
|
||||||
|
final int textEnd) {
|
||||||
|
final ArrayList<PbsToken> stack = new ArrayList<>();
|
||||||
|
final ArrayList<SpanPair> 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<SpanPair> identifierSpans(final ReadOnlyList<PbsToken> tokens) {
|
||||||
|
final ArrayList<SpanPair> 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<SpanPair> 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<SpanPair> spans, final Span span) {
|
||||||
|
if (span == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addPair(spans, toOffset(span.getStart()), toOffset(span.getEnd()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPair(final List<SpanPair> 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<SpanPair> ranges,
|
||||||
|
final LinkedHashSet<String> 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<SpanPair> folding = new ArrayList<>();
|
||||||
|
private final ArrayList<SpanPair> covered = new ArrayList<>();
|
||||||
|
private final ArrayList<SpanPair> blocks = new ArrayList<>();
|
||||||
|
private final ArrayList<SpanPair> declarations = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<SpanPair> folding() {
|
||||||
|
return folding;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cover(final Span span) {
|
||||||
|
addSpan(covered, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedHashSet<String> coveredKeys() {
|
||||||
|
final LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||||
|
for (final SpanPair span : covered) {
|
||||||
|
keys.add(span.key());
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SpanPair> blocks() {
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SpanPair> declarations() {
|
||||||
|
return declarations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -153,6 +153,37 @@ public interface FrontendLanguageService {
|
|||||||
return documentLinks(request);
|
return documentLinks(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean foldingRangesSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<FrontendFoldingRange> foldingRanges(final FrontendDocumentRequest request) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<FrontendFoldingRange> foldingRanges(
|
||||||
|
final FrontendDocumentRequest request,
|
||||||
|
final FrontendEditorialContext editorialContext) {
|
||||||
|
return foldingRanges(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean selectionRangesSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<FrontendSelectionRange> selectionRanges(
|
||||||
|
final FrontendDocumentRequest request,
|
||||||
|
final List<Integer> offsets) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<FrontendSelectionRange> selectionRanges(
|
||||||
|
final FrontendDocumentRequest request,
|
||||||
|
final FrontendEditorialContext editorialContext,
|
||||||
|
final List<Integer> offsets) {
|
||||||
|
return selectionRanges(request, offsets);
|
||||||
|
}
|
||||||
|
|
||||||
default boolean codeActionsSupported() {
|
default boolean codeActionsSupported() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,6 +33,12 @@ class FrontendLanguageServiceTest {
|
|||||||
assertFalse(service.documentLinksSupported());
|
assertFalse(service.documentLinksSupported());
|
||||||
assertTrue(service.documentLinks(request).isEmpty());
|
assertTrue(service.documentLinks(request).isEmpty());
|
||||||
assertTrue(service.documentLinks(request, null).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());
|
assertFalse(service.codeActionsSupported());
|
||||||
assertTrue(service.codeActions(request).isEmpty());
|
assertTrue(service.codeActions(request).isEmpty());
|
||||||
assertTrue(service.codeActions(request, null).isEmpty());
|
assertTrue(service.codeActions(request, null).isEmpty());
|
||||||
|
|||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package p.studio.lsp.messages;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record BaselineFoldingRanges(List<BaselineFoldingRange> ranges) {
|
||||||
|
public BaselineFoldingRanges {
|
||||||
|
ranges = ranges == null ? List.of() : List.copyOf(ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BaselineFoldingRanges empty() {
|
||||||
|
return new BaselineFoldingRanges(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package p.studio.lsp.messages;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record BaselineSelectionRanges(List<BaselineSelectionRange> ranges) {
|
||||||
|
public BaselineSelectionRanges {
|
||||||
|
ranges = ranges == null ? List.of() : List.copyOf(ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BaselineSelectionRanges empty() {
|
||||||
|
return new BaselineSelectionRanges(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,8 @@ public record BaselineServerDescription(
|
|||||||
boolean renameSupported,
|
boolean renameSupported,
|
||||||
boolean codeActionsSupported,
|
boolean codeActionsSupported,
|
||||||
boolean documentLinksSupported,
|
boolean documentLinksSupported,
|
||||||
|
boolean foldingRangesSupported,
|
||||||
|
boolean selectionRangesSupported,
|
||||||
List<String> semanticTokenTypes,
|
List<String> semanticTokenTypes,
|
||||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||||
List<BaselineVisualTheme> visualThemes,
|
List<BaselineVisualTheme> visualThemes,
|
||||||
|
|||||||
@ -10,6 +10,9 @@ import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
|||||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||||
import p.studio.lsp.messages.BaselineCodeActions;
|
import p.studio.lsp.messages.BaselineCodeActions;
|
||||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
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.BaselinePrepareRename;
|
||||||
import p.studio.lsp.messages.BaselineRename;
|
import p.studio.lsp.messages.BaselineRename;
|
||||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||||
@ -65,6 +68,18 @@ public interface LanguageServiceBridge {
|
|||||||
return BaselineDocumentLinks.empty();
|
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<DocumentPosition> positions) {
|
||||||
|
return BaselineSelectionRanges.empty();
|
||||||
|
}
|
||||||
|
|
||||||
BaselineCodeActions codeActions(
|
BaselineCodeActions codeActions(
|
||||||
LspProjectContext context,
|
LspProjectContext context,
|
||||||
String documentUri,
|
String documentUri,
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import p.studio.compiler.models.*;
|
|||||||
import p.studio.compiler.services.FrontendCodeAction;
|
import p.studio.compiler.services.FrontendCodeAction;
|
||||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||||
import p.studio.compiler.services.FrontendDocumentLink;
|
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.FrontendLanguageService;
|
||||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||||
@ -61,6 +63,12 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
|||||||
final boolean documentLinksSupported = languageService
|
final boolean documentLinksSupported = languageService
|
||||||
.map(FrontendLanguageService::documentLinksSupported)
|
.map(FrontendLanguageService::documentLinksSupported)
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
|
final boolean foldingRangesSupported = languageService
|
||||||
|
.map(FrontendLanguageService::foldingRangesSupported)
|
||||||
|
.orElse(false);
|
||||||
|
final boolean selectionRangesSupported = languageService
|
||||||
|
.map(FrontendLanguageService::selectionRangesSupported)
|
||||||
|
.orElse(false);
|
||||||
return new BaselineServerDescription(
|
return new BaselineServerDescription(
|
||||||
"Prometeu Studio LSP",
|
"Prometeu Studio LSP",
|
||||||
"0.1.0",
|
"0.1.0",
|
||||||
@ -75,6 +83,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
|||||||
languageServicePresent,
|
languageServicePresent,
|
||||||
codeActionsSupported,
|
codeActionsSupported,
|
||||||
documentLinksSupported,
|
documentLinksSupported,
|
||||||
|
foldingRangesSupported,
|
||||||
|
selectionRangesSupported,
|
||||||
presentation.semanticKeys(),
|
presentation.semanticKeys(),
|
||||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||||
@ -393,6 +403,120 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
|||||||
.orElseGet(BaselineDocumentLinks::empty);
|
.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<DocumentPosition> 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<DocumentPosition> 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<Integer> 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<BaselineFoldingRange> mapFoldingRanges(
|
||||||
|
final List<FrontendFoldingRange> ranges,
|
||||||
|
final DocumentPositionMapper mapper) {
|
||||||
|
if (ranges == null || ranges.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final ArrayList<BaselineFoldingRange> 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<BaselineSelectionRange> mapSelectionRanges(
|
||||||
|
final List<FrontendSelectionRange> ranges,
|
||||||
|
final DocumentPositionMapper mapper) {
|
||||||
|
if (ranges == null || ranges.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final ArrayList<BaselineSelectionRange> 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
|
@Override
|
||||||
public BaselineCodeActions codeActions(
|
public BaselineCodeActions codeActions(
|
||||||
final LspProjectContext context,
|
final LspProjectContext context,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
|
|||||||
import org.eclipse.lsp4j.services.LanguageClient;
|
import org.eclipse.lsp4j.services.LanguageClient;
|
||||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||||
|
import p.studio.lsp.messages.DocumentPosition;
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.services.LanguageServiceBridge;
|
import p.studio.lsp.services.LanguageServiceBridge;
|
||||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||||
@ -168,6 +169,31 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
|||||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
|
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<FoldingRange>> 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<List<SelectionRange>> selectionRange(final SelectionRangeParams params) {
|
||||||
|
final String uri = params.getTextDocument().getUri();
|
||||||
|
final String text = documentTextByUri.get(uri);
|
||||||
|
final ArrayList<DocumentPosition> 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
|
@Override
|
||||||
public CompletableFuture<List<DocumentLink>> documentLink(final DocumentLinkParams params) {
|
public CompletableFuture<List<DocumentLink>> documentLink(final DocumentLinkParams params) {
|
||||||
final String uri = params.getTextDocument().getUri();
|
final String uri = params.getTextDocument().getUri();
|
||||||
|
|||||||
@ -58,6 +58,12 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
|||||||
documentLinkOptions.setResolveProvider(false);
|
documentLinkOptions.setResolveProvider(false);
|
||||||
capabilities.setDocumentLinkProvider(documentLinkOptions);
|
capabilities.setDocumentLinkProvider(documentLinkOptions);
|
||||||
}
|
}
|
||||||
|
if (description.foldingRangesSupported()) {
|
||||||
|
capabilities.setFoldingRangeProvider(true);
|
||||||
|
}
|
||||||
|
if (description.selectionRangesSupported()) {
|
||||||
|
capabilities.setSelectionRangeProvider(true);
|
||||||
|
}
|
||||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||||
semanticTokens.setFull(true);
|
semanticTokens.setFull(true);
|
||||||
@ -181,6 +187,47 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
|||||||
return List.copyOf(mapped);
|
return List.copyOf(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FoldingRange> mapFoldingRanges(final BaselineFoldingRanges ranges) {
|
||||||
|
if (ranges == null || ranges.ranges().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final ArrayList<FoldingRange> 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<SelectionRange> mapSelectionRanges(final BaselineSelectionRanges ranges) {
|
||||||
|
if (ranges == null || ranges.ranges().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final ArrayList<SelectionRange> 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
|
@Override
|
||||||
public List<Either<Command, CodeAction>> mapCodeActions(
|
public List<Either<Command, CodeAction>> mapCodeActions(
|
||||||
final BaselineCodeActions actions,
|
final BaselineCodeActions actions,
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import org.eclipse.lsp4j.SemanticTokens;
|
|||||||
import org.eclipse.lsp4j.Diagnostic;
|
import org.eclipse.lsp4j.Diagnostic;
|
||||||
import org.eclipse.lsp4j.DocumentLink;
|
import org.eclipse.lsp4j.DocumentLink;
|
||||||
import org.eclipse.lsp4j.DocumentSymbol;
|
import org.eclipse.lsp4j.DocumentSymbol;
|
||||||
|
import org.eclipse.lsp4j.FoldingRange;
|
||||||
|
import org.eclipse.lsp4j.SelectionRange;
|
||||||
import org.eclipse.lsp4j.Location;
|
import org.eclipse.lsp4j.Location;
|
||||||
import org.eclipse.lsp4j.Range;
|
import org.eclipse.lsp4j.Range;
|
||||||
import org.eclipse.lsp4j.SymbolInformation;
|
import org.eclipse.lsp4j.SymbolInformation;
|
||||||
@ -21,6 +23,8 @@ import org.eclipse.lsp4j.PrepareRenameDefaultBehavior;
|
|||||||
import org.eclipse.lsp4j.PrepareRenameResult;
|
import org.eclipse.lsp4j.PrepareRenameResult;
|
||||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
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.BaselineDocumentSymbols;
|
||||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||||
import p.studio.lsp.messages.BaselineCodeActions;
|
import p.studio.lsp.messages.BaselineCodeActions;
|
||||||
@ -65,6 +69,14 @@ public interface ProtocolMessageMapper {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default List<FoldingRange> mapFoldingRanges(BaselineFoldingRanges ranges) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
CompletionList mapCompletion(BaselineCompletion completion);
|
CompletionList mapCompletion(BaselineCompletion completion);
|
||||||
|
|
||||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import p.studio.compiler.services.FrontendProvider;
|
|||||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||||
|
import p.studio.lsp.messages.DocumentPosition;
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.utilities.logs.LogAggregator;
|
import p.studio.utilities.logs.LogAggregator;
|
||||||
import p.studio.utilities.structures.ReadOnlySet;
|
import p.studio.utilities.structures.ReadOnlySet;
|
||||||
@ -77,6 +78,9 @@ class CompilerLanguageServiceBridgeTest {
|
|||||||
new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1)))
|
new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1)))
|
||||||
.actions().isEmpty());
|
.actions().isEmpty());
|
||||||
assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().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
|
@Test
|
||||||
@ -772,6 +776,8 @@ class CompilerLanguageServiceBridgeTest {
|
|||||||
assertTrue(description.renameSupported());
|
assertTrue(description.renameSupported());
|
||||||
assertTrue(description.codeActionsSupported());
|
assertTrue(description.codeActionsSupported());
|
||||||
assertTrue(description.documentLinksSupported());
|
assertTrue(description.documentLinksSupported());
|
||||||
|
assertTrue(description.foldingRangesSupported());
|
||||||
|
assertTrue(description.selectionRangesSupported());
|
||||||
assertEquals(1, description.semanticHostProjections().size());
|
assertEquals(1, description.semanticHostProjections().size());
|
||||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||||
|
|||||||
@ -106,6 +106,8 @@ class PrometeuLanguageServerTest {
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
List.of(new BaselineSemanticHostProjection(
|
List.of(new BaselineSemanticHostProjection(
|
||||||
"vscode",
|
"vscode",
|
||||||
|
|||||||
@ -58,6 +58,8 @@ final class Lsp4jProtocolMessageMapperTest {
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
List.of("demo-keyword"),
|
List.of("demo-keyword"),
|
||||||
List.of(new BaselineSemanticHostProjection(
|
List.of(new BaselineSemanticHostProjection(
|
||||||
"vscode",
|
"vscode",
|
||||||
@ -86,6 +88,8 @@ final class Lsp4jProtocolMessageMapperTest {
|
|||||||
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
|
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
|
||||||
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
|
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
|
||||||
assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().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 experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||||
@ -308,6 +312,8 @@ final class Lsp4jProtocolMessageMapperTest {
|
|||||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||||
final var unsupported = mapper.mapInitializeResult(serverDescription(false));
|
final var unsupported = mapper.mapInitializeResult(serverDescription(false));
|
||||||
assertNull(unsupported.getCapabilities().getDocumentLinkProvider());
|
assertNull(unsupported.getCapabilities().getDocumentLinkProvider());
|
||||||
|
assertNull(unsupported.getCapabilities().getFoldingRangeProvider());
|
||||||
|
assertNull(unsupported.getCapabilities().getSelectionRangeProvider());
|
||||||
|
|
||||||
final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of(
|
final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of(
|
||||||
new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25))));
|
new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25))));
|
||||||
@ -335,6 +341,8 @@ final class Lsp4jProtocolMessageMapperTest {
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
documentLinksSupported,
|
documentLinksSupported,
|
||||||
|
documentLinksSupported,
|
||||||
|
documentLinksSupported,
|
||||||
List.of("demo-keyword"),
|
List.of("demo-keyword"),
|
||||||
List.of(new BaselineSemanticHostProjection(
|
List.of(new BaselineSemanticHostProjection(
|
||||||
"vscode",
|
"vscode",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user