Skip to content

feat(hover): render footer file:line locations as clickable links - #389

Open
geircodes wants to merge 1 commit into
msarson:version-1.0.1from
geircodes:feat/hover-clickable-locations
Open

feat(hover): render footer file:line locations as clickable links#389
geircodes wants to merge 1 commit into
msarson:version-1.0.1from
geircodes:feat/hover-clickable-locations

Conversation

@geircodes

Copy link
Copy Markdown

Hover: make the file:line footer locations clickable

Companion PR: geircodes/ClarionAssistant#feat/hover-position-font-links (Monaco-side: hover
reposition/font sync + a registerLinkOpener that makes the links this PR produces actually navigate
in the Monaco-embedding host). This PR's links work fine in VS Code on their own; the companion PR is
what makes them clickable in a host that embeds Monaco instead. Originally scoped as a look-and-feel
Issue+patch; re-scoped to a PR given the combined size of the two changes.

Here is what it looks like after:

  image

What happens today

Every hover footer that cites a location renders as plain text:

**LocVar** — `LONG`
🔧 Local procedure variable
```clarion
LocVar  LONG
```
demo.clw:2

…and for a procedure, the declaration/implementation pair:

demo.inc:2 → demo.clw:3

The hover has just told you exactly where the symbol lives, but getting there means F12 (which
re-resolves from scratch and picks one target) or reading the line number and navigating by hand.
The decl → impl footer is the case that stings most: it names two different places and neither is
reachable from the tooltip.

Proposal

Render each location as a markdown link to the file with an #L<line> fragment:

[demo.inc:2](file:///c%3A/proj/demo.inc#L2) → [demo.clw:3](file:///c%3A/proj/demo.clw#L3)

#L<line> is editor-core's own convention: VS Code's opener service strips the fragment into a
selection before opening (extractSelection, which accepts #L12, #12 and #L12,5). So the link
navigates with:

  • no new client-side command, and no isTrusted markdown (only command: links need that — file:
    links are opened by the standard opener),
  • no extra LSP round-trip on click,
  • nothing to register in the client at all for VS Code.

Hosts that embed Monaco rather than VS Code can honor the identical link by registering a
monaco.editor.registerLinkOpener that routes file: URIs to their own navigation (verified working
against Monaco 0.52.2 — same extractSelection code path, so the same link form serves both; this is
what the companion PR adds).

Each location is linked separately, since jumping to a prototype and jumping to a body are
different intents.

Behaviour when a location isn't openable

toFileUri() returns null for test:// fixture URIs and any other non-file scheme, and the footer
falls back to today's exact plain text. So hover output is unchanged wherever a link would be dead —
including the whole test fixture suite.

Scope

A helper trio (locationLink / locationBaseName / toFileUri) on HoverFormatter, used across four
files and thirteen footer sites in total:

HoverFormatter.ts (six sites):

Method Footer
formatVariable declaring file:line
formatClassMember declaring file:line
formatMethodCall declaration and implementation (two links)
formatMethodImplementation declaration file:line
formatProcedure MAP declaration and implementation (two links)

formatParameter is left alone — ParameterInfo carries no file, only a line, so there is nothing to
link to.

Three other resolver files (seven more sites) that built their footer as a raw
${fileName}:${lineNumber} template string and never called HoverFormatter.locationLink at all, so
they rendered as plain, non-clickable text even after the HoverFormatter sites above were fixed:

File Method Cross-file?
VariableHoverResolver.ts findModuleVariableHover same-file only
VariableHoverResolver.ts buildGlobalVariableHover (reached via searchEquatesFile, findInIncludesAndEquates) yes — equates/INCLUDE search
MethodHoverResolver.ts class-prefix declaration footer (hovering the class name in Class.Method PROCEDURE) same-file only
StructureFieldResolver.ts findFieldInTokens same-file only
StructureFieldResolver.ts resolveTypeNameHover's equates.clw fallback yes — equates.clw
StructureFieldResolver.ts findTypeDeclarationInIncludes yes — INCLUDE chain
StructureFieldResolver.ts findFieldInTypeIncludes yes — INCLUDE chain

Fixed by promoting HoverFormatter.locationLink from private to a plain method (all three resolvers
already hold a formatter: HoverFormatter instance via constructor injection, so no new wiring was
needed) and replacing each raw footer string with a call to it.

One false alarm ruled out during the audit: MethodHoverResolver.resolveChainedMethodCall's
implLocationStr looks unlinked (it's a raw ${uri}:${line} string) but is passed into
HoverFormatter.formatMethodCall, which re-parses it and does call locationLink — that path already
worked, no change needed.

Two incidental cleanups fall out of the HoverFormatter change:

  • formatMethodCall built declLocationStr separately in the try and the catch; the label
    derives from the URI alone, so it moves above the try and the duplicate goes away.
  • formatProcedure built its labels from the decoded path used for the fs read. The link needs
    the original (already percent-encoded) Location.uri, which is also the more correct source for
    the label, so both footers now read from mapDecl.uri / procImpl.uri.
  • The path import is no longer used in HoverFormatter.ts and is removed.

A real cross-file bug this surfaces well

Hovering a PRE:Field-style reference (e.g. FILE_LABEL:FieldName) that resolves into a different,
already-open-elsewhere file previously showed the correct location as inert text — the footer named
the right file and line, but there was no way to jump there short of reading it and navigating by hand.
That's exactly the buildGlobalVariableHover/equates-and-INCLUDE-search row in the table above.

Verification

  • npm run test:server: all passing, 0 failing. Existing assertions such as
    hoverText.includes('utils.clw:57') and content.includes('.clw:') keep passing unchanged, because
    the link label preserves the exact name.ext:line text — no test asserts whole-footer equality.
  • tsc --noEmit clean.
  • New test file HoverFormatter.LocationLink.test.ts, pinning the link shape directly: a plain Windows
    path with a drive letter (unescaped, matching pathToFileURL's canonical form), a path containing a
    space (percent-encoded in the URI, not the label), an already-formed file:// URI passed through
    untouched, a non-file scheme falling back to plain text, and two locations in a decl → impl footer
    linking independently.
  • Live-tested by the developer across several rounds on a real deployment (with the companion PR
    deployed too): single- and multi-location links, drive-letter/space encoding, and cross-file
    resolution through equates/INCLUDE chains all confirmed working correctly.

Known gap, deliberately not chased here

HoverRouter.ts's handleImplementsHover (hovering an interface name in IMPLEMENTS(...)) pushes a
bare filename string straight into the markdown, bypassing HoverFormatter entirely — not touched by
this PR, flagged for separate follow-up. A related but likely-unrelated case (a plain procedure-call
hover showing a wrong, non-clickable filename because the correct target is gated behind a
COMPILE/OMIT section the resolver doesn't evaluate) was also observed but not chased down here —
out of scope for this link-rendering change.

Every hover footer that cites a location (a variable's declaration, a
class member, a decl -> impl pair, a MAP declaration/implementation)
rendered as plain text -- the hover tells you exactly where the symbol
lives, but reaching it means F12 (which re-resolves from scratch and
picks one target) or reading the line number and navigating by hand.

Render each location as a markdown link with an #L<line> fragment
instead: [demo.clw:12](file:///.../demo.clw#L12). That fragment is
editor-core's own "open at this line" convention -- VS Code's opener
service strips it into a selection before opening, so the link
navigates with no client-side command, no isTrusted markdown, and no
extra LSP round-trip. A host embedding Monaco gets the same behavior
by registering a link opener for file: URIs.

Each location in a multi-location footer (declaration -> implementation)
is linked separately, since jumping to the prototype and jumping to the
body are different intents.

New locationLink/toFileUri helpers on HoverFormatter build the link,
normalising either a file:// URI or a plain OS path (drive letter left
unescaped -- the canonical form Node's pathToFileURL and vscode-uri both
produce) and falling back to the previous plain text for non-file
schemes (test:// fixtures and similar), so existing test assertions
that check for the "name.ext:line" substring keep passing unchanged.

New test: HoverFormatter.LocationLink.test.ts, pinning the link shape
for a plain path, a path containing a space, an already-formed file://
URI, a non-file-scheme fallback, and independent linking of a two-part
footer.
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.

1 participant