SymbolHeader.svelte 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <!--
  2. The focus card: what this symbol is, where it lives, and the three claims
  3. worth making before the body (design spec §3.2).
  4. The badges are the honesty layer. "exported" and "hub · N callers" are facts
  5. about reach; the test badge is the one that changes behaviour — an amber
  6. "No test reaches this within 3 caller hops" is the difference between editing
  7. freely and editing carefully, so it is stated in the header rather than left
  8. to be inferred from an empty rail.
  9. -->
  10. <script lang="ts">
  11. import KindGlyph from '../KindGlyph.svelte';
  12. import { fileHref } from '../../lib/router.svelte';
  13. import { kindPhrase, plural } from '../../lib/symbol-model';
  14. import type {
  15. WireNodeDetail,
  16. WireNodeRef,
  17. WireRelation,
  18. WireSymbolPayload,
  19. } from '../../lib/api';
  20. interface Props {
  21. payload: WireSymbolPayload;
  22. onopen: (node: WireNodeRef) => void;
  23. }
  24. let { payload, onopen }: Props = $props();
  25. let node = $derived<WireNodeDetail>(payload.node);
  26. let tests = $derived(payload.tests);
  27. /** `extends`/`implements` this symbol declares, and the ones declared on it. */
  28. let supertypes = $derived(
  29. payload.outgoing.items.filter((r) => r.edgeKinds.some((k) => k === 'extends' || k === 'implements'))
  30. );
  31. let subtypes = $derived(
  32. payload.incoming.items.filter((r) => r.edgeKinds.some((k) => k === 'extends' || k === 'implements'))
  33. );
  34. const TYPE_CHIP_LIMIT = 12;
  35. let typeChips = $derived(payload.typesUsed.slice(0, TYPE_CHIP_LIMIT));
  36. function relationWord(relation: WireRelation): string {
  37. return relation.edgeKinds.includes('implements') ? 'implements' : 'extends';
  38. }
  39. /**
  40. * The test claim, worded to exactly what was checked. An interrupted search
  41. * (`exhaustive: false`) only ever established that no test calls the symbol
  42. * directly, so the badge must not widen that to three hops.
  43. */
  44. let testBadge = $derived.by(() => {
  45. if (tests.reached) {
  46. return {
  47. warn: false,
  48. text: `Reached by tests · ${plural(tests.fileCount, 'file')} within ${tests.hopsSearched} hop${tests.hopsSearched === 1 ? '' : 's'}`,
  49. title: tests.files.join(', '),
  50. };
  51. }
  52. return {
  53. warn: true,
  54. text: tests.exhaustive
  55. ? `No test reaches this within ${tests.hopsSearched} caller hops`
  56. : 'No test calls this directly',
  57. title: tests.exhaustive
  58. ? 'No test file reaches this symbol within the caller hops searched.'
  59. : 'The caller search ran out of budget — only direct callers were checked.',
  60. };
  61. });
  62. </script>
  63. <div class="card-h">
  64. <KindGlyph kind={node.kind} titled />
  65. <h1>{node.name}</h1>
  66. <span class="kindword">{kindPhrase(node)}</span>
  67. <span class="loc mono">
  68. <a href={fileHref(node.file, { line: node.line })}>{node.file}</a>:{node.line}–{node.endLine}
  69. · {plural(node.lines, 'line')}
  70. </span>
  71. </div>
  72. {#if payload.ancestors.length > 0}
  73. <div class="parents mono">
  74. in {#each payload.ancestors as ancestor, i (ancestor.id)}{#if i > 0}<span class="sep"> › </span
  75. >{/if}<button type="button" onclick={() => onopen(ancestor)}>{ancestor.name}</button
  76. >{/each}
  77. </div>
  78. {/if}
  79. <div class="badges">
  80. {#if node.exported}<span class="badge">exported</span>{/if}
  81. {#if payload.counts.hub}
  82. <span class="badge hub" title="Changing this reaches a lot of the repo">
  83. hub · {plural(payload.counts.callers, 'caller')}
  84. </span>
  85. {/if}
  86. {#if payload.drift}
  87. <span class="badge warn" title="The line ranges below come from the last index sync">
  88. <span class="sw"></span>changed on disk after the last index sync
  89. </span>
  90. {/if}
  91. <span class="badge" class:warn={testBadge.warn} title={testBadge.title}>
  92. <span class="sw"></span>{testBadge.text}
  93. </span>
  94. </div>
  95. {#if node.signature}
  96. <div class="sig">{node.name}{node.signature}</div>
  97. {/if}
  98. {#if node.docstring}
  99. <div class="doc">{node.docstring}</div>
  100. {/if}
  101. {#if supertypes.length > 0 || subtypes.length > 0 || typeChips.length > 0}
  102. <div class="rel">
  103. {#if supertypes.length > 0}
  104. <span>
  105. {#each supertypes as relation (relation.node.id)}
  106. {relationWord(relation)}
  107. <button type="button" class="chip" onclick={() => onopen(relation.node)}>
  108. {relation.node.name}
  109. </button>
  110. {/each}
  111. </span>
  112. {/if}
  113. {#if subtypes.length > 0}
  114. <span>
  115. {subtypes[0]?.edgeKinds.includes('implements') ? 'implemented by' : 'extended by'}
  116. {#each subtypes as relation (relation.node.id)}
  117. <button type="button" class="chip" onclick={() => onopen(relation.node)}>
  118. {relation.node.name}
  119. </button>
  120. {/each}
  121. </span>
  122. {/if}
  123. {#if typeChips.length > 0}
  124. <span>
  125. uses types
  126. {#each typeChips as relation (relation.node.id)}
  127. <button type="button" class="chip" onclick={() => onopen(relation.node)}>
  128. {relation.node.name}
  129. </button>
  130. {/each}
  131. {#if payload.typesUsed.length > TYPE_CHIP_LIMIT}
  132. <span class="dim">+{payload.typesUsed.length - TYPE_CHIP_LIMIT}</span>
  133. {/if}
  134. </span>
  135. {/if}
  136. </div>
  137. {/if}
  138. <style>
  139. .card-h {
  140. display: flex;
  141. flex-wrap: wrap;
  142. align-items: baseline;
  143. gap: 6px 12px;
  144. }
  145. .card-h h1 {
  146. margin: 0;
  147. font: 600 20px/1.2 var(--mono);
  148. letter-spacing: -0.01em;
  149. }
  150. .kindword {
  151. color: var(--ink-3);
  152. font-size: 12.5px;
  153. }
  154. .loc {
  155. color: var(--ink-2);
  156. font-size: 11.5px;
  157. }
  158. .loc a:hover {
  159. text-decoration: underline;
  160. }
  161. .parents {
  162. margin-top: 6px;
  163. color: var(--ink-3);
  164. font-size: 11.5px;
  165. }
  166. .parents button {
  167. color: inherit;
  168. font: inherit;
  169. }
  170. .parents button:hover {
  171. color: var(--ink);
  172. text-decoration: underline;
  173. }
  174. .parents .sep {
  175. color: var(--ink-4);
  176. }
  177. .badges {
  178. display: flex;
  179. flex-wrap: wrap;
  180. gap: 6px;
  181. margin-top: 10px;
  182. }
  183. .badge {
  184. display: inline-flex;
  185. align-items: center;
  186. gap: 5px;
  187. padding: 2px 7px;
  188. border: 1px solid var(--rule-soft);
  189. background: var(--paper);
  190. color: var(--ink-2);
  191. font-size: 11.5px;
  192. }
  193. /* Amber is used here and nowhere else in the app. */
  194. .badge.warn {
  195. border-color: var(--amber);
  196. background: var(--amber-soft);
  197. color: var(--amber);
  198. }
  199. .badge.hub {
  200. border-color: var(--ink);
  201. }
  202. .sw {
  203. display: inline-block;
  204. width: 8px;
  205. height: 8px;
  206. border: 1px solid currentColor;
  207. }
  208. .badge.warn .sw {
  209. background: currentColor;
  210. }
  211. .sig {
  212. margin-top: 10px;
  213. color: var(--ink-2);
  214. font: 12px var(--mono);
  215. white-space: pre-wrap;
  216. word-break: break-word;
  217. }
  218. .doc {
  219. margin-top: 8px;
  220. max-width: 70ch;
  221. color: var(--ink-2);
  222. font-size: 12.5px;
  223. white-space: pre-wrap;
  224. }
  225. .rel {
  226. display: flex;
  227. flex-wrap: wrap;
  228. align-items: baseline;
  229. gap: 6px;
  230. margin-top: 10px;
  231. color: var(--ink-3);
  232. font-size: 12px;
  233. }
  234. .rel > span {
  235. display: inline-flex;
  236. flex-wrap: wrap;
  237. align-items: baseline;
  238. gap: 6px;
  239. }
  240. .chip {
  241. padding: 1px 6px;
  242. border: 1px solid var(--rule-soft);
  243. background: var(--paper);
  244. color: var(--ink-2);
  245. font: 11.5px var(--mono);
  246. }
  247. .chip:hover {
  248. border-color: var(--ink);
  249. color: var(--ink);
  250. }
  251. </style>