-
Notifications
You must be signed in to change notification settings - Fork 906
fix(desktop): v2 file reveal — expand, highlight, scroll, open sidebar #4536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3f663a5
45fbcc3
8a64559
e2329f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { scrollTreeToRow } from "./scrollTreeToRow"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { type FileTree, prepareFileTreeInput } from "@pierre/trees"; | ||
| import { asDirectoryHandle } from "../treePath"; | ||
|
|
||
| /** | ||
| * Center `targetKey` in the file-tree viewport. | ||
| * | ||
| * Pierre auto-scrolls focused rows only when DOM focus lives inside the tree | ||
| * (FileTreeView shouldOwnDomFocus gate), so programmatic reveals don't scroll. | ||
| * The public FileTree API doesn't expose the focused row index or a reveal/ | ||
| * scrollTo method, so we replicate Pierre's sort + visibility math: | ||
| * | ||
| * 1. Sort knownPaths via Pierre's own `prepareFileTreeInput` (directories | ||
| * before files at each depth, case-insensitive natural sort within). | ||
| * 2. Walk the sorted list, skipping paths whose ancestors are collapsed, | ||
| * to find the target's visible index. | ||
| * 3. scrollTop = index * itemHeight, centered in the viewport. | ||
| * | ||
| * Returns true if it scrolled (or the row was already in view), false if it | ||
| * couldn't locate the scroll element or target. | ||
| */ | ||
| export function scrollTreeToRow( | ||
| model: FileTree, | ||
| knownPaths: ReadonlySet<string>, | ||
| targetKey: string, | ||
| itemHeight: number, | ||
| ): boolean { | ||
| const scrollEl = model | ||
| .getFileTreeContainer() | ||
| ?.shadowRoot?.querySelector('[data-file-tree-virtualized-scroll="true"]'); | ||
| if (!(scrollEl instanceof HTMLElement)) return false; | ||
|
|
||
| const visibleIndex = computeVisibleRowIndex(targetKey, knownPaths, model); | ||
| if (visibleIndex < 0) return false; | ||
|
|
||
| const viewportHeight = scrollEl.clientHeight; | ||
| const targetTop = visibleIndex * itemHeight; | ||
| const targetBottom = targetTop + itemHeight; | ||
| const currentTop = scrollEl.scrollTop; | ||
| const currentBottom = currentTop + viewportHeight; | ||
|
|
||
| if (targetTop >= currentTop && targetBottom <= currentBottom) return true; | ||
| scrollEl.scrollTop = Math.max( | ||
| 0, | ||
| targetTop - (viewportHeight - itemHeight) / 2, | ||
| ); | ||
| return true; | ||
| } | ||
|
|
||
| function computeVisibleRowIndex( | ||
| targetKey: string, | ||
| knownPaths: ReadonlySet<string>, | ||
| model: FileTree, | ||
| ): number { | ||
| const prepared = prepareFileTreeInput(Array.from(knownPaths)); | ||
| let index = 0; | ||
| for (const path of prepared.paths) { | ||
| if (path === targetKey) { | ||
| return isPathVisible(path, model) ? index : -1; | ||
| } | ||
| if (isPathVisible(path, model)) index++; | ||
| } | ||
| return -1; | ||
|
Comment on lines
+49
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/utils/scrollTreeToRow/scrollTreeToRow.ts
Line: 49-62
Comment:
**O(n × depth) visible-index walk on every scroll**
`computeVisibleRowIndex` calls `isPathVisible(path, model)` for every path before the target, and each `isPathVisible` call re-walks all ancestors of that path. For a workspace with thousands of files at several levels of depth this is measurably expensive per reveal, even inside `requestAnimationFrame`. A simple optimization is to track a `Set<string>` of directories already confirmed expanded and skip the ancestor walk for subsequent paths that share the same parent chain.
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
|
|
||
| function isPathVisible(path: string, model: FileTree): boolean { | ||
| const trimmed = path.endsWith("/") ? path.slice(0, -1) : path; | ||
| let lastSlash = trimmed.lastIndexOf("/"); | ||
| if (lastSlash < 0) return true; | ||
| let parent = trimmed.slice(0, lastSlash); | ||
| while (parent) { | ||
| const handle = asDirectoryHandle(model.getItem(`${parent}/`)); | ||
| if (!handle?.isExpanded()) return false; | ||
| lastSlash = parent.lastIndexOf("/"); | ||
| if (lastSlash < 0) break; | ||
| parent = parent.slice(0, lastSlash); | ||
| } | ||
| return true; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clientHeightmay be 0 when sidebar CSS transition is still runninghandleQuickOpenSelectFileopens the sidebar with a state update, thenopenFilePane→revealawaitsfetchDircalls. When all directories are already cached (loadedDirshits), those awaits resolve in a single microtask tick, so therequestAnimationFramecan fire while the sidebar's open animation is mid-flight andscrollEl.clientHeightreads 0. The centering formulatargetTop - (viewportHeight - itemHeight) / 2then becomestargetTop + itemHeight/2, placing the row near the top of the viewport rather than centered once the sidebar fully expands.Prompt To Fix With AI