implements PLN-0036 studio inline hint rendering and rollout

This commit is contained in:
bQUARKz 2026-04-03 11:40:52 +01:00
parent 09d9bb4c96
commit 55e821989c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 496 additions and 7 deletions

View File

@ -15,4 +15,4 @@
{"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]}
{"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]} {"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]}
{"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]} {"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]}
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"open","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]} {"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]}

View File

@ -0,0 +1,127 @@
---
id: PLN-0036
ticket: studio-editor-inline-type-hints-for-let-bindings
title: Studio inline hint rendering and rollout
status: done
created: 2026-04-03
completed: 2026-04-03
tags: [studio, editor, inline-hints, rendering, rollout, richtextfx]
---
## Objective
Implement the Studio host rendering path for transported inline hints, including any explicit transitional stage and convergence to real inline rendering.
## Background
DEC-0015 requires the final user-visible capability to be real inline rendering in the editor flow. A transitional approximate stage is allowed only if explicitly treated as a rollout stage and not as the final architecture. Hints must remain decorative and must survive partial valid spans under degraded analysis.
The current editor uses `CodeArea` with `StyleSpans`, gutter graphics, and semantic overlays, which is sufficient for color and gutter behavior but not obviously sufficient for true inline non-text decorations without substrate work.
## Scope
### Included
- Add a Studio host rendering path for transported inline hints.
- Preserve decorative-only behavior.
- Preserve valid hints under partial degradation.
- If needed, implement an explicit transitional rendering wave and a follow-up convergence step to real inline rendering.
- Add editor tests for rendering behavior and document-model isolation.
### Excluded
- Frontend hint eligibility policy.
- LSP transport contract design.
- Non-editor consumers of hint payloads.
## Execution Steps
### Step 1 - Evaluate and choose the editor substrate for real inline rendering
**What:** Determine the exact editor primitive needed to support true inline hint rendering.
**How:** Evaluate the current `CodeArea`-based surface and decide whether real inline hints can be supported by extension or require a migration to a richer editor substrate.
This step must explicitly document:
- why the chosen substrate can satisfy the decision;
- whether a transitional approximation is required;
- what migration path exists if the current substrate is insufficient.
**File(s):**
- `prometeu-studio/src/main/java/p/studio/workspaces/editor/`
- any editor utility or custom rendering surfaces introduced by the chosen approach
### Step 2 - Implement decorative host rendering for transported hints
**What:** Render LSP-transported hints in the editor without mutating document text.
**How:** Add a host rendering layer that:
- consumes inline hint payloads from analyze results;
- renders them at deterministic anchors;
- keeps them out of the persisted text model;
- keeps caret, selection, copy/paste, and editing semantics text-only.
If a transitional approximation is required, it must be explicitly isolated and labeled as wave 1 rather than as final architecture.
**File(s):**
- `prometeu-studio/src/main/java/p/studio/workspaces/editor/`
- `prometeu-studio/src/main/resources/themes/` if hint-specific host styling is needed
### Step 3 - Converge to real inline rendering if wave 1 is approximate
**What:** Ensure the implementation reaches the decision-mandated final state.
**How:** If step 2 introduces an approximate rendering wave, add the explicit follow-up work needed to move to true inline rendering and retire the approximation.
This convergence work must not be left implicit.
**File(s):**
- the same editor rendering surfaces chosen in steps 1 and 2
### Step 4 - Add Studio rendering and interaction tests
**What:** Lock decorative behavior and partial-degradation rendering in Studio tests.
**How:** Add tests that verify:
- hints render when transported;
- hints do not modify the text buffer;
- hints do not become part of copy/paste semantics;
- valid hints remain visible even when only some hint spans are available;
- the final path uses real inline rendering once the convergence step is complete.
**File(s):**
- `prometeu-studio/src/test/java/p/studio/workspaces/editor/`
## Test Requirements
### Unit Tests
- Rendering-layer tests for anchor mapping and decorative-only behavior.
- Tests ensuring text buffer content remains unchanged.
### Integration Tests
- Editor tests consuming analyze results with inline hints.
- Partial-degradation tests preserving valid hint spans.
- Convergence tests for the final inline-real path if a transitional wave exists.
### Manual Verification
- Open a PBS file with inferred `let` bindings and confirm hints render next to the binding without entering the file text.
- Confirm cursor movement and copy/paste ignore hint text.
- Confirm partially valid hints remain visible when some bindings fail analysis.
## Acceptance Criteria
- [ ] Studio can render transported inline hints without mutating document text.
- [ ] Decorative-only interaction rules are enforced.
- [ ] Partial valid hints remain visible under degraded analysis.
- [ ] If a transitional approximation exists, the path to real inline rendering is explicitly implemented and not left as implied follow-up.
## Dependencies
- Accepted decision `DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md`
- `PLN-0033-inline-hint-spec-and-contract-propagation.md`
- `PLN-0034-lsp-inline-hint-transport-contract.md`
- `PLN-0035-pbs-inline-type-hint-payload-production.md`
## Risks
- The current editor substrate may not support inline-real rendering cleanly without deeper refactoring.
- A temporary approximation may linger unless the convergence step is explicitly tracked and executed.
- Decorative behavior may be accidentally violated if the rendering layer leaks into text model operations.

View File

@ -1,16 +1,20 @@
package p.studio.workspaces.editor; package p.studio.workspaces.editor;
import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyleSpans;
import p.studio.lsp.dtos.LspInlineHintDTO;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Objects; import java.util.Objects;
record EditorDocumentHighlightingResult( record EditorDocumentHighlightingResult(
EditorDocumentHighlightOwner owner, EditorDocumentHighlightOwner owner,
StyleSpans<Collection<String>> styleSpans) { StyleSpans<Collection<String>> styleSpans,
List<LspInlineHintDTO> inlineHints) {
EditorDocumentHighlightingResult { EditorDocumentHighlightingResult {
owner = Objects.requireNonNull(owner, "owner"); owner = Objects.requireNonNull(owner, "owner");
styleSpans = Objects.requireNonNull(styleSpans, "styleSpans"); styleSpans = Objects.requireNonNull(styleSpans, "styleSpans");
inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints"));
} }
} }

View File

@ -12,6 +12,7 @@ final class EditorDocumentHighlightingRouter {
final EditorDocumentPresentation presentation, final EditorDocumentPresentation presentation,
final LspAnalyzeDocumentResult analysis) { final LspAnalyzeDocumentResult analysis) {
final var localHighlighting = presentation.highlight(fileBuffer.content()); final var localHighlighting = presentation.highlight(fileBuffer.content());
final var inlineHints = EditorDocumentInlineHintRouter.route(fileBuffer, analysis);
if (fileBuffer.frontendDocument() if (fileBuffer.frontendDocument()
&& analysis != null && analysis != null
&& presentation.supportsSemanticHighlighting() && presentation.supportsSemanticHighlighting()
@ -20,10 +21,12 @@ final class EditorDocumentHighlightingRouter {
EditorDocumentHighlightOwner.LSP, EditorDocumentHighlightOwner.LSP,
EditorDocumentSemanticHighlighting.overlay( EditorDocumentSemanticHighlighting.overlay(
localHighlighting, localHighlighting,
EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights()))); EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())),
inlineHints);
} }
return new EditorDocumentHighlightingResult( return new EditorDocumentHighlightingResult(
EditorDocumentHighlightOwner.LOCAL, EditorDocumentHighlightOwner.LOCAL,
localHighlighting); localHighlighting,
inlineHints);
} }
} }

View File

@ -0,0 +1,20 @@
package p.studio.workspaces.editor;
import p.studio.lsp.dtos.LspInlineHintDTO;
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
import java.util.List;
final class EditorDocumentInlineHintRouter {
private EditorDocumentInlineHintRouter() {
}
static List<LspInlineHintDTO> route(
final EditorOpenFileBuffer fileBuffer,
final LspAnalyzeDocumentResult analysis) {
if (!fileBuffer.frontendDocument() || analysis == null || analysis.inlineHints().isEmpty()) {
return List.of();
}
return analysis.inlineHints();
}
}

View File

@ -0,0 +1,72 @@
package p.studio.workspaces.editor;
import p.studio.lsp.dtos.LspInlineHintDTO;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
final class EditorInlineHintLayout {
static final double HORIZONTAL_GAP = 6.0;
private EditorInlineHintLayout() {
}
static List<Placement> layout(
final List<LspInlineHintDTO> inlineHints,
final AnchorBoundsResolver anchorBoundsResolver) {
final var placements = new ArrayList<Placement>();
for (final var inlineHint : inlineHints) {
final var anchorBounds = anchorBoundsResolver.resolve(inlineHint);
if (anchorBounds.isEmpty()) {
continue;
}
final var bounds = anchorBounds.orElseThrow();
placements.add(new Placement(
inlineHint,
displayText(inlineHint),
categoryStyleClass(inlineHint),
bounds.maxX() + HORIZONTAL_GAP,
bounds.minY(),
bounds.height()));
}
return List.copyOf(placements);
}
static String displayText(final LspInlineHintDTO inlineHint) {
return "[%s]".formatted(inlineHint.label());
}
static String categoryStyleClass(final LspInlineHintDTO inlineHint) {
final var normalizedCategory = inlineHint.category()
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+", "")
.replaceAll("-+$", "");
return normalizedCategory.isBlank()
? "editor-inline-hint-generic"
: "editor-inline-hint-" + normalizedCategory;
}
@FunctionalInterface
interface AnchorBoundsResolver {
Optional<AnchorBounds> resolve(LspInlineHintDTO inlineHint);
}
record AnchorBounds(
double minX,
double minY,
double maxX,
double height) {
}
record Placement(
LspInlineHintDTO inlineHint,
String displayText,
String categoryStyleClass,
double screenX,
double screenY,
double anchorHeight) {
}
}

View File

@ -0,0 +1,122 @@
package p.studio.workspaces.editor;
import javafx.application.Platform;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.model.TwoDimensional;
import org.reactfx.Subscription;
import p.studio.lsp.dtos.LspInlineHintDTO;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
final class EditorInlineHintOverlay {
private final CodeArea codeArea;
private final Pane overlayPane = new Pane();
private final Map<LspInlineHintDTO, Label> labelsByHint = new LinkedHashMap<>();
private final Subscription viewportSubscription;
private List<LspInlineHintDTO> inlineHints = List.of();
EditorInlineHintOverlay(final CodeArea codeArea) {
this.codeArea = Objects.requireNonNull(codeArea, "codeArea");
overlayPane.setMouseTransparent(true);
overlayPane.setPickOnBounds(false);
overlayPane.getStyleClass().add("editor-inline-hint-layer");
codeArea.estimatedScrollXProperty().addListener((ignored, previous, current) -> refresh());
codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) -> refresh());
codeArea.layoutBoundsProperty().addListener((ignored, previous, current) -> refresh());
codeArea.textProperty().addListener((ignored, previous, current) -> refresh());
overlayPane.layoutBoundsProperty().addListener((ignored, previous, current) -> refresh());
viewportSubscription = codeArea.viewportDirtyEvents().subscribe(ignored -> refresh());
}
Node node() {
return overlayPane;
}
void setInlineHints(final List<LspInlineHintDTO> inlineHints) {
this.inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints"));
rebuildLabels();
scheduleRefresh();
}
void clear() {
setInlineHints(List.of());
}
void dispose() {
viewportSubscription.unsubscribe();
}
private void rebuildLabels() {
labelsByHint.clear();
overlayPane.getChildren().clear();
for (final var inlineHint : inlineHints) {
final var label = new Label(EditorInlineHintLayout.displayText(inlineHint));
label.setManaged(false);
label.setMouseTransparent(true);
label.setPickOnBounds(false);
label.getStyleClass().add("editor-inline-hint");
label.getStyleClass().add(EditorInlineHintLayout.categoryStyleClass(inlineHint));
labelsByHint.put(inlineHint, label);
overlayPane.getChildren().add(label);
}
}
private void scheduleRefresh() {
Platform.runLater(this::refresh);
}
private void refresh() {
if (overlayPane.getScene() == null || inlineHints.isEmpty()) {
overlayPane.getChildren().forEach(node -> node.setVisible(false));
return;
}
final var placements = EditorInlineHintLayout.layout(inlineHints, this::resolveAnchorBounds);
final var placedHints = new java.util.HashSet<LspInlineHintDTO>();
for (final var placement : placements) {
final var label = labelsByHint.get(placement.inlineHint());
if (label == null) {
continue;
}
label.applyCss();
label.autosize();
final Point2D localPoint = overlayPane.screenToLocal(placement.screenX(), placement.screenY());
if (!Double.isFinite(localPoint.getX()) || !Double.isFinite(localPoint.getY())) {
label.setVisible(false);
continue;
}
final double layoutY = localPoint.getY() + Math.max(0.0d, (placement.anchorHeight() - label.getHeight()) / 2.0d);
label.relocate(localPoint.getX(), layoutY);
label.setVisible(true);
placedHints.add(placement.inlineHint());
}
labelsByHint.forEach((inlineHint, label) -> {
if (!placedHints.contains(inlineHint)) {
label.setVisible(false);
}
});
}
private Optional<EditorInlineHintLayout.AnchorBounds> resolveAnchorBounds(final LspInlineHintDTO inlineHint) {
if (codeArea.getLength() == 0) {
return Optional.empty();
}
final int anchorOffsetExclusive = Math.max(inlineHint.anchor().startOffset() + 1, inlineHint.anchor().endOffset());
final int anchorCharOffset = Math.max(0, Math.min(codeArea.getLength() - 1, anchorOffsetExclusive - 1));
final var position = codeArea.offsetToPosition(anchorCharOffset, TwoDimensional.Bias.Forward);
final Optional<Bounds> characterBounds = codeArea.getCharacterBoundsOnScreen(position.getMajor(), position.getMinor());
return characterBounds.map(bounds -> new EditorInlineHintLayout.AnchorBounds(
bounds.getMinX(),
bounds.getMinY(),
bounds.getMaxX(),
bounds.getHeight()));
}
}

View File

@ -30,6 +30,7 @@ public final class EditorWorkspace extends Workspace {
private final BorderPane root = new BorderPane(); private final BorderPane root = new BorderPane();
private final CodeArea codeArea = new CodeArea(); private final CodeArea codeArea = new CodeArea();
private final VirtualizedScrollPane<CodeArea> codeScroller = new VirtualizedScrollPane<>(codeArea); private final VirtualizedScrollPane<CodeArea> codeScroller = new VirtualizedScrollPane<>(codeArea);
private final EditorInlineHintOverlay inlineHintOverlay = new EditorInlineHintOverlay(codeArea);
private final Button saveButton = new Button(); private final Button saveButton = new Button();
private final Button saveAllButton = new Button(); private final Button saveAllButton = new Button();
private final EditorWarningStrip warningStrip = new EditorWarningStrip(); private final EditorWarningStrip warningStrip = new EditorWarningStrip();
@ -90,7 +91,7 @@ public final class EditorWorkspace extends Workspace {
@Override @Override
public void unLoad() { public void unLoad() {
inlineHintOverlay.dispose();
} }
public CodeArea codeArea() { return codeArea; } public CodeArea codeArea() { return codeArea; }
@ -146,6 +147,7 @@ public final class EditorWorkspace extends Workspace {
try { try {
codeArea.replaceText(fileBuffer.content()); codeArea.replaceText(fileBuffer.content());
codeArea.setStyleSpans(0, highlighting.styleSpans()); codeArea.setStyleSpans(0, highlighting.styleSpans());
inlineHintOverlay.setInlineHints(highlighting.inlineHints());
codeArea.moveTo(0); codeArea.moveTo(0);
codeArea.requestFollowCaret(); codeArea.requestFollowCaret();
} finally { } finally {
@ -195,6 +197,7 @@ public final class EditorWorkspace extends Workspace {
try { try {
codeArea.replaceText(""); codeArea.replaceText("");
codeArea.setStyleSpans(0, presentation.highlight("")); codeArea.setStyleSpans(0, presentation.highlight(""));
inlineHintOverlay.clear();
codeArea.moveTo(0); codeArea.moveTo(0);
codeArea.requestFollowCaret(); codeArea.requestFollowCaret();
} finally { } finally {
@ -247,10 +250,12 @@ public final class EditorWorkspace extends Workspace {
} }
private VBox buildCenterColumn() { private VBox buildCenterColumn() {
final var editorSurface = new VBox(0, warningStrip, codeScroller); final var editorViewport = new StackPane(codeScroller, inlineHintOverlay.node());
editorViewport.getStyleClass().add("editor-workspace-editor-viewport");
final var editorSurface = new VBox(0, warningStrip, editorViewport);
editorSurface.getStyleClass().add("editor-workspace-editor-surface"); editorSurface.getStyleClass().add("editor-workspace-editor-surface");
codeScroller.getStyleClass().add("editor-workspace-code-scroller"); codeScroller.getStyleClass().add("editor-workspace-code-scroller");
VBox.setVgrow(codeScroller, Priority.ALWAYS); VBox.setVgrow(editorViewport, Priority.ALWAYS);
final var centerColumn = new VBox(12, tabStrip, editorSurface); final var centerColumn = new VBox(12, tabStrip, editorSurface);
centerColumn.getStyleClass().add("editor-workspace-center-column"); centerColumn.getStyleClass().add("editor-workspace-center-column");
VBox.setVgrow(editorSurface, Priority.ALWAYS); VBox.setVgrow(editorSurface, Priority.ALWAYS);

View File

@ -707,6 +707,25 @@
-fx-border-width: 1; -fx-border-width: 1;
} }
.editor-workspace-editor-viewport {
-fx-background-color: transparent;
}
.editor-inline-hint-layer {
-fx-background-color: transparent;
}
.editor-inline-hint {
-fx-text-fill: rgba(182, 196, 209, 0.72);
-fx-font-family: "JetBrains Mono Medium", "JetBrains Mono", "Iosevka", "Cascadia Mono", "IBM Plex Mono", monospace;
-fx-font-size: 14px;
-fx-opacity: 0.82;
}
.editor-inline-hint-type {
-fx-text-fill: rgba(135, 198, 114, 0.82);
}
.editor-workspace-code-area-type-text .text { .editor-workspace-code-area-type-text .text {
-fx-fill: #eef3f8; -fx-fill: #eef3f8;
} }

View File

@ -13,6 +13,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
final class EditorDocumentHighlightingRouterTest { final class EditorDocumentHighlightingRouterTest {
@ -47,6 +48,7 @@ final class EditorDocumentHighlightingRouterTest {
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner());
assertTrue(result.inlineHints().isEmpty());
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword"));
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function"));
} }
@ -80,6 +82,7 @@ final class EditorDocumentHighlightingRouterTest {
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner());
assertTrue(result.inlineHints().isEmpty());
} }
@Test @Test
@ -113,6 +116,7 @@ final class EditorDocumentHighlightingRouterTest {
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner());
assertTrue(result.inlineHints().isEmpty());
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword"));
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function"));
} }
@ -148,6 +152,7 @@ final class EditorDocumentHighlightingRouterTest {
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner());
assertTrue(result.inlineHints().isEmpty());
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service"));
} }
@ -182,11 +187,79 @@ final class EditorDocumentHighlightingRouterTest {
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner());
assertTrue(result.inlineHints().isEmpty());
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword"));
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service"));
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function"));
} }
@Test
void frontendDocumentsPreserveTransportedInlineHintsEvenWithoutSemanticHighlightCoverage() {
final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer(
Path.of("/tmp/example/src/main.pbs"),
"main.pbs",
"pbs",
"fn main() -> void { let value = 1; }",
"LF",
true,
VfsDocumentAccessMode.READ_ONLY,
false);
final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("highlight")),
new LspSemanticPresentationDTO(
List.of("pbs-function"),
List.of("/themes/pbs/semantic-highlighting.css")),
List.of(),
List.of(),
List.of(new p.studio.lsp.dtos.LspInlineHintDTO(new LspRangeDTO(24, 29), "int", "type")),
List.of(),
List.of(),
List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
fileBuffer,
registry.resolve("pbs", analysis.semanticPresentation()),
analysis);
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner());
assertEquals(1, result.inlineHints().size());
assertEquals("int", result.inlineHints().getFirst().label());
}
@Test
void nonFrontendDocumentsDiscardTransportedInlineHints() {
final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer(
Path.of("/tmp/example/prometeu.json"),
"prometeu.json",
"json",
"{\n \"name\": \"Example\"\n}\n",
"LF",
false,
VfsDocumentAccessMode.EDITABLE,
false);
final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult(
new LspSessionStateDTO(true, List.of("highlight")),
new LspSemanticPresentationDTO(List.of(), List.of()),
List.of(),
List.of(),
List.of(new p.studio.lsp.dtos.LspInlineHintDTO(new LspRangeDTO(0, 1), "ignored", "type")),
List.of(),
List.of(),
List.of());
final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route(
fileBuffer,
registry.resolve("json"),
analysis);
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner());
assertTrue(result.inlineHints().isEmpty());
}
private boolean containsStyle( private boolean containsStyle(
final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans, final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans,
final String styleClass) { final String styleClass) {

View File

@ -0,0 +1,44 @@
package p.studio.workspaces.editor;
import org.junit.jupiter.api.Test;
import p.studio.lsp.dtos.LspInlineHintDTO;
import p.studio.lsp.dtos.LspRangeDTO;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
final class EditorInlineHintLayoutTest {
@Test
void layoutCreatesDecorativePlacementsForHintsWithResolvedAnchors() {
final var hint = new LspInlineHintDTO(new LspRangeDTO(10, 15), "int", "type");
final var placements = EditorInlineHintLayout.layout(
List.of(hint),
ignored -> Optional.of(new EditorInlineHintLayout.AnchorBounds(20.0d, 8.0d, 40.0d, 16.0d)));
assertEquals(1, placements.size());
assertEquals("[int]", placements.getFirst().displayText());
assertEquals("editor-inline-hint-type", placements.getFirst().categoryStyleClass());
assertEquals(46.0d, placements.getFirst().screenX());
assertEquals(8.0d, placements.getFirst().screenY());
assertEquals(16.0d, placements.getFirst().anchorHeight());
}
@Test
void layoutKeepsValidHintsWhenOnlySomeAnchorsResolve() {
final var stable = new LspInlineHintDTO(new LspRangeDTO(10, 15), "int", "type");
final var missing = new LspInlineHintDTO(new LspRangeDTO(30, 35), "Player", "type");
final var placements = EditorInlineHintLayout.layout(
List.of(stable, missing),
hint -> hint == stable
? Optional.of(new EditorInlineHintLayout.AnchorBounds(12.0d, 4.0d, 28.0d, 14.0d))
: Optional.empty());
assertEquals(1, placements.size());
assertEquals(stable, placements.getFirst().inlineHint());
assertEquals("[int]", placements.getFirst().displayText());
}
}