Vendor opencode source for docker build
This commit is contained in:
10
opencode/packages/ui/src/context/code.tsx
Normal file
10
opencode/packages/ui/src/context/code.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { ValidComponent } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
const ctx = createSimpleContext<ValidComponent, { component: ValidComponent }>({
|
||||
name: "CodeComponent",
|
||||
init: (props) => props.component,
|
||||
})
|
||||
|
||||
export const CodeComponentProvider = ctx.provider
|
||||
export const useCodeComponent = ctx.use
|
||||
74
opencode/packages/ui/src/context/data.tsx
Normal file
74
opencode/packages/ui/src/context/data.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
Message,
|
||||
Session,
|
||||
Part,
|
||||
FileDiff,
|
||||
SessionStatus,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
QuestionAnswer,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
|
||||
type Data = {
|
||||
session: Session[]
|
||||
session_status: {
|
||||
[sessionID: string]: SessionStatus
|
||||
}
|
||||
session_diff: {
|
||||
[sessionID: string]: FileDiff[]
|
||||
}
|
||||
session_diff_preload?: {
|
||||
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
||||
}
|
||||
permission?: {
|
||||
[sessionID: string]: PermissionRequest[]
|
||||
}
|
||||
question?: {
|
||||
[sessionID: string]: QuestionRequest[]
|
||||
}
|
||||
message: {
|
||||
[sessionID: string]: Message[]
|
||||
}
|
||||
part: {
|
||||
[messageID: string]: Part[]
|
||||
}
|
||||
}
|
||||
|
||||
export type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
}) => void
|
||||
|
||||
export type QuestionReplyFn = (input: { requestID: string; answers: QuestionAnswer[] }) => void
|
||||
|
||||
export type QuestionRejectFn = (input: { requestID: string }) => void
|
||||
|
||||
export type NavigateToSessionFn = (sessionID: string) => void
|
||||
|
||||
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
name: "Data",
|
||||
init: (props: {
|
||||
data: Data
|
||||
directory: string
|
||||
onPermissionRespond?: PermissionRespondFn
|
||||
onQuestionReply?: QuestionReplyFn
|
||||
onQuestionReject?: QuestionRejectFn
|
||||
onNavigateToSession?: NavigateToSessionFn
|
||||
}) => {
|
||||
return {
|
||||
get store() {
|
||||
return props.data
|
||||
},
|
||||
get directory() {
|
||||
return props.directory
|
||||
},
|
||||
respondToPermission: props.onPermissionRespond,
|
||||
replyToQuestion: props.onQuestionReply,
|
||||
rejectQuestion: props.onQuestionReject,
|
||||
navigateToSession: props.onNavigateToSession,
|
||||
}
|
||||
},
|
||||
})
|
||||
163
opencode/packages/ui/src/context/dialog.tsx
Normal file
163
opencode/packages/ui/src/context/dialog.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createRoot,
|
||||
createSignal,
|
||||
getOwner,
|
||||
onCleanup,
|
||||
type Owner,
|
||||
type ParentProps,
|
||||
runWithOwner,
|
||||
useContext,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
|
||||
type DialogElement = () => JSX.Element
|
||||
|
||||
type Active = {
|
||||
id: string
|
||||
node: JSX.Element
|
||||
dispose: () => void
|
||||
owner: Owner
|
||||
onClose?: () => void
|
||||
setClosing: (closing: boolean) => void
|
||||
}
|
||||
|
||||
const Context = createContext<ReturnType<typeof init>>()
|
||||
|
||||
function init() {
|
||||
const [active, setActive] = createSignal<Active | undefined>()
|
||||
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||
const lock = { value: false }
|
||||
|
||||
onCleanup(() => {
|
||||
if (timer.current === undefined) return
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
const current = active()
|
||||
if (!current || lock.value) return
|
||||
lock.value = true
|
||||
current.onClose?.()
|
||||
current.setClosing(true)
|
||||
|
||||
const id = current.id
|
||||
if (timer.current !== undefined) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
|
||||
timer.current = setTimeout(() => {
|
||||
timer.current = undefined
|
||||
current.dispose()
|
||||
if (active()?.id === id) setActive(undefined)
|
||||
lock.value = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!active()) return
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape") return
|
||||
close()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, true)
|
||||
onCleanup(() => window.removeEventListener("keydown", onKeyDown, true))
|
||||
})
|
||||
|
||||
const show = (element: DialogElement, owner: Owner, onClose?: () => void) => {
|
||||
// Immediately dispose any existing dialog when showing a new one
|
||||
const current = active()
|
||||
if (current) {
|
||||
current.dispose()
|
||||
setActive(undefined)
|
||||
}
|
||||
|
||||
if (timer.current !== undefined) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
lock.value = false
|
||||
|
||||
const id = Math.random().toString(36).slice(2)
|
||||
let dispose: (() => void) | undefined
|
||||
let setClosing: ((closing: boolean) => void) | undefined
|
||||
|
||||
const node = runWithOwner(owner, () =>
|
||||
createRoot((d: () => void) => {
|
||||
dispose = d
|
||||
const [closing, setClosingSignal] = createSignal(false)
|
||||
setClosing = setClosingSignal
|
||||
return (
|
||||
<Kobalte
|
||||
modal
|
||||
open={!closing()}
|
||||
onOpenChange={(open: boolean) => {
|
||||
if (open) return
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Overlay data-component="dialog-overlay" onClick={close} />
|
||||
{element()}
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
if (!dispose || !setClosing) return
|
||||
|
||||
setActive({ id, node, dispose, owner, onClose, setClosing })
|
||||
}
|
||||
|
||||
return {
|
||||
get active() {
|
||||
return active()
|
||||
},
|
||||
close,
|
||||
show,
|
||||
}
|
||||
}
|
||||
|
||||
export function DialogProvider(props: ParentProps) {
|
||||
const ctx = init()
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{props.children}
|
||||
<div data-component="dialog-stack">{ctx.active?.node}</div>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDialog() {
|
||||
const ctx = useContext(Context)
|
||||
const owner = getOwner()
|
||||
|
||||
if (!owner) {
|
||||
throw new Error("useDialog must be used within a DialogProvider")
|
||||
}
|
||||
if (!ctx) {
|
||||
throw new Error("useDialog must be used within a DialogProvider")
|
||||
}
|
||||
|
||||
return {
|
||||
get active() {
|
||||
return ctx.active
|
||||
},
|
||||
show(element: DialogElement, onClose?: () => void) {
|
||||
const base = ctx.active?.owner ?? owner
|
||||
ctx.show(element, base, onClose)
|
||||
},
|
||||
close() {
|
||||
ctx.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
10
opencode/packages/ui/src/context/diff.tsx
Normal file
10
opencode/packages/ui/src/context/diff.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { ValidComponent } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
const ctx = createSimpleContext<ValidComponent, { component: ValidComponent }>({
|
||||
name: "DiffComponent",
|
||||
init: (props) => props.component,
|
||||
})
|
||||
|
||||
export const DiffComponentProvider = ctx.provider
|
||||
export const useDiffComponent = ctx.use
|
||||
37
opencode/packages/ui/src/context/helper.tsx
Normal file
37
opencode/packages/ui/src/context/helper.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createContext, createMemo, Show, useContext, type ParentProps, type Accessor } from "solid-js"
|
||||
|
||||
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
|
||||
name: string
|
||||
init: ((input: Props) => T) | (() => T)
|
||||
gate?: boolean
|
||||
}) {
|
||||
const ctx = createContext<T>()
|
||||
|
||||
return {
|
||||
provider: (props: ParentProps<Props>) => {
|
||||
const init = input.init(props)
|
||||
const gate = input.gate ?? true
|
||||
|
||||
if (!gate) {
|
||||
return <ctx.Provider value={init}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
// Access init.ready inside the memo to make it reactive for getter properties
|
||||
const isReady = createMemo(() => {
|
||||
// @ts-expect-error
|
||||
const ready = init.ready as Accessor<boolean> | boolean | undefined
|
||||
return ready === undefined || (typeof ready === "function" ? ready() : ready)
|
||||
})
|
||||
return (
|
||||
<Show when={isReady()}>
|
||||
<ctx.Provider value={init}>{props.children}</ctx.Provider>
|
||||
</Show>
|
||||
)
|
||||
},
|
||||
use() {
|
||||
const value = useContext(ctx)
|
||||
if (!value) throw new Error(`${input.name} context must be used within a context provider`)
|
||||
return value
|
||||
},
|
||||
}
|
||||
}
|
||||
38
opencode/packages/ui/src/context/i18n.tsx
Normal file
38
opencode/packages/ui/src/context/i18n.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createContext, useContext, type Accessor, type ParentProps } from "solid-js"
|
||||
import { dict as en } from "../i18n/en"
|
||||
|
||||
export type UiI18nKey = keyof typeof en
|
||||
|
||||
export type UiI18nParams = Record<string, string | number | boolean>
|
||||
|
||||
export type UiI18n = {
|
||||
locale: Accessor<string>
|
||||
t: (key: UiI18nKey, params?: UiI18nParams) => string
|
||||
}
|
||||
|
||||
function resolveTemplate(text: string, params?: UiI18nParams) {
|
||||
if (!params) return text
|
||||
return text.replace(/{{\s*([^}]+?)\s*}}/g, (_, rawKey) => {
|
||||
const key = String(rawKey)
|
||||
const value = params[key]
|
||||
return value === undefined ? "" : String(value)
|
||||
})
|
||||
}
|
||||
|
||||
const fallback: UiI18n = {
|
||||
locale: () => "en",
|
||||
t: (key, params) => {
|
||||
const value = en[key] ?? String(key)
|
||||
return resolveTemplate(value, params)
|
||||
},
|
||||
}
|
||||
|
||||
const Context = createContext<UiI18n>(fallback)
|
||||
|
||||
export function I18nProvider(props: ParentProps<{ value: UiI18n }>) {
|
||||
return <Context.Provider value={props.value}>{props.children}</Context.Provider>
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
return useContext(Context)
|
||||
}
|
||||
5
opencode/packages/ui/src/context/index.ts
Normal file
5
opencode/packages/ui/src/context/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./helper"
|
||||
export * from "./data"
|
||||
export * from "./diff"
|
||||
export * from "./dialog"
|
||||
export * from "./i18n"
|
||||
511
opencode/packages/ui/src/context/marked.tsx
Normal file
511
opencode/packages/ui/src/context/marked.tsx
Normal file
@@ -0,0 +1,511 @@
|
||||
import { marked } from "marked"
|
||||
import markedKatex from "marked-katex-extension"
|
||||
import markedShiki from "marked-shiki"
|
||||
import katex from "katex"
|
||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
|
||||
|
||||
registerCustomTheme("OpenCode", () => {
|
||||
return Promise.resolve({
|
||||
name: "OpenCode",
|
||||
colors: {
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "var(--text-base)",
|
||||
"gitDecoration.addedResourceForeground": "var(--syntax-diff-add)",
|
||||
"gitDecoration.deletedResourceForeground": "var(--syntax-diff-delete)",
|
||||
// "gitDecoration.conflictingResourceForeground": "#ffca00",
|
||||
// "gitDecoration.modifiedResourceForeground": "#1a76d4",
|
||||
// "gitDecoration.untrackedResourceForeground": "#00cab1",
|
||||
// "gitDecoration.ignoredResourceForeground": "#84848A",
|
||||
// "terminal.titleForeground": "#adadb1",
|
||||
// "terminal.titleInactiveForeground": "#84848A",
|
||||
// "terminal.background": "#141415",
|
||||
// "terminal.foreground": "#adadb1",
|
||||
// "terminal.ansiBlack": "#141415",
|
||||
// "terminal.ansiRed": "#ff2e3f",
|
||||
// "terminal.ansiGreen": "#0dbe4e",
|
||||
// "terminal.ansiYellow": "#ffca00",
|
||||
// "terminal.ansiBlue": "#008cff",
|
||||
// "terminal.ansiMagenta": "#c635e4",
|
||||
// "terminal.ansiCyan": "#08c0ef",
|
||||
// "terminal.ansiWhite": "#c6c6c8",
|
||||
// "terminal.ansiBrightBlack": "#141415",
|
||||
// "terminal.ansiBrightRed": "#ff2e3f",
|
||||
// "terminal.ansiBrightGreen": "#0dbe4e",
|
||||
// "terminal.ansiBrightYellow": "#ffca00",
|
||||
// "terminal.ansiBrightBlue": "#008cff",
|
||||
// "terminal.ansiBrightMagenta": "#c635e4",
|
||||
// "terminal.ansiBrightCyan": "#08c0ef",
|
||||
// "terminal.ansiBrightWhite": "#c6c6c8",
|
||||
},
|
||||
tokenColors: [
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment", "string.comment"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-comment)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["entity.other.attribute-name"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-property)", // maybe attribute
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["constant", "entity.name.constant", "variable.other.constant", "variable.language", "entity"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-constant)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["entity.name", "meta.export.default", "meta.definition.variable"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-type)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["meta.object.member"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-primitive)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"variable.parameter.function",
|
||||
"meta.jsx.children",
|
||||
"meta.block",
|
||||
"meta.tag.attributes",
|
||||
"entity.name.constant",
|
||||
"meta.embedded.expression",
|
||||
"meta.template.expression",
|
||||
"string.other.begin.yaml",
|
||||
"string.other.end.yaml",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--syntax-punctuation)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.function", "support.type.primitive"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-primitive)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["support.class.component"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-type)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "keyword",
|
||||
settings: {
|
||||
foreground: "var(--syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"keyword.operator",
|
||||
"storage.type.function.arrow",
|
||||
"punctuation.separator.key-value.css",
|
||||
"entity.name.tag.yaml",
|
||||
"punctuation.separator.key-value.mapping.yaml",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--syntax-operator)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["storage", "storage.type"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["storage.modifier.package", "storage.modifier.import", "storage.type.java"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-primitive)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"string",
|
||||
"punctuation.definition.string",
|
||||
"string punctuation.section.embedded source",
|
||||
"entity.name.tag",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--syntax-string)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "support",
|
||||
settings: {
|
||||
foreground: "var(--syntax-primitive)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["support.type.object.module", "variable.other.object", "support.type.property-name.css"],
|
||||
settings: {
|
||||
foreground: "var(--syntax-object)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.property-name",
|
||||
settings: {
|
||||
foreground: "var(--syntax-property)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "variable",
|
||||
settings: {
|
||||
foreground: "var(--syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "variable.other",
|
||||
settings: {
|
||||
foreground: "var(--syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"invalid.broken",
|
||||
"invalid.illegal",
|
||||
"invalid.unimplemented",
|
||||
"invalid.deprecated",
|
||||
"message.error",
|
||||
"markup.deleted",
|
||||
"meta.diff.header.from-file",
|
||||
"punctuation.definition.deleted",
|
||||
"brackethighlighter.unmatched",
|
||||
"token.error-token",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--syntax-critical)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "carriage-return",
|
||||
settings: {
|
||||
foreground: "var(--syntax-keyword)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string source",
|
||||
settings: {
|
||||
foreground: "var(--syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "string variable",
|
||||
settings: {
|
||||
foreground: "var(--syntax-constant)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"source.regexp",
|
||||
"string.regexp",
|
||||
"string.regexp.character-class",
|
||||
"string.regexp constant.character.escape",
|
||||
"string.regexp source.ruby.embedded",
|
||||
"string.regexp string.regexp.arbitrary-repitition",
|
||||
"string.regexp constant.character.escape",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--syntax-regexp)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "support.constant",
|
||||
settings: {
|
||||
foreground: "var(--syntax-primitive)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "support.variable",
|
||||
settings: {
|
||||
foreground: "var(--syntax-variable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.module-reference",
|
||||
settings: {
|
||||
foreground: "var(--syntax-info)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "punctuation.definition.list.begin.markdown",
|
||||
settings: {
|
||||
foreground: "var(--syntax-punctuation)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading", "markup.heading entity.name"],
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--syntax-info)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.quote",
|
||||
settings: {
|
||||
foreground: "var(--syntax-info)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.italic",
|
||||
settings: {
|
||||
fontStyle: "italic",
|
||||
// foreground: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "markup.bold",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--text-strong)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"markup.raw",
|
||||
"markup.inserted",
|
||||
"meta.diff.header.to-file",
|
||||
"punctuation.definition.inserted",
|
||||
"markup.changed",
|
||||
"punctuation.definition.changed",
|
||||
"markup.ignored",
|
||||
"markup.untracked",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--text-base)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.diff.range",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.diff.header",
|
||||
settings: {
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.separator",
|
||||
settings: {
|
||||
fontStyle: "bold",
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.output",
|
||||
settings: {
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "meta.export.default",
|
||||
settings: {
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"brackethighlighter.tag",
|
||||
"brackethighlighter.curly",
|
||||
"brackethighlighter.round",
|
||||
"brackethighlighter.square",
|
||||
"brackethighlighter.angle",
|
||||
"brackethighlighter.quote",
|
||||
],
|
||||
settings: {
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["constant.other.reference.link", "string.other.link"],
|
||||
settings: {
|
||||
fontStyle: "underline",
|
||||
foreground: "var(--syntax-unknown)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "token.info-token",
|
||||
settings: {
|
||||
foreground: "var(--syntax-info)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "token.warn-token",
|
||||
settings: {
|
||||
foreground: "var(--syntax-warning)",
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: "token.debug-token",
|
||||
settings: {
|
||||
foreground: "var(--syntax-info)",
|
||||
},
|
||||
},
|
||||
],
|
||||
semanticTokenColors: {
|
||||
comment: "var(--syntax-comment)",
|
||||
string: "var(--syntax-string)",
|
||||
number: "var(--syntax-constant)",
|
||||
regexp: "var(--syntax-regexp)",
|
||||
keyword: "var(--syntax-keyword)",
|
||||
variable: "var(--syntax-variable)",
|
||||
parameter: "var(--syntax-variable)",
|
||||
property: "var(--syntax-property)",
|
||||
function: "var(--syntax-primitive)",
|
||||
method: "var(--syntax-primitive)",
|
||||
type: "var(--syntax-type)",
|
||||
class: "var(--syntax-type)",
|
||||
namespace: "var(--syntax-type)",
|
||||
enumMember: "var(--syntax-primitive)",
|
||||
"variable.constant": "var(--syntax-constant)",
|
||||
"variable.defaultLibrary": "var(--syntax-unknown)",
|
||||
},
|
||||
} as unknown as ThemeRegistrationResolved)
|
||||
})
|
||||
|
||||
function renderMathInText(text: string): string {
|
||||
let result = text
|
||||
|
||||
// Display math: $$...$$
|
||||
const displayMathRegex = /\$\$([\s\S]*?)\$\$/g
|
||||
result = result.replace(displayMathRegex, (_, math) => {
|
||||
try {
|
||||
return katex.renderToString(math, {
|
||||
displayMode: true,
|
||||
throwOnError: false,
|
||||
})
|
||||
} catch {
|
||||
return `$$${math}$$`
|
||||
}
|
||||
})
|
||||
|
||||
// Inline math: $...$
|
||||
const inlineMathRegex = /(?<!\$)\$(?!\$)((?:[^$\\]|\\.)+?)\$(?!\$)/g
|
||||
result = result.replace(inlineMathRegex, (_, math) => {
|
||||
try {
|
||||
return katex.renderToString(math, {
|
||||
displayMode: false,
|
||||
throwOnError: false,
|
||||
})
|
||||
} catch {
|
||||
return `$${math}$`
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function renderMathExpressions(html: string): string {
|
||||
// Split on code/pre/kbd tags to avoid processing their contents
|
||||
const codeBlockPattern = /(<(?:pre|code|kbd)[^>]*>[\s\S]*?<\/(?:pre|code|kbd)>)/gi
|
||||
const parts = html.split(codeBlockPattern)
|
||||
|
||||
return parts
|
||||
.map((part, i) => {
|
||||
// Odd indices are the captured code blocks - leave them alone
|
||||
if (i % 2 === 1) return part
|
||||
// Process math only in non-code parts
|
||||
return renderMathInText(part)
|
||||
})
|
||||
.join("")
|
||||
}
|
||||
|
||||
async function highlightCodeBlocks(html: string): Promise<string> {
|
||||
const codeBlockRegex = /<pre><code(?:\s+class="language-([^"]*)")?>([\s\S]*?)<\/code><\/pre>/g
|
||||
const matches = [...html.matchAll(codeBlockRegex)]
|
||||
if (matches.length === 0) return html
|
||||
|
||||
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
|
||||
|
||||
let result = html
|
||||
for (const match of matches) {
|
||||
const [fullMatch, lang, escapedCode] = match
|
||||
const code = escapedCode
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
|
||||
let language = lang || "text"
|
||||
if (!(language in bundledLanguages)) {
|
||||
language = "text"
|
||||
}
|
||||
if (!highlighter.getLoadedLanguages().includes(language)) {
|
||||
await highlighter.loadLanguage(language as BundledLanguage)
|
||||
}
|
||||
|
||||
const highlighted = highlighter.codeToHtml(code, {
|
||||
lang: language,
|
||||
theme: "OpenCode",
|
||||
tabindex: false,
|
||||
})
|
||||
result = result.replace(fullMatch, () => highlighted)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export type NativeMarkdownParser = (markdown: string) => Promise<string>
|
||||
|
||||
export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({
|
||||
name: "Marked",
|
||||
init: (props: { nativeParser?: NativeMarkdownParser }) => {
|
||||
const jsParser = marked.use(
|
||||
{
|
||||
renderer: {
|
||||
link({ href, title, text }) {
|
||||
const titleAttr = title ? ` title="${title}"` : ""
|
||||
return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
|
||||
},
|
||||
},
|
||||
},
|
||||
markedKatex({
|
||||
throwOnError: false,
|
||||
nonStandard: true,
|
||||
}),
|
||||
markedShiki({
|
||||
async highlight(code, lang) {
|
||||
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
|
||||
if (!(lang in bundledLanguages)) {
|
||||
lang = "text"
|
||||
}
|
||||
if (!highlighter.getLoadedLanguages().includes(lang)) {
|
||||
await highlighter.loadLanguage(lang as BundledLanguage)
|
||||
}
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: lang || "text",
|
||||
theme: "OpenCode",
|
||||
tabindex: false,
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
if (props.nativeParser) {
|
||||
const nativeParser = props.nativeParser
|
||||
return {
|
||||
async parse(markdown: string): Promise<string> {
|
||||
const html = await nativeParser(markdown)
|
||||
const withMath = renderMathExpressions(html)
|
||||
return highlightCodeBlocks(withMath)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return jsParser
|
||||
},
|
||||
})
|
||||
20
opencode/packages/ui/src/context/worker-pool.tsx
Normal file
20
opencode/packages/ui/src/context/worker-pool.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { WorkerPoolManager } from "@pierre/diffs/worker"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export type WorkerPools = {
|
||||
unified: WorkerPoolManager | undefined
|
||||
split: WorkerPoolManager | undefined
|
||||
}
|
||||
|
||||
const ctx = createSimpleContext<WorkerPools, { pools: WorkerPools }>({
|
||||
name: "WorkerPool",
|
||||
init: (props) => props.pools,
|
||||
})
|
||||
|
||||
export const WorkerPoolProvider = ctx.provider
|
||||
|
||||
export function useWorkerPool(diffStyle: "unified" | "split" | undefined) {
|
||||
const pools = ctx.use()
|
||||
if (diffStyle === "split") return pools.split
|
||||
return pools.unified
|
||||
}
|
||||
Reference in New Issue
Block a user