diff.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. import {
  2. type FileContents,
  3. FileDiff,
  4. type DiffLineAnnotation,
  5. type HunkData,
  6. FileDiffOptions,
  7. registerCustomTheme,
  8. ThemeRegistrationResolved,
  9. } from "@pierre/precision-diffs"
  10. import { ComponentProps, createEffect, splitProps } from "solid-js"
  11. export type DiffProps<T = {}> = FileDiffOptions<T> & {
  12. before: FileContents
  13. after: FileContents
  14. annotations?: DiffLineAnnotation<T>[]
  15. class?: string
  16. classList?: ComponentProps<"div">["classList"]
  17. }
  18. // interface ThreadMetadata {
  19. // threadId: string
  20. // }
  21. export function Diff<T>(props: DiffProps<T>) {
  22. let container!: HTMLDivElement
  23. const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
  24. // const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [
  25. // {
  26. // side: "additions",
  27. // // The line number specified for an annotation is the visual line number
  28. // // you see in the number column of a diff
  29. // lineNumber: 16,
  30. // metadata: { threadId: "68b329da9893e34099c7d8ad5cb9c940" },
  31. // },
  32. // ]
  33. // If you ever want to update the options for an instance, simple call
  34. // 'setOptions' with the new options. Bear in mind, this does NOT merge
  35. // existing properties, it's a full replace
  36. // instance.setOptions({
  37. // ...instance.options,
  38. // theme: "pierre-dark",
  39. // themes: undefined,
  40. // })
  41. //
  42. // When ready to render, simply call .render with old/new file, optional
  43. // annotations and a container element to hold the diff
  44. createEffect(() => {
  45. const instance = new FileDiff<T>({
  46. theme: "OpenCode",
  47. // When using the 'themes' prop, 'themeType' allows you to force 'dark'
  48. // or 'light' theme, or inherit from the OS ('system') theme.
  49. themeType: "system",
  50. // Disable the line numbers for your diffs, generally not recommended
  51. disableLineNumbers: false,
  52. // Whether code should 'wrap' with long lines or 'scroll'.
  53. overflow: "wrap",
  54. // Normally you shouldn't need this prop, but if you don't provide a
  55. // valid filename or your file doesn't have an extension you may want to
  56. // override the automatic detection. You can specify that language here:
  57. // https://shiki.style/languages
  58. // lang?: SupportedLanguages;
  59. // 'diffStyle' controls whether the diff is presented side by side or
  60. // in a unified (single column) view
  61. diffStyle: "unified",
  62. // Line decorators to help highlight changes.
  63. // 'bars' (default):
  64. // Shows some red-ish or green-ish (theme dependent) bars on the left
  65. // edge of relevant lines
  66. //
  67. // 'classic':
  68. // shows '+' characters on additions and '-' characters on deletions
  69. //
  70. // 'none':
  71. // No special diff indicators are shown
  72. diffIndicators: "bars",
  73. // By default green-ish or red-ish background are shown on added and
  74. // deleted lines respectively. Disable that feature here
  75. disableBackground: false,
  76. // Diffs are split up into hunks, this setting customizes what to show
  77. // between each hunk.
  78. //
  79. // 'line-info' (default):
  80. // Shows a bar that tells you how many lines are collapsed. If you are
  81. // using the oldFile/newFile API then you can click those bars to
  82. // expand the content between them
  83. //
  84. // 'metadata':
  85. // Shows the content you'd see in a normal patch file, usually in some
  86. // format like '@@ -60,6 +60,22 @@'. You cannot use these to expand
  87. // hidden content
  88. //
  89. // 'simple':
  90. // Just a subtle bar separator between each hunk
  91. // hunkSeparators: "line-info",
  92. hunkSeparators(hunkData: HunkData) {
  93. const fragment = document.createDocumentFragment()
  94. const numCol = document.createElement("div")
  95. numCol.innerHTML = `<svg data-slot="diff-hunk-separator-line-number-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.97978 14.0204L8.62623 13.6668L9.33334 12.9597L9.68689 13.3133L9.33333 13.6668L8.97978 14.0204ZM12 16.3335L12.3535 16.6871L12 17.0406L11.6464 16.687L12 16.3335ZM14.3131 13.3133L14.6667 12.9597L15.3738 13.6668L15.0202 14.0204L14.6667 13.6668L14.3131 13.3133ZM12.5 16.0002V16.5002H11.5V16.0002H12H12.5ZM9.33333 13.6668L9.68689 13.3133L12.3535 15.9799L12 16.3335L11.6464 16.687L8.97978 14.0204L9.33333 13.6668ZM12 16.3335L11.6464 15.9799L14.3131 13.3133L14.6667 13.6668L15.0202 14.0204L12.3535 16.6871L12 16.3335ZM6.5 8.00016V7.50016H8.5V8.00016V8.50016H6.5V8.00016ZM9.5 8.00016V7.50016H11.5V8.00016V8.50016H9.5V8.00016ZM12.5 8.00016V7.50016H14.5V8.00016V8.50016H12.5V8.00016ZM15.5 8.00016V7.50016H17.5V8.00016V8.50016H15.5V8.00016ZM12 10.5002H12.5V16.0002H12H11.5V10.5002H12Z" fill="currentColor"/></svg> `
  96. numCol.dataset["slot"] = "diff-hunk-separator-line-number"
  97. fragment.appendChild(numCol)
  98. const contentCol = document.createElement("div")
  99. contentCol.dataset["slot"] = "diff-hunk-separator-content"
  100. const span = document.createElement("span")
  101. span.dataset["slot"] = "diff-hunk-separator-content-span"
  102. span.textContent = `${hunkData.lines} unmodified lines`
  103. contentCol.appendChild(span)
  104. fragment.appendChild(contentCol)
  105. return fragment
  106. },
  107. // On lines that have both additions and deletions, we can run a
  108. // separate diff check to mark parts of the lines that change.
  109. // 'none':
  110. // Do not show these secondary highlights
  111. //
  112. // 'char':
  113. // Show changes at a per character granularity
  114. //
  115. // 'word':
  116. // Show changes but rounded up to word boundaries
  117. //
  118. // 'word-alt' (default):
  119. // Similar to 'word', however we attempt to minimize single character
  120. // gaps between highlighted changes
  121. lineDiffType: "word-alt",
  122. // If lines exceed these character lengths then we won't perform the
  123. // line lineDiffType check
  124. maxLineDiffLength: 1000,
  125. // If any line in the diff exceeds this value then we won't attempt to
  126. // syntax highlight the diff
  127. maxLineLengthForHighlighting: 1000,
  128. // Enabling this property will hide the file header with file name and
  129. // diff stats.
  130. disableFileHeader: true,
  131. // You can optionally pass a render function for rendering out line
  132. // annotations. Just return the dom node to render
  133. // renderAnnotation(annotation: DiffLineAnnotation<T>): HTMLElement {
  134. // // Despite the diff itself being rendered in the shadow dom,
  135. // // annotations are inserted via the web components 'slots' api and you
  136. // // can use all your normal normal css and styling for them
  137. // const element = document.createElement("div")
  138. // element.innerText = annotation.metadata.threadId
  139. // return element
  140. // },
  141. ...others,
  142. })
  143. container.innerHTML = ""
  144. instance.render({
  145. oldFile: local.before,
  146. newFile: local.after,
  147. lineAnnotations: local.annotations,
  148. containerWrapper: container,
  149. })
  150. })
  151. return (
  152. <div
  153. data-component="diff"
  154. style={{
  155. "--pjs-font-family": "var(--font-family-mono)",
  156. "--pjs-font-size": "var(--font-size-small)",
  157. "--pjs-line-height": "24px",
  158. "--pjs-tab-size": 2,
  159. "--pjs-font-features": "var(--font-family-mono--font-feature-settings)",
  160. "--pjs-header-font-family": "var(--font-family-sans)",
  161. "--pjs-gap-block": 0,
  162. "--pjs-min-number-column-width": "4ch",
  163. }}
  164. ref={container}
  165. />
  166. )
  167. }
  168. registerCustomTheme("OpenCode", () => {
  169. return Promise.resolve({
  170. name: "OpenCode",
  171. colors: {
  172. "editor.background": "transparent",
  173. "editor.foreground": "var(--text-base)",
  174. "gitDecoration.addedResourceForeground": "var(--syntax-diff-add)",
  175. "gitDecoration.deletedResourceForeground": "var(--syntax-diff-delete)",
  176. // "gitDecoration.conflictingResourceForeground": "#ffca00",
  177. // "gitDecoration.modifiedResourceForeground": "#1a76d4",
  178. // "gitDecoration.untrackedResourceForeground": "#00cab1",
  179. // "gitDecoration.ignoredResourceForeground": "#84848A",
  180. // "terminal.titleForeground": "#adadb1",
  181. // "terminal.titleInactiveForeground": "#84848A",
  182. // "terminal.background": "#141415",
  183. // "terminal.foreground": "#adadb1",
  184. // "terminal.ansiBlack": "#141415",
  185. // "terminal.ansiRed": "#ff2e3f",
  186. // "terminal.ansiGreen": "#0dbe4e",
  187. // "terminal.ansiYellow": "#ffca00",
  188. // "terminal.ansiBlue": "#008cff",
  189. // "terminal.ansiMagenta": "#c635e4",
  190. // "terminal.ansiCyan": "#08c0ef",
  191. // "terminal.ansiWhite": "#c6c6c8",
  192. // "terminal.ansiBrightBlack": "#141415",
  193. // "terminal.ansiBrightRed": "#ff2e3f",
  194. // "terminal.ansiBrightGreen": "#0dbe4e",
  195. // "terminal.ansiBrightYellow": "#ffca00",
  196. // "terminal.ansiBrightBlue": "#008cff",
  197. // "terminal.ansiBrightMagenta": "#c635e4",
  198. // "terminal.ansiBrightCyan": "#08c0ef",
  199. // "terminal.ansiBrightWhite": "#c6c6c8",
  200. },
  201. tokenColors: [
  202. {
  203. scope: ["comment", "punctuation.definition.comment", "string.comment"],
  204. settings: {
  205. foreground: "var(--syntax-comment)",
  206. },
  207. },
  208. {
  209. scope: ["entity.other.attribute-name"],
  210. settings: {
  211. foreground: "var(--syntax-property)", // maybe attribute
  212. },
  213. },
  214. {
  215. scope: ["constant", "entity.name.constant", "variable.other.constant", "variable.language", "entity"],
  216. settings: {
  217. foreground: "var(--syntax-constant)",
  218. },
  219. },
  220. {
  221. scope: ["entity.name", "meta.export.default", "meta.definition.variable"],
  222. settings: {
  223. foreground: "var(--syntax-type)",
  224. },
  225. },
  226. {
  227. scope: ["meta.object.member"],
  228. settings: {
  229. foreground: "var(--syntax-primitive)",
  230. },
  231. },
  232. {
  233. scope: [
  234. "variable.parameter.function",
  235. "meta.jsx.children",
  236. "meta.block",
  237. "meta.tag.attributes",
  238. "entity.name.constant",
  239. "meta.embedded.expression",
  240. "meta.template.expression",
  241. "string.other.begin.yaml",
  242. "string.other.end.yaml",
  243. ],
  244. settings: {
  245. foreground: "var(--syntax-punctuation)",
  246. },
  247. },
  248. {
  249. scope: ["entity.name.function", "support.type.primitive"],
  250. settings: {
  251. foreground: "var(--syntax-primitive)",
  252. },
  253. },
  254. {
  255. scope: ["support.class.component"],
  256. settings: {
  257. foreground: "var(--syntax-type)",
  258. },
  259. },
  260. {
  261. scope: "keyword",
  262. settings: {
  263. foreground: "var(--syntax-keyword)",
  264. },
  265. },
  266. {
  267. scope: [
  268. "keyword.operator",
  269. "storage.type.function.arrow",
  270. "punctuation.separator.key-value.css",
  271. "entity.name.tag.yaml",
  272. "punctuation.separator.key-value.mapping.yaml",
  273. ],
  274. settings: {
  275. foreground: "var(--syntax-operator)",
  276. },
  277. },
  278. {
  279. scope: ["storage", "storage.type"],
  280. settings: {
  281. foreground: "var(--syntax-keyword)",
  282. },
  283. },
  284. {
  285. scope: ["storage.modifier.package", "storage.modifier.import", "storage.type.java"],
  286. settings: {
  287. foreground: "var(--syntax-primitive)",
  288. },
  289. },
  290. {
  291. scope: [
  292. "string",
  293. "punctuation.definition.string",
  294. "string punctuation.section.embedded source",
  295. "entity.name.tag",
  296. ],
  297. settings: {
  298. foreground: "var(--syntax-string)",
  299. },
  300. },
  301. {
  302. scope: "support",
  303. settings: {
  304. foreground: "var(--syntax-primitive)",
  305. },
  306. },
  307. {
  308. scope: ["support.type.object.module", "variable.other.object", "support.type.property-name.css"],
  309. settings: {
  310. foreground: "var(--syntax-object)",
  311. },
  312. },
  313. {
  314. scope: "meta.property-name",
  315. settings: {
  316. foreground: "var(--syntax-property)",
  317. },
  318. },
  319. {
  320. scope: "variable",
  321. settings: {
  322. foreground: "var(--syntax-variable)",
  323. },
  324. },
  325. {
  326. scope: "variable.other",
  327. settings: {
  328. foreground: "var(--syntax-variable)",
  329. },
  330. },
  331. {
  332. scope: [
  333. "invalid.broken",
  334. "invalid.illegal",
  335. "invalid.unimplemented",
  336. "invalid.deprecated",
  337. "message.error",
  338. "markup.deleted",
  339. "meta.diff.header.from-file",
  340. "punctuation.definition.deleted",
  341. "brackethighlighter.unmatched",
  342. "token.error-token",
  343. ],
  344. settings: {
  345. foreground: "var(--syntax-critical)",
  346. },
  347. },
  348. {
  349. scope: "carriage-return",
  350. settings: {
  351. foreground: "var(--syntax-keyword)",
  352. },
  353. },
  354. {
  355. scope: "string source",
  356. settings: {
  357. foreground: "var(--syntax-variable)",
  358. },
  359. },
  360. {
  361. scope: "string variable",
  362. settings: {
  363. foreground: "var(--syntax-constant)",
  364. },
  365. },
  366. {
  367. scope: [
  368. "source.regexp",
  369. "string.regexp",
  370. "string.regexp.character-class",
  371. "string.regexp constant.character.escape",
  372. "string.regexp source.ruby.embedded",
  373. "string.regexp string.regexp.arbitrary-repitition",
  374. "string.regexp constant.character.escape",
  375. ],
  376. settings: {
  377. foreground: "var(--syntax-regexp)",
  378. },
  379. },
  380. {
  381. scope: "support.constant",
  382. settings: {
  383. foreground: "var(--syntax-primitive)",
  384. },
  385. },
  386. {
  387. scope: "support.variable",
  388. settings: {
  389. foreground: "var(--syntax-variable)",
  390. },
  391. },
  392. {
  393. scope: "meta.module-reference",
  394. settings: {
  395. foreground: "var(--syntax-info)",
  396. },
  397. },
  398. {
  399. scope: "punctuation.definition.list.begin.markdown",
  400. settings: {
  401. foreground: "var(--syntax-punctuation)",
  402. },
  403. },
  404. {
  405. scope: ["markup.heading", "markup.heading entity.name"],
  406. settings: {
  407. fontStyle: "bold",
  408. foreground: "var(--syntax-info)",
  409. },
  410. },
  411. {
  412. scope: "markup.quote",
  413. settings: {
  414. foreground: "var(--syntax-info)",
  415. },
  416. },
  417. {
  418. scope: "markup.italic",
  419. settings: {
  420. fontStyle: "italic",
  421. // foreground: "",
  422. },
  423. },
  424. {
  425. scope: "markup.bold",
  426. settings: {
  427. fontStyle: "bold",
  428. foreground: "var(--text-strong)",
  429. },
  430. },
  431. {
  432. scope: [
  433. "markup.raw",
  434. "markup.inserted",
  435. "meta.diff.header.to-file",
  436. "punctuation.definition.inserted",
  437. "markup.changed",
  438. "punctuation.definition.changed",
  439. "markup.ignored",
  440. "markup.untracked",
  441. ],
  442. settings: {
  443. foreground: "var(--text-base)",
  444. },
  445. },
  446. {
  447. scope: "meta.diff.range",
  448. settings: {
  449. fontStyle: "bold",
  450. foreground: "var(--syntax-unknown)",
  451. },
  452. },
  453. {
  454. scope: "meta.diff.header",
  455. settings: {
  456. foreground: "var(--syntax-unknown)",
  457. },
  458. },
  459. {
  460. scope: "meta.separator",
  461. settings: {
  462. fontStyle: "bold",
  463. foreground: "var(--syntax-unknown)",
  464. },
  465. },
  466. {
  467. scope: "meta.output",
  468. settings: {
  469. foreground: "var(--syntax-unknown)",
  470. },
  471. },
  472. {
  473. scope: "meta.export.default",
  474. settings: {
  475. foreground: "var(--syntax-unknown)",
  476. },
  477. },
  478. {
  479. scope: [
  480. "brackethighlighter.tag",
  481. "brackethighlighter.curly",
  482. "brackethighlighter.round",
  483. "brackethighlighter.square",
  484. "brackethighlighter.angle",
  485. "brackethighlighter.quote",
  486. ],
  487. settings: {
  488. foreground: "var(--syntax-unknown)",
  489. },
  490. },
  491. {
  492. scope: ["constant.other.reference.link", "string.other.link"],
  493. settings: {
  494. fontStyle: "underline",
  495. foreground: "var(--syntax-unknown)",
  496. },
  497. },
  498. {
  499. scope: "token.info-token",
  500. settings: {
  501. foreground: "var(--syntax-info)",
  502. },
  503. },
  504. {
  505. scope: "token.warn-token",
  506. settings: {
  507. foreground: "var(--syntax-warning)",
  508. },
  509. },
  510. {
  511. scope: "token.debug-token",
  512. settings: {
  513. foreground: "var(--syntax-info)",
  514. },
  515. },
  516. ],
  517. semanticTokenColors: {
  518. comment: "var(--syntax-comment)",
  519. string: "var(--syntax-string)",
  520. number: "var(--syntax-constant)",
  521. regexp: "var(--syntax-regexp)",
  522. keyword: "var(--syntax-keyword)",
  523. variable: "var(--syntax-variable)",
  524. parameter: "var(--syntax-variable)",
  525. property: "var(--syntax-property)",
  526. function: "var(--syntax-primitive)",
  527. method: "var(--syntax-primitive)",
  528. type: "var(--syntax-type)",
  529. class: "var(--syntax-type)",
  530. namespace: "var(--syntax-type)",
  531. enumMember: "var(--syntax-primitive)",
  532. "variable.constant": "var(--syntax-constant)",
  533. "variable.defaultLibrary": "var(--syntax-unknown)",
  534. },
  535. } as unknown as ThemeRegistrationResolved)
  536. })