implements PLN-0137 (4/7) document-formatting

Advertise full-document formatting only. The formatter reprints the
token stream and changes indentation by four spaces where brace or
parenthesis nesting changes. Doc text-block interiors stay as written,
comments stay on their current line, and declarations are not reordered.
Range formatting and on-type formatting stay off.
This commit is contained in:
bQUARKz 2026-09-22 08:50:18 +01:00
parent ce2e3e4cd3
commit 7629a73e0a
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
17 changed files with 366 additions and 0 deletions

View File

@ -294,6 +294,8 @@ When a frontend exposes folding ranges, the ranges MUST come from recovered synt
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. 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.
When a frontend exposes formatting, it MUST be full-document formatting only. Range formatting and on-type formatting MUST NOT be advertised. The formatter MUST reprint the existing token stream. It MUST NOT reorder declarations, join lines, or split lines. Indentation MUST be four spaces and MUST change only where brace or parenthesis nesting changes. The interior of a `Doc` text block MUST be copied unchanged. A comment MUST stay on the line of the token it already follows. A missing formatting capability MUST NOT be advertised and MUST produce an empty edit 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.

View File

@ -16,6 +16,7 @@ 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.PbsDocumentFormatter;
import p.studio.compiler.pbs.PbsStructureRanges; 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;
@ -475,6 +476,17 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
offsets); offsets);
} }
@Override
public boolean formattingSupported() {
return true;
}
@Override
public Optional<String> formatDocument(final FrontendDocumentRequest request) {
return Optional.of(PbsDocumentFormatter.format(
request == null || request.documentText() == null ? "" : request.documentText()));
}
@Override @Override
public boolean codeActionsSupported() { public boolean codeActionsSupported() {
return true; return true;

View File

@ -0,0 +1,179 @@
package p.studio.compiler.pbs;
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.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;
public final class PbsDocumentFormatter {
private static final int INDENT_SPACES = 4;
private PbsDocumentFormatter() {
}
public static String format(final String documentText) {
final String text = documentText == null ? "" : documentText;
if (text.isEmpty()) {
return text;
}
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
final ArrayList<SourceLine> lines = splitLines(text);
final StringBuilder formatted = new StringBuilder(text.length());
int depth = 0;
for (final SourceLine line : lines) {
if (isDocInterior(line, tokens)) {
formatted.append(line.content()).append(line.ending());
continue;
}
final int leadingClosers = leadingClosers(line, tokens);
final int indent = Math.max(0, depth - leadingClosers);
formatted.append(reindent(line.content(), indent)).append(line.ending());
depth = depthAfter(line, tokens, depth);
}
return formatted.toString();
}
private static boolean isDocInterior(final SourceLine line, final ReadOnlyList<PbsToken> tokens) {
for (final PbsToken token : tokens) {
if (token.kind() != PbsTokenKind.DOC_TEXT_BLOCK) {
continue;
}
if (line.startByte() > token.start() && line.endByte() <= token.end()) {
return true;
}
}
return false;
}
private static int leadingClosers(final SourceLine line, final ReadOnlyList<PbsToken> tokens) {
int closers = 0;
boolean seenCode = false;
for (final PbsToken token : tokens) {
if (token.start() < line.startByte() || token.start() >= line.endByte()) {
continue;
}
if (token.kind() == PbsTokenKind.COMMENT) {
continue;
}
if (!seenCode && isCloser(token.kind())) {
closers += 1;
continue;
}
seenCode = true;
}
return closers;
}
private static int depthAfter(
final SourceLine line,
final ReadOnlyList<PbsToken> tokens,
final int depth) {
int next = depth;
for (final PbsToken token : tokens) {
if (token.start() < line.startByte() || token.start() >= line.endByte()) {
continue;
}
if (isOpener(token.kind())) {
next += 1;
} else if (isCloser(token.kind())) {
next = Math.max(0, next - 1);
}
}
return next;
}
private static boolean isOpener(final PbsTokenKind kind) {
return kind == PbsTokenKind.LEFT_BRACE || kind == PbsTokenKind.LEFT_PAREN;
}
private static boolean isCloser(final PbsTokenKind kind) {
return kind == PbsTokenKind.RIGHT_BRACE || kind == PbsTokenKind.RIGHT_PAREN;
}
private static String reindent(final String content, final int indent) {
int index = 0;
while (index < content.length()) {
final char character = content.charAt(index);
if (character != ' ' && character != '\t') {
break;
}
index += 1;
}
if (index == content.length()) {
return content;
}
return " ".repeat(indent * INDENT_SPACES) + content.substring(index);
}
private static ArrayList<SourceLine> splitLines(final String text) {
final ArrayList<SourceLine> lines = new ArrayList<>();
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
int charIndex = 0;
int byteIndex = 0;
int lineCharStart = 0;
int lineByteStart = 0;
while (charIndex < text.length()) {
final int codePoint = text.codePointAt(charIndex);
final int charWidth = Character.charCount(codePoint);
final int byteWidth = utf8Length(codePoint);
if (codePoint == '\n') {
lines.add(new SourceLine(
text.substring(lineCharStart, charIndex),
"\n",
lineByteStart,
byteIndex));
charIndex += charWidth;
byteIndex += byteWidth;
lineCharStart = charIndex;
lineByteStart = byteIndex;
continue;
}
if (codePoint == '\r' && charIndex + charWidth < text.length() && text.codePointAt(charIndex + charWidth) == '\n') {
lines.add(new SourceLine(
text.substring(lineCharStart, charIndex),
"\r\n",
lineByteStart,
byteIndex));
charIndex += charWidth + 1;
byteIndex += byteWidth + 1;
lineCharStart = charIndex;
lineByteStart = byteIndex;
continue;
}
charIndex += charWidth;
byteIndex += byteWidth;
}
if (lineCharStart < text.length()) {
lines.add(new SourceLine(
text.substring(lineCharStart),
"",
lineByteStart,
bytes.length));
}
if (lines.isEmpty()) {
lines.add(new SourceLine(text, "", 0, bytes.length));
}
return lines;
}
private static int utf8Length(final int codePoint) {
if (codePoint <= 0x7F) {
return 1;
}
if (codePoint <= 0x7FF) {
return 2;
}
if (codePoint <= 0xFFFF) {
return 3;
}
return 4;
}
private record SourceLine(String content, String ending, int startByte, int endByte) {
}
}

View File

@ -0,0 +1,53 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsDocumentFormatterTest {
@Test
void indentsOnlyWhereBraceOrParenNestingChanges() {
final String source = """
fn length(value: int) -> int {
return value;
}
fn other() -> int {
return 1;
}
""";
final String formatted = PbsDocumentFormatter.format(source);
assertEquals("""
fn length(value: int) -> int {
return value;
}
fn other() -> int {
return 1;
}
""", formatted);
assertTrue(formatted.indexOf("fn length") < formatted.indexOf("fn other"));
assertFalse(formatted.contains("\nreturn value;\nreturn 1;"));
}
@Test
void leavesDocTextBlockInteriorAndTrailingCommentUntouched() {
final String source = """
[Doc(markdown = \"\"\"
hello
\"\"\")]
fn length() -> int { // keep
return 1;
}
""";
final String formatted = PbsDocumentFormatter.format(source);
assertTrue(formatted.contains(" hello\n"));
assertTrue(formatted.contains("fn length() -> int { // keep"));
assertTrue(formatted.contains("\n return 1;\n"));
}
}

View File

@ -184,6 +184,14 @@ public interface FrontendLanguageService {
return selectionRanges(request, offsets); return selectionRanges(request, offsets);
} }
default boolean formattingSupported() {
return false;
}
default java.util.Optional<String> formatDocument(final FrontendDocumentRequest request) {
return java.util.Optional.empty();
}
default boolean codeActionsSupported() { default boolean codeActionsSupported() {
return false; return false;
} }

View File

@ -36,6 +36,8 @@ class FrontendLanguageServiceTest {
assertFalse(service.foldingRangesSupported()); assertFalse(service.foldingRangesSupported());
assertTrue(service.foldingRanges(request).isEmpty()); assertTrue(service.foldingRanges(request).isEmpty());
assertTrue(service.foldingRanges(request, null).isEmpty()); assertTrue(service.foldingRanges(request, null).isEmpty());
assertFalse(service.formattingSupported());
assertTrue(service.formatDocument(request).isEmpty());
assertFalse(service.selectionRangesSupported()); assertFalse(service.selectionRangesSupported());
assertTrue(service.selectionRanges(request, List.of(0)).isEmpty()); assertTrue(service.selectionRanges(request, List.of(0)).isEmpty());
assertTrue(service.selectionRanges(request, null, List.of(0)).isEmpty()); assertTrue(service.selectionRanges(request, null, List.of(0)).isEmpty());

View File

@ -0,0 +1,13 @@
package p.studio.lsp.messages;
import java.util.List;
public record BaselineFormatting(List<BaselineTextEdit> edits) {
public BaselineFormatting {
edits = edits == null ? List.of() : List.copyOf(edits);
}
public static BaselineFormatting empty() {
return new BaselineFormatting(List.of());
}
}

View File

@ -19,6 +19,7 @@ public record BaselineServerDescription(
boolean documentLinksSupported, boolean documentLinksSupported,
boolean foldingRangesSupported, boolean foldingRangesSupported,
boolean selectionRangesSupported, boolean selectionRangesSupported,
boolean formattingSupported,
List<String> semanticTokenTypes, List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections, List<BaselineSemanticHostProjection> semanticHostProjections,
List<BaselineVisualTheme> visualThemes, List<BaselineVisualTheme> visualThemes,

View File

@ -0,0 +1,18 @@
package p.studio.lsp.messages;
import java.util.Objects;
public record BaselineTextEdit(
int startLine,
int startCharacter,
int endLine,
int endCharacter,
String newText) {
public BaselineTextEdit {
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
throw new IllegalArgumentException("text edit coordinates must not be negative");
}
newText = Objects.requireNonNull(newText, "newText");
}
}

View File

@ -11,6 +11,7 @@ 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.BaselineFoldingRanges;
import p.studio.lsp.messages.BaselineFormatting;
import p.studio.lsp.messages.BaselineSelectionRanges; import p.studio.lsp.messages.BaselineSelectionRanges;
import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.DocumentPosition;
import p.studio.lsp.messages.BaselinePrepareRename; import p.studio.lsp.messages.BaselinePrepareRename;
@ -72,6 +73,10 @@ public interface LanguageServiceBridge {
return BaselineFoldingRanges.empty(); return BaselineFoldingRanges.empty();
} }
default BaselineFormatting formatDocument(LspProjectContext context, String documentUri, String text) {
return BaselineFormatting.empty();
}
default BaselineSelectionRanges selectionRanges( default BaselineSelectionRanges selectionRanges(
LspProjectContext context, LspProjectContext context,
String documentUri, String documentUri,

View File

@ -69,6 +69,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
final boolean selectionRangesSupported = languageService final boolean selectionRangesSupported = languageService
.map(FrontendLanguageService::selectionRangesSupported) .map(FrontendLanguageService::selectionRangesSupported)
.orElse(false); .orElse(false);
final boolean formattingSupported = languageService
.map(FrontendLanguageService::formattingSupported)
.orElse(false);
return new BaselineServerDescription( return new BaselineServerDescription(
"Prometeu Studio LSP", "Prometeu Studio LSP",
"0.1.0", "0.1.0",
@ -85,6 +88,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
documentLinksSupported, documentLinksSupported,
foldingRangesSupported, foldingRangesSupported,
selectionRangesSupported, selectionRangesSupported,
formattingSupported,
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(),
@ -403,6 +407,34 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
.orElseGet(BaselineDocumentLinks::empty); .orElseGet(BaselineDocumentLinks::empty);
} }
@Override
public BaselineFormatting formatDocument(
final LspProjectContext context,
final String documentUri,
final String text) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty() || !languageService.orElseThrow().formattingSupported()) {
return BaselineFormatting.empty();
}
final String effectiveText = text == null ? "" : text;
final String formatted = languageService.orElseThrow().formatDocument(new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
effectiveText)).orElse(effectiveText);
if (formatted.equals(effectiveText)) {
return BaselineFormatting.empty();
}
final DocumentPosition end = new DocumentPositionMapper(effectiveText)
.positionOf(effectiveText.getBytes(StandardCharsets.UTF_8).length);
return new BaselineFormatting(List.of(new BaselineTextEdit(
0,
0,
end.line(),
end.character(),
formatted)));
}
@Override @Override
public BaselineFoldingRanges foldingRanges( public BaselineFoldingRanges foldingRanges(
final LspProjectContext context, final LspProjectContext context,

View File

@ -169,6 +169,14 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
} }
@Override
public CompletableFuture<List<? extends TextEdit>> formatting(final DocumentFormattingParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapFormatting(
languageServiceBridge.formatDocument(project, uri, text)));
}
@Override @Override
public CompletableFuture<List<FoldingRange>> foldingRange(final FoldingRangeRequestParams params) { public CompletableFuture<List<FoldingRange>> foldingRange(final FoldingRangeRequestParams params) {
final String uri = params.getTextDocument().getUri(); final String uri = params.getTextDocument().getUri();

View File

@ -64,6 +64,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
if (description.selectionRangesSupported()) { if (description.selectionRangesSupported()) {
capabilities.setSelectionRangeProvider(true); capabilities.setSelectionRangeProvider(true);
} }
if (description.formattingSupported()) {
capabilities.setDocumentFormattingProvider(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);
@ -187,6 +190,22 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return List.copyOf(mapped); return List.copyOf(mapped);
} }
@Override
public List<TextEdit> mapFormatting(final BaselineFormatting formatting) {
if (formatting == null || formatting.edits().isEmpty()) {
return List.of();
}
final ArrayList<TextEdit> mapped = new ArrayList<>();
for (final BaselineTextEdit edit : formatting.edits()) {
mapped.add(new TextEdit(
new Range(
new Position(edit.startLine(), edit.startCharacter()),
new Position(edit.endLine(), edit.endCharacter())),
edit.newText()));
}
return List.copyOf(mapped);
}
@Override @Override
public List<FoldingRange> mapFoldingRanges(final BaselineFoldingRanges ranges) { public List<FoldingRange> mapFoldingRanges(final BaselineFoldingRanges ranges) {
if (ranges == null || ranges.ranges().isEmpty()) { if (ranges == null || ranges.ranges().isEmpty()) {

View File

@ -12,6 +12,7 @@ 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.FoldingRange;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.SelectionRange; 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;
@ -24,6 +25,7 @@ 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.BaselineFoldingRanges;
import p.studio.lsp.messages.BaselineFormatting;
import p.studio.lsp.messages.BaselineSelectionRanges; 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;
@ -73,6 +75,10 @@ public interface ProtocolMessageMapper {
return List.of(); return List.of();
} }
default List<TextEdit> mapFormatting(BaselineFormatting formatting) {
return List.of();
}
default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) { default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) {
return List.of(); return List.of();
} }

View File

@ -78,6 +78,7 @@ 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.formatDocument(context, documentUri, "fn main() {}").edits().isEmpty());
assertTrue(bridge.foldingRanges(context, documentUri, "fn main() {}").ranges().isEmpty()); assertTrue(bridge.foldingRanges(context, documentUri, "fn main() {}").ranges().isEmpty());
assertTrue(bridge.selectionRanges(context, documentUri, "fn main() {}", List.of(new DocumentPosition(0, 0))) assertTrue(bridge.selectionRanges(context, documentUri, "fn main() {}", List.of(new DocumentPosition(0, 0)))
.ranges().isEmpty()); .ranges().isEmpty());
@ -778,6 +779,7 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(description.documentLinksSupported()); assertTrue(description.documentLinksSupported());
assertTrue(description.foldingRangesSupported()); assertTrue(description.foldingRangesSupported());
assertTrue(description.selectionRangesSupported()); assertTrue(description.selectionRangesSupported());
assertTrue(description.formattingSupported());
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()

View File

@ -108,6 +108,7 @@ class PrometeuLanguageServerTest {
false, false,
false, false,
false, false,
false,
List.of(), List.of(),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",

View File

@ -60,6 +60,7 @@ final class Lsp4jProtocolMessageMapperTest {
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",
@ -90,6 +91,9 @@ final class Lsp4jProtocolMessageMapperTest {
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().getFoldingRangeProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getSelectionRangeProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getSelectionRangeProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentFormattingProvider().getLeft());
assertNull(result.getCapabilities().getDocumentRangeFormattingProvider());
assertNull(result.getCapabilities().getDocumentOnTypeFormattingProvider());
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"));
@ -343,6 +347,7 @@ final class Lsp4jProtocolMessageMapperTest {
documentLinksSupported, documentLinksSupported,
documentLinksSupported, documentLinksSupported,
documentLinksSupported, documentLinksSupported,
documentLinksSupported,
List.of("demo-keyword"), List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",