sourcemonkey 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,460 @@
1
+ // The tabbed code view: tab strip, highlighted code pane, line
2
+ // highlighting, clickable line-number gutter, and `#file:line:col`
3
+ // URL-fragment sync.
4
+ //
5
+ // Split out of `userscript.ts` so one implementation serves both
6
+ // viewer modes — an installed script (whose tab bodies are already in
7
+ // storage) and a preview of a script that isn't installed yet (whose
8
+ // `@require` bodies are fetched on demand, hence `TabBody` allowing a
9
+ // thunk). The Log tab is optional: previews don't have one.
10
+ //
11
+ // The view owns which tab is selected and which line is highlighted;
12
+ // the page owns what the tabs are and what the log pane contains.
13
+ import { linkifyCodeBlock } from './code-links.js';
14
+ // Side-effect import: installs the `userscript-header` token that
15
+ // `code-links.ts` keys its header linkification off. Imported here
16
+ // because this is the module that calls `Prism.highlightElement`, so
17
+ // any page reusing the view gets the grammar with it.
18
+ import './prism-userscript.js';
19
+ import { buildHash, parseHash } from './script-url-hash.js';
20
+ export class CodeTabsView {
21
+ els;
22
+ hooks;
23
+ tabList = [];
24
+ // Index of the currently selected tab. Preserved across re-renders
25
+ // when the new tab list still has an entry at that index; otherwise
26
+ // falls back to 0 (the main script).
27
+ activeIdx = 0;
28
+ // Highlight state tracked alongside activeIdx so the URL hash can be
29
+ // rebuilt from a single source of truth. `undefined` means "no line
30
+ // highlighted". Cleared on tab switch (a new tab's content has no
31
+ // highlight to start) and set by `highlightLine` after a paint.
32
+ highlightLine;
33
+ highlightCol;
34
+ // Bumped on every paint so a slow lazy body can't overwrite a tab
35
+ // the user has since switched away from — including a switch to the
36
+ // Log tab, which paints no code at all.
37
+ paintEpoch = 0;
38
+ // Line/col a caller asked to highlight while the active tab's body
39
+ // was still loading. `scrollToAndHighlightLine` needs the rendered
40
+ // line-numbers gutter to measure against, so the request is parked
41
+ // here and replayed once the body paints. Cleared by any paint that
42
+ // doesn't have one pending, so it can't leak onto a later tab.
43
+ pendingHighlight;
44
+ // Set once the first tab list arrives, so `setTabs` knows whether to
45
+ // seed the selection from `location.hash` (first paint) or preserve
46
+ // the user's current tab (re-render).
47
+ seeded = false;
48
+ cachedCharWidth = 0;
49
+ constructor(els, hooks = {}) {
50
+ this.els = els;
51
+ this.hooks = hooks;
52
+ window.addEventListener('hashchange', () => this.applyHash());
53
+ }
54
+ get tabs() {
55
+ return this.tabList;
56
+ }
57
+ get activeIndex() {
58
+ return this.activeIdx;
59
+ }
60
+ // Install a new tab list and repaint. On the very first call the
61
+ // selection is seeded from `location.hash` so the right tab paints
62
+ // immediately — without it the main tab would flash before
63
+ // `applyHash` swapped it.
64
+ setTabs(tabs) {
65
+ this.tabList = tabs;
66
+ if (!this.seeded) {
67
+ this.seeded = true;
68
+ this.activeIdx = this.resolveHashTarget(location.hash).tabIndex;
69
+ }
70
+ else if (this.activeIdx >= tabs.length) {
71
+ this.activeIdx = 0;
72
+ // The highlight pointed at a tab that no longer exists — drop it
73
+ // so the URL hash doesn't claim a line on the fallback tab the
74
+ // user never selected.
75
+ this.resetHighlight();
76
+ }
77
+ this.paint();
78
+ }
79
+ // Forget the current selection so the NEXT `setTabs` seeds from
80
+ // `location.hash` again, exactly as the first paint does.
81
+ //
82
+ // For in-page navigation to another copy of the script (see the
83
+ // viewer's `navigateToScript`): the tab list about to arrive belongs
84
+ // to a different file, so preserving "tab 3" — the third `@require`
85
+ // of the script the user just left — would land them somewhere
86
+ // arbitrary. The hash is the new page's own instruction (`#Log` from
87
+ // a Status link, nothing from a Location link).
88
+ reseedFromHash() {
89
+ this.seeded = false;
90
+ this.activeIdx = 0;
91
+ this.resetHighlight();
92
+ }
93
+ // Wipe the view (no script to show).
94
+ clear() {
95
+ this.tabList = [];
96
+ this.activeIdx = 0;
97
+ this.resetHighlight();
98
+ this.paintEpoch++;
99
+ this.els.tabsEl.replaceChildren();
100
+ this.els.tabsEl.hidden = true;
101
+ this.els.contentEl.textContent = '';
102
+ this.els.logPaneEl.hidden = true;
103
+ this.els.codePaneEl.hidden = false;
104
+ }
105
+ // Index of the code tab whose `sourceUrl` matches, or -1. Used by
106
+ // the log pane to decide whether a stack ref is one of ours.
107
+ findTabBySourceUrl(url) {
108
+ return this.tabList.findIndex((t) => t.kind === 'code' && t.sourceUrl !== '' && t.sourceUrl === url);
109
+ }
110
+ // Switch to the given tab (if not already active), then scroll the
111
+ // code pane to bring the requested line into view and highlight it.
112
+ // `col` (1-based) positions the vertical accent bar at the character
113
+ // column from the stack ref. Pass `undefined` (e.g. line-number
114
+ // gutter clicks) to place the bar at the start of the line.
115
+ //
116
+ // `tabLine` is optional because not every navigation has a line to
117
+ // offer: a diagnostic about a file as a whole (a missing metadata
118
+ // block) knows which file to open and nothing more. Omitting it selects the
119
+ // tab and leaves no highlight behind — including when the tab was
120
+ // already active and carrying one from a previous jump, which would
121
+ // otherwise read as this navigation's answer.
122
+ goToTabLine(tabIndex, tabLine, col) {
123
+ if (this.activeIdx !== tabIndex) {
124
+ this.activeIdx = tabIndex;
125
+ this.resetHighlight();
126
+ this.paint();
127
+ }
128
+ if (tabLine === undefined)
129
+ this.clearLineHighlight();
130
+ else
131
+ this.scrollToAndHighlightLine(tabLine, col);
132
+ this.updateUrlHash();
133
+ }
134
+ // Hydrate the view from `location.hash`. Called on initial load,
135
+ // when the user manually edits the URL, on Back/Forward, and after
136
+ // any re-render path that wipes the flash overlay (Refresh / enable
137
+ // toggle / storage-change rehydrate). `scroll` defaults to true so
138
+ // navigation jumps to the line; the re-render paths pass false so a
139
+ // Refresh doesn't yank the user's scroll position.
140
+ applyHash(scroll = true) {
141
+ if (this.tabList.length === 0)
142
+ return;
143
+ const target = this.resolveHashTarget(location.hash);
144
+ if (this.activeIdx !== target.tabIndex) {
145
+ this.activeIdx = target.tabIndex;
146
+ this.resetHighlight();
147
+ this.paint();
148
+ }
149
+ if (target.line !== undefined && this.tabList[this.activeIdx]?.kind === 'code') {
150
+ this.scrollToAndHighlightLine(target.line, target.col, scroll);
151
+ }
152
+ else {
153
+ this.clearLineHighlight();
154
+ }
155
+ // Normalize the URL so a partial / invalid hash collapses to the
156
+ // canonical form for whatever state we just landed in.
157
+ this.updateUrlHash();
158
+ }
159
+ // ----- painting ------------------------------------------------
160
+ paint() {
161
+ const { tabsEl, codePaneEl, contentEl, logPaneEl } = this.els;
162
+ // Bumped before anything else so every paint — including one that
163
+ // shows the Log tab and touches no code — invalidates a lazy body
164
+ // still in flight for the tab we're leaving.
165
+ const epoch = ++this.paintEpoch;
166
+ tabsEl.replaceChildren();
167
+ // Always render the strip — even a single main-script tab gives the
168
+ // page a consistent shape and tells the user the viewer is tabbed.
169
+ tabsEl.hidden = false;
170
+ this.tabList.forEach((tab, idx) => {
171
+ const btn = document.createElement('button');
172
+ btn.type = 'button';
173
+ btn.className = 'tab' + (idx === this.activeIdx ? ' active' : '');
174
+ btn.setAttribute('role', 'tab');
175
+ btn.setAttribute('aria-selected', idx === this.activeIdx ? 'true' : 'false');
176
+ btn.textContent = tab.label;
177
+ if (tab.kind === 'code' && tab.title)
178
+ btn.title = tab.title;
179
+ if (tab.kind === 'code' && tab.mark) {
180
+ const mark = document.createElement('span');
181
+ mark.className = 'tab-mark';
182
+ mark.textContent = tab.mark;
183
+ btn.appendChild(mark);
184
+ }
185
+ btn.addEventListener('click', () => {
186
+ if (this.activeIdx === idx)
187
+ return;
188
+ this.activeIdx = idx;
189
+ this.resetHighlight();
190
+ this.paint();
191
+ this.updateUrlHash();
192
+ });
193
+ tabsEl.appendChild(btn);
194
+ });
195
+ const active = this.tabList[this.activeIdx];
196
+ if (!active)
197
+ return;
198
+ if (active.kind === 'log') {
199
+ codePaneEl.hidden = true;
200
+ logPaneEl.hidden = false;
201
+ this.hooks.onShowLog?.();
202
+ return;
203
+ }
204
+ logPaneEl.hidden = true;
205
+ codePaneEl.hidden = false;
206
+ if (typeof active.body === 'string') {
207
+ this.paintCode(active.body, active.fileUrl);
208
+ this.applyPendingHighlight();
209
+ return;
210
+ }
211
+ // Lazy body: show a placeholder, fetch, then paint — unless the
212
+ // user switched tabs while the fetch was in flight.
213
+ contentEl.textContent = 'Loading…';
214
+ void active
215
+ .body()
216
+ .then((text) => {
217
+ // Cache in place so re-selecting this tab is instant.
218
+ active.body = text;
219
+ if (epoch !== this.paintEpoch)
220
+ return;
221
+ this.paintCode(text, active.fileUrl);
222
+ // Any highlight requested while this was loading had nothing
223
+ // to measure against; now there is.
224
+ this.applyPendingHighlight();
225
+ })
226
+ .catch((err) => {
227
+ if (epoch !== this.paintEpoch)
228
+ return;
229
+ // A failed body has no lines, so drop the pending highlight
230
+ // rather than leaving it to fire on some later tab.
231
+ this.pendingHighlight = undefined;
232
+ contentEl.textContent = `Could not load this file: ${err instanceof Error ? err.message : String(err)}`;
233
+ this.updateUrlHash();
234
+ });
235
+ }
236
+ applyPendingHighlight() {
237
+ const pending = this.pendingHighlight;
238
+ if (!pending)
239
+ return;
240
+ this.pendingHighlight = undefined;
241
+ this.scrollToAndHighlightLine(pending.line, pending.col, pending.scroll);
242
+ this.updateUrlHash();
243
+ }
244
+ paintCode(body, fileUrl) {
245
+ const { contentEl } = this.els;
246
+ // textContent escapes; Prism then walks the node and replaces text
247
+ // with highlighted spans. Doing it this way (rather than innerHTML)
248
+ // keeps untrusted file content from being interpreted as markup.
249
+ contentEl.textContent = body;
250
+ Prism.highlightElement(contentEl);
251
+ linkifyCodeBlock(contentEl, fileUrl);
252
+ this.installGutterClickHandlers();
253
+ }
254
+ // Wire up clicks on each line-number span in the gutter so the user
255
+ // can highlight any line (clearing any prior highlight). The gutter
256
+ // spans are populated by the Prism line-numbers plugin during
257
+ // `Prism.highlightElement`, so this must run after that call.
258
+ installGutterClickHandlers() {
259
+ const rows = this.els.contentEl.querySelector('.line-numbers-rows');
260
+ if (!rows)
261
+ return;
262
+ rows.classList.add('clickable');
263
+ const spans = rows.children;
264
+ for (let i = 0; i < spans.length; i++) {
265
+ const span = spans[i];
266
+ const lineNumber = i + 1;
267
+ span.addEventListener('click', () => {
268
+ this.scrollToAndHighlightLine(lineNumber, undefined, false);
269
+ this.updateUrlHash();
270
+ });
271
+ }
272
+ }
273
+ scrollToAndHighlightLine(tabLine, col,
274
+ // Gutter clicks pass `false`: the user is clicking on a row
275
+ // they're already looking at, so re-centering it is jarring.
276
+ // Stack-ref clicks (default `true`) need the scroll — the
277
+ // target line may be off-screen or in a different tab.
278
+ scroll = true) {
279
+ const { codePaneEl, contentEl } = this.els;
280
+ if (codePaneEl.hidden)
281
+ return;
282
+ const active = this.tabList[this.activeIdx];
283
+ if (active?.kind === 'code' && typeof active.body !== 'string') {
284
+ // The body is still being fetched, so the code pane holds a
285
+ // placeholder with no line-numbers gutter to measure against.
286
+ // Park the request; `applyPendingHighlight` replays it as soon
287
+ // as the real content paints. `paint()` resolves the thunk into
288
+ // a string before that, so this can't park a second time.
289
+ this.pendingHighlight = { line: tabLine, col, scroll };
290
+ return;
291
+ }
292
+ const api = Prism.plugins?.lineNumbers;
293
+ if (!api?.getLine)
294
+ return;
295
+ const gutterSpan = api.getLine(codePaneEl, tabLine);
296
+ if (!gutterSpan)
297
+ return;
298
+ // gutterSpan's offsetParent is `.line-numbers-rows`, which is
299
+ // positioned absolutely at top: 0 of <code>. So its offsetTop
300
+ // doubles as the y-coordinate of the matching code line within
301
+ // <code>, and inside the <pre> the same value lines up with the
302
+ // pre's scrollable content.
303
+ const top = gutterSpan.offsetTop;
304
+ const height = gutterSpan.offsetHeight || 0;
305
+ if (scroll) {
306
+ codePaneEl.scrollTop = top - codePaneEl.clientHeight / 2 + height / 2;
307
+ }
308
+ // Replace any leftover highlight so successive clicks land on the
309
+ // newest line only (we never stack overlays).
310
+ for (const prior of Array.from(contentEl.querySelectorAll('.line-flash'))) {
311
+ prior.remove();
312
+ }
313
+ const flash = document.createElement('div');
314
+ flash.className = 'line-flash';
315
+ flash.style.top = `${top}px`;
316
+ flash.style.height = `${height}px`;
317
+ // Accent bar at the column position, but only when the caller
318
+ // supplied one (stack-ref clicks carry `col`; line-number gutter
319
+ // clicks don't). Skip on line 1: the wrapper prefix shares that
320
+ // physical line, so a reported column points into the shim rather
321
+ // than user code.
322
+ if (col !== undefined && col > 0 && tabLine > 1) {
323
+ const cs = getComputedStyle(codePaneEl);
324
+ // Tabs in V8's column count are one character each, but the
325
+ // viewer renders them as `tab-size` columns wide (Prism's CSS
326
+ // sets 4). Walk the source line and expand each tab to the next
327
+ // tab stop so the bar lands on the actual rendered glyph.
328
+ const lineText = this.lineTextAt(tabLine);
329
+ const tabSize = parseInt(cs.tabSize, 10) || 8;
330
+ const visualCol = expandToVisualColumn(lineText, col, tabSize);
331
+ // `.line-flash` extends 0.8em left of <code>'s content area so
332
+ // the highlight can cover the header band's negative-margin
333
+ // extension. The bar's `left:` is measured from the flash's
334
+ // edge, so add that 0.8em back to anchor at code's column 0.
335
+ const fontSize = parseFloat(cs.fontSize) || 13;
336
+ const flashLeftEm = 0.8;
337
+ const bar = document.createElement('div');
338
+ bar.className = 'line-flash-col-bar';
339
+ bar.style.left = `${flashLeftEm * fontSize + visualCol * this.measureCodeCharWidth()}px`;
340
+ flash.appendChild(bar);
341
+ }
342
+ contentEl.appendChild(flash);
343
+ // Update the tracked highlight state so the URL-hash builder can
344
+ // read the currently-highlighted line/col. Callers that initiate a
345
+ // UI change (gutter click, stack-ref click) then call
346
+ // updateUrlHash() to push this to location.hash.
347
+ this.highlightLine = tabLine;
348
+ this.highlightCol = col;
349
+ }
350
+ clearLineHighlight() {
351
+ for (const prior of Array.from(this.els.contentEl.querySelectorAll('.line-flash'))) {
352
+ prior.remove();
353
+ }
354
+ this.resetHighlight();
355
+ }
356
+ // Forget the current highlight AND any request parked behind a
357
+ // loading body — a parked request belongs to the tab it was made
358
+ // for, and must not replay onto whatever the user switched to.
359
+ resetHighlight() {
360
+ this.highlightLine = undefined;
361
+ this.highlightCol = undefined;
362
+ this.pendingHighlight = undefined;
363
+ }
364
+ // Read the Nth (1-based) line of the currently-displayed source
365
+ // from <code>'s text content. The line-numbers gutter's children
366
+ // are empty spans (line numbers are CSS counters, not text), so
367
+ // textContent reconstructs the source faithfully.
368
+ lineTextAt(tabLine) {
369
+ const text = this.els.contentEl.textContent ?? '';
370
+ return text.split('\n')[tabLine - 1] ?? '';
371
+ }
372
+ // Cached monospace character width in CSS pixels for the code-pane
373
+ // font. Computed lazily on first highlight; the viewer's font never
374
+ // changes during a session, so a single measurement is enough.
375
+ measureCodeCharWidth() {
376
+ if (this.cachedCharWidth > 0)
377
+ return this.cachedCharWidth;
378
+ const probe = document.createElement('span');
379
+ // Measure across 20 'M's so sub-pixel rounding averages out.
380
+ probe.textContent = 'M'.repeat(20);
381
+ probe.style.visibility = 'hidden';
382
+ probe.style.position = 'absolute';
383
+ probe.style.whiteSpace = 'pre';
384
+ this.els.contentEl.appendChild(probe);
385
+ const w = probe.getBoundingClientRect().width / 20;
386
+ probe.remove();
387
+ this.cachedCharWidth = w;
388
+ return w;
389
+ }
390
+ // ----- URL fragment navigation ---------------------------------
391
+ //
392
+ // `location.hash` mirrors the current tab + highlight so the URL
393
+ // is copyable and the page reopens in the same place.
394
+ // <empty> main script, no highlight
395
+ // #Log Log tab
396
+ // #<filename> that code tab
397
+ // #<filename>:<line> + highlighted line
398
+ // #<filename>:<line>:<col> + column accent bar
399
+ // See `src/extension/script-url-hash.ts` for the grammar / parser.
400
+ // Push the current view state to `location.hash` via
401
+ // `history.replaceState` (no Back-button entries; no hashchange
402
+ // event re-fires into our own listener). No-op when the fragment
403
+ // is already correct.
404
+ updateUrlHash() {
405
+ const tab = this.tabList[this.activeIdx];
406
+ if (!tab)
407
+ return;
408
+ const fragment = tab.kind === 'log'
409
+ ? buildHash({ tab: 'log' })
410
+ : buildHash({
411
+ tab: this.activeIdx === 0 ? 'main' : 'require',
412
+ filename: tab.label,
413
+ // A highlight parked behind a loading body counts: dropping
414
+ // it here would strip `:line:col` out of the user's URL
415
+ // while the fetch is in flight, and it isn't put back.
416
+ line: this.highlightLine ?? this.pendingHighlight?.line,
417
+ col: this.highlightCol ?? this.pendingHighlight?.col,
418
+ });
419
+ const desired = fragment ? '#' + fragment : '';
420
+ if (location.hash === desired)
421
+ return;
422
+ // Empty fragment → drop the `#` from the URL entirely.
423
+ const next = desired || location.pathname + location.search;
424
+ history.replaceState(null, '', next);
425
+ }
426
+ // Resolve a raw hash string against the current tab list. Falls back
427
+ // to the main tab when the filename doesn't match — in that case
428
+ // `line` and `col` are dropped because the line number was relative
429
+ // to the missing file, not main.
430
+ resolveHashTarget(rawHash) {
431
+ const parsed = parseHash(rawHash);
432
+ if (parsed.kind === 'log') {
433
+ const idx = this.tabList.findIndex((t) => t.kind === 'log');
434
+ return { tabIndex: idx >= 0 ? idx : 0 };
435
+ }
436
+ if (parsed.kind === 'code') {
437
+ const idx = this.tabList.findIndex((t) => t.kind === 'code' && t.label === parsed.filename);
438
+ if (idx >= 0)
439
+ return { tabIndex: idx, line: parsed.line, col: parsed.col };
440
+ }
441
+ return { tabIndex: 0 };
442
+ }
443
+ }
444
+ // Convert a 1-based column from V8's stack trace into a 0-based
445
+ // visual column count, expanding each tab character to the next
446
+ // `tabSize` boundary. V8 counts every tab as one column; the
447
+ // viewer renders it as enough spaces to reach the next tab stop.
448
+ export function expandToVisualColumn(lineText, col, tabSize) {
449
+ let visualCol = 0;
450
+ const stop = Math.min(Math.max(col - 1, 0), lineText.length);
451
+ for (let i = 0; i < stop; i++) {
452
+ if (lineText.charCodeAt(i) === 9) {
453
+ visualCol += tabSize - (visualCol % tabSize);
454
+ }
455
+ else {
456
+ visualCol++;
457
+ }
458
+ }
459
+ return visualCol;
460
+ }