Skip to content

feat(data-types): wire the .dt code view into the ST language server (DOPE-537) - #998

Merged
JoaoGSP merged 4 commits into
developmentfrom
feature/DOPE-537-datatype-lsp-goto-def
Aug 7, 2026
Merged

feat(data-types): wire the .dt code view into the ST language server (DOPE-537)#998
JoaoGSP merged 4 commits into
developmentfrom
feature/DOPE-537-datatype-lsp-goto-def

Conversation

@JoaoGSP

@JoaoGSP JoaoGSP commented Aug 7, 2026

Copy link
Copy Markdown
Member

Wires the per-type .dt code view (DOPE-535) into the ST language server, and upgrades go-to-definition to land inside it. Last implementation card of DOPE-385 — only the flag flip (DOPE-542) remains.

Mirror of https://github.com/Autonomy-Logic/openplc-web/pull/657

What changed

A URI for the buffer. dtViewUri(name)inmemory://dtview/<name>.dt, alongside the existing pouvars:// pair. VariablesCodeEditor gained a modelUri prop so a non-POU surface can route to the LSP; pouName still works unchanged for POUs.

One offset, many providers. New dataTypeLineSpans() in the serializer returns each type's {start, length} in the aggregate document. resolveStLspContext remaps dtview:// onto DATA_TYPES_URI with span.start - 1 — both frames open with a TYPE line, so that single shift covers completion, hover, signature help, definition, references, document symbols and formatting. findDataTypeAtLine now reuses the same helper instead of recomputing the cursor walk by hand.

Spans are derived from the serializer on demand rather than registered in body-offsets: no registration lifecycle to keep in sync, and no window where a datatype edit and a stored offset disagree.

Go-to-definition. routeToDataTypeCodeView opens the tab, switches to code mode and places the cursor at the in-entry line, tagged with a new 'data-type' cursor target so a POU jump can't bleed into a datatype tab. With the flag off it degrades to today's behaviour — form tab, no cursor.

Semantic tokens and diagnostics needed more than a shift, because the model's text and the document the answers come from are two different strings that agree only while the buffer is committed. Four pieces, each earned during manual validation:

  • The token window carries the entry's own lines and is rebased onto the view's frame through a new outputStartLine argument. Widening the window instead — which is what I did first — drags in the previous entry's last line, whose columns overrun the 4-character TYPE line, and Monaco rejects the batch with end character > model.getLineLength.
  • While the buffer diverges from the store, the window is empty. No colours beats colours describing the previous text.
  • A store change re-drives tokens via a new LanguageService.refreshSemanticTokens() (the registration already had refresh(); it just wasn't reachable). Without it, committing leaves the model's text untouched and Monaco never re-queries, so wrong colours persist until the view is remounted.
  • The diagnostics mirror caches each publish together with the spans it was computed against and replays it when a .dt model mounts later. Replaying through freshly recomputed spans lands markers on the wrong line — or drops them entirely — once the store has moved.

Verification

Validated interactively against a running project, six passes: line alignment, diagnostics (right line, no bleed between types), hover/completion, rename while the view is open, delete while it's open, and POU-side regressions. Several defects were found and fixed in that loop, including the ones described above and a go-to-definition cursor that pinned the tab in code mode — the forced switch is now keyed on the cursor's identity, not the current display, matching the variables editor.

Automated: tsc clean in both repos, validate:arch passes, eslint 0 errors, prettier clean, mirror gate diffs: 0. Editor jest 6083 pass / 0 fail. Web vitest: 6087 pass with 78 failures across 9 files that are identical on a clean development (DOPE-549). 21 tests added — span arithmetic against the rendered document, dtViewUri round-trip and non-collision, datatype go-to-definition (declaration line, struct field, out-of-range, flag-off fallback), the token-window rebase including the exact regression it fixes, and a component test pinning the toggle-after-jump behaviour.

Flag

Everything user-visible stays behind isDataTypeFilesEnabled(), still false. DOPE-542 owns the flip.

Filed while validating

  • DOPE-551 — Ctrl+hover navigates without a click; go-to-definition mutates the store inside provideDefinition. Pre-existing, affects every redirect branch.
  • DOPE-552 — the POU variables code view has the same stale-token and lost-marker defects this PR fixes for .dt. Pre-existing, confirmed by A/B.
  • DOPE-553 — deleting a referenced data type silently kills LSP analysis of the whole datatypes document. Pre-existing; recreating the type restores it.
  • RTOP-247 — strucpp resolves a STRUCT field name to a global function block when the names collide. VAR blocks and array declarations are unaffected.

Summary by CodeRabbit

  • New Features
    • Go-to-definition navigation opens referenced data types in code view and positions the cursor on the relevant declaration or field.
    • Data-type code views provide accurate navigation, syntax highlighting, diagnostics, and line mapping.
    • Users can return from code view to the table view after navigation.
    • When data-type files are unavailable, navigation falls back to the standard form view.
  • Bug Fixes
    • Improved semantic-token refresh and positioning for data-type editors.
    • Improved handling of malformed navigation references to prevent errors.

JoaoGSP and others added 2 commits August 7, 2026 17:24
Go-to-definition on a user type now lands in that type's own code view
with the cursor on the declaration — a struct field target lands on its
field line — instead of merely opening the form tab (DOPE-537).

The buffer gets a real identity (`inmemory://dtview/<name>.dt`) so the
LSP can recognise it, and `resolveStLspContext` remaps it onto the
aggregate datatypes document. Both frames open with a `TYPE` line, so a
single shift derived from the type's span covers completion, hover,
signature help, definition, references and formatting at once. The span
map is computed from the serializer rather than tracked in the offset
registry: there is no registration lifecycle to keep in sync and no
window where a datatype edit and the stored offset disagree.

Semantic tokens and diagnostics need more than a shift, because the
model's text and the document the answers come from are two different
strings that only agree while the buffer is committed:

  - The token window holds the entry's own lines and is rebased onto the
    view's frame via `outputStartLine`. Widening the window instead
    would drag in the previous entry's last line, whose columns overrun
    the 4-character `TYPE` line and make Monaco reject the whole batch.
  - While the buffer diverges from the store the window is empty. No
    colours beats colours describing the previous text.
  - A store change re-drives both, since the model's text is untouched
    by it and Monaco would otherwise never re-query.
  - The diagnostics mirror caches each publish together with the spans
    it was computed against, and replays it when a model mounts later.
    Replaying through freshly computed spans puts markers on the wrong
    line, or drops them, once the store has moved.

The go-to-definition cursor is deliberately keyed on its own identity
and not on the current display: including the display would re-fire the
forced switch when the user toggles back to the table and pin the tab in
code mode.

Refs DOPE-537

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvGrhB1LyJz9MBwYhjoBDY
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11354748-678a-435e-855d-fdf204085beb

📥 Commits

Reviewing files that changed from the base of the PR and between 528c046 and 63ad4b2.

📒 Files selected for processing (4)
  • src/frontend/services/st-lsp/__tests__/dtview-context.test.ts
  • src/frontend/services/st-lsp/dtview-context.ts
  • src/frontend/services/st-lsp/goto-definition-redirect.ts
  • src/frontend/services/st-lsp/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/frontend/services/st-lsp/goto-definition-redirect.ts
  • src/frontend/services/st-lsp/index.ts

Walkthrough

This PR adds per-data-type code-view URIs, definition navigation, cursor positioning, Monaco model selection, semantic-token rebasing, diagnostic synchronization, and regression coverage.

Changes

Data-type code-view integration

Layer / File(s) Summary
Data-type spans and view contracts
src/frontend/utils/PLC/data-type-serializer.ts, src/frontend/services/st-lsp/types.ts, src/frontend/services/st-lsp/dtview-context.ts, src/frontend/store/slices/editor/types.ts, src/frontend/services/st-lsp/__tests__/*, src/frontend/utils/PLC/__tests__/*
Data-type spans, synthetic view URIs, frame offsets, safe URI parsing, view-context helpers, diagnostics filtering, and the data-type cursor target are defined and tested.
Semantic-token rebasing and refresh
src/frontend/services/lsp-shared/internal/semantic-tokens-shift.ts, src/frontend/services/lsp-shared/semantic-tokens.ts, src/frontend/services/lsp-shared/start-language-service.ts, src/frontend/services/lsp-shared/__tests__/semantic-tokens-shift.test.ts
Semantic tokens support an output-line origin. The language service exposes token refresh behavior.
ST LSP data-type synchronization
src/frontend/services/st-lsp/index.ts
Data-type views map to aggregate documents. Tokens are clipped and rebased. Diagnostics are mirrored to mounted models and refreshed with store changes.
Definition redirects and editor flow
src/frontend/services/st-lsp/goto-definition-redirect.ts, src/frontend/components/_features/[workspace]/data-type/index.tsx, src/frontend/components/_organisms/variables-code-editor/index.tsx, src/frontend/services/st-lsp/__tests__/goto-definition-redirect.test.ts, src/frontend/components/_features/[workspace]/data-type/__tests__/code-view-toggle.test.tsx
Definition navigation opens data-type code views when enabled and positions the cursor. The editor accepts explicit model URIs and can return to table view.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LSPClient
  participant GotoDefinitionRedirect
  participant DataTypeEditor
  participant VariablesCodeEditor
  participant STLSPAdapter
  LSPClient->>GotoDefinitionRedirect: request data-type definition
  GotoDefinitionRedirect->>DataTypeEditor: open code view and set cursor
  DataTypeEditor->>VariablesCodeEditor: pass model URI and cursor position
  VariablesCodeEditor->>STLSPAdapter: request mapped view data
  STLSPAdapter-->>VariablesCodeEditor: return rebased tokens and diagnostics
Loading

Possibly related PRs

Poem

A rabbit maps each type with care,
Jumps to declarations in the editor there.
Tokens shift and diagnostics align,
Views switch back at the proper time.
Each data type follows its line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: integrating per-type .dt code views with the ST language server.
Description check ✅ Passed The description thoroughly covers the changes, verification, feature-flag status, references, and known issues, but omits the template's DOD checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/DOPE-537-datatype-lsp-goto-def

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/services/st-lsp/index.ts`:
- Around line 149-162: Update applyDataTypeDiagnostics to clear markers for
deleted or locally modified .dt models before applying diagnostics: compare each
model against its corresponding store data serialized with
serializeDataTypeToText, clear markers when the type is absent or
model.getValue() differs, and only map current diagnostics for matching,
unchanged models. Add regression coverage for deleted types and uncommitted
local edits.

In `@src/frontend/services/st-lsp/types.ts`:
- Around line 157-162: Update parseDtViewUri to safely handle decodeURIComponent
failures for malformed percent-encoded paths, such as invalid sequences in the
captured URI segment. Return null when decoding fails, while preserving the
existing decoded data type name for valid dtview URIs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77d6b198-89b1-42e2-8532-be08d6221cb1

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef73f8 and e545b2b.

📒 Files selected for processing (15)
  • src/frontend/components/_features/[workspace]/data-type/__tests__/code-view-toggle.test.tsx
  • src/frontend/components/_features/[workspace]/data-type/index.tsx
  • src/frontend/components/_organisms/variables-code-editor/index.tsx
  • src/frontend/services/lsp-shared/__tests__/semantic-tokens-shift.test.ts
  • src/frontend/services/lsp-shared/internal/semantic-tokens-shift.ts
  • src/frontend/services/lsp-shared/semantic-tokens.ts
  • src/frontend/services/lsp-shared/start-language-service.ts
  • src/frontend/services/st-lsp/__tests__/goto-definition-redirect.test.ts
  • src/frontend/services/st-lsp/__tests__/types.test.ts
  • src/frontend/services/st-lsp/goto-definition-redirect.ts
  • src/frontend/services/st-lsp/index.ts
  • src/frontend/services/st-lsp/types.ts
  • src/frontend/store/slices/editor/types.ts
  • src/frontend/utils/PLC/__tests__/data-type-serializer.test.ts
  • src/frontend/utils/PLC/data-type-serializer.ts

Comment thread src/frontend/services/st-lsp/index.ts
Comment thread src/frontend/services/st-lsp/types.ts
…parsers

`parsePouUri`, `parsePouVarsUri` and `parseDtViewUri` decoded their name
segment with a bare `decodeURIComponent`, which raises `URIError` on
input like `%ZZ`. All three run inside `resolveStLspContext`, on every
model URI the providers see, so one malformed URI would take hover,
completion and definition down for that model rather than simply not
matching.

Not reachable today — these URIs are only minted by the matching
builders, which encode the name — but the guard belongs in all three
rather than in whichever one was touched last.

Refs DOPE-537

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvGrhB1LyJz9MBwYhjoBDY

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/frontend/services/st-lsp/__tests__/types.test.ts (1)

85-87: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the encoded dtViewUri string directly.

The current round-trip assertion checks only the parser result. It would still pass if dtViewUri emitted an unencoded space or slash. Add an exact serialization assertion for a name containing both characters.

Proposed test assertion
  it('round-trips a data type name, encoding included', () => {
    expect(dtViewUri('Motor')).toBe('inmemory://dtview/Motor.dt')
+   expect(dtViewUri('My Type/Variant')).toBe(
+     'inmemory://dtview/My%20Type%2FVariant.dt',
+   )
    expect(parseDtViewUri(dtViewUri('My Type'))).toBe('My Type')
  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/services/st-lsp/__tests__/types.test.ts` around lines 85 - 87,
Update the round-trip test around dtViewUri and parseDtViewUri to assert the
exact encoded URI produced for a data type name containing both a space and a
slash, while retaining the parser round-trip assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/frontend/services/st-lsp/__tests__/types.test.ts`:
- Around line 85-87: Update the round-trip test around dtViewUri and
parseDtViewUri to assert the exact encoded URI produced for a data type name
containing both a space and a slash, while retaining the parser round-trip
assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1840ba2f-e4dd-48c4-b37a-d5213ddbd188

📥 Commits

Reviewing files that changed from the base of the PR and between e545b2b and 528c046.

📒 Files selected for processing (2)
  • src/frontend/services/st-lsp/__tests__/types.test.ts
  • src/frontend/services/st-lsp/types.ts

@JoaoGSP
JoaoGSP requested a review from marconetsf August 7, 2026 21:26

@marconetsf marconetsf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of the .dt LSP wiring. The design reads well and the manual-validation notes in the description are genuinely useful — the four semantic-token defects you describe finding and fixing are exactly the ones this shape invites. Six comments below, all on the seam between the two coordinate frames.

The one I'd treat as blocking is the fallback in resolveStLspContext; the rest are a mix of correctness, a feature-flag escape, and the test gap that would have caught them.

Comment thread src/frontend/services/st-lsp/index.ts Outdated
Comment thread src/frontend/services/st-lsp/types.ts
Comment thread src/frontend/services/st-lsp/index.ts
Comment thread src/frontend/services/st-lsp/index.ts Outdated
Comment thread src/frontend/services/st-lsp/goto-definition-redirect.ts Outdated
Comment thread src/frontend/services/st-lsp/index.ts
A `.dt` view whose name has no entry in the aggregate document fell back
to `span?.start ?? 1`, which is the first entry's mapping — so hover,
completion and go-to-definition answered for an unrelated type. An
unparseable `.dt` file reaches this path with a live, visible buffer.
Pass the view's own un-indexed URI through instead: strucpp guards every
handler on the document being known, so each provider answers nothing.

Move the frame arithmetic into `dtview-context.ts` so it can be tested
without a worker, and drop the duplicated shift in the diagnostics
fan-out.

Gate the store subscription on `isDataTypeFilesEnabled()` and on a `.dt`
model actually being mounted. `refreshSemanticTokens()` re-tokenises
every model in the ST language, so with the flag off a datatype table
edit was triggering a worker round trip per open ST editor.

Convert the goto-definition cursor at the call site, next to the POU
conversions, rather than half inside the routing helper.

Reported by review on #657 / #998. The frame-line aliasing (DOPE-554)
and the formatting end-clip (DOPE-555) are tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvGrhB1LyJz9MBwYhjoBDY
@JoaoGSP

JoaoGSP commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Review round — 63ad4b270 / b9855a01

Six findings from @marconetsf. All six hold up; two describe a mechanism slightly off, both in the direction of the finding being more valid than written. Four fixed here, two carried to cards.

Fixed

Unknown .dt name resolved as the first type. span?.start ?? 1 produced lineOffset = 0, which is exactly the first entry's mapping, so hover / completion / go-to-definition answered for an unrelated type. Now the view's own un-indexed URI passes through and every provider answers nothing — strucpp guards each handler on the document being known.

The reachable path turned out not to be the deleted-type case my comment described (deleteElement already tears down the model, tab and file entry). It's an unparseable .dt file: no entry in dataTypes by construction, tab pre-opened in code mode, live visible buffer.

Store subscription ran outside the feature flag. refreshSemanticTokens() fires a language-wide emitter, so with the flag off a datatype table edit cost one worker round trip per open ST editor. Now gated on the flag and on a dtview:// model being mounted.

Seam arithmetic extracted and tested. st-lsp/dtview-context.ts holds dtViewSpan / dtViewLineOffset / dtViewWindow / diagnosticsInSpan as pure functions. 11 new cases: first / middle / last entry, missing name, empty document, window excluding the frame line, diagnostics at both span edges. Also removes the duplicated shift in the diagnostics fan-out.

Goto-definition conversion moved to the call site, matching routeToPouPreamble / routeToPouBody. Dropped the dead Math.max(1, …) clamp.

Carried to cards

DOPE-554 — .dt frame lines alias onto the neighbouring type. A view's TYPE and END_TYPE lines map outside its own span, so hover and Ctrl+click there answer for the adjacent type. Confirmed by arithmetic. The fix is a window on LspContext so out-of-span positions return null — that lands in lsp-shared, shared with the Python LSP, and wants a regression pass this PR isn't set up to give it. Must land before the .dt view reaches production; the flag flip (DOPE-542) only takes it to staging.

DOPE-555 — Format Document splices in text from the rest of the document. defaultFilterFormattingEdits has no upper bound. The review left open whether strucpp answers formatting; it does (documentFormattingProvider: true, handler registered, formatting.js in the bundle). Also pre-existing on pouvars://, so the card covers both surfaces with one hook.

Validation

Web tsc clean, eslint 0. Editor tsc clean, eslint 0, jest 6096 passed / 0 failed. Mirror 1016 files, 0 diffs. Feature flag ships false.

Web vitest reports 3 failures in boot.test.ts; identical without these changes — DOPE-549, a jest-authored suite running under vitest.

Earlier CodeRabbit round (malformed URI decode, stale markers) was closed in 528c0468c.

@marconetsf
marconetsf self-requested a review August 7, 2026 21:54
@JoaoGSP
JoaoGSP merged commit e1c9ea6 into development Aug 7, 2026
25 checks passed
@JoaoGSP
JoaoGSP deleted the feature/DOPE-537-datatype-lsp-goto-def branch August 7, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants