ENG-2114 Insert active result as a link at cursor - #1314
Open
trangdoan982 wants to merge 17 commits into
Open
Conversation
…back Add a Scorer seam over Obsidian's prepareFuzzySearch, plus the candidate fetch and ranking functions the advanced node search panel needs. Separate the candidate fetch from scoring so the vault scan runs once per search-surface open rather than once per keystroke, and route it through getFilesWithNodeTypeId, which already falls back to vault iteration when Datacore is unavailable. Score and render the same string (file.basename) so SearchResult.matches offsets stay aligned with what renderResults re-slices. Filter by node type before scoring, which leaves results identical but shrinks the number of scorer calls on the per-keystroke path. Rank on SearchResult.score alone; Array.prototype.sort is stable, so equal scores keep candidate order without an explicit tie-break. An empty query returns the full filtered set in title order rather than nothing, so a type filter alone still narrows to a visible list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add the discourse node search surface: a Modal hosting a React root, a result list ranked by the QueryEngine functions from ENG-2108, and a Markdown preview of the active result. Register it as "Open node search" with no default hotkey, so users bind their own and we avoid colliding with core or community bindings. Highlight matched substrings with Obsidian's renderResults, passing the same string that was scored. Using the platform renderer rather than hand-rolled markup means highlights inherit theme styling, which is the code path that produced the equivalent Roam bug. Open with every node listed in title order rather than an empty prompt, so the modal doubles as a node browser. Model candidate loading as a discriminated union covering loading, ready, empty and error; the fetch is synchronous today, but semantic search will make it a network call and threading those states through later costs far more than carrying them now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
renderResults applies Obsidian's suggestion highlight, which is styled for the quick switcher rather than for search. Point it at --text-highlight-bg instead, the variable behind the yellow in Obsidian's own search view, so matches read the same way there, here, and in the Roam implementation. Target the span element rather than Obsidian's internal class name: renderResults wraps matched ranges in spans and leaves unmatched text as bare text nodes, so every span inside the title is a match, and the rule survives a class rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The preview pane read the newly selected note asynchronously while `content` still held the previous note's text, so the render effect fired once with the new file's path and the old file's body — the header showed one note while the pane rendered another. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the Roam result row: the node type is a rounded badge of the first three letters, inline before the title, reusing the colors the editor already paints discourse tags with so a type reads the same in both places. Author names now resolve through `plugin.settings.userNames`, which `fetchUserNames` fills with one query for every person in the vault's spaces. The modal refreshes it at most once per open, and only when an imported node is actually missing a name, so nothing queries per result. Resolution also moved to the selected result, which is the only one whose author is displayed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getNodeTagColors clamped any index past the twelfth node type to 0, so every
type beyond the palette length shared one color. Cycling spreads them instead.
This also changes existing tag colors for vaults with more than twelve types.
Author resolution now distinguishes the two cases the scope doc separates: no
authorId means the note is local ("You"), while an authorId that cannot be
resolved from settings or Supabase stays "Unknown" rather than claiming local
authorship. A non-numeric authorId counts as present-but-unresolvable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the arrow-key handler from the search input to the modal container, so navigation keeps working when focus moves elsewhere inside the modal, and so result actions have one place to live when they arrive. Mirrors the Roam dialog, which binds its handler at the same level. Activate rows on hover as well as click, again matching Roam. Suppress the mouseenter that fires when scrolling drags a row under a stationary cursor — that is the list moving, not the user choosing, and honouring it makes arrow keys jump back a row. Prevent the default on mousedown so clicking a result never pulls focus out of the input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A "?" chip told the reader nothing except that something was wrong. Roam handles
the same case by storing the type's label on each result at index time and
falling back to that; we have no stored label, but node formats are
`PREFIX - {content}`, so the title still carries the prefix the badge would have
shown. A note whose type was deleted, or imported from a differently configured
vault, now reads QUE or CLM instead of ?.
Omit the chip entirely when the title has no prefix either. Abbreviating the
note's own words would produce a confident-looking label that says nothing about
its type, which is worse than no label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The palette-cycling fix is a real one — past the twelfth node type every type collapsed to a single colour — but it is a behaviour change to a util shared with the editor, and nothing in the search modal needs it: this vault has nine node types, so clamping and cycling agree. Reverted here so the search PR stays to the search surface; worth its own change. Also drop the badge comments that restated their code, keeping the one that explains what Roam does differently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The revert left a whitespace-only diff: the pre-commit formatter collapsed a double blank line the file already had. Committing without it so colorUtils drops out of this PR entirely rather than appearing as a one-line change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A narrowing query rebuilds `results` before the reset effect runs, so the old index could point past the new list for one render — blanking the preview and leaving no row highlighted. Clamping at render covers that frame; the effect still resets the state so arrow keys continue from the top. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Add platform-aware keyboard hint symbols
Obsidian renders modifiers as glyphs on macOS and as words on Windows and
Linux. Roam's search footer hardcoded the macOS glyphs at each call site and
showed the wrong hint on Windows (ENG-2000); routing every hint through one
map is what keeps that from repeating.
`formatHintKeys` takes `isMacOS` so the non-mac branch can be exercised
without that platform.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Add footer action bar with open in active pane and split
Enter opens the active result in the current pane, Shift+Enter in a split, and
both close the modal. Mod+Enter and Alt+Enter deliberately fall through, so
the insert action (ENG-2114) can claim Mod+Enter as it does in Roam.
The footer reuses Obsidian's own `prompt-instruction` markup, the classes
`SuggestModal.setInstructions()` emits, so it matches the native quick
switcher. This modal extends plain `Modal`, so that API is unavailable. Its
actions are left-aligned rather than centred because they sit under a
full-width result list.
The Enter branch lives in the existing wrapper `onKeyDown`, which ENG-2109
moved off the input so result actions would have one place to live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Open the active result in a new tab rather than the current one
Replacing the page the user was already reading loses their place, which is the
opposite of what a lookup surface should do. `getLeaf("tab")` adds a tab to the
main panel instead, so the previous note stays open behind it.
This reuses the existing `openFileInNewTab`, so the `openFileInActivePane`
helper added earlier in this branch is no longer needed. The label now reads
"open in new tab" to match.
Diverges from the ticket's stated Solution, which specified `getLeaf(false)`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Style footer keys as caps, matching Roam
Obsidian's `prompt-instruction-command` is bold with no border, which made the
lone `esc` hint read as emphasis rather than as a key. Roam's search footer
draws every key as a bordered cap instead, so `esc` sits with the rest of the
set.
Keeps the `prompt-instructions` container for its native type and spacing, and
takes the cap's border, radius, and background from Obsidian's CSS variables so
it still follows the active theme.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Drop the results list tooltip
Obsidian renders `aria-label` as a hover tooltip, so labelling the listbox meant
a tooltip covered the results as soon as the pointer entered the list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Make close clickable and drop the duplicate badge tooltip
The close hint was the only footer item that ignored a click, which read as
broken next to two working actions. It now goes through the same `FooterAction`
as the others and calls the modal's own close.
The badge carried both `title` and `aria-label` with the same text, so hovering
one stacked a native tooltip on top of Obsidian's. Keeping `aria-label`, since
Obsidian's is the themed one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ENG-2113 Let focused footer buttons handle their own Enter
A footer button reached by Tab had its bubbling Enter intercepted by the modal's
keydown handler, whose preventDefault suppressed the button's native click. So
Enter on close opened a new tab, and Enter on split opened a new tab too.
Also moves the footer's layout onto Tailwind utilities, leaving only the four
properties Obsidian defends with `button:not(.clickable-icon)` and
`button:hover` — both (0,1,1), which outrank a single utility class — plus
`font-size`, which has no inherit utility. Trims comments that explained
history rather than the code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Snapshot the active Markdown editor and cursor in the modal's constructor, before open() mounts the modal and takes focus, then insert a link built by generateMarkdownLink so the vault's link-format settings are honoured. The footer action is absent rather than disabled when nothing was being edited: there is no cursor for it to refer to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
trangdoan982
force-pushed
the
eng-2109-create-node-search-modal-with-ranked-results-and-preview
branch
2 times, most recently
from
August 22, 2026 04:34
c97cabe to
470df4f
Compare
Contributor
PR size/scope checkThis PR is over our review-size guideline.
Please split this into smaller PRs unless there is a clear reason the changes need to land together. If keeping it as one PR, please add a brief justification covering:
|
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.
https://www.loom.com/share/62b142bea41b4df6b4e0e19956dc160f
What
⌘↵/Ctrl+Enteron the active result inserts a link to it at the cursor you left behind, then closes the search and hands focus back to the editor. The footer gets a matchinginsert link at cursoraction.NodeSearchModal's constructor — that runs while the editor still owns the cursor, beforeopen()mounts the modal and takes focus.app.fileManager.generateMarkdownLink(file, sourcePath), so the vault's wikilink-vs-markdown and shortest-path-vs-absolute settings are honoured. Nothing is hand-built.insertPageRefAtRange).editor.hasFocus()is deliberately not part of the gate — opening the search from the command palette means that palette already took focus, so ahasFocus()check would hide the action on its most common invocation path. The gate is "an activeMarkdownViewin source mode" instead.Verification
No test infra exists in
apps/obsidian(no vitest, notestscript), so this was verified against a running Obsidian 1.13.7 driven over CDP — 19/19 assertions, twice consecutively:before[[CLM - a node…]]| after[[CLM - a node to be type-casted]][CLM…](CLM%20-%20a%20node….md)hasFocus: true, cursor past the linkreplace [[CLM…]] pleaseTypes pass; no new lint findings.
🤖 Generated with Claude Code