1import { FileTree } from "pierre-trees" 2import { Turbo } from "@hotwired/turbo-rails" 3import TurboMorphEnabledController from "controllers/turbo_morph_enabled_controller" 4 5// Mirrors Ruby's String#parameterize for ASCII paths. 6function pathToAnchor(path) { 7 return path.toLowerCase().replace(/[^a-z0-9_]+/g, "-").replace(/^-+|-+$/g, "") 8} 9 10export default class extends TurboMorphEnabledController { 11 static values = { files: Array } 12 13 connect() { 14 super.connect() 15 16 const files = this.filesValue 17 const paths = files.map(f => f.path) 18 const gitStatus = files.filter(f => f.status).map(({ path, status }) => ({ path, status })) 19 // A stacked page anchors every diff, so the tree scrolls. The standalone 20 // single-diff page gives each file a page of its own and sends the urls 21 // with them, so there the tree navigates. 22 this.urls = new Map(files.filter(f => f.url).map(({ path, url }) => [path, url])) 23 24 this.fileTree = new FileTree({ 25 paths, 26 initialSelectedPaths: this.selectedPaths(), 27 gitStatus, 28 icons: { set: "minimal", colored: false }, 29 search: true, 30 fileTreeSearchMode: "hide-non-matches", 31 initialExpansion: "open", 32 onSelectionChange: this.handleSelectionChange.bind(this), 33 // remove permanent padding for scrollbar, don't show filenames in git 34 // status color, and suppress the keyboard-focus ring on the first row 35 // while the search input has real focus. 36 unsafeCSS: ` 37 [data-file-tree-virtualized-scroll="true"] { scrollbar-gutter: auto; } 38 [data-file-tree-search-input] { padding-inline: 8px; } 39 [data-file-tree-search-input]:focus-visible { outline-offset: -2px; } 40 [data-item-git-status] > [data-item-section="content"] { color: var(--trees-fg); } 41 `, 42 }) 43 this.fileTree.render({ fileTreeContainer: this.element }) 44 45 this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this) 46 document.addEventListener("keydown", this.handleGlobalKeydown) 47 } 48 49 disconnect() { 50 super.disconnect() 51 document.removeEventListener("keydown", this.handleGlobalKeydown) 52 this.fileTree?.unmount() 53 this.fileTree?.cleanUp() 54 this.fileTree = null 55 } 56 57 handleGlobalKeydown(event) { 58 if (event.key !== "t" || event.metaKey || event.ctrlKey || event.altKey) return 59 60 // Keys that originate inside a shadow-DOM input (like Pierre's search) 61 // retarget to the host element, so event.target hides the input. Walk 62 // the composed path to see the true originating element. 63 const origin = event.composedPath()[0] 64 if (!origin) return 65 if (origin.tagName === "INPUT" || origin.tagName === "TEXTAREA" || origin.isContentEditable) return 66 67 // Focus the input directly rather than calling fileTree.openSearch(), 68 // which also sets data-item-focused on the first row and paints a 69 // keyboard-nav ring before the user has typed anything. 70 const input = this.element.shadowRoot?.querySelector("[data-file-tree-search-input]") 71 if (!input) return 72 73 event.preventDefault() 74 input.focus() 75 } 76 77 // The row for the file this page is showing, so the tree opens with it 78 // highlighted rather than with nothing selected. Empty on a stacked page, 79 // which shows every file at once and so has no single current one. 80 selectedPaths() { 81 const current = decodeURIComponent(window.location.pathname) 82 for (const [path, url] of this.urls) { 83 if (decodeURIComponent(url) === current) return [path] 84 } 85 return [] 86 } 87 88 handleSelectionChange(selectedPaths) { 89 const [path] = selectedPaths 90 if (!path) return 91 92 const url = this.urls.get(path) 93 if (url) return Turbo.visit(url) 94 95 const anchor = pathToAnchor(path) 96 const target = document.getElementById(anchor) 97 if (target) { 98 target.scrollIntoView({ behavior: "smooth", block: "start" }) 99 // Pass history.state through rather than null: dropping Turbo's restoration state makes 100 // popstate take the historyPoppedWithEmptyState path, which skips cancelling in-flight 101 // visits and caches the current DOM under this URL. 102 history.replaceState(history.state, "", `#${anchor}`) 103 } else { 104 window.location.hash = anchor 105 } 106 } 107}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.