theme.tsx 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
  2. import path from "path"
  3. import { createEffect, createMemo, onMount } from "solid-js"
  4. import { useSync } from "@tui/context/sync"
  5. import { createSimpleContext } from "./helper"
  6. import aura from "./theme/aura.json" with { type: "json" }
  7. import ayu from "./theme/ayu.json" with { type: "json" }
  8. import catppuccin from "./theme/catppuccin.json" with { type: "json" }
  9. import cobalt2 from "./theme/cobalt2.json" with { type: "json" }
  10. import dracula from "./theme/dracula.json" with { type: "json" }
  11. import everforest from "./theme/everforest.json" with { type: "json" }
  12. import flexoki from "./theme/flexoki.json" with { type: "json" }
  13. import github from "./theme/github.json" with { type: "json" }
  14. import gruvbox from "./theme/gruvbox.json" with { type: "json" }
  15. import kanagawa from "./theme/kanagawa.json" with { type: "json" }
  16. import material from "./theme/material.json" with { type: "json" }
  17. import matrix from "./theme/matrix.json" with { type: "json" }
  18. import monokai from "./theme/monokai.json" with { type: "json" }
  19. import nightowl from "./theme/nightowl.json" with { type: "json" }
  20. import nord from "./theme/nord.json" with { type: "json" }
  21. import onedark from "./theme/one-dark.json" with { type: "json" }
  22. import opencode from "./theme/opencode.json" with { type: "json" }
  23. import palenight from "./theme/palenight.json" with { type: "json" }
  24. import rosepine from "./theme/rosepine.json" with { type: "json" }
  25. import solarized from "./theme/solarized.json" with { type: "json" }
  26. import synthwave84 from "./theme/synthwave84.json" with { type: "json" }
  27. import tokyonight from "./theme/tokyonight.json" with { type: "json" }
  28. import vesper from "./theme/vesper.json" with { type: "json" }
  29. import zenburn from "./theme/zenburn.json" with { type: "json" }
  30. import { useKV } from "./kv"
  31. import { useRenderer } from "@opentui/solid"
  32. import { createStore, produce } from "solid-js/store"
  33. import { Global } from "@/global"
  34. import { Filesystem } from "@/util/filesystem"
  35. type ThemeColors = {
  36. primary: RGBA
  37. secondary: RGBA
  38. accent: RGBA
  39. error: RGBA
  40. warning: RGBA
  41. success: RGBA
  42. info: RGBA
  43. text: RGBA
  44. textMuted: RGBA
  45. selectedListItemText: RGBA
  46. background: RGBA
  47. backgroundPanel: RGBA
  48. backgroundElement: RGBA
  49. backgroundMenu: RGBA
  50. border: RGBA
  51. borderActive: RGBA
  52. borderSubtle: RGBA
  53. diffAdded: RGBA
  54. diffRemoved: RGBA
  55. diffContext: RGBA
  56. diffHunkHeader: RGBA
  57. diffHighlightAdded: RGBA
  58. diffHighlightRemoved: RGBA
  59. diffAddedBg: RGBA
  60. diffRemovedBg: RGBA
  61. diffContextBg: RGBA
  62. diffLineNumber: RGBA
  63. diffAddedLineNumberBg: RGBA
  64. diffRemovedLineNumberBg: RGBA
  65. markdownText: RGBA
  66. markdownHeading: RGBA
  67. markdownLink: RGBA
  68. markdownLinkText: RGBA
  69. markdownCode: RGBA
  70. markdownBlockQuote: RGBA
  71. markdownEmph: RGBA
  72. markdownStrong: RGBA
  73. markdownHorizontalRule: RGBA
  74. markdownListItem: RGBA
  75. markdownListEnumeration: RGBA
  76. markdownImage: RGBA
  77. markdownImageText: RGBA
  78. markdownCodeBlock: RGBA
  79. syntaxComment: RGBA
  80. syntaxKeyword: RGBA
  81. syntaxFunction: RGBA
  82. syntaxVariable: RGBA
  83. syntaxString: RGBA
  84. syntaxNumber: RGBA
  85. syntaxType: RGBA
  86. syntaxOperator: RGBA
  87. syntaxPunctuation: RGBA
  88. }
  89. type Theme = ThemeColors & {
  90. _hasSelectedListItemText: boolean
  91. }
  92. export function selectedForeground(theme: Theme): RGBA {
  93. // If theme explicitly defines selectedListItemText, use it
  94. if (theme._hasSelectedListItemText) {
  95. return theme.selectedListItemText
  96. }
  97. // For transparent backgrounds, calculate contrast based on primary color
  98. if (theme.background.a === 0) {
  99. const { r, g, b } = theme.primary
  100. const luminance = 0.299 * r + 0.587 * g + 0.114 * b
  101. return luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255)
  102. }
  103. // Fall back to background color
  104. return theme.background
  105. }
  106. type HexColor = `#${string}`
  107. type RefName = string
  108. type Variant = {
  109. dark: HexColor | RefName
  110. light: HexColor | RefName
  111. }
  112. type ColorValue = HexColor | RefName | Variant | RGBA
  113. type ThemeJson = {
  114. $schema?: string
  115. defs?: Record<string, HexColor | RefName>
  116. theme: Omit<Record<keyof ThemeColors, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
  117. selectedListItemText?: ColorValue
  118. backgroundMenu?: ColorValue
  119. }
  120. }
  121. export const DEFAULT_THEMES: Record<string, ThemeJson> = {
  122. aura,
  123. ayu,
  124. catppuccin,
  125. cobalt2,
  126. dracula,
  127. everforest,
  128. flexoki,
  129. github,
  130. gruvbox,
  131. kanagawa,
  132. material,
  133. matrix,
  134. monokai,
  135. nightowl,
  136. nord,
  137. ["one-dark"]: onedark,
  138. opencode,
  139. palenight,
  140. rosepine,
  141. solarized,
  142. synthwave84,
  143. tokyonight,
  144. vesper,
  145. zenburn,
  146. }
  147. function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
  148. const defs = theme.defs ?? {}
  149. function resolveColor(c: ColorValue): RGBA {
  150. if (c instanceof RGBA) return c
  151. if (typeof c === "string") {
  152. if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0)
  153. if (c.startsWith("#")) return RGBA.fromHex(c)
  154. if (defs[c]) {
  155. return resolveColor(defs[c])
  156. } else if (theme.theme[c as keyof ThemeColors] !== undefined) {
  157. return resolveColor(theme.theme[c as keyof ThemeColors]!)
  158. } else {
  159. throw new Error(`Color reference "${c}" not found in defs or theme`)
  160. }
  161. }
  162. if (typeof c === "number") {
  163. return ansiToRgba(c)
  164. }
  165. return resolveColor(c[mode])
  166. }
  167. const resolved = Object.fromEntries(
  168. Object.entries(theme.theme)
  169. .filter(([key]) => key !== "selectedListItemText" && key !== "backgroundMenu")
  170. .map(([key, value]) => {
  171. return [key, resolveColor(value)]
  172. }),
  173. ) as Partial<ThemeColors>
  174. // Handle selectedListItemText separately since it's optional
  175. const hasSelectedListItemText = theme.theme.selectedListItemText !== undefined
  176. if (hasSelectedListItemText) {
  177. resolved.selectedListItemText = resolveColor(theme.theme.selectedListItemText!)
  178. } else {
  179. // Backward compatibility: if selectedListItemText is not defined, use background color
  180. // This preserves the current behavior for all existing themes
  181. resolved.selectedListItemText = resolved.background
  182. }
  183. // Handle backgroundMenu - optional with fallback to backgroundElement
  184. if (theme.theme.backgroundMenu !== undefined) {
  185. resolved.backgroundMenu = resolveColor(theme.theme.backgroundMenu)
  186. } else {
  187. resolved.backgroundMenu = resolved.backgroundElement
  188. }
  189. return {
  190. ...resolved,
  191. _hasSelectedListItemText: hasSelectedListItemText,
  192. } as Theme
  193. }
  194. function ansiToRgba(code: number): RGBA {
  195. // Standard ANSI colors (0-15)
  196. if (code < 16) {
  197. const ansiColors = [
  198. "#000000", // Black
  199. "#800000", // Red
  200. "#008000", // Green
  201. "#808000", // Yellow
  202. "#000080", // Blue
  203. "#800080", // Magenta
  204. "#008080", // Cyan
  205. "#c0c0c0", // White
  206. "#808080", // Bright Black
  207. "#ff0000", // Bright Red
  208. "#00ff00", // Bright Green
  209. "#ffff00", // Bright Yellow
  210. "#0000ff", // Bright Blue
  211. "#ff00ff", // Bright Magenta
  212. "#00ffff", // Bright Cyan
  213. "#ffffff", // Bright White
  214. ]
  215. return RGBA.fromHex(ansiColors[code] ?? "#000000")
  216. }
  217. // 6x6x6 Color Cube (16-231)
  218. if (code < 232) {
  219. const index = code - 16
  220. const b = index % 6
  221. const g = Math.floor(index / 6) % 6
  222. const r = Math.floor(index / 36)
  223. const val = (x: number) => (x === 0 ? 0 : x * 40 + 55)
  224. return RGBA.fromInts(val(r), val(g), val(b))
  225. }
  226. // Grayscale Ramp (232-255)
  227. if (code < 256) {
  228. const gray = (code - 232) * 10 + 8
  229. return RGBA.fromInts(gray, gray, gray)
  230. }
  231. // Fallback for invalid codes
  232. return RGBA.fromInts(0, 0, 0)
  233. }
  234. export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
  235. name: "Theme",
  236. init: (props: { mode: "dark" | "light" }) => {
  237. const sync = useSync()
  238. const kv = useKV()
  239. const [store, setStore] = createStore({
  240. themes: DEFAULT_THEMES,
  241. mode: kv.get("theme_mode", props.mode),
  242. active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
  243. ready: false,
  244. })
  245. createEffect(async () => {
  246. const custom = await getCustomThemes()
  247. setStore(
  248. produce((draft) => {
  249. Object.assign(draft.themes, custom)
  250. draft.ready = true
  251. }),
  252. )
  253. })
  254. const renderer = useRenderer()
  255. renderer
  256. .getPalette({
  257. size: 16,
  258. })
  259. .then((colors) => {
  260. if (!colors.palette[0]) return
  261. setStore("themes", "system", generateSystem(colors, store.mode))
  262. })
  263. const values = createMemo(() => {
  264. return resolveTheme(store.themes[store.active] ?? store.themes.opencode, store.mode)
  265. })
  266. const syntax = createMemo(() => generateSyntax(values()))
  267. const subtleSyntax = createMemo(() => generateSubtleSyntax(values()))
  268. return {
  269. theme: new Proxy(values(), {
  270. get(_target, prop) {
  271. // @ts-expect-error
  272. return values()[prop]
  273. },
  274. }),
  275. get selected() {
  276. return store.active
  277. },
  278. all() {
  279. return store.themes
  280. },
  281. syntax,
  282. subtleSyntax,
  283. mode() {
  284. return store.mode
  285. },
  286. setMode(mode: "dark" | "light") {
  287. setStore("mode", mode)
  288. kv.set("theme_mode", mode)
  289. },
  290. set(theme: string) {
  291. setStore("active", theme)
  292. kv.set("theme", theme)
  293. },
  294. get ready() {
  295. return store.ready
  296. },
  297. }
  298. },
  299. })
  300. const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json")
  301. async function getCustomThemes() {
  302. const directories = [
  303. Global.Path.config,
  304. ...(await Array.fromAsync(
  305. Filesystem.up({
  306. targets: [".opencode"],
  307. start: process.cwd(),
  308. }),
  309. )),
  310. ]
  311. const result: Record<string, ThemeJson> = {}
  312. for (const dir of directories) {
  313. for await (const item of CUSTOM_THEME_GLOB.scan({
  314. absolute: true,
  315. followSymlinks: true,
  316. dot: true,
  317. cwd: dir,
  318. })) {
  319. const name = path.basename(item, ".json")
  320. result[name] = await Bun.file(item).json()
  321. }
  322. }
  323. return result
  324. }
  325. function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
  326. const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
  327. const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
  328. const palette = colors.palette.filter((x) => x !== null).map((x) => RGBA.fromHex(x))
  329. const isDark = mode == "dark"
  330. // Generate gray scale based on terminal background
  331. const grays = generateGrayScale(bg, isDark)
  332. const textMuted = generateMutedTextColor(bg, isDark)
  333. // ANSI color references
  334. const ansiColors = {
  335. black: palette[0],
  336. red: palette[1],
  337. green: palette[2],
  338. yellow: palette[3],
  339. blue: palette[4],
  340. magenta: palette[5],
  341. cyan: palette[6],
  342. white: palette[7],
  343. }
  344. return {
  345. theme: {
  346. // Primary colors using ANSI
  347. primary: ansiColors.cyan,
  348. secondary: ansiColors.magenta,
  349. accent: ansiColors.cyan,
  350. // Status colors using ANSI
  351. error: ansiColors.red,
  352. warning: ansiColors.yellow,
  353. success: ansiColors.green,
  354. info: ansiColors.cyan,
  355. // Text colors
  356. text: fg,
  357. textMuted,
  358. selectedListItemText: bg,
  359. // Background colors
  360. background: bg,
  361. backgroundPanel: grays[2],
  362. backgroundElement: grays[3],
  363. backgroundMenu: grays[3],
  364. // Border colors
  365. borderSubtle: grays[6],
  366. border: grays[7],
  367. borderActive: grays[8],
  368. // Diff colors
  369. diffAdded: ansiColors.green,
  370. diffRemoved: ansiColors.red,
  371. diffContext: grays[7],
  372. diffHunkHeader: grays[7],
  373. diffHighlightAdded: ansiColors.green,
  374. diffHighlightRemoved: ansiColors.red,
  375. diffAddedBg: grays[2],
  376. diffRemovedBg: grays[2],
  377. diffContextBg: grays[1],
  378. diffLineNumber: grays[6],
  379. diffAddedLineNumberBg: grays[3],
  380. diffRemovedLineNumberBg: grays[3],
  381. // Markdown colors
  382. markdownText: fg,
  383. markdownHeading: fg,
  384. markdownLink: ansiColors.blue,
  385. markdownLinkText: ansiColors.cyan,
  386. markdownCode: ansiColors.green,
  387. markdownBlockQuote: ansiColors.yellow,
  388. markdownEmph: ansiColors.yellow,
  389. markdownStrong: fg,
  390. markdownHorizontalRule: grays[7],
  391. markdownListItem: ansiColors.blue,
  392. markdownListEnumeration: ansiColors.cyan,
  393. markdownImage: ansiColors.blue,
  394. markdownImageText: ansiColors.cyan,
  395. markdownCodeBlock: fg,
  396. // Syntax colors
  397. syntaxComment: textMuted,
  398. syntaxKeyword: ansiColors.magenta,
  399. syntaxFunction: ansiColors.blue,
  400. syntaxVariable: fg,
  401. syntaxString: ansiColors.green,
  402. syntaxNumber: ansiColors.yellow,
  403. syntaxType: ansiColors.cyan,
  404. syntaxOperator: ansiColors.cyan,
  405. syntaxPunctuation: fg,
  406. },
  407. }
  408. }
  409. function generateGrayScale(bg: RGBA, isDark: boolean): Record<number, RGBA> {
  410. const grays: Record<number, RGBA> = {}
  411. // RGBA stores floats in range 0-1, convert to 0-255
  412. const bgR = bg.r * 255
  413. const bgG = bg.g * 255
  414. const bgB = bg.b * 255
  415. const luminance = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB
  416. for (let i = 1; i <= 12; i++) {
  417. const factor = i / 12.0
  418. let grayValue: number
  419. let newR: number
  420. let newG: number
  421. let newB: number
  422. if (isDark) {
  423. if (luminance < 10) {
  424. grayValue = Math.floor(factor * 0.4 * 255)
  425. newR = grayValue
  426. newG = grayValue
  427. newB = grayValue
  428. } else {
  429. const newLum = luminance + (255 - luminance) * factor * 0.4
  430. const ratio = newLum / luminance
  431. newR = Math.min(bgR * ratio, 255)
  432. newG = Math.min(bgG * ratio, 255)
  433. newB = Math.min(bgB * ratio, 255)
  434. }
  435. } else {
  436. if (luminance > 245) {
  437. grayValue = Math.floor(255 - factor * 0.4 * 255)
  438. newR = grayValue
  439. newG = grayValue
  440. newB = grayValue
  441. } else {
  442. const newLum = luminance * (1 - factor * 0.4)
  443. const ratio = newLum / luminance
  444. newR = Math.max(bgR * ratio, 0)
  445. newG = Math.max(bgG * ratio, 0)
  446. newB = Math.max(bgB * ratio, 0)
  447. }
  448. }
  449. grays[i] = RGBA.fromInts(Math.floor(newR), Math.floor(newG), Math.floor(newB))
  450. }
  451. return grays
  452. }
  453. function generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA {
  454. // RGBA stores floats in range 0-1, convert to 0-255
  455. const bgR = bg.r * 255
  456. const bgG = bg.g * 255
  457. const bgB = bg.b * 255
  458. const bgLum = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB
  459. let grayValue: number
  460. if (isDark) {
  461. if (bgLum < 10) {
  462. // Very dark/black background
  463. grayValue = 180 // #b4b4b4
  464. } else {
  465. // Scale up for lighter dark backgrounds
  466. grayValue = Math.min(Math.floor(160 + bgLum * 0.3), 200)
  467. }
  468. } else {
  469. if (bgLum > 245) {
  470. // Very light/white background
  471. grayValue = 75 // #4b4b4b
  472. } else {
  473. // Scale down for darker light backgrounds
  474. grayValue = Math.max(Math.floor(100 - (255 - bgLum) * 0.2), 60)
  475. }
  476. }
  477. return RGBA.fromInts(grayValue, grayValue, grayValue)
  478. }
  479. function generateSyntax(theme: Theme) {
  480. return SyntaxStyle.fromTheme(getSyntaxRules(theme))
  481. }
  482. function generateSubtleSyntax(theme: Theme) {
  483. const rules = getSyntaxRules(theme)
  484. return SyntaxStyle.fromTheme(
  485. rules.map((rule) => {
  486. if (rule.style.foreground) {
  487. const fg = rule.style.foreground
  488. return {
  489. ...rule,
  490. style: {
  491. ...rule.style,
  492. foreground: RGBA.fromInts(
  493. Math.round(fg.r * 255),
  494. Math.round(fg.g * 255),
  495. Math.round(fg.b * 255),
  496. Math.round(0.6 * 255),
  497. ),
  498. },
  499. }
  500. }
  501. return rule
  502. }),
  503. )
  504. }
  505. function getSyntaxRules(theme: Theme) {
  506. return [
  507. {
  508. scope: ["prompt"],
  509. style: {
  510. foreground: theme.accent,
  511. },
  512. },
  513. {
  514. scope: ["extmark.file"],
  515. style: {
  516. foreground: theme.warning,
  517. bold: true,
  518. },
  519. },
  520. {
  521. scope: ["extmark.agent"],
  522. style: {
  523. foreground: theme.secondary,
  524. bold: true,
  525. },
  526. },
  527. {
  528. scope: ["extmark.paste"],
  529. style: {
  530. foreground: theme.background,
  531. background: theme.warning,
  532. bold: true,
  533. },
  534. },
  535. {
  536. scope: ["comment"],
  537. style: {
  538. foreground: theme.syntaxComment,
  539. italic: true,
  540. },
  541. },
  542. {
  543. scope: ["comment.documentation"],
  544. style: {
  545. foreground: theme.syntaxComment,
  546. italic: true,
  547. },
  548. },
  549. {
  550. scope: ["string", "symbol"],
  551. style: {
  552. foreground: theme.syntaxString,
  553. },
  554. },
  555. {
  556. scope: ["number", "boolean"],
  557. style: {
  558. foreground: theme.syntaxNumber,
  559. },
  560. },
  561. {
  562. scope: ["character.special"],
  563. style: {
  564. foreground: theme.syntaxString,
  565. },
  566. },
  567. {
  568. scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
  569. style: {
  570. foreground: theme.syntaxKeyword,
  571. italic: true,
  572. },
  573. },
  574. {
  575. scope: ["keyword.type"],
  576. style: {
  577. foreground: theme.syntaxType,
  578. bold: true,
  579. italic: true,
  580. },
  581. },
  582. {
  583. scope: ["keyword.function", "function.method"],
  584. style: {
  585. foreground: theme.syntaxFunction,
  586. },
  587. },
  588. {
  589. scope: ["keyword"],
  590. style: {
  591. foreground: theme.syntaxKeyword,
  592. italic: true,
  593. },
  594. },
  595. {
  596. scope: ["keyword.import"],
  597. style: {
  598. foreground: theme.syntaxKeyword,
  599. },
  600. },
  601. {
  602. scope: ["operator", "keyword.operator", "punctuation.delimiter"],
  603. style: {
  604. foreground: theme.syntaxOperator,
  605. },
  606. },
  607. {
  608. scope: ["keyword.conditional.ternary"],
  609. style: {
  610. foreground: theme.syntaxOperator,
  611. },
  612. },
  613. {
  614. scope: ["variable", "variable.parameter", "function.method.call", "function.call"],
  615. style: {
  616. foreground: theme.syntaxVariable,
  617. },
  618. },
  619. {
  620. scope: ["variable.member", "function", "constructor"],
  621. style: {
  622. foreground: theme.syntaxFunction,
  623. },
  624. },
  625. {
  626. scope: ["type", "module"],
  627. style: {
  628. foreground: theme.syntaxType,
  629. },
  630. },
  631. {
  632. scope: ["constant"],
  633. style: {
  634. foreground: theme.syntaxNumber,
  635. },
  636. },
  637. {
  638. scope: ["property"],
  639. style: {
  640. foreground: theme.syntaxVariable,
  641. },
  642. },
  643. {
  644. scope: ["class"],
  645. style: {
  646. foreground: theme.syntaxType,
  647. },
  648. },
  649. {
  650. scope: ["parameter"],
  651. style: {
  652. foreground: theme.syntaxVariable,
  653. },
  654. },
  655. {
  656. scope: ["punctuation", "punctuation.bracket"],
  657. style: {
  658. foreground: theme.syntaxPunctuation,
  659. },
  660. },
  661. {
  662. scope: ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin"],
  663. style: {
  664. foreground: theme.error,
  665. },
  666. },
  667. {
  668. scope: ["variable.super"],
  669. style: {
  670. foreground: theme.error,
  671. },
  672. },
  673. {
  674. scope: ["string.escape", "string.regexp"],
  675. style: {
  676. foreground: theme.syntaxKeyword,
  677. },
  678. },
  679. {
  680. scope: ["keyword.directive"],
  681. style: {
  682. foreground: theme.syntaxKeyword,
  683. italic: true,
  684. },
  685. },
  686. {
  687. scope: ["punctuation.special"],
  688. style: {
  689. foreground: theme.syntaxOperator,
  690. },
  691. },
  692. {
  693. scope: ["keyword.modifier"],
  694. style: {
  695. foreground: theme.syntaxKeyword,
  696. italic: true,
  697. },
  698. },
  699. {
  700. scope: ["keyword.exception"],
  701. style: {
  702. foreground: theme.syntaxKeyword,
  703. italic: true,
  704. },
  705. },
  706. // Markdown specific styles
  707. {
  708. scope: ["markup.heading"],
  709. style: {
  710. foreground: theme.markdownHeading,
  711. bold: true,
  712. },
  713. },
  714. {
  715. scope: ["markup.heading.1"],
  716. style: {
  717. foreground: theme.markdownHeading,
  718. bold: true,
  719. },
  720. },
  721. {
  722. scope: ["markup.heading.2"],
  723. style: {
  724. foreground: theme.markdownHeading,
  725. bold: true,
  726. },
  727. },
  728. {
  729. scope: ["markup.heading.3"],
  730. style: {
  731. foreground: theme.markdownHeading,
  732. bold: true,
  733. },
  734. },
  735. {
  736. scope: ["markup.heading.4"],
  737. style: {
  738. foreground: theme.markdownHeading,
  739. bold: true,
  740. },
  741. },
  742. {
  743. scope: ["markup.heading.5"],
  744. style: {
  745. foreground: theme.markdownHeading,
  746. bold: true,
  747. },
  748. },
  749. {
  750. scope: ["markup.heading.6"],
  751. style: {
  752. foreground: theme.markdownHeading,
  753. bold: true,
  754. },
  755. },
  756. {
  757. scope: ["markup.bold", "markup.strong"],
  758. style: {
  759. foreground: theme.markdownStrong,
  760. bold: true,
  761. },
  762. },
  763. {
  764. scope: ["markup.italic"],
  765. style: {
  766. foreground: theme.markdownEmph,
  767. italic: true,
  768. },
  769. },
  770. {
  771. scope: ["markup.list"],
  772. style: {
  773. foreground: theme.markdownListItem,
  774. },
  775. },
  776. {
  777. scope: ["markup.quote"],
  778. style: {
  779. foreground: theme.markdownBlockQuote,
  780. italic: true,
  781. },
  782. },
  783. {
  784. scope: ["markup.raw", "markup.raw.block"],
  785. style: {
  786. foreground: theme.markdownCode,
  787. },
  788. },
  789. {
  790. scope: ["markup.raw.inline"],
  791. style: {
  792. foreground: theme.markdownCode,
  793. background: theme.background,
  794. },
  795. },
  796. {
  797. scope: ["markup.link"],
  798. style: {
  799. foreground: theme.markdownLink,
  800. underline: true,
  801. },
  802. },
  803. {
  804. scope: ["markup.link.label"],
  805. style: {
  806. foreground: theme.markdownLinkText,
  807. underline: true,
  808. },
  809. },
  810. {
  811. scope: ["markup.link.url"],
  812. style: {
  813. foreground: theme.markdownLink,
  814. underline: true,
  815. },
  816. },
  817. {
  818. scope: ["label"],
  819. style: {
  820. foreground: theme.markdownLinkText,
  821. },
  822. },
  823. {
  824. scope: ["spell", "nospell"],
  825. style: {
  826. foreground: theme.text,
  827. },
  828. },
  829. {
  830. scope: ["conceal"],
  831. style: {
  832. foreground: theme.textMuted,
  833. },
  834. },
  835. // Additional common highlight groups
  836. {
  837. scope: ["string.special", "string.special.url"],
  838. style: {
  839. foreground: theme.markdownLink,
  840. underline: true,
  841. },
  842. },
  843. {
  844. scope: ["character"],
  845. style: {
  846. foreground: theme.syntaxString,
  847. },
  848. },
  849. {
  850. scope: ["float"],
  851. style: {
  852. foreground: theme.syntaxNumber,
  853. },
  854. },
  855. {
  856. scope: ["comment.error"],
  857. style: {
  858. foreground: theme.error,
  859. italic: true,
  860. bold: true,
  861. },
  862. },
  863. {
  864. scope: ["comment.warning"],
  865. style: {
  866. foreground: theme.warning,
  867. italic: true,
  868. bold: true,
  869. },
  870. },
  871. {
  872. scope: ["comment.todo", "comment.note"],
  873. style: {
  874. foreground: theme.info,
  875. italic: true,
  876. bold: true,
  877. },
  878. },
  879. {
  880. scope: ["namespace"],
  881. style: {
  882. foreground: theme.syntaxType,
  883. },
  884. },
  885. {
  886. scope: ["field"],
  887. style: {
  888. foreground: theme.syntaxVariable,
  889. },
  890. },
  891. {
  892. scope: ["type.definition"],
  893. style: {
  894. foreground: theme.syntaxType,
  895. bold: true,
  896. },
  897. },
  898. {
  899. scope: ["keyword.export"],
  900. style: {
  901. foreground: theme.syntaxKeyword,
  902. },
  903. },
  904. {
  905. scope: ["attribute", "annotation"],
  906. style: {
  907. foreground: theme.warning,
  908. },
  909. },
  910. {
  911. scope: ["tag"],
  912. style: {
  913. foreground: theme.error,
  914. },
  915. },
  916. {
  917. scope: ["tag.attribute"],
  918. style: {
  919. foreground: theme.syntaxKeyword,
  920. },
  921. },
  922. {
  923. scope: ["tag.delimiter"],
  924. style: {
  925. foreground: theme.syntaxOperator,
  926. },
  927. },
  928. {
  929. scope: ["markup.strikethrough"],
  930. style: {
  931. foreground: theme.textMuted,
  932. },
  933. },
  934. {
  935. scope: ["markup.underline"],
  936. style: {
  937. foreground: theme.text,
  938. underline: true,
  939. },
  940. },
  941. {
  942. scope: ["markup.list.checked"],
  943. style: {
  944. foreground: theme.success,
  945. },
  946. },
  947. {
  948. scope: ["markup.list.unchecked"],
  949. style: {
  950. foreground: theme.textMuted,
  951. },
  952. },
  953. {
  954. scope: ["diff.plus"],
  955. style: {
  956. foreground: theme.diffAdded,
  957. background: theme.diffAddedBg,
  958. },
  959. },
  960. {
  961. scope: ["diff.minus"],
  962. style: {
  963. foreground: theme.diffRemoved,
  964. background: theme.diffRemovedBg,
  965. },
  966. },
  967. {
  968. scope: ["diff.delta"],
  969. style: {
  970. foreground: theme.diffContext,
  971. background: theme.diffContextBg,
  972. },
  973. },
  974. {
  975. scope: ["error"],
  976. style: {
  977. foreground: theme.error,
  978. bold: true,
  979. },
  980. },
  981. {
  982. scope: ["warning"],
  983. style: {
  984. foreground: theme.warning,
  985. bold: true,
  986. },
  987. },
  988. {
  989. scope: ["info"],
  990. style: {
  991. foreground: theme.info,
  992. },
  993. },
  994. {
  995. scope: ["debug"],
  996. style: {
  997. foreground: theme.textMuted,
  998. },
  999. },
  1000. ]
  1001. }