dev/pbs-lsp-remaining-editor-surface #31
@ -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