feat(hover): render footer file:line locations as clickable links - #389
Open
geircodes wants to merge 1 commit into
Open
feat(hover): render footer file:line locations as clickable links#389geircodes wants to merge 1 commit into
geircodes wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hover: make the
file:linefooter locations clickableCompanion PR:
geircodes/ClarionAssistant#feat/hover-position-font-links(Monaco-side: hoverreposition/font sync + a
registerLinkOpenerthat makes the links this PR produces actually navigatein 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:
What happens today
Every hover footer that cites a location renders as plain text:
…and for a procedure, the declaration/implementation pair:
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 → implfooter is the case that stings most: it names two different places and neither isreachable from the tooltip.
Proposal
Render each location as a markdown link to the file with an
#L<line>fragment:#L<line>is editor-core's own convention: VS Code's opener service strips the fragment into aselection before opening (
extractSelection, which accepts#L12,#12and#L12,5). So the linknavigates with:
isTrustedmarkdown (onlycommand:links need that —file:links are opened by the standard opener),
Hosts that embed Monaco rather than VS Code can honor the identical link by registering a
monaco.editor.registerLinkOpenerthat routesfile:URIs to their own navigation (verified workingagainst Monaco 0.52.2 — same
extractSelectioncode path, so the same link form serves both; this iswhat 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 fortest://fixture URIs and any other non-file scheme, and the footerfalls 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) onHoverFormatter, used across fourfiles and thirteen footer sites in total:
HoverFormatter.ts(six sites):formatVariableformatClassMemberformatMethodCallformatMethodImplementationformatProcedureformatParameteris left alone —ParameterInfocarries no file, only a line, so there is nothing tolink to.
Three other resolver files (seven more sites) that built their footer as a raw
${fileName}:${lineNumber}template string and never calledHoverFormatter.locationLinkat all, sothey rendered as plain, non-clickable text even after the
HoverFormattersites above were fixed:VariableHoverResolver.tsfindModuleVariableHoverVariableHoverResolver.tsbuildGlobalVariableHover(reached viasearchEquatesFile,findInIncludesAndEquates)MethodHoverResolver.tsClass.Method PROCEDURE)StructureFieldResolver.tsfindFieldInTokensStructureFieldResolver.tsresolveTypeNameHover's equates.clw fallbackStructureFieldResolver.tsfindTypeDeclarationInIncludesStructureFieldResolver.tsfindFieldInTypeIncludesFixed by promoting
HoverFormatter.locationLinkfromprivateto a plain method (all three resolversalready hold a
formatter: HoverFormatterinstance via constructor injection, so no new wiring wasneeded) and replacing each raw footer string with a call to it.
One false alarm ruled out during the audit:
MethodHoverResolver.resolveChainedMethodCall'simplLocationStrlooks unlinked (it's a raw${uri}:${line}string) but is passed intoHoverFormatter.formatMethodCall, which re-parses it and does calllocationLink— that path alreadyworked, no change needed.
Two incidental cleanups fall out of the
HoverFormatterchange:formatMethodCallbuiltdeclLocationStrseparately in thetryand thecatch; the labelderives from the URI alone, so it moves above the
tryand the duplicate goes away.formatProcedurebuilt its labels from the decoded path used for thefsread. The link needsthe original (already percent-encoded)
Location.uri, which is also the more correct source forthe label, so both footers now read from
mapDecl.uri/procImpl.uri.pathimport is no longer used inHoverFormatter.tsand 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 ashoverText.includes('utils.clw:57')andcontent.includes('.clw:')keep passing unchanged, becausethe link label preserves the exact
name.ext:linetext — no test asserts whole-footer equality.tsc --noEmitclean.HoverFormatter.LocationLink.test.ts, pinning the link shape directly: a plain Windowspath with a drive letter (unescaped, matching
pathToFileURL's canonical form), a path containing aspace (percent-encoded in the URI, not the label), an already-formed
file://URI passed throughuntouched, a non-file scheme falling back to plain text, and two locations in a
decl → implfooterlinking independently.
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'shandleImplementsHover(hovering an interface name inIMPLEMENTS(...)) pushes abare filename string straight into the markdown, bypassing
HoverFormatterentirely — not touched bythis 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/OMITsection the resolver doesn't evaluate) was also observed but not chased down here —out of scope for this link-rendering change.