Skip to content

Add citation/reference hover preview (RefHover)#5669

Merged
kjk merged 17 commits into
masterfrom
add-reference-hovering
Jun 12, 2026
Merged

Add citation/reference hover preview (RefHover)#5669
kjk merged 17 commits into
masterfrom
add-reference-hovering

Conversation

@koppor

@koppor koppor commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #5611 with a single commit.

This update is entirely written by claude - "only" driven by me in a code-and-fix manager

Screenshot:

grafik

Hovering an internal link (citation, cross-reference, figure, equation,
heading, code listing, or abbreviation) shows a popup previewing the
destination region instead of jumping there.

- RefHover: popup lifecycle, geometry, wheel-scroll and Ctrl+wheel zoom,
  /XYZ zoom-hint handling, cursor-leave hiding, annotation-tooltip
  suppression when overlapping an internal link
- RefHoverDetect: pure detectors for citations, captions, headings,
  figures/equations, with data-driven caption/heading dictionaries and
  region detection; covered by RefHover_ut unit tests
- EngineBase/EngineMupdf: expose destination text/region lookup
- Settings: options for the hover preview
- vs2022 project files and docs updated
@kjk kjk force-pushed the add-reference-hovering branch from 7cfb441 to a49b5d9 Compare June 11, 2026 20:37
@kjk

kjk commented Jun 11, 2026

Copy link
Copy Markdown
Member

I'm testing changes.

  1. When navigating (e.g. by clicking) the preview should go away

  2. When showing hover and scrolling with mouse wheel, the preview is scrolled, not the document. Is this intentional?

  3. When scrolling away (e.g. with up / down arrows), the preview remains

I guess that 1 and 3 are caused by the fact that preview is only updated on mouse move but not on scroll or other navigation changes.

  1. There's yellowish border around preview window but not on the bottom?

@kjk

kjk commented Jun 11, 2026

Copy link
Copy Markdown
Member

820AOperatingInstructions.pdf

I understand that picking the area to show is a heuristic.

Maybe it can be tweaked for the attached document, in page 4 the following links:

  1. Support Overview
  2. User Support Web Site
  3. Product and Support Information
  4. Unpacking the Package Contents

If not, I understand.

kjk and others added 16 commits June 12, 2026 13:07
Commit a49b5d9 hand-edited the generated vs2022 project files
without adding the new files to premake5.files.lua, so the next
premake regeneration would have dropped them, causing link errors.
The hand edits were also incomplete (RefHover.h and RefHoverDetect.h
missing from vcxproj includes, RefHoverDetect.cpp missing from
filters, test_util filters not updated).

Add RefHover.* / RefHoverDetect.* to premake5.files.lua and
regenerate the vs2022 projects with bin\premake5.exe vs2022, which
fixes all the inconsistencies and confirms everything else was in
sync (the regeneration is additions only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hover timer survives tab switches, document reloads and closes,
so when it fired the pending destination page could be invalid for
the document now in the tab, and GetZoomReal() on an out-of-range
page dereferences a null PageInfo. Validate with ValidPageNo() and
hide the popup instead.

Also reject internal links pointing past the last page (possible in
malformed documents) in IsInternalLinkDest().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The topmost popup was only hidden by mouse movement, so it stayed on
screen (showing content from the wrong document, or over another
application) after keyboard tab switches, closing the document,
Alt+Tab, and during text selection / drags.

- hide in CloseDocumentInCurrentTab(), which covers tab switch (via
  LoadModelIntoTab), close and reload; this also kills a pending
  hover timer that otherwise leaked into the home page wndproc and
  resets the displayed page so the popup wheel handlers can never
  use a stale page number against a different document engine
- hide on WM_ACTIVATE / WA_INACTIVE (Alt+Tab generates no
  WM_MOUSELEAVE)
- hide on left button down: a click either follows the hovered link
  or starts a selection / drag

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Meant to be used when launching SumatraPDF.exe for ad-hoc testing by
humans or agents:
- always starts a new instance, even with ReuseInstance = true
- does not restore a session, only loads files given on the cmd-line
- does not save settings, so a test run leaves no trace in the
  settings of the user

Implemented as gForTesting global. agents.md tells agents to always
use it for ad-hoc testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DetectEntryBox constrained its initial start-glyph search to the
destination column but then dropped the constraint: the walk to the
line start took the leftmost same-y glyph anywhere on the page and
the bracket-label search accepted any "[" at smaller x. In 2-column
papers both latched onto the other column, and the bounding-box
passes had no right bound, so the popup showed a full-width band of
unrelated text instead of the one bibliography entry.

- walk to the line start via a gap-bounded line run: never cross a
  horizontal gap wider than a column gutter, while still recovering
  the entry left edge when the link destX points mid-line
- bound the bracket-label search to a hanging-indent distance
- skip glyphs right of the entry column (line-run right edge plus
  slack) in the box passes and the forward scan

Add 2-column unit tests for entries in either column; they fail
against the previous detector and pass now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RefHoverSchedule() skips a re-render when hovering the same link by
comparing the incoming raw link destination against displayed.destX/Y,
but RefHoverOnTimer() stored the resolved values there (destY
recovered from source text, destX coerced to 0). For page-level
destinations (abbreviation / glossary links, /XYZ at page top) they
never compared equal, so every pause of the mouse within the same
link re-ran detection plus a synchronous render and re-anchored the
popup at the new cursor position. Store the raw link values instead;
they are only read by the dedupe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The annotation notification was suppressed over internal links even
with EnableCitationHover = false, when no hover popup would ever
appear in its place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MatchWordAt() folded case with towlower(), which in the "C" locale
the process runs under only folds ASCII, so accented dictionary words
("sección", "capítulo") never matched all-caps Spanish headings like
"SECCIÓN 2" - despite the comment claiming all-caps text matches.
Fold with CharLowerW() instead, which uses the OS linguistic tables
regardless of the CRT locale.

Add a unit test with an all-caps accented heading; it fails with the
towlower() version and passes now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The popup border, cursor gap, wheel-scroll step and popup size caps
were raw pixels, so on high-dpi monitors the border and cursor gap
were visually smaller and the popup size caps too tight. Scale them
at point of use with the popup window dpi (handles per-monitor dpi).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ShowPopup could compute a negative width / height when the page is
barely visible or there is no room on either side of the cursor,
showing a degenerate 0-sized topmost window; hide the popup instead.

Track the actual RegisterClassW() result so that a (theoretical)
registration failure doesn't re-run window creation on every mouse
move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PageDestinationMupdf documents destX / destY == -1 as "not resolved"
but NewPageDestinationMupdf assigned them from zero-initialized
locals even when ResolveLink() failed, so GetDestPoint2() reported a
{0,0} anchor for unresolved destinations. Assign only on success.
Harmless today since callers filter on pageNo first, but a trap for
future callers trusting the documented contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- add Italian (tabella, sezione, capitolo) and Portuguese (tabela,
  seção, secção) words the dictionary comments claimed but didn''t
  have, and make the per-language comments honest about shared roots
- require a trailing word boundary in heading-prefix matching so a
  bibliography entry starting "Sections of ..." isn''t misrouted to
  the landscape heading view by matching "section"
- pick the topmost caption below the region in the caption-extension
  search instead of the first one in glyph-array order; PDFs draw
  text in arbitrary order (as documented elsewhere in this file), so
  a far-down caption drawn early could extend the popup far past the
  relevant one

Add unit tests for all three; they fail against the previous
detector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add tests for the author-year hanging-indent path (rule b), the
single-line description-list path (rule d) and the vertical-gap path
(rule c). Rework BodyTextParenRejected so the "(N)" label is alone on
its line: previously the line-trailing rule rejected the input before
the left-half rule it claims to test was ever consulted, so the test
passed even with that rule deleted.

Verified by mutation: disabling each pinned rule fails the matching
test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert kCaptionWords / kHeadingPrefixWords from nullptr-terminated
WCHAR* arrays to \0-separated utf8 strings navigated with
seqstrings::Next(), converting entries with ToWStrTemp() at the
match sites. Covered by the existing dictionary unit tests,
including the accented-word round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CitationHoverDelay is how long (in ms) to hover an internal-document
link before the popup shows, replacing the hard-coded 300 ms. A
negative value disables the popup (the equivalent of the old
EnableCitationHover = false). Defaults to 300.

Also fix clang-format / clang-tidy detection in cmd/util.ts: VS 18
ships the llvm tools in Llvm\x64\bin, not Llvm\bin, which made
gen-settings.ts die halfway through generation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Toggles the citation / reference hover preview via the
CitationHoverDelay advanced setting: sets it to -1 (disabled) when
enabled, back to the default 300 ms when disabled. The command
palette shows the resulting state like other toggle commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kjk kjk merged commit c545cd2 into master Jun 12, 2026
2 checks passed
@kjk kjk deleted the add-reference-hovering branch June 12, 2026 13:10
@kjk

kjk commented Jun 12, 2026

Copy link
Copy Markdown
Member

Thanks, I've made some tweaks / fixes.

Notably, I changed EnableCitationHover to CitationHoverDelay (more flexible; retains ability to disable by setting to -1)

Also added CmdToggleHoverPreview to enable / disable from command palette.

I'm not sure about wheel scroll to be used for preview. What do other programs implementing this do?

This is inherently an heuristic so I encourage more testing and tweaking the preview area detector. I'm sure once it's in pre-release, people will send feedback

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.

Can we have a snapshot feature like Skim in MacOS Popup preview of PDF links

2 participants