mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-21 20:50:09 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 840ff9306d |
@@ -1042,6 +1042,7 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@opentui/core@0.5.9": "patches/@opentui%2Fcore@0.5.9.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
|
||||
+2
-1
@@ -178,6 +178,7 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
|
||||
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch"
|
||||
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch",
|
||||
"@opentui/core@0.5.9": "patches/@opentui%2Fcore@0.5.9.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1193,6 +1193,81 @@ test.each(["manual", "select"] as const)(
|
||||
},
|
||||
)
|
||||
|
||||
test.each([80, 120].flatMap((width) => ["fence", "list", "write"].map((kind) => ({ width, kind }))))(
|
||||
"cached session code is colored on the first tab-return frame: %j",
|
||||
async (input) => {
|
||||
await using state = await tmpdir()
|
||||
const content = `const cachedValue_${input.width}_${input.kind}: number = 42`
|
||||
const session = {
|
||||
id: "ses_cached_highlight",
|
||||
title: "Cached highlight fixture",
|
||||
projectID: "project",
|
||||
location: { directory },
|
||||
agent: "build",
|
||||
model: { providerID: "fixture", id: "model" },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 2 },
|
||||
}
|
||||
await using setup = await createAppFixture({
|
||||
width: input.width,
|
||||
state: state.path,
|
||||
config: { animations: false, tabs: { enabled: true }, session: { sidebar: "hide" } },
|
||||
args: { sessionID: session.id },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
|
||||
if (url.pathname === `/api/session/${session.id}/message`)
|
||||
return json({
|
||||
data: [
|
||||
{
|
||||
id: "msg_cached_highlight",
|
||||
type: "assistant",
|
||||
agent: session.agent,
|
||||
model: session.model,
|
||||
time: { created: 2, completed: 3 },
|
||||
content: [
|
||||
input.kind === "write"
|
||||
? {
|
||||
type: "tool",
|
||||
id: "call_cached_highlight",
|
||||
name: "write",
|
||||
time: { created: 2, completed: 3 },
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { path: `${directory}/snippet.ts`, content },
|
||||
content: [{ type: "text", text: "Wrote file" }],
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: "text",
|
||||
text:
|
||||
input.kind === "list"
|
||||
? `- Example:\n\n \`\`\`typescript\n ${content}\n \`\`\``
|
||||
: `\`\`\`typescript\n${content}\n\`\`\``,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
cursor: {},
|
||||
})
|
||||
if (url.pathname === `/api/session/${session.id}/inbox`) return json({ data: [] })
|
||||
if (url.pathname === `/api/session/${session.id}/permission`) return json({ data: [] })
|
||||
},
|
||||
})
|
||||
await setup.ready
|
||||
const line = () =>
|
||||
setup.captureSpans().lines.find((line) => line.spans.some((span) => span.text.includes("cachedValue")))
|
||||
await setup.waitForFrame(() => (line()?.spans.filter((span) => span.text.trim()).length ?? 0) > 4)
|
||||
const highlighted = line()
|
||||
setup.mockInput.pressKey("x", { ctrl: true })
|
||||
setup.mockInput.pressKey("n")
|
||||
await setup.waitForFrame((frame) => !frame.includes("cachedValue"))
|
||||
setup.mockInput.pressKey("1", { ctrl: true })
|
||||
await setup.waitForFrame((frame) => frame.includes("cachedValue"))
|
||||
expect(line()).toEqual(highlighted)
|
||||
},
|
||||
)
|
||||
|
||||
async function createAppFixture(
|
||||
input: {
|
||||
width?: number
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import {
|
||||
CodeRenderable,
|
||||
MarkdownRenderable,
|
||||
RGBA,
|
||||
SyntaxStyle,
|
||||
TreeSitterClient,
|
||||
t,
|
||||
type TextChunk,
|
||||
} from "@opentui/core"
|
||||
import { createTestRenderer } from "@opentui/core/testing"
|
||||
import { render } from "@opentui/solid"
|
||||
import { tmpdir } from "./fixture/fixture"
|
||||
|
||||
const content = "const cachedValue: number = 42"
|
||||
|
||||
test.each([false, true])(
|
||||
"cached code paints colored before the next promise resolves, streaming=%s",
|
||||
async (streaming) => {
|
||||
await using app = await fixture()
|
||||
const result = await app.client.highlightOnce(content, "typescript")
|
||||
const pending = Promise.withResolvers<typeof result>()
|
||||
app.client.highlightOnce = () => pending.promise
|
||||
const code = new CodeRenderable(app.renderer, {
|
||||
content,
|
||||
filetype: "typescript",
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
streaming,
|
||||
drawUnstyledText: !streaming,
|
||||
})
|
||||
app.renderer.root.add(code)
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(code.isHighlighting).toBe(true)
|
||||
expect(app.captureCharFrame()).toContain(content)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ff0000"))
|
||||
const first = app.captureSpans()
|
||||
pending.resolve(result)
|
||||
await code.highlightingDone
|
||||
await app.renderOnce()
|
||||
expect(app.captureSpans()).toEqual(first)
|
||||
} finally {
|
||||
pending.resolve(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test("an uncached code block still paints plaintext before highlighting completes", async () => {
|
||||
await using app = await fixture()
|
||||
const pending = Promise.withResolvers<Awaited<ReturnType<TreeSitterClient["highlightOnce"]>>>()
|
||||
app.client.highlightOnce = () => pending.promise
|
||||
const code = new CodeRenderable(app.renderer, {
|
||||
content,
|
||||
filetype: "typescript",
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
fg: "#ffffff",
|
||||
})
|
||||
app.renderer.root.add(code)
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(code.isHighlighting).toBe(true)
|
||||
expect(app.captureCharFrame()).toContain(content)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ffffff"))
|
||||
} finally {
|
||||
pending.resolve({ highlights: [] })
|
||||
await code.highlightingDone
|
||||
}
|
||||
})
|
||||
|
||||
test.each([false, true])("cached Markdown fences paint immediately, nested=%s", async (nested) => {
|
||||
await using app = await fixture()
|
||||
const result = await app.client.highlightOnce(content, "typescript")
|
||||
const pending = Promise.withResolvers<typeof result>()
|
||||
app.client.highlightOnce = () => pending.promise
|
||||
app.renderer.root.add(
|
||||
new MarkdownRenderable(app.renderer, {
|
||||
content: nested
|
||||
? `- Example:\n\n \`\`\`typescript\n ${content}\n \`\`\``
|
||||
: `\`\`\`typescript\n${content}\n\`\`\``,
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
streaming: false,
|
||||
internalBlockMode: "top-level",
|
||||
}),
|
||||
)
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain(content)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ff0000"))
|
||||
} finally {
|
||||
pending.resolve(result)
|
||||
}
|
||||
})
|
||||
|
||||
test("cached paint follows current theme, content and language without a sticky seed", async () => {
|
||||
await using app = await fixture()
|
||||
const second = "const secondCachedValue: number = 7"
|
||||
await app.client.highlightOnce(content, "typescript")
|
||||
await app.client.highlightOnce(second, "typescript")
|
||||
const pending = Promise.withResolvers<Awaited<ReturnType<TreeSitterClient["highlightOnce"]>>>()
|
||||
app.client.highlightOnce = () => pending.promise
|
||||
const alternate = SyntaxStyle.fromStyles({ default: { fg: "#ffffff" }, keyword: { fg: "#0000ff" } })
|
||||
const code = new CodeRenderable(app.renderer, {
|
||||
content,
|
||||
filetype: "typescript",
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
fg: "#ffffff",
|
||||
})
|
||||
app.renderer.root.add(code)
|
||||
try {
|
||||
code.syntaxStyle = alternate
|
||||
await app.renderOnce()
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#0000ff"))
|
||||
code.content = "const miss = 0"
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("const miss = 0")
|
||||
expect(app.captureCharFrame()).not.toContain("cachedValue")
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ffffff"))
|
||||
code.content = second
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain(second)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#0000ff"))
|
||||
code.filetype = "text"
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain(second)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ffffff"))
|
||||
} finally {
|
||||
pending.resolve({ highlights: [] })
|
||||
await code.highlightingDone
|
||||
app.renderer.destroy()
|
||||
alternate.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("completed highlights use a bounded LRU and disappear when the client is destroyed", async () => {
|
||||
await using app = await fixture()
|
||||
const first = await app.client.highlightOnce(content, "typescript")
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toEqual(first)
|
||||
expect(await app.client.highlightOnce(content, "typescript")).toEqual(first)
|
||||
expect(app.client.getCachedHighlight(content, "javascript")).toBeUndefined()
|
||||
await Promise.all(
|
||||
Array.from({ length: 499 }, (_, index) => app.client.highlightOnce(`const value${index} = 1`, "typescript")),
|
||||
)
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toEqual(first)
|
||||
await app.client.highlightOnce("const extra = 1", "typescript")
|
||||
expect(app.client.getCachedHighlight("const value0 = 1", "typescript")).toBeUndefined()
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toEqual(first)
|
||||
await app.client.destroy()
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("parser changes invalidate completed results and failed highlighting stays retryable", async () => {
|
||||
await using app = await fixture()
|
||||
await app.client.highlightOnce(content, "typescript")
|
||||
app.client.addFiletypeParser({
|
||||
filetype: "missing-fixture",
|
||||
wasm: `${app.directory}/missing.wasm`,
|
||||
queries: { highlights: [] },
|
||||
})
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toBeUndefined()
|
||||
const failed = await app.client.highlightOnce(content, "missing-fixture")
|
||||
expect(failed.warning).toBeDefined()
|
||||
expect(app.client.getCachedHighlight(content, "missing-fixture")).toBeUndefined()
|
||||
expect(await app.client.highlightOnce(content, "missing-fixture")).toEqual(failed)
|
||||
await app.client.highlightOnce(content, "typescript")
|
||||
await app.client.clearCache()
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("custom highlight transforms do not mutate cached ranges", async () => {
|
||||
await using app = await fixture()
|
||||
const result = await app.client.highlightOnce(content, "typescript")
|
||||
const expected = structuredClone(result)
|
||||
result.highlights?.forEach((highlight) => {
|
||||
highlight[2] = "comment"
|
||||
})
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toEqual(expected)
|
||||
const code = new CodeRenderable(app.renderer, {
|
||||
content,
|
||||
filetype: "typescript",
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
onHighlight(highlights) {
|
||||
highlights.forEach((highlight) => {
|
||||
highlight[2] = "comment"
|
||||
})
|
||||
return highlights
|
||||
},
|
||||
})
|
||||
app.renderer.root.add(code)
|
||||
await app.renderOnce()
|
||||
await code.highlightingDone
|
||||
expect(app.client.getCachedHighlight(content, "typescript")).toEqual(expected)
|
||||
})
|
||||
|
||||
test.each(["callback", "subclass"])("cached paint does not bypass a custom chunk %s", async (kind) => {
|
||||
await using app = await fixture()
|
||||
await app.client.highlightOnce(content, "typescript")
|
||||
const pending = Promise.withResolvers<TextChunk[]>()
|
||||
class DeferredCode extends CodeRenderable {
|
||||
protected override transformChunks() {
|
||||
return pending.promise
|
||||
}
|
||||
}
|
||||
const Render = kind === "subclass" ? DeferredCode : CodeRenderable
|
||||
const code = new Render(app.renderer, {
|
||||
content,
|
||||
filetype: "typescript",
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
fg: "#ffffff",
|
||||
onChunks: kind === "callback" ? () => pending.promise : undefined,
|
||||
})
|
||||
app.renderer.root.add(code)
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain(content)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ffffff"))
|
||||
pending.resolve(t`Formatted fixture`.chunks)
|
||||
await code.highlightingDone
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("Formatted fixture")
|
||||
expect(app.captureCharFrame()).not.toContain(content)
|
||||
} finally {
|
||||
pending.resolve(t`Formatted fixture`.chunks)
|
||||
await code.highlightingDone
|
||||
}
|
||||
})
|
||||
|
||||
test("an explicit initial styled value takes precedence over cached syntax", async () => {
|
||||
await using app = await fixture()
|
||||
const result = await app.client.highlightOnce(content, "typescript")
|
||||
const pending = Promise.withResolvers<typeof result>()
|
||||
app.client.highlightOnce = () => pending.promise
|
||||
const code = new CodeRenderable(app.renderer, {
|
||||
content,
|
||||
filetype: "typescript",
|
||||
syntaxStyle: app.syntax,
|
||||
treeSitterClient: app.client,
|
||||
initialStyledText: t`Prepared fixture`,
|
||||
})
|
||||
app.renderer.root.add(code)
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("Prepared fixture")
|
||||
expect(app.captureCharFrame()).not.toContain(content)
|
||||
} finally {
|
||||
pending.resolve(result)
|
||||
await code.highlightingDone
|
||||
}
|
||||
})
|
||||
|
||||
test("Solid can assign cached content before its syntax style", async () => {
|
||||
await using app = await fixture()
|
||||
await app.client.highlightOnce(content, "typescript")
|
||||
await render(
|
||||
() => (
|
||||
<code
|
||||
{...{
|
||||
treeSitterClient: app.client,
|
||||
filetype: "typescript",
|
||||
content,
|
||||
syntaxStyle: app.syntax,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
app.renderer,
|
||||
)
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain(content)
|
||||
expect(keyword(app)).toEqual(RGBA.fromHex("#ff0000"))
|
||||
})
|
||||
|
||||
test.each([false, true])("first layout uses the final conceal setting, cached=%s", async (cached) => {
|
||||
await using app = await fixture()
|
||||
if (cached) await app.client.highlightOnce("**abcd**", "markdown")
|
||||
const pending = Promise.withResolvers<Awaited<ReturnType<TreeSitterClient["highlightOnce"]>>>()
|
||||
app.client.highlightOnce = () => pending.promise
|
||||
app.resize(4, 12)
|
||||
try {
|
||||
await render(
|
||||
() => (
|
||||
<code
|
||||
{...{
|
||||
treeSitterClient: app.client,
|
||||
filetype: "markdown",
|
||||
syntaxStyle: app.syntax,
|
||||
content: "**abcd**",
|
||||
conceal: false,
|
||||
wrapMode: "char",
|
||||
width: 4,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
app.renderer,
|
||||
)
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("**ab\ncd**")
|
||||
expect(app.renderer.root.getChildren()[0].height).toBe(2)
|
||||
} finally {
|
||||
pending.resolve({ highlights: [] })
|
||||
}
|
||||
})
|
||||
|
||||
function keyword(app: Awaited<ReturnType<typeof fixture>>) {
|
||||
return app
|
||||
.captureSpans()
|
||||
.lines.flatMap((line) => line.spans)
|
||||
.find((span) => span.text.includes("const"))?.fg
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const directory = await tmpdir()
|
||||
const client = new TreeSitterClient({ dataPath: directory.path })
|
||||
const syntax = SyntaxStyle.fromStyles({ default: { fg: "#ffffff" }, keyword: { fg: "#ff0000" } })
|
||||
const output = await createTestRenderer({ width: 80, height: 12, useThread: false })
|
||||
return {
|
||||
...output,
|
||||
client,
|
||||
syntax,
|
||||
directory: directory.path,
|
||||
async [Symbol.asyncDispose]() {
|
||||
if (!output.renderer.isDestroyed) output.renderer.destroy()
|
||||
syntax.destroy()
|
||||
await client.destroy()
|
||||
await directory[Symbol.asyncDispose]()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
diff --git a/node_modules/@opentui/core/.bun-tag-1ebf445c0c1c7e45 b/.bun-tag-1ebf445c0c1c7e45
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/node_modules/@opentui/core/.bun-tag-8b2df39a1d17e4c2 b/.bun-tag-8b2df39a1d17e4c2
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/chunk-bun-b0662dgp.js b/chunk-bun-b0662dgp.js
|
||||
index be58d3ea205d26d3f3a33137a43fa50727e9abe7..cea6b5d507de1115951fc0bb3f53d4ba338b1cc9 100644
|
||||
--- a/chunk-bun-b0662dgp.js
|
||||
+++ b/chunk-bun-b0662dgp.js
|
||||
@@ -8517,6 +8517,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
initializePromise;
|
||||
initializeResolvers;
|
||||
messageCallbacks = new Map;
|
||||
+ highlightCache = new Map;
|
||||
messageIdCounter = 0;
|
||||
editQueues = new Map;
|
||||
debouncer;
|
||||
@@ -8599,6 +8600,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
if (this.worker !== worker) {
|
||||
return;
|
||||
}
|
||||
+ this.highlightCache = new Map;
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
this.worker = undefined;
|
||||
@@ -8655,6 +8657,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
}
|
||||
async handleReset() {
|
||||
+ this.highlightCache = new Map;
|
||||
this.buffers.clear();
|
||||
await this.stopWorker();
|
||||
this.startWorker();
|
||||
@@ -8748,6 +8751,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
return path;
|
||||
}
|
||||
addFiletypeParser(filetypeParser) {
|
||||
+ this.highlightCache = new Map;
|
||||
this.sendWorkerMessage({ type: "ADD_FILETYPE_PARSER", filetypeParser: this.resolveFiletypeParser(filetypeParser) });
|
||||
}
|
||||
resolveFiletypeParser(filetypeParser) {
|
||||
@@ -8773,7 +8777,21 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
});
|
||||
}
|
||||
+ getCachedHighlight(content, filetype) {
|
||||
+ const key = JSON.stringify([filetype, content]);
|
||||
+ const result = this.highlightCache.get(key);
|
||||
+ if (result) {
|
||||
+ this.highlightCache.delete(key);
|
||||
+ this.highlightCache.set(key, result);
|
||||
+ }
|
||||
+ return result === undefined ? undefined : structuredClone(result);
|
||||
+ }
|
||||
async highlightOnce(content, filetype) {
|
||||
+ const cached = this.getCachedHighlight(content, filetype);
|
||||
+ if (cached)
|
||||
+ return cached;
|
||||
+ // Invalidation replaces this map so older requests cannot refill the cache.
|
||||
+ const cache = this.highlightCache;
|
||||
if (!this.initialized) {
|
||||
try {
|
||||
await this.initialize();
|
||||
@@ -8783,7 +8801,20 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
const messageId = `oneshot_${this.messageIdCounter++}`;
|
||||
return new Promise((resolve3, reject) => {
|
||||
- this.messageCallbacks.set(messageId, { resolve: resolve3, reject });
|
||||
+ this.messageCallbacks.set(messageId, {
|
||||
+ resolve: (result) => {
|
||||
+ if (this.highlightCache === cache && !result.error && !result.warning && result.highlights) {
|
||||
+ const key = JSON.stringify([filetype, content]);
|
||||
+ cache.delete(key);
|
||||
+ cache.set(key, structuredClone(result));
|
||||
+ if (cache.size > 500) {
|
||||
+ cache.delete(cache.keys().next().value);
|
||||
+ }
|
||||
+ }
|
||||
+ resolve3(result);
|
||||
+ },
|
||||
+ reject
|
||||
+ });
|
||||
try {
|
||||
this.sendWorkerMessage({
|
||||
type: "ONESHOT_HIGHLIGHT",
|
||||
@@ -9029,6 +9060,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
if (this.destroyPromise) {
|
||||
return this.destroyPromise;
|
||||
}
|
||||
+ this.highlightCache = new Map;
|
||||
let resolveDestroy;
|
||||
let rejectDestroy;
|
||||
const destroyPromise = new Promise((resolve3, reject) => {
|
||||
@@ -9094,6 +9126,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
if (this.options.dataPath === dataPath) {
|
||||
return;
|
||||
}
|
||||
+ this.highlightCache = new Map;
|
||||
this.options.dataPath = dataPath;
|
||||
if (this.initialized && this.worker) {
|
||||
const messageId = `update_datapath_${this.messageIdCounter++}`;
|
||||
@@ -9122,6 +9155,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
}
|
||||
async clearCache() {
|
||||
+ this.highlightCache = new Map;
|
||||
if (!this.initialized || !this.worker) {
|
||||
throw new Error("Cannot clear cache: client is not initialized");
|
||||
}
|
||||
diff --git a/chunk-bun-jxfx3h5k.js b/chunk-bun-jxfx3h5k.js
|
||||
index 5150999349c76c2f632c789e77f4800282691270..edb4f2c951e483c67de802b0512b7bd2676d4770 100644
|
||||
--- a/chunk-bun-jxfx3h5k.js
|
||||
+++ b/chunk-bun-jxfx3h5k.js
|
||||
@@ -3191,13 +3191,16 @@ class CodeRenderable extends TextBufferRenderable {
|
||||
this._onHighlight = options.onHighlight;
|
||||
this._onChunks = options.onChunks;
|
||||
if (this._content.length > 0) {
|
||||
- if (this._initialStyledText && this._drawUnstyledText) {
|
||||
- this.textBuffer.setStyledText(this._initialStyledText);
|
||||
- } else {
|
||||
- this.textBuffer.setText(this._content);
|
||||
+ const cached = this.applyCachedHighlights();
|
||||
+ if (!cached) {
|
||||
+ if (this._initialStyledText && this._drawUnstyledText) {
|
||||
+ this.textBuffer.setStyledText(this._initialStyledText);
|
||||
+ } else {
|
||||
+ this.textBuffer.setText(this._content);
|
||||
+ }
|
||||
}
|
||||
this.updateTextInfo();
|
||||
- this._shouldRenderTextBuffer = this._drawUnstyledText || !this._filetype;
|
||||
+ this._shouldRenderTextBuffer = cached || this._drawUnstyledText || !this._filetype;
|
||||
}
|
||||
this._highlightsDirty = this._content.length > 0;
|
||||
}
|
||||
@@ -3212,6 +3215,10 @@ class CodeRenderable extends TextBufferRenderable {
|
||||
if (this._content !== value) {
|
||||
this._content = value;
|
||||
this.invalidateHighlights();
|
||||
+ if (this.applyCachedHighlights()) {
|
||||
+ this.updateTextInfo();
|
||||
+ return;
|
||||
+ }
|
||||
if (this._streaming && this._filetype && !this._drawUnstyledText) {
|
||||
this.requestRender();
|
||||
return;
|
||||
@@ -3356,9 +3363,32 @@ class CodeRenderable extends TextBufferRenderable {
|
||||
const modified = await this._onChunks(chunks, context);
|
||||
return modified ?? chunks;
|
||||
}
|
||||
+ applyCachedHighlights() {
|
||||
+ if (!this._filetype || !this._syntaxStyle || this._initialStyledText || this._onHighlight || this._onChunks || this.transformChunks !== CodeRenderable.prototype.transformChunks)
|
||||
+ return false;
|
||||
+ const highlights = this._treeSitterClient.getCachedHighlight(this._content, this._filetype)?.highlights;
|
||||
+ if (!highlights?.length)
|
||||
+ return false;
|
||||
+ const chunks = treeSitterToTextChunks(this._content, highlights, this._syntaxStyle, {
|
||||
+ enabled: this._conceal,
|
||||
+ baseHighlight: this._baseHighlight
|
||||
+ });
|
||||
+ this.textBuffer.setStyledText(new StyledText(chunks));
|
||||
+ this.setRenderedLineSources(this.getConcealLinesSourceMap(this._content, highlights));
|
||||
+ this._shouldRenderTextBuffer = true;
|
||||
+ return true;
|
||||
+ }
|
||||
+ onLifecyclePass = () => {
|
||||
+ if (this._highlightsDirty && this.applyCachedHighlights())
|
||||
+ this.updateTextInfo();
|
||||
+ };
|
||||
ensureVisibleTextBeforeHighlight() {
|
||||
if (this.isDestroyed)
|
||||
return;
|
||||
+ if (this.applyCachedHighlights()) {
|
||||
+ this.updateTextInfo();
|
||||
+ return;
|
||||
+ }
|
||||
const content = this._content;
|
||||
if (!this._filetype) {
|
||||
this._shouldRenderTextBuffer = true;
|
||||
diff --git a/chunk-node-dcyj0dm5.js b/chunk-node-dcyj0dm5.js
|
||||
index 83e49b1de936315dcfcdca5338f56ce7dc3996e6..b1fdcb98dcf3600c4b6640695f1f2ceac004bdf0 100644
|
||||
--- a/chunk-node-dcyj0dm5.js
|
||||
+++ b/chunk-node-dcyj0dm5.js
|
||||
@@ -3190,13 +3190,16 @@ class CodeRenderable extends TextBufferRenderable {
|
||||
this._onHighlight = options.onHighlight;
|
||||
this._onChunks = options.onChunks;
|
||||
if (this._content.length > 0) {
|
||||
- if (this._initialStyledText && this._drawUnstyledText) {
|
||||
- this.textBuffer.setStyledText(this._initialStyledText);
|
||||
- } else {
|
||||
- this.textBuffer.setText(this._content);
|
||||
+ const cached = this.applyCachedHighlights();
|
||||
+ if (!cached) {
|
||||
+ if (this._initialStyledText && this._drawUnstyledText) {
|
||||
+ this.textBuffer.setStyledText(this._initialStyledText);
|
||||
+ } else {
|
||||
+ this.textBuffer.setText(this._content);
|
||||
+ }
|
||||
}
|
||||
this.updateTextInfo();
|
||||
- this._shouldRenderTextBuffer = this._drawUnstyledText || !this._filetype;
|
||||
+ this._shouldRenderTextBuffer = cached || this._drawUnstyledText || !this._filetype;
|
||||
}
|
||||
this._highlightsDirty = this._content.length > 0;
|
||||
}
|
||||
@@ -3211,6 +3214,10 @@ class CodeRenderable extends TextBufferRenderable {
|
||||
if (this._content !== value) {
|
||||
this._content = value;
|
||||
this.invalidateHighlights();
|
||||
+ if (this.applyCachedHighlights()) {
|
||||
+ this.updateTextInfo();
|
||||
+ return;
|
||||
+ }
|
||||
if (this._streaming && this._filetype && !this._drawUnstyledText) {
|
||||
this.requestRender();
|
||||
return;
|
||||
@@ -3355,9 +3362,32 @@ class CodeRenderable extends TextBufferRenderable {
|
||||
const modified = await this._onChunks(chunks, context);
|
||||
return modified ?? chunks;
|
||||
}
|
||||
+ applyCachedHighlights() {
|
||||
+ if (!this._filetype || !this._syntaxStyle || this._initialStyledText || this._onHighlight || this._onChunks || this.transformChunks !== CodeRenderable.prototype.transformChunks)
|
||||
+ return false;
|
||||
+ const highlights = this._treeSitterClient.getCachedHighlight(this._content, this._filetype)?.highlights;
|
||||
+ if (!highlights?.length)
|
||||
+ return false;
|
||||
+ const chunks = treeSitterToTextChunks(this._content, highlights, this._syntaxStyle, {
|
||||
+ enabled: this._conceal,
|
||||
+ baseHighlight: this._baseHighlight
|
||||
+ });
|
||||
+ this.textBuffer.setStyledText(new StyledText(chunks));
|
||||
+ this.setRenderedLineSources(this.getConcealLinesSourceMap(this._content, highlights));
|
||||
+ this._shouldRenderTextBuffer = true;
|
||||
+ return true;
|
||||
+ }
|
||||
+ onLifecyclePass = () => {
|
||||
+ if (this._highlightsDirty && this.applyCachedHighlights())
|
||||
+ this.updateTextInfo();
|
||||
+ };
|
||||
ensureVisibleTextBeforeHighlight() {
|
||||
if (this.isDestroyed)
|
||||
return;
|
||||
+ if (this.applyCachedHighlights()) {
|
||||
+ this.updateTextInfo();
|
||||
+ return;
|
||||
+ }
|
||||
const content = this._content;
|
||||
if (!this._filetype) {
|
||||
this._shouldRenderTextBuffer = true;
|
||||
diff --git a/chunk-node-zcz10bhr.js b/chunk-node-zcz10bhr.js
|
||||
index ff0f5b6685d8f2fd40f66feaf7984be5cd8413a3..b8227bc3b15383fdae280a710f3548412fa51cac 100644
|
||||
--- a/chunk-node-zcz10bhr.js
|
||||
+++ b/chunk-node-zcz10bhr.js
|
||||
@@ -8496,6 +8496,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
initializePromise;
|
||||
initializeResolvers;
|
||||
messageCallbacks = new Map;
|
||||
+ highlightCache = new Map;
|
||||
messageIdCounter = 0;
|
||||
editQueues = new Map;
|
||||
debouncer;
|
||||
@@ -8578,6 +8579,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
if (this.worker !== worker) {
|
||||
return;
|
||||
}
|
||||
+ this.highlightCache = new Map;
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
this.worker = undefined;
|
||||
@@ -8634,6 +8636,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
}
|
||||
async handleReset() {
|
||||
+ this.highlightCache = new Map;
|
||||
this.buffers.clear();
|
||||
await this.stopWorker();
|
||||
this.startWorker();
|
||||
@@ -8727,6 +8730,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
return path;
|
||||
}
|
||||
addFiletypeParser(filetypeParser) {
|
||||
+ this.highlightCache = new Map;
|
||||
this.sendWorkerMessage({ type: "ADD_FILETYPE_PARSER", filetypeParser: this.resolveFiletypeParser(filetypeParser) });
|
||||
}
|
||||
resolveFiletypeParser(filetypeParser) {
|
||||
@@ -8752,7 +8756,21 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
});
|
||||
}
|
||||
+ getCachedHighlight(content, filetype) {
|
||||
+ const key = JSON.stringify([filetype, content]);
|
||||
+ const result = this.highlightCache.get(key);
|
||||
+ if (result) {
|
||||
+ this.highlightCache.delete(key);
|
||||
+ this.highlightCache.set(key, result);
|
||||
+ }
|
||||
+ return result === undefined ? undefined : structuredClone(result);
|
||||
+ }
|
||||
async highlightOnce(content, filetype) {
|
||||
+ const cached = this.getCachedHighlight(content, filetype);
|
||||
+ if (cached)
|
||||
+ return cached;
|
||||
+ // Invalidation replaces this map so older requests cannot refill the cache.
|
||||
+ const cache = this.highlightCache;
|
||||
if (!this.initialized) {
|
||||
try {
|
||||
await this.initialize();
|
||||
@@ -8762,7 +8780,20 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
const messageId = `oneshot_${this.messageIdCounter++}`;
|
||||
return new Promise((resolve3, reject) => {
|
||||
- this.messageCallbacks.set(messageId, { resolve: resolve3, reject });
|
||||
+ this.messageCallbacks.set(messageId, {
|
||||
+ resolve: (result) => {
|
||||
+ if (this.highlightCache === cache && !result.error && !result.warning && result.highlights) {
|
||||
+ const key = JSON.stringify([filetype, content]);
|
||||
+ cache.delete(key);
|
||||
+ cache.set(key, structuredClone(result));
|
||||
+ if (cache.size > 500) {
|
||||
+ cache.delete(cache.keys().next().value);
|
||||
+ }
|
||||
+ }
|
||||
+ resolve3(result);
|
||||
+ },
|
||||
+ reject
|
||||
+ });
|
||||
try {
|
||||
this.sendWorkerMessage({
|
||||
type: "ONESHOT_HIGHLIGHT",
|
||||
@@ -9008,6 +9039,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
if (this.destroyPromise) {
|
||||
return this.destroyPromise;
|
||||
}
|
||||
+ this.highlightCache = new Map;
|
||||
let resolveDestroy;
|
||||
let rejectDestroy;
|
||||
const destroyPromise = new Promise((resolve3, reject) => {
|
||||
@@ -9073,6 +9105,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
if (this.options.dataPath === dataPath) {
|
||||
return;
|
||||
}
|
||||
+ this.highlightCache = new Map;
|
||||
this.options.dataPath = dataPath;
|
||||
if (this.initialized && this.worker) {
|
||||
const messageId = `update_datapath_${this.messageIdCounter++}`;
|
||||
@@ -9101,6 +9134,7 @@ class TreeSitterClient extends EventEmitter2 {
|
||||
}
|
||||
}
|
||||
async clearCache() {
|
||||
+ this.highlightCache = new Map;
|
||||
if (!this.initialized || !this.worker) {
|
||||
throw new Error("Cannot clear cache: client is not initialized");
|
||||
}
|
||||
diff --git a/lib/tree-sitter/client.d.ts b/lib/tree-sitter/client.d.ts
|
||||
index cb8c63ba847e48b3b9fd053e48daa47e228f0699..942daf13d770d26da4692f10574bc8f698a84ad1 100644
|
||||
--- a/lib/tree-sitter/client.d.ts
|
||||
+++ b/lib/tree-sitter/client.d.ts
|
||||
@@ -14,6 +14,7 @@ export declare class TreeSitterClient extends EventEmitter<TreeSitterClientEvent
|
||||
private initializePromise;
|
||||
private initializeResolvers;
|
||||
private messageCallbacks;
|
||||
+ private highlightCache;
|
||||
private messageIdCounter;
|
||||
private editQueues;
|
||||
private debouncer;
|
||||
@@ -43,6 +44,11 @@ export declare class TreeSitterClient extends EventEmitter<TreeSitterClientEvent
|
||||
addFiletypeParser(filetypeParser: FiletypeParserOptions): void;
|
||||
private resolveFiletypeParser;
|
||||
getPerformance(): Promise<PerformanceStats>;
|
||||
+ getCachedHighlight(content: string, filetype: string): {
|
||||
+ highlights?: SimpleHighlight[];
|
||||
+ warning?: string;
|
||||
+ error?: string;
|
||||
+ } | undefined;
|
||||
highlightOnce(content: string, filetype: string): Promise<{
|
||||
highlights?: SimpleHighlight[];
|
||||
warning?: string;
|
||||
diff --git a/renderables/Code.d.ts b/renderables/Code.d.ts
|
||||
index 9f660f2502da274748ece24a42e613203fb7b500..5d54249be0693a60822ba89f2eb8c93ceced2207 100644
|
||||
--- a/renderables/Code.d.ts
|
||||
+++ b/renderables/Code.d.ts
|
||||
@@ -90,6 +90,8 @@ export declare class CodeRenderable extends TextBufferRenderable {
|
||||
get isHighlighting(): boolean;
|
||||
get highlightingDone(): Promise<void>;
|
||||
protected transformChunks(chunks: TextChunk[], context: ChunkRenderContext): Promise<TextChunk[]>;
|
||||
+ private applyCachedHighlights;
|
||||
+ onLifecyclePass: () => void;
|
||||
private ensureVisibleTextBeforeHighlight;
|
||||
private startHighlight;
|
||||
private runHighlights;
|
||||
Reference in New Issue
Block a user