From 799d36569e149ec994ea3f658604cda71ee239f7 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:54:18 -0700 Subject: [PATCH] chore(deps): upgrade @pierre/diffs to 1.5.2 and @pierre/trees to 1.0.0-beta.6 Move the editable Files surface to the 1.4 edit API. Pierre highlights an edit session on the main thread, so the panel stays read-only until the worker's highlight has painted, then attaches the editor. Upstream now covers the stale-highlight, grammar-readiness and option-forcing hunks of our patch. The rest shrinks to the subpath exports and the wrapped-row height retention from #10018. The parser now decodes git-quoted names itself, so drop our second decode, which turned a literal backslash into nothing. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/package.json | 2 +- .../mobile/src/features/review/reviewModel.ts | 5 +- apps/web/package.json | 2 +- .../components/diffs/AnnotatableCodeView.tsx | 2 +- .../diffs/StyledDiffCodeView.test.tsx | 7 +- .../components/diffs/StyledDiffCodeView.tsx | 8 +- .../src/components/files/FilePreviewPanel.tsx | 286 +++++++++++------ .../files/ReadOnlySourcePreview.tsx | 2 +- .../files/fileContentRevision.test.ts | 25 +- .../components/files/fileContentRevision.ts | 18 -- .../files/fileEditorHighlight.test.ts | 299 ------------------ .../files/fileEditorLanguageReadiness.test.ts | 199 ------------ .../files/fileEditorVirtualization.test.ts | 56 ++-- .../files/projectFilesQueryState.ts | 12 + .../pullRequest/PullRequestCodeTab.tsx | 4 +- apps/web/src/lib/diffRendering.test.ts | 4 + apps/web/src/lib/diffRendering.ts | 15 +- apps/web/vite.config.ts | 2 +- patches/@pierre%2Fdiffs@1.3.0-beta.10.patch | 238 -------------- patches/@pierre%2Fdiffs@1.5.2.patch | 146 +++++++++ pnpm-lock.yaml | 118 +++++-- pnpm-workspace.yaml | 6 +- 22 files changed, 505 insertions(+), 951 deletions(-) delete mode 100644 apps/web/src/components/files/fileEditorHighlight.test.ts delete mode 100644 apps/web/src/components/files/fileEditorLanguageReadiness.test.ts delete mode 100644 patches/@pierre%2Fdiffs@1.3.0-beta.10.patch create mode 100644 patches/@pierre%2Fdiffs@1.5.2.patch diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 602ffb66bace..53b5476d3a2d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -131,7 +131,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:", - "@pierre/trees": "1.0.0-beta.4", + "@pierre/trees": "1.0.0-beta.6", "@types/react": "~19.3.0", "@types/react-dom": "~19.2.3", "babel-preset-expo": "~58.0.8", diff --git a/apps/mobile/src/features/review/reviewModel.ts b/apps/mobile/src/features/review/reviewModel.ts index 86ded9a6264e..d2e6c1254617 100644 --- a/apps/mobile/src/features/review/reviewModel.ts +++ b/apps/mobile/src/features/review/reviewModel.ts @@ -2,7 +2,6 @@ import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types"; import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; import type { ReviewDiffPreviewSource } from "@t3tools/contracts"; -import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; import * as Arr from "effect/Array"; import { pipe } from "effect/Function"; import * as Order from "effect/Order"; @@ -374,8 +373,8 @@ function buildRenderableRows(file: FileDiffMetadata): ReadonlyArray total + hunk.additionLines, 0); const deletions = file.hunks.reduce((total, hunk) => total + hunk.deletionLines, 0); const cacheKey = file.cacheKey ?? `${previousPath ?? "none"}:${path}:${file.type}`; diff --git a/apps/web/package.json b/apps/web/package.json index 53c53c1c7cf9..d17795f15c5d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -25,7 +25,7 @@ "@legendapp/list": "catalog:", "@noble/hashes": "catalog:", "@pierre/diffs": "catalog:", - "@pierre/trees": "1.0.0-beta.4", + "@pierre/trees": "1.0.0-beta.6", "@t3tools/client-runtime": "workspace:*", "@t3tools/contracts": "workspace:*", "@t3tools/shared": "workspace:*", diff --git a/apps/web/src/components/diffs/AnnotatableCodeView.tsx b/apps/web/src/components/diffs/AnnotatableCodeView.tsx index 52b538f92b0f..ab4f545ed07b 100644 --- a/apps/web/src/components/diffs/AnnotatableCodeView.tsx +++ b/apps/web/src/components/diffs/AnnotatableCodeView.tsx @@ -35,7 +35,7 @@ interface DiffCommentAnnotationGroup { } type DiffCommentLineAnnotation = DiffLineAnnotation; -export type AnnotatableCodeViewHandle = CodeViewHandle; +export type AnnotatableCodeViewHandle = CodeViewHandle; const EMPTY_REVIEW_COMMENTS: ReadonlyArray = []; function annotationSide(range: SelectedLineRange): AnnotationSide { diff --git a/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx b/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx index ee249888a415..3f01a62e549f 100644 --- a/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx +++ b/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx @@ -98,7 +98,7 @@ vi.mock("@pierre/diffs/worker/worker.js?worker", async () => { vi.mock("@pierre/diffs/react", async (importOriginal) => ({ ...(await importOriginal()), - CodeView: (props: CodeViewProps) => { + CodeView: (props: CodeViewProps) => { return props.items?.map((item) => item.type === "file" ? : null, ); @@ -115,6 +115,7 @@ function FileOutput({ file }: { file: FileContents }) { testState.renderPools.push(pool); const renderer = new FileRenderer( { theme: "pierre-dark", preferredHighlighter: "shiki-wasm" }, + undefined, render, pool, ); @@ -301,8 +302,10 @@ describe("code-view worker lifecycle", () => { expect(pool.getStats().totalWorkers).toBe(2); expect(testState.terminations).toHaveLength(0); await act(async () => renderer!.update(renderViews(0))); + // Pierre gives up on a held startup after 10 seconds; the idle timer then tears the pool down. + const timedOut = expect(pending).rejects.toThrow("worker initialization timed out"); await vi.advanceTimersByTimeAsync(30_000); - await pending; + await timedOut; await Promise.all(testState.terminations); await act(async () => { for (const deliver of testState.heldResponses) deliver(); diff --git a/apps/web/src/components/diffs/StyledDiffCodeView.tsx b/apps/web/src/components/diffs/StyledDiffCodeView.tsx index cdd9fe272225..e18d9ca4ba82 100644 --- a/apps/web/src/components/diffs/StyledDiffCodeView.tsx +++ b/apps/web/src/components/diffs/StyledDiffCodeView.tsx @@ -261,16 +261,16 @@ const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS} `; export type StyledDiffCodeViewOptions = Omit< - NonNullable["options"]>, + NonNullable["options"]>, "unsafeCSS" | "itemMetrics" | "layout" >; type StyledDiffCodeViewProps = ( - | Omit, "options"> - | Omit, "options"> + | Omit, "options"> + | Omit, "options"> ) & { readonly options?: StyledDiffCodeViewOptions; - readonly viewerRef?: Ref>; + readonly viewerRef?: Ref>; /** * Appended to the shared stylesheet inside the viewer's shadow root, for a surface that has * to restyle chrome the viewer owns — such as replacing its per-file line counts. diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 5ae0753429fd..5ce7a4cc2b2a 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -11,9 +11,23 @@ import { isWorkspaceImagePreviewPath, isWorkspaceVideoPreviewPath, } from "@t3tools/shared/filePreview"; -import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; -import { Editor } from "@pierre/diffs/editor"; -import { EditProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; +import { + DEFAULT_TOKENIZE_MAX_LENGTH, + VirtualizedFile, + getFiletypeFromFileName, + type File as FileInstance, + type FileContents, + type PostRenderPhase, + type SelectedLineRange, +} from "@pierre/diffs"; +import { + Editor, + type EditorChangeEvent, + type EditorFactory, + type EditorOptions, +} from "@pierre/diffs/edit"; +import type { WorkerPoolManager } from "@pierre/diffs/worker"; +import { EditProvider, File, Virtualizer, useWorkerPool } from "@pierre/diffs/react"; import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; import { isAtomCommandInterrupted, @@ -80,7 +94,7 @@ import { import SourceFilePreview from "./ReadOnlySourcePreview"; import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; -import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; +import { projectFileCacheKey } from "./fileContentRevision"; import { filePreviewReadErrorMessage, isMarkdownPreviewFile, @@ -91,6 +105,7 @@ import { import { useFileSaveCoordinator } from "./useFileSaveCoordinator"; import { getOptimisticProjectFileQueryData, + getProjectFileContents, setProjectFileQueryData, useProjectFileQuery, } from "./projectFilesQueryState"; @@ -117,7 +132,12 @@ const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; const RENDER_MARKDOWN_STORAGE_KEY = "t3code.renderMarkdown"; const RENDER_BROWSER_FILE_STORAGE_KEY = "t3code.renderBrowserFile"; const RENDER_TABLE_STORAGE_KEY = "t3code.renderTable"; -type FilePostRender = NonNullable["onPostRender"]>; +// Shared by the read-only and annotated surfaces, so it is generic over annotation metadata. +type FilePostRender = ( + fileContainer: HTMLElement, + instance: FileInstance, + phase: PostRenderPhase, +) => void; function WorkspaceImagePreview(props: { readonly environmentId: EnvironmentId; @@ -547,6 +567,67 @@ function useFileLineReveal( ); } +const createFileEditor: EditorFactory = ( + editorType, + options, + editStateKey, +) => new Editor(editorType, options, editStateKey); + +function editableFileContents( + environmentId: EnvironmentId, + cwd: string, + relativePath: string, + contents: string, +): FileContents { + return { + name: relativePath, + contents, + cacheKey: `editor:${environmentId}:${projectFileCacheKey(cwd, relativePath, contents)}`, + }; +} + +function needsWorkerHighlight(workerPool: WorkerPoolManager | undefined, file: FileContents) { + if (workerPool?.isWorkingPool() !== true) return false; + if ((file.lang ?? getFiletypeFromFileName(file.name)) === "text") return false; + let lines = 1; + for ( + let index = file.contents.indexOf("\n"); + index !== -1; + index = file.contents.indexOf("\n", index + 1) + ) { + lines += 1; + } + return lines <= DEFAULT_TOKENIZE_MAX_LENGTH; +} + +/** + * Pierre highlights an active edit session on the main thread, so each version + * of the file becomes editable only once it has rendered the worker's + * highlight. A failed worker highlight falls back to main-thread highlighting. + */ +function useEditableAfterHighlight(file: FileContents) { + const workerPool = useWorkerPool(); + const [highlightedFile, setHighlightedFile] = useState(null); + const needsHighlight = useMemo(() => needsWorkerHighlight(workerPool, file), [file, workerPool]); + const ready = !needsHighlight || highlightedFile === file; + + useEffect(() => { + if (ready || workerPool === undefined) return; + workerPool.primeFileHighlightCache(file).catch(() => setHighlightedFile(file)); + }, [file, ready, workerPool]); + + const onPostRender = useCallback( + (renderedFile: FileContents | undefined, phase: PostRenderPhase) => { + if (ready || phase === "unmount" || renderedFile?.cacheKey !== file.cacheKey) return; + // The pool caches a result just before the instance renders it, so a + // render that sees the cache has painted highlighted rows. + if (workerPool?.getFileResultCache(file) !== undefined) setHighlightedFile(file); + }, + [file, ready, workerPool], + ); + return { ready, onPostRender }; +} + interface EditableFileSurfaceProps { environmentId: EnvironmentId; cwd: string; @@ -597,46 +678,65 @@ function EditableFileSurface({ relativePath, onPendingChange, }); - const editor = useMemo( - () => - new Editor({ - persistState: true, - persistStateStorage: "inMemory", - onChange: (file, nextLineAnnotations) => { - setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); - saveCoordinator.change(file.contents); - if (nextLineAnnotations) { - const remapped = remapFileCommentAnnotations( - nextLineAnnotations as FileCommentLineAnnotation[], - ); - setLineAnnotations(remapped); - for (const annotation of remapped) { - for (const entry of annotation.metadata.entries) { - if (entry.kind !== "comment") continue; - addReviewComment( - composerDraftTarget, - buildFileReviewComment({ - id: entry.id, - filePath: relativePath, - startLine: entry.startLine, - endLine: entry.endLine, - text: entry.text, - contents: file.contents, - }), - ); - } - } - } - }, - }), - [addReviewComment, composerDraftTarget, cwd, environmentId, relativePath, saveCoordinator], + // The editor owns the draft, so its own edits echoing back through the file + // query keep the file identity. Only a change from elsewhere replaces it. + const [externalFile, setExternalFile] = useState(() => + editableFileContents(environmentId, cwd, relativePath, contents), + ); + const [editedContents, setEditedContents] = useState(null); + if (contents !== (editedContents ?? externalFile.contents)) { + setExternalFile(editableFileContents(environmentId, cwd, relativePath, contents)); + setEditedContents(null); + } + const { ready: editable, onPostRender: onEditablePostRender } = + useEditableAfterHighlight(externalFile); + const editorRef = useRef | null>(null); + const editorOptions = useMemo>( + () => ({ + onAttach: (editor) => { + editorRef.current = editor; + }, + onComplete: () => { + editorRef.current = null; + }, + }), + [], ); - useEffect( - () => () => { - editor.cleanUp(); + // Mirrors the draft out to the save queue, the optimistic file query and the + // composer's review comments. + const handleEditChange = useCallback( + ({ + file, + lineAnnotations: nextLineAnnotations, + }: EditorChangeEvent<"file", FileCommentAnnotationGroup, undefined>) => { + // Adopting an external change reports it as an edit; it is already on disk. + if (file.contents === getProjectFileContents(environmentId, cwd, relativePath)) return; + setEditedContents(file.contents); + setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); + saveCoordinator.change(file.contents); + if (!nextLineAnnotations) return; + const remapped = remapFileCommentAnnotations(nextLineAnnotations); + // The editor hands back the array it was given until an edit moves an annotation. + setLineAnnotations((current) => (current === nextLineAnnotations ? current : remapped)); + for (const annotation of remapped) { + for (const entry of annotation.metadata.entries) { + if (entry.kind !== "comment") continue; + addReviewComment( + composerDraftTarget, + buildFileReviewComment({ + id: entry.id, + filePath: relativePath, + startLine: entry.startLine, + endLine: entry.endLine, + text: entry.text, + contents: file.contents, + }), + ); + } + } }, - [editor], + [addReviewComment, composerDraftTarget, cwd, environmentId, relativePath, saveCoordinator], ); const removeAnnotationEntry = useCallback( @@ -695,47 +795,44 @@ function EditableFileSurface({ ], ); - const beginComment = useCallback( - (range: SelectedLineRange) => { - editor.setSelections([]); - editor.blur(); - const { startLine, endLine } = normalizeFileCommentRange(range); - const draftEntry: FileCommentAnnotationEntry = { - id: nextFileCommentId(), - kind: "draft", - startLine, - endLine, - text: "", - }; - setLineAnnotations((current) => { - const withoutDraft = current.flatMap((annotation) => { - const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); - return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; - }); - const existingIndex = withoutDraft.findIndex( - (annotation) => annotation.lineNumber === endLine, - ); - if (existingIndex < 0) { - return [ - ...withoutDraft, - { - lineNumber: endLine, - metadata: { entries: [draftEntry] }, - }, - ]; - } - return withoutDraft.map((annotation, index) => - index === existingIndex - ? { - ...annotation, - metadata: { entries: [...annotation.metadata.entries, draftEntry] }, - } - : annotation, - ); + const beginComment = useCallback((range: SelectedLineRange) => { + editorRef.current?.setSelections([]); + editorRef.current?.blur(); + const { startLine, endLine } = normalizeFileCommentRange(range); + const draftEntry: FileCommentAnnotationEntry = { + id: nextFileCommentId(), + kind: "draft", + startLine, + endLine, + text: "", + }; + setLineAnnotations((current) => { + const withoutDraft = current.flatMap((annotation) => { + const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); + return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; }); - }, - [editor], - ); + const existingIndex = withoutDraft.findIndex( + (annotation) => annotation.lineNumber === endLine, + ); + if (existingIndex < 0) { + return [ + ...withoutDraft, + { + lineNumber: endLine, + metadata: { entries: [draftEntry] }, + }, + ]; + } + return withoutDraft.map((annotation, index) => + index === existingIndex + ? { + ...annotation, + metadata: { entries: [...annotation.metadata.entries, draftEntry] }, + } + : annotation, + ); + }); + }, []); const hasOpenCommentForm = lineAnnotations.some((annotation) => annotation.metadata.entries.some((entry) => entry.kind === "draft"), ); @@ -744,11 +841,11 @@ function EditableFileSurface({ if (!root) return; return installFileEditorDismissal({ root, - editor, + editor: { setSelections: (selections) => editorRef.current?.setSelections(selections) }, isBlocked: () => hasOpenCommentForm, onDismiss: () => setSelectedRange(null), }); - }, [editor, hasOpenCommentForm, setSelectedRange]); + }, [hasOpenCommentForm, setSelectedRange]); const handleLineSelectionEnd = useCallback( (range: SelectedLineRange | null) => { setSelectedRange(range); @@ -762,6 +859,7 @@ function EditableFileSurface({ const handlePostRender = useCallback( (fileContainer, instance, phase) => { onPostRender(fileContainer, instance, phase); + onEditablePostRender(instance.file, phase); if (selectionFrameRef.current !== null) { cancelAnimationFrame(selectionFrameRef.current); @@ -775,11 +873,11 @@ function EditableFileSurface({ instance.setSelectedLines(selectedRange, { notify: false }); }); }, - [onPostRender, selectedRange], + [onEditablePostRender, onPostRender, selectedRange], ); return ( - +
- file={{ - name: relativePath, - contents, - cacheKey: projectFileEditorCacheKey( - environmentId, - cwd, - relativePath, - contents, - editor.getFile(), - ), - }} + file={externalFile} + edit={editable} + editorOptions={editorOptions} + onEditChange={handleEditChange} options={{ disableFileHeader: true, enableGutterUtility: !hasOpenCommentForm, @@ -832,7 +923,6 @@ function EditableFileSurface({
)} className="min-h-full" - contentEditable /> diff --git a/apps/web/src/components/files/ReadOnlySourcePreview.tsx b/apps/web/src/components/files/ReadOnlySourcePreview.tsx index 8a59982c6f95..a4438ebf72a0 100644 --- a/apps/web/src/components/files/ReadOnlySourcePreview.tsx +++ b/apps/web/src/components/files/ReadOnlySourcePreview.tsx @@ -17,7 +17,7 @@ export default function ReadOnlySourcePreview(props: { readonly name: string; readonly text: string; readonly cacheKey?: string; - readonly onPostRender?: FileOptions["onPostRender"]; + readonly onPostRender?: FileOptions["onPostRender"]; }) { const { resolvedTheme } = useTheme(); const wordWrap = useClientSettings((settings) => settings.wordWrap); diff --git a/apps/web/src/components/files/fileContentRevision.test.ts b/apps/web/src/components/files/fileContentRevision.test.ts index 4a8bb3d5522b..e54f28e0d235 100644 --- a/apps/web/src/components/files/fileContentRevision.test.ts +++ b/apps/web/src/components/files/fileContentRevision.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; +import { projectFileCacheKey } from "./fileContentRevision"; describe("file cache identity", () => { it("changes for same-length edits", () => { @@ -8,27 +8,4 @@ describe("file cache identity", () => { projectFileCacheKey("/repo", "file.json", "nodeVeasdrs"), ); }); - - it("keeps editor identity stable for locally edited contents", () => { - const cacheKey = projectFileEditorCacheKey("local", "/repo", "file.json", "after", undefined); - - expect( - projectFileEditorCacheKey("local", "/repo", "file.json", "after edit", { - cacheKey, - contents: "after edit", - }), - ).toBe(cacheKey); - }); - - it("rotates editor identity for external contents and environments", () => { - const cacheKey = projectFileEditorCacheKey("local", "/repo", "file.json", "before", undefined); - const editorFile = { cacheKey, contents: "before" }; - - expect( - projectFileEditorCacheKey("local", "/repo", "file.json", "external edit", editorFile), - ).not.toBe(cacheKey); - expect(projectFileEditorCacheKey("remote", "/repo", "file.json", "before", undefined)).not.toBe( - cacheKey, - ); - }); }); diff --git a/apps/web/src/components/files/fileContentRevision.ts b/apps/web/src/components/files/fileContentRevision.ts index b4e1698a34dc..f376f7fde9ec 100644 --- a/apps/web/src/components/files/fileContentRevision.ts +++ b/apps/web/src/components/files/fileContentRevision.ts @@ -10,21 +10,3 @@ function fileContentRevision(contents: string): string { export function projectFileCacheKey(cwd: string, relativePath: string, contents: string): string { return `${cwd}:${relativePath}:${fileContentRevision(contents)}`; } - -interface EditorFileIdentity { - readonly cacheKey?: string; - readonly contents: string; -} - -export function projectFileEditorCacheKey( - environmentId: string, - cwd: string, - relativePath: string, - contents: string, - editorFile: EditorFileIdentity | undefined, -): string { - if (editorFile?.contents === contents && editorFile.cacheKey) { - return editorFile.cacheKey; - } - return `editor:${environmentId}:${projectFileCacheKey(cwd, relativePath, contents)}`; -} diff --git a/apps/web/src/components/files/fileEditorHighlight.test.ts b/apps/web/src/components/files/fileEditorHighlight.test.ts deleted file mode 100644 index 39624d953c09..000000000000 --- a/apps/web/src/components/files/fileEditorHighlight.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { - FileRenderer, - getSharedHighlighter, - type BaseCodeOptions, - type DiffsHighlighter, - type FileContents, - type HighlightedToken, - type RenderRange, -} from "@pierre/diffs"; -import { TextDocument } from "@pierre/diffs/editor"; -import { WorkerPoolManager, type WorkerRequest, type WorkerResponse } from "@pierre/diffs/worker"; -import * as NodeWorkerThreads from "node:worker_threads"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; - -type DocumentChange = NonNullable["applyEdits"]>>; -interface Tokenizer { - readonly themeType: "light" | "dark"; - tokenize(change: DocumentChange, range: RenderRange): Map; - cleanUp(): void; -} - -// This dependency-internal tokenizer is the one used by Editor.#rerender. -const tokenizerUrl = new URL("./editor/tokenizer.js", import.meta.resolve("@pierre/diffs")); -const { EditorTokenizer } = (await import(/* @vite-ignore */ tokenizerUrl.href)) as { - EditorTokenizer: new (options: { - codeOptions: BaseCodeOptions; - highlighter: DiffsHighlighter; - textDocument: TextDocument; - setStyle: (style: string) => void; - onDeferTokenize: (lines: Map, theme: "light" | "dark") => void; - }) => Tokenizer; -}; - -const workerModule = import.meta.resolve("@pierre/diffs/worker/worker.js"); -const source = Array.from( - { length: 7_000 }, - (_, index) => - `export const section${index} =

Long wrapped source line ${index} for the file editor.

;`, -).join("\n"); -const options = { - theme: "pierre-dark", - themeType: "dark", - preferredHighlighter: "shiki-wasm", - useTokenTransformer: true, - overflow: "wrap", - disableFileHeader: true, -} as const; -const range: RenderRange = { - startingLine: 6_950, - totalLines: 150, - bufferBefore: 0, - bufferAfter: 0, -}; - -interface HeldResponse { - data: WorkerResponse; - deliver: () => void; -} -let responses: HeldResponse[]; -let responseWaiters: ((response: HeldResponse) => void)[]; -let terminationPromises: Promise[]; -let pool: WorkerPoolManager; -let renderer: FileRenderer; -let tokenizer: Tokenizer; -let file: FileContents; -let document: TextDocument; -const animationFrames = new Set>(); - -function nextResponse(): Promise { - const response = responses.shift(); - return response - ? Promise.resolve(response) - : new Promise((resolve) => responseWaiters.push(resolve)); -} - -class WorkerTransport { - private readonly worker = new NodeWorkerThreads.Worker( - `const { parentPort, workerData } = require("node:worker_threads"); - globalThis.self = { - addEventListener(type, listener) { - if (type === "message") parentPort.on("message", data => listener({ data })); - if (type === "error") process.on("uncaughtException", listener); - } - }; - globalThis.postMessage = data => parentPort.postMessage(data); - import(workerData.moduleUrl);`, - { eval: true, workerData: { moduleUrl: workerModule }, execArgv: [] }, - ); - - addEventListener( - type: "message" | "error", - listener: (event: { data: WorkerResponse } | Error) => void, - ) { - if (type === "error") { - this.worker.on("error", listener); - return; - } - this.worker.on("message", (data: WorkerResponse) => { - const response = { data, deliver: () => listener({ data }) }; - if (data.type !== "success" || data.requestType !== "file") { - response.deliver(); - return; - } - const waiter = responseWaiters.shift(); - if (waiter) waiter(response); - else responses.push(response); - }); - } - - postMessage(message: WorkerRequest) { - this.worker.postMessage(message, []); - } - - terminate() { - terminationPromises.push(this.worker.terminate()); - } -} - -function applyChange(change: DocumentChange) { - // Keep the installed editor's non-DOM order, including the existing contents patch. - renderer.updateRenderCache(tokenizer.tokenize(change, range), tokenizer.themeType); - file.contents = document.getText(); - if (change.lineDelta !== 0) renderer.applyDocumentChange(document); -} - -function append(text: string) { - const position = document.positionAt(document.getText().length); - const change = document.applyEdits([ - { range: { start: position, end: position }, newText: text }, - ]); - expect(change).toBeDefined(); - applyChange(change!); -} - -function undo() { - const change = document.undo()?.[0]; - expect(change).toBeDefined(); - applyChange(change!); -} - -function renderContents() { - const result = renderer.renderFile(file, range); - expect(result?.totalLines).toBe(document.lineCount); - return renderer.renderFullHTML(result!); -} - -beforeEach(async () => { - responses = []; - responseWaiters = []; - terminationPromises = []; - vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { - const frame = setImmediate(() => { - animationFrames.delete(frame); - callback(0); - }); - animationFrames.add(frame); - return frame; - }); - vi.stubGlobal("cancelAnimationFrame", (frame: ReturnType) => { - animationFrames.delete(frame); - clearImmediate(frame); - }); - vi.stubGlobal("window", { matchMedia: () => ({ matches: true }) }); - pool = new WorkerPoolManager( - // Adapt browser transport only; Pierre's real worker produces each response. - { workerFactory: () => new WorkerTransport() as unknown as globalThis.Worker, poolSize: 1 }, - options, - ); - await pool.initialize(["tsx"]); - const highlighter = await getSharedHighlighter({ - themes: ["pierre-dark"], - langs: ["tsx"], - preferredHighlighter: "shiki-wasm", - }); - file = { name: "wrapped.tsx", contents: source, cacheKey: "editable-file" }; - document = new TextDocument(file.name, source, "tsx"); - renderer = new FileRenderer(options, () => {}, pool); - tokenizer = new EditorTokenizer({ - codeOptions: options, - highlighter, - textDocument: document, - setStyle: () => {}, - onDeferTokenize: (lines, theme) => renderer.updateRenderCache(lines, theme), - }); - renderContents(); -}); - -async function cleanUpFixture() { - tokenizer?.cleanUp(); - renderer?.cleanUp(); - pool?.terminate(); - await Promise.all(terminationPromises); - // Pool termination can queue a final broadcast after its workers have exited. - for (const frame of animationFrames) clearImmediate(frame); - animationFrames.clear(); - vi.unstubAllGlobals(); -} - -afterEach(cleanUpFixture); - -describe("editable file highlighting", () => { - it("cleans up an already terminated worker pool", async () => { - (await nextResponse()).deliver(); - expect(pool.getStats().totalWorkers).toBe(1); - pool.terminate(); - await Promise.all(terminationPromises); - expect(pool.getStats().totalWorkers).toBe(0); - - const animationFrame = globalThis.requestAnimationFrame; - const cancelFrame = globalThis.cancelAnimationFrame; - const window = globalThis.window; - try { - await cleanUpFixture(); - // Deliver the real Immediate queue after cleanup has removed the browser globals. - await new Promise((resolve) => setImmediate(resolve)); - } finally { - vi.stubGlobal("requestAnimationFrame", animationFrame); - vi.stubGlobal("cancelAnimationFrame", cancelFrame); - vi.stubGlobal("window", window); - } - }); - - it("still accepts an asynchronous highlight when the file has not changed", async () => { - expect(renderContents()).not.toContain('style="color:'); - (await nextResponse()).deliver(); - expect(renderContents()).toContain('style="color:'); - expect(pool.getFileResultCache(file)).toBeDefined(); - }); - - it.each([1, 60])( - "ignores a dispatched highlight after %i Enter edits and highlights the new version", - async (count) => { - const oldResponse = await nextResponse(); - for (let index = 0; index < count; index += 1) append("\n"); - append("export const EDITED_MARKER = 1;"); - oldResponse.deliver(); - expect(renderContents()).toContain("EDITED_MARKER"); - const currentResponse = await nextResponse(); - currentResponse.deliver(); - expect(renderContents()).toContain("EDITED_MARKER"); - const firstLines = renderer.renderFile(file, { ...range, startingLine: 0, totalLines: 20 }); - expect(renderer.renderFullHTML(firstLines!)).toContain('style="color:'); - expect(document.lineCount).toBe(7_000 + count); - }, - ); - - it("does not replace a same-line edit with stale tokens", async () => { - const oldResponse = await nextResponse(); - append(" EDITED_MARKER"); - oldResponse.deliver(); - expect(renderContents()).toContain("EDITED_MARKER"); - expect(document.lineCount).toBe(7_000); - (await nextResponse()).deliver(); - expect(renderContents()).toContain("EDITED_MARKER"); - }); - - it("keeps undo edits after an older highlight arrives", async () => { - const oldResponse = await nextResponse(); - append("\nexport const RETAINED_MARKER = 1;"); - append("\nexport const UNDONE_MARKER = 2;"); - undo(); - oldResponse.deliver(); - const html = renderContents(); - expect(html).toContain("RETAINED_MARKER"); - expect(html).not.toContain("UNDONE_MARKER"); - (await nextResponse()).deliver(); - expect(renderContents()).toContain("RETAINED_MARKER"); - undo(); - expect(document.getText()).toBe(source); - expect(renderContents()).not.toContain("RETAINED_MARKER"); - const redone = document.redo()?.[0]; - expect(redone).toBeDefined(); - applyChange(redone!); - expect(renderContents()).toContain("RETAINED_MARKER"); - }); - - it("evicts the pre-edit shared cache without losing already-highlighted lines", async () => { - (await nextResponse()).deliver(); - expect(pool.getFileResultCache(file)).toBeDefined(); - append("\nexport const EDITED_MARKER = 1;"); - expect(pool.getFileResultCache(file)).toBeUndefined(); - expect(renderContents()).toContain("EDITED_MARKER"); - const firstLines = renderer.renderFile(file, { ...range, startingLine: 0, totalLines: 20 }); - expect(renderer.renderFullHTML(firstLines!)).toContain('style="color:'); - }); - - it("reopens the edited file with the same cache key while an old response is pending", async () => { - const oldResponse = await nextResponse(); - append("\nexport const REOPENED_MARKER = 1;"); - renderer.cleanUp(); - renderer = new FileRenderer(options, () => {}, pool); - file = { ...file }; - expect(renderContents()).toContain("REOPENED_MARKER"); - oldResponse.deliver(); - (await nextResponse()).deliver(); - expect(renderContents()).toContain("REOPENED_MARKER"); - expect(renderContents()).toContain('style="color:'); - }); -}); diff --git a/apps/web/src/components/files/fileEditorLanguageReadiness.test.ts b/apps/web/src/components/files/fileEditorLanguageReadiness.test.ts deleted file mode 100644 index d6f64a55c9b1..000000000000 --- a/apps/web/src/components/files/fileEditorLanguageReadiness.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - FileRenderer, - disposeHighlighter, - getSharedHighlighter, - type BaseCodeOptions, - type DiffsHighlighter, - type FileContents, - type HighlightedToken, -} from "@pierre/diffs"; -import { TextDocument } from "@pierre/diffs/editor"; -import { WorkerPoolManager, type WorkerRequest, type WorkerResponse } from "@pierre/diffs/worker"; -import * as NodeWorkerThreads from "node:worker_threads"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; - -type DocumentChange = NonNullable["applyEdits"]>>; -interface Tokenizer { - tokenize(change: DocumentChange): Map; - cleanUp(): void; -} - -const tokenizerUrl = new URL("./editor/tokenizer.js", import.meta.resolve("@pierre/diffs")); -const { EditorTokenizer } = (await import(/* @vite-ignore */ tokenizerUrl.href)) as { - EditorTokenizer: new (options: { - codeOptions: BaseCodeOptions; - highlighter: DiffsHighlighter; - textDocument: TextDocument; - setStyle: (style: string) => void; - onDeferTokenize: () => void; - }) => Tokenizer; -}; - -const workerModule = import.meta.resolve("@pierre/diffs/worker/worker.js"); -const options = { - theme: "pierre-dark", - themeType: "dark", - preferredHighlighter: "shiki-wasm", - useTokenTransformer: true, -} as const; -const source = "export const View = () =>
Ready
;"; -let pool: WorkerPoolManager; -let renderer: FileRenderer; -let terminationPromises: Promise[]; -const pendingAnimationFrames = new Set>(); - -class WorkerTransport { - private readonly worker = new NodeWorkerThreads.Worker( - `const { parentPort, workerData } = require("node:worker_threads"); - globalThis.self = { - addEventListener(type, listener) { - if (type === "message") parentPort.on("message", data => listener({ data })); - if (type === "error") process.on("uncaughtException", listener); - } - }; - globalThis.postMessage = data => parentPort.postMessage(data); - import(workerData.moduleUrl);`, - { eval: true, workerData: { moduleUrl: workerModule }, execArgv: [] }, - ); - - addEventListener( - type: "message" | "error", - listener: (event: { data: WorkerResponse } | Error) => void, - ) { - if (type === "error") this.worker.on("error", listener); - else this.worker.on("message", (data: WorkerResponse) => listener({ data })); - } - - postMessage(message: WorkerRequest) { - this.worker.postMessage(message, []); - } - - terminate() { - terminationPromises.push(this.worker.terminate()); - } -} - -function firstEnter(highlighter: DiffsHighlighter, file: FileContents, language: string) { - const document = new TextDocument(file.name, file.contents, language); - const tokenizer = new EditorTokenizer({ - codeOptions: options, - highlighter, - textDocument: document, - setStyle: () => {}, - onDeferTokenize: () => {}, - }); - try { - const end = document.positionAt(file.contents.length); - const change = document.applyEdits([{ range: { start: end, end }, newText: "\n" }]); - expect(change).toBeDefined(); - // This is the synchronous first edit, before the tokenizer's debounced prebuild. - const dirtyLines = tokenizer.tokenize(change!); - expect([...dirtyLines.keys()]).toEqual([0, 1]); - expect(document.getText()).toBe(`${file.contents}\n`); - } finally { - tokenizer.cleanUp(); - } -} - -beforeEach(async () => { - terminationPromises = []; - vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { - const handle = setImmediate(() => { - pendingAnimationFrames.delete(handle); - callback(0); - }); - pendingAnimationFrames.add(handle); - return handle; - }); - vi.stubGlobal("cancelAnimationFrame", (handle: ReturnType) => { - pendingAnimationFrames.delete(handle); - clearImmediate(handle); - }); - vi.stubGlobal("window", { matchMedia: () => ({ matches: true }) }); - await disposeHighlighter(); - pool = new WorkerPoolManager( - // Adapt transport only. The installed Pierre worker resolves and highlights the file. - { workerFactory: () => new WorkerTransport() as unknown as globalThis.Worker, poolSize: 1 }, - options, - ); - await pool.initialize(); - renderer = new FileRenderer(options, undefined, pool); -}); - -afterEach(async () => { - renderer?.cleanUp(); - pool?.terminate(); - await Promise.all(terminationPromises); - await disposeHighlighter(); - // Drain the pool's final state broadcast before removing the animation frame stubs. - await new Promise((resolve) => setImmediate(resolve)); - for (const handle of pendingAnimationFrames) clearImmediate(handle); - pendingAnimationFrames.clear(); - vi.unstubAllGlobals(); -}); - -describe("editable file language readiness", () => { - it.each(["hydrate", "renderFile"] as const)( - "%s prepares the inferred language before the first edit of a worker-highlighted file", - async (method) => { - const file = { name: "cold.tsx", contents: source, cacheKey: "cold-tsx" }; - await pool.primeFileHighlightCache(file); - expect(pool.getFileResultCache(file)).toBeDefined(); - const mainHighlighter = await getSharedHighlighter({ - themes: ["pierre-dark"], - langs: ["text"], - }); - expect(mainHighlighter.getLoadedLanguages()).not.toContain("tsx"); - renderer[method](file); - // Read-only worker rendering must not load editor grammars on the main thread. - expect(mainHighlighter.getLoadedLanguages()).not.toContain("tsx"); - const highlighter = await renderer.initializeHighlighter(); - firstEnter(highlighter, file, "tsx"); - }, - ); - - it.each(["hydrate", "renderFile"] as const)( - "%s respects an explicit language when the filename suggests plain text", - async (method) => { - const file: FileContents = { - name: "source.txt", - lang: "tsx", - contents: source, - cacheKey: "explicit-tsx", - }; - await pool.primeFileHighlightCache(file); - renderer[method](file); - firstEnter(await renderer.initializeHighlighter(), file, "tsx"); - }, - ); - - it("loads a newly opened language after reusing a worker-backed renderer", async () => { - const previousFile: FileContents = { - name: "previous.ts", - contents: "export const value = 1;", - cacheKey: "previous-ts", - }; - await getSharedHighlighter({ themes: ["pierre-dark"], langs: ["typescript"] }); - renderer.renderFile(previousFile); - firstEnter(await renderer.initializeHighlighter(), previousFile, "typescript"); - const nextFile = { name: "next.tsx", contents: source, cacheKey: "next-tsx" }; - renderer.renderFile(nextFile); - firstEnter(await renderer.initializeHighlighter(), nextFile, "tsx"); - }); - - it("prepares a hydrated non-worker file even when its theme was already loaded", async () => { - renderer.cleanUp(); - renderer = new FileRenderer(options); - const file = { name: "local.tsx", contents: source, cacheKey: "local-tsx" }; - renderer.hydrate(file); - firstEnter(await renderer.initializeHighlighter(), file, "tsx"); - }); - - it("keeps plain text editable without loading an unrelated grammar", async () => { - const file = { name: "notes.txt", contents: "Plain text", cacheKey: "plain-text" }; - renderer.renderFile(file); - const highlighter = await renderer.initializeHighlighter(); - firstEnter(highlighter, file, "text"); - expect(highlighter.getLoadedLanguages()).not.toContain("tsx"); - }); -}); diff --git a/apps/web/src/components/files/fileEditorVirtualization.test.ts b/apps/web/src/components/files/fileEditorVirtualization.test.ts index cf293dd47254..111ff291a7a8 100644 --- a/apps/web/src/components/files/fileEditorVirtualization.test.ts +++ b/apps/web/src/components/files/fileEditorVirtualization.test.ts @@ -4,7 +4,7 @@ import { Virtualizer, type FileContents, } from "@pierre/diffs"; -import { Editor, TextDocument } from "@pierre/diffs/editor"; +import { Editor, TextDocument } from "@pierre/diffs/edit"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const renderingManagerUrl = new URL( @@ -22,10 +22,21 @@ class MeasuredElement { children: MeasuredElement[] = []; dataset: Record = {}; nextElementSibling: MeasuredElement | null = null; + shadowRoot: MeasuredElement | null = null; width = 283; constructor(readonly height = 0) {} + attachShadow() { + this.shadowRoot ??= new MeasuredElement(); + return this.shadowRoot; + } + + appendChild(child: MeasuredElement) { + this.children.push(child); + return child; + } + getBoundingClientRect() { MeasuredElement.geometryReads += 1; return { top: 0, height: this.height, width: this.width }; @@ -108,8 +119,10 @@ class LayoutVirtualizer extends Virtualizer { class MeasuredFile extends VirtualizedFile { override top = 0; + // Rows are measured by hand here; the virtualizer only needs to reconcile them. + override onRender = () => false; - override attachEditor(editor: Parameters[0]) { + override __attachEditor(editor: Parameters[0]) { this.editor = editor; return () => { this.editor = undefined; @@ -117,7 +130,9 @@ class MeasuredFile extends VirtualizedFile { } async initialize(file: FileContents) { - this.prepareCodeViewItem(file, 0); + this.updateCodeViewLayout(file, 0); + // Document changes require the session an attached editor installs. + (this as unknown as { installEditSession(file: FileContents): void }).installEditSession(file); await this.fileRenderer.initializeHighlighter(); expect( this.fileRenderer.renderFile(file, { @@ -180,7 +195,7 @@ class MeasuredFile extends VirtualizedFile { } const instances: MeasuredFile[] = []; -const editors: Editor[] = []; +const editors: Editor<"file", undefined, undefined>[] = []; beforeAll(async () => { await getSharedHighlighter({ @@ -225,7 +240,7 @@ async function makeFixture( cacheKey: `wrapped:${overflow}`, lang: "text", }; - const document = new TextDocument(file.name, contents, "text"); + const document = new TextDocument<"file", undefined>(file.name, contents, "text"); const instance = new MeasuredFile( { overflow, @@ -274,7 +289,7 @@ describe("wrapped editor document changes", () => { const before = instance.getLinePosition(previousLastLine); expect(before).toEqual({ top: 120328, height: 60 }); const viewport = { top: before!.top - 100, bottom: before!.top + 80 }; - expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2156 }); + expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2096 }); append(); @@ -282,7 +297,7 @@ describe("wrapped editor document changes", () => { expect(instance.getLinePosition(previousLastLine)).toEqual({ top: before!.top, height: 20 }); expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120348, height: 20 }); expect(instance.getVirtualizedHeight()).toBe(120376); - expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2136 }); + expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2096 }); }); it("invalidates changed and shifted rows after an insertion in the middle", async () => { @@ -373,7 +388,7 @@ describe("wrapped editor document changes", () => { const { instance, file, append } = await makeFixture(); append(); instance.setMetrics({ hunkLineCount: 50, lineHeight: 24, diffHeaderHeight: 44, spacing: 8 }); - instance.prepareCodeViewItem(file, 0); + instance.updateCodeViewLayout(file, 0); expect(instance.getLinePosition(6001)).toEqual({ top: 144008, height: 24 }); }); @@ -381,7 +396,7 @@ describe("wrapped editor document changes", () => { const { instance, file, append } = await makeFixture(); append(); instance.setLineAnnotations([{ lineNumber: 10, metadata: undefined }]); - instance.prepareCodeViewItem(file, 0); + instance.updateCodeViewLayout(file, 0); expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 }); }); }); @@ -476,11 +491,11 @@ describe("wrapped measurement widths", () => { "document", Object.assign(new EditorElement(), { createElement: () => new EditorElement() }), ); - const first = new Editor(); + const first = new Editor("file"); editors.push(first); first.edit(instance); first.cleanUp(); - const second = new Editor(); + const second = new Editor("file"); editors.push(second); second.edit(instance); instance.resizeContent(482.25); @@ -531,12 +546,16 @@ class EditorElement extends MeasuredElement { style: Record = {}; parentElement: EditorElement | null = null; - appendChild(child: EditorElement) { + override appendChild(child: EditorElement) { child.parentElement = this; this.children.push(child); return child; } + append(child: EditorElement) { + this.appendChild(child); + } + prepend(child: EditorElement) { child.parentElement = this; this.children.unshift(child); @@ -630,14 +649,15 @@ async function makeEditorFixture(lineCount: number) { langs: ["text"], preferredHighlighter: "shiki-wasm", }); - const editor = new Editor(); + const editor = new Editor("file"); editors.push(editor); editor.edit(instance); - editor.__syncRenderView(highlighter, measuredElement(host), file, undefined, { - startingLine: 0, - totalLines: 1, - bufferBefore: 0, - bufferAfter: 0, + editor.__syncRenderView({ + highlighter, + fileContainer: measuredElement(host), + file, + lineAnnotations: undefined, + renderRange: { startingLine: 0, totalLines: 1, bufferBefore: 0, bufferAfter: 0 }, }); const append = (count: number) => { const lines = editor.getText().split("\n"); diff --git a/apps/web/src/components/files/projectFilesQueryState.ts b/apps/web/src/components/files/projectFilesQueryState.ts index 0b3ca2040e88..ad33483ecd53 100644 --- a/apps/web/src/components/files/projectFilesQueryState.ts +++ b/apps/web/src/components/files/projectFilesQueryState.ts @@ -89,6 +89,18 @@ export function getOptimisticProjectFileQueryData( return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null; } +/** The contents the Files panel shows, read outside React so it is current within a frame. */ +export function getProjectFileContents( + environmentId: EnvironmentId, + cwd: string, + relativePath: string, +): string | undefined { + const optimistic = getOptimisticProjectFileQueryData(environmentId, cwd, relativePath); + if (optimistic) return optimistic.contents; + const result = appAtomRegistry.get(getProjectFileQueryAtom(environmentId, cwd, relativePath)); + return Option.getOrUndefined(AsyncResult.value(result))?.contents; +} + export function confirmProjectFileQueryData( environmentId: EnvironmentId, cwd: string, diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index ae8a2b2d344e..e1c6f1de0453 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -254,7 +254,9 @@ function PullRequestCodeTab({ readonly slices: ReadonlyArray; }>({ key: "", cursor: null, slices: NO_SLICES }); const parseCache = useRef(new Map()); - const [viewer, setViewer] = useState | null>(null); + const [viewer, setViewer] = useState | null>( + null, + ); const referenceKey = pullRequestReviewKey(reference); const commit = selectedCommitOid; diff --git a/apps/web/src/lib/diffRendering.test.ts b/apps/web/src/lib/diffRendering.test.ts index 7b8857f32a12..20114f6ef1b5 100644 --- a/apps/web/src/lib/diffRendering.test.ts +++ b/apps/web/src/lib/diffRendering.test.ts @@ -402,6 +402,10 @@ describe("a file whose name a patch header cannot carry plainly", () => { expect(pathOf(quotedPatch("line\\nfile.txt"))).toBe("line\nfile.txt"); }); + it("keeps a backslash the name really has", () => { + expect(pathOf(quotedPatch("back\\\\slash.txt"))).toBe("back\\slash.txt"); + }); + it("reads the octal a host with core.quotePath on writes for a name outside ASCII", () => { expect(pathOf(quotedPatch("caf\\303\\251/r\\303\\251sum\\303\\251.ts"))).toBe("café/résumé.ts"); }); diff --git a/apps/web/src/lib/diffRendering.ts b/apps/web/src/lib/diffRendering.ts index c99349a7dafd..dcc6bb2b79d7 100644 --- a/apps/web/src/lib/diffRendering.ts +++ b/apps/web/src/lib/diffRendering.ts @@ -1,7 +1,6 @@ import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; import { parseDiffFromFile } from "@pierre/diffs"; import type { FileDiffMetadata } from "@pierre/diffs/types"; -import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; const DIFF_THEME_NAMES = { light: "pierre-light", @@ -201,17 +200,11 @@ export function getRenderablePatch( } /** - * What the patch called the file, as the file's own name. Git writes a name holding a tab, a - * newline, a quote or a backslash quoted and escaped, and the parser hands one of those back still - * escaped. A viewed mark, a review comment and a file's contents are all asked for by this path, - * and the host knows the file only under the name it really has. + * The file's own name. Git quotes and escapes a name holding a tab, a newline, a quote or a + * backslash, and the parser decodes it, so this is the name the host knows the file by. */ -function fileDiffPath(raw: string): string { - return unquoteGitPatchPath(raw); -} - export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { - return fileDiffPath(fileDiff.name ?? fileDiff.prevName ?? ""); + return fileDiff.name ?? fileDiff.prevName ?? ""; } /** @@ -219,7 +212,7 @@ export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { * path, and the hosts that resolve a diff position against both sides need both names. */ export function resolveFileDiffPreviousPath(fileDiff: FileDiffMetadata): string { - return fileDiffPath(fileDiff.prevName ?? fileDiff.name ?? ""); + return fileDiff.prevName ?? fileDiff.name ?? ""; } /** diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index fd60c4f2a397..cf343e26a4fc 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -190,7 +190,7 @@ export default defineConfig(() => { "@clerk/clerk-js", "@clerk/react/internal", "@pierre/diffs", - "@pierre/diffs/editor", + "@pierre/diffs/edit", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js", "effect/Array", diff --git a/patches/@pierre%2Fdiffs@1.3.0-beta.10.patch b/patches/@pierre%2Fdiffs@1.3.0-beta.10.patch deleted file mode 100644 index 5cf80feb3356..000000000000 --- a/patches/@pierre%2Fdiffs@1.3.0-beta.10.patch +++ /dev/null @@ -1,238 +0,0 @@ -diff --git a/dist/components/VirtualizedFile.d.ts b/dist/components/VirtualizedFile.d.ts ---- a/dist/components/VirtualizedFile.d.ts -+++ b/dist/components/VirtualizedFile.d.ts -@@ -42,7 +42,7 @@ declare class VirtualizedFile extends File { - private computeApproximateSize; - setVisibility(visible: boolean): void; - rerender(): void; -- applyDocumentChange(textDocument: DiffsTextDocument, newLineAnnotations?: LineAnnotation[], shouldUpdateBuffer?: boolean): void; -+ applyDocumentChange(textDocument: DiffsTextDocument, newLineAnnotations?: LineAnnotation[], shouldUpdateBuffer?: boolean, startLine?: number): void; - protected renderPreparedFile({ - fileContainer, - file, -diff --git a/dist/components/VirtualizedFile.js b/dist/components/VirtualizedFile.js ---- a/dist/components/VirtualizedFile.js -+++ b/dist/components/VirtualizedFile.js -@@ -20,6 +20,7 @@ - cache = { - heights: /* @__PURE__ */ new Map(), - checkpoints: [], -+ codeWidth: void 0, - fileAnnotationHeight: 0 - }; - isVisible = false; -@@ -31,6 +32,8 @@ - super(options, workerManager, isContainerManaged); - this.virtualizer = virtualizer; - this.metrics = metrics; -+ const simpleVirtualizer = this.getSimpleVirtualizer(); -+ if (simpleVirtualizer != null) this.resizeManager.onResize = () => simpleVirtualizer.requestHeightReconcile(this); - } - setMetrics(metrics, force = false) { - if (!force && areObjectsEqual(this.metrics, metrics)) return; -@@ -70,10 +73,12 @@ - if (this.isAdvancedMode()) throw new Error("VirtualizedFile.setThemeType cannot be used inside CodeView. Update CodeView options instead."); - super.setThemeType(themeType); - } -- resetLayoutCache(recompute = false, resetRenderRange = true) { -+ resetLayoutCache(recompute = false, resetRenderRange = true, startLine = 0) { - this.layoutDirty = true; -- this.cache.fileAnnotationHeight = 0; -- if (this.cache.heights.size > 0) this.cache.heights.clear(); -+ if (startLine === 0) this.cache.fileAnnotationHeight = 0; -+ // Dropping unchanged wrapped rows moves the viewport before they can be remeasured. -+ if (startLine === 0) this.cache.heights.clear(); -+ else for (const lineIndex of this.cache.heights.keys()) if (lineIndex >= startLine) this.cache.heights.delete(lineIndex); - if (this.cache.checkpoints.length > 0) this.cache.checkpoints.length = 0; - if (this.renderRange != null && resetRenderRange) this.renderRange = void 0; - if (recompute && this.isSimpleMode()) this.computeApproximateSize(); -@@ -91,6 +96,13 @@ - if (this.code == null) return hasHeightChange; - const content = this.code.children[1]; - if (!(content instanceof HTMLElement)) return hasHeightChange; -+ const codeWidth = this.code.getBoundingClientRect().width; -+ if (!(codeWidth > 0)) return hasHeightChange; -+ if (this.cache.codeWidth != null && this.cache.codeWidth !== codeWidth) { -+ this.resetLayoutCache(false, false); -+ hasHeightChange = true; -+ } -+ this.cache.codeWidth = codeWidth; - const hasFileAnnotations = includesFileAnnotations(this.lineAnnotations); - if (this.renderRange != null && hasFileAnnotations && shouldRenderFileAnnotations(this.renderRange)) { - const nextFileAnnotationHeight = measureFileAnnotationHeight(content) ?? 0; -@@ -287,11 +299,11 @@ - this.forceRenderOverride = true; - this.virtualizer.instanceChanged(this, false); - } -- applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false) { -+ applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false, startLine = 0) { - const previousRenderRange = this.renderRange; - super.applyDocumentChange(textDocument, newLineAnnotations); - this.getSimpleVirtualizer()?.markDOMDirty(); -- this.resetLayoutCache(this.isSimpleMode(), false); -+ this.resetLayoutCache(this.isSimpleMode(), false, startLine); - if (shouldUpdateBuffer && previousRenderRange !== void 0 && this.file !== void 0) { - const windowSpecs = this.virtualizer.getWindowSpecs(); - const renderRange = this.computeRenderRangeFromWindow(this.file, this.top ?? 0, windowSpecs); -diff --git a/dist/editor/editor.js b/dist/editor/editor.js -index ff78e2a..f9df318 100644 ---- a/dist/editor/editor.js -+++ b/dist/editor/editor.js -@@ -146,14 +146,11 @@ var Editor = class { - const file = fileInstance.__getCurrentFile?.(); - if (file !== void 0) requirePersistedCacheKey(file); - } -- const { useTokenTransformer, enableGutterUtility, enableLineSelection, lineHoverHighlight = "disabled", ...rest } = fileInstance.options; -- if (useTokenTransformer !== true || enableGutterUtility === true || enableLineSelection === true || lineHoverHighlight !== "disabled") { -+ const { useTokenTransformer, ...rest } = fileInstance.options; -+ if (useTokenTransformer !== true) { - fileInstance.setOptions({ - ...rest, -- useTokenTransformer: true, -- enableGutterUtility: false, -- enableLineSelection: false, -- lineHoverHighlight: "disabled" -+ useTokenTransformer: true - }); - fileInstance.rerender(); - } -@@ -908,6 +905,7 @@ var Editor = class { - return lineNumber - 1; - }; - this.#editorEventDisposes.push(addEventListener(gutterEl, "pointerdown", (e) => { -+ if (this.#fileInstance?.options.enableLineSelection === true) return; - const gutterRow = resolveGutterTarget(e.composedPath()[0]); - if (gutterRow?.dataset.lineType === "change-deletion") { - const code = gutterRow.closest("[data-code]"); -@@ -1522,6 +1520,12 @@ var Editor = class { - if (gutterEl !== void 0) gutterEl.style.gridRow = "span " + gridRow; - } - fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange, didLineCountChange); -+ if (fileInstance.file !== void 0) fileInstance.file.contents = textDocument.getText(); -- if (didLineCountChange) fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer); -+ if (didLineCountChange) { -+ const previousLineCount = change.lineCount - change.lineDelta; -+ // A wider or narrower line-number gutter can rewrap unchanged rows. -+ const layoutStartLine = String(previousLineCount).length === String(change.lineCount).length ? change.startLine : 0; -+ fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer, layoutStartLine); -+ } - if (this.#isDiff && (this.#diffSyle === "unified" || didLineCountChange)) this.#resetCache(); - if (newLineAnnotations !== void 0) { -@@ -1788,6 +1787,7 @@ var Editor = class { - } - } - #setSelectedLinesSafe(range, lineNumberOnly = false) { -+ if (this.#fileInstance?.options.controlledSelection === true) return; - try { - this.#fileInstance?.setSelectedLines(range, { - notify: false, -diff --git a/dist/managers/ResizeManager.d.ts b/dist/managers/ResizeManager.d.ts ---- a/dist/managers/ResizeManager.d.ts -+++ b/dist/managers/ResizeManager.d.ts -@@ -5,6 +5,8 @@ - columnVariables?: ResizeManagerColumnVariableMode; - } - declare class ResizeManager { -+ /** Schedule owner measurement after an observed code or gutter size change. */ -+ onResize?: () => void; - private static resizeObserver; - private static managersByElement; - private static getResizeObserver; -diff --git a/dist/managers/ResizeManager.js b/dist/managers/ResizeManager.js ---- a/dist/managers/ResizeManager.js -+++ b/dist/managers/ResizeManager.js -@@ -19,6 +19,7 @@ - for (const [manager, managerEntries] of entriesByManager) manager.handleResizeEntries(managerEntries); - } - observedNodes = /* @__PURE__ */ new Map(); -+ onResize; - setup(pre, { disableAnnotations, columnVariables = "apply" }) { - const annotationUpdates = /* @__PURE__ */ new Set(); - const applyColumnVariables = columnVariables === "apply"; -@@ -212,6 +213,7 @@ - this.applyAnnotationUpdates(annotationUpdates); - annotationUpdates.clear(); - this.applyColumnUpdates(codeUpdates); -+ if (codeUpdates.size > 0) this.onResize?.(); - codeUpdates.clear(); - } - applyAnnotationUpdates(annotationUpdates) { -diff --git a/dist/react/utils/useFileInstance.js b/dist/react/utils/useFileInstance.js -index e9f62f5..af82a46 100644 ---- a/dist/react/utils/useFileInstance.js -+++ b/dist/react/utils/useFileInstance.js -@@ -91,10 +91,7 @@ function mergeFileOptions({ options, controlledSelection, contentEditable, hasCu - }; - if (needsEditorOptions) merged = { - ...merged, -- useTokenTransformer: true, -- enableGutterUtility: false, -- enableLineSelection: false, -- lineHoverHighlight: "disabled" -+ useTokenTransformer: true - }; - return merged; - } -diff --git a/dist/renderers/FileRenderer.js b/dist/renderers/FileRenderer.js ---- a/dist/renderers/FileRenderer.js -+++ b/dist/renderers/FileRenderer.js -@@ -107,10 +107,10 @@ - result: massiveFile ? void 0 : cache?.result, - renderRange: void 0 - }; -+ this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); - if (this.workerManager?.isWorkingPool() === true) { - if (this.renderCache.result == null && !massiveFile) this.workerManager.highlightFileAST(this, file); - } else if (this.highlighter == null) { -- this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); - this.initializeHighlighter(); - } - } -@@ -163,6 +163,8 @@ - if (this.renderCache == null) return; - const { file, result } = this.renderCache; - if (result == null) return; -+ this.workerManager?.cleanUpTasks(this); -+ if (file.cacheKey != null) this.workerManager?.evictFileFromCache(file.cacheKey); - const lineCache = this.lineCache != null && isLineCacheForFile(this.lineCache, file) ? this.lineCache : void 0; - for (const [line, tokens] of dirtyLines) { - if (lineCache != null && line < lineCache.lines.length) { -@@ -268,6 +270,7 @@ - const forcePlainText = !hasContent || isFilePlainText(file) || isFileMassive(lines.length, this.getTokenizeMaxLength()); - const newContent = !areFilesEqual(file, this.renderCache.file); - const newRenderRange = !areRenderRangesEqual(this.renderCache.renderRange, renderRange); -+ this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); - if (this.workerManager?.isWorkingPool() === true) { - if (forcePlainText || this.renderCache.result == null || !this.renderCache.highlighted && (newContent || newRenderRange)) { - this.renderCache.file = file; -@@ -278,7 +281,6 @@ - } - if (!forcePlainText && hasContent && (!this.renderCache.highlighted || forceHighlight)) this.workerManager.highlightFileAST(this, file); - } else { -- this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); - const hasThemes = this.highlighter != null && areThemesAttached(options.theme); - const hasLangs = this.highlighter != null && areLanguagesAttached(this.computedLang); - const canHighlight = !forcePlainText && hasLangs; -diff --git a/package.json b/package.json -index ff61c90..1e170e5 100644 ---- a/package.json -+++ b/package.json -@@ -55,6 +55,18 @@ - "./worker/worker-portable.js": { - "types": "./dist/worker/worker-portable.d.ts", - "import": "./dist/worker/worker-portable.js" -+ }, -+ "./types": { -+ "types": "./dist/types.d.ts", -+ "import": "./dist/types.js" -+ }, -+ "./utils/getFiletypeFromFileName": { -+ "types": "./dist/utils/getFiletypeFromFileName.d.ts", -+ "import": "./dist/utils/getFiletypeFromFileName.js" -+ }, -+ "./utils/parsePatchFiles": { -+ "types": "./dist/utils/parsePatchFiles.d.ts", -+ "import": "./dist/utils/parsePatchFiles.js" - } - }, - "publishConfig": { diff --git a/patches/@pierre%2Fdiffs@1.5.2.patch b/patches/@pierre%2Fdiffs@1.5.2.patch new file mode 100644 index 000000000000..f1984ea41835 --- /dev/null +++ b/patches/@pierre%2Fdiffs@1.5.2.patch @@ -0,0 +1,146 @@ +diff --git a/dist/components/VirtualizedFile.d.ts b/dist/components/VirtualizedFile.d.ts +--- a/dist/components/VirtualizedFile.d.ts ++++ b/dist/components/VirtualizedFile.d.ts +@@ -49,7 +49,7 @@ + setVisibility(visible: boolean): void; + rerender(): void; + syncGhostTextRows(): void; +- applyDocumentChange(textDocument: TextDocument<'file', LAnnotation>, newLineAnnotations?: LineAnnotation[], shouldUpdateBuffer?: boolean): void; ++ applyDocumentChange(textDocument: TextDocument<'file', LAnnotation>, newLineAnnotations?: LineAnnotation[], shouldUpdateBuffer?: boolean, startLine?: number): void; + render({ fileContainer, file, forceRender, lineAnnotations, ...props }: FileRenderProps): boolean; + protected finalizeRender(): void; + private updatePendingRender; +diff --git a/dist/components/VirtualizedFile.js b/dist/components/VirtualizedFile.js +--- a/dist/components/VirtualizedFile.js ++++ b/dist/components/VirtualizedFile.js +@@ -23,6 +23,7 @@ + heights: /* @__PURE__ */ new Map(), + checkpoints: [], + fileAnnotationHeight: 0, ++ codeWidth: void 0, + ghostTextRows: NO_GHOST_TEXT_ROWS + }; + pendingRender; +@@ -35,6 +36,8 @@ + super(options, workerManager, isContainerManaged); + this.virtualizer = virtualizer; + this.metrics = metrics; ++ const simpleVirtualizer = this.getSimpleVirtualizer(); ++ if (simpleVirtualizer != null) this.resizeManager.onResize = () => simpleVirtualizer.requestHeightReconcile(this); + } + setMetrics(metrics, force = false) { + const nextMetrics = computeVirtualFileMetrics(metrics); +@@ -86,10 +89,12 @@ + if (this.isAdvancedMode()) throw new Error("VirtualizedFile.setThemeType cannot be used inside CodeView. Update CodeView options instead."); + super.setThemeType(themeType); + } +- resetLayoutCache(recompute = false, resetRenderRange = true) { ++ resetLayoutCache(recompute = false, resetRenderRange = true, startLine = 0) { + this.layoutDirty = true; +- this.cache.fileAnnotationHeight = 0; +- if (this.cache.heights.size > 0) this.cache.heights.clear(); ++ if (startLine === 0) this.cache.fileAnnotationHeight = 0; ++ // Dropping unchanged wrapped rows moves the viewport before they can be remeasured. ++ if (startLine === 0) this.cache.heights.clear(); ++ else for (const lineIndex of this.cache.heights.keys()) if (lineIndex >= startLine) this.cache.heights.delete(lineIndex); + if (this.cache.checkpoints.length > 0) this.cache.checkpoints.length = 0; + this.cache.ghostTextRows = NO_GHOST_TEXT_ROWS; + if (this.renderRange != null && resetRenderRange) this.renderRange = void 0; +@@ -135,6 +140,13 @@ + if (this.code == null) return hasHeightChange; + const content = this.code.children[1]; + if (!(content instanceof HTMLElement)) return hasHeightChange; ++ const codeWidth = this.code.getBoundingClientRect().width; ++ if (!(codeWidth > 0)) return hasHeightChange; ++ if (this.cache.codeWidth != null && this.cache.codeWidth !== codeWidth) { ++ this.resetLayoutCache(false, false); ++ hasHeightChange = true; ++ } ++ this.cache.codeWidth = codeWidth; + if (this.renderRange != null && shouldRenderFileAnnotations(this.renderRange)) { + const nextFileAnnotationHeight = measureFileAnnotationHeight(content) ?? 0; + if (nextFileAnnotationHeight !== this.cache.fileAnnotationHeight) { +@@ -358,12 +370,12 @@ + codeView.instanceChanged(this, true); + } else this.getSimpleVirtualizer()?.requestHeightReconcile(this); + } +- applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false) { ++ applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false, startLine = 0) { + const { renderRange: previousRenderRange } = this; + this.getAdvancedVirtualizer()?.capturePendingLayoutAnchor(); + super.applyDocumentChange(textDocument, newLineAnnotations); + this.getSimpleVirtualizer()?.markDOMDirty(); +- this.resetLayoutCache(this.isSimpleMode(), false); ++ this.resetLayoutCache(this.isSimpleMode(), false, startLine); + const file = this.getRenderedFile(); + if (!this.isSimpleMode()) this.computeApproximateSize(true); + else if (shouldUpdateBuffer && previousRenderRange != null && file != null) { +diff --git a/dist/editor/editor.js b/dist/editor/editor.js +--- a/dist/editor/editor.js ++++ b/dist/editor/editor.js +@@ -1899,7 +1899,11 @@ + changedDocumentLines: this.#isDiff && !didLineCountChange ? getChangedDocumentLines(textDocument, change) : void 0, + documentLineCount: textDocument.lineCount + }); +- if (didLineCountChange) applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer); ++ if (didLineCountChange) { ++ // A wider or narrower line-number gutter can rewrap unchanged rows. ++ const layoutStartLine = String(change.previousLineCount).length === String(change.lineCount).length ? change.startLine : 0; ++ applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer, layoutStartLine); ++ } + if (didLineCountChange || this.#isDiff && this.#diffSyle === "unified") this.#resetCache(); + if (newLineAnnotations != null) { + this.#lineAnnotations = newLineAnnotations; +diff --git a/dist/managers/ResizeManager.d.ts b/dist/managers/ResizeManager.d.ts +--- a/dist/managers/ResizeManager.d.ts ++++ b/dist/managers/ResizeManager.d.ts +@@ -5,6 +5,8 @@ + columnVariables?: ResizeManagerColumnVariableMode; + } + declare class ResizeManager { ++ /** Schedule owner measurement after an observed code or gutter size change. */ ++ onResize?: () => void; + private static resizeObserver; + private static managersByElement; + private static getResizeObserver; +diff --git a/dist/managers/ResizeManager.js b/dist/managers/ResizeManager.js +--- a/dist/managers/ResizeManager.js ++++ b/dist/managers/ResizeManager.js +@@ -19,6 +19,7 @@ + for (const [manager, managerEntries] of entriesByManager) manager.handleResizeEntries(managerEntries); + } + observedNodes = /* @__PURE__ */ new Map(); ++ onResize; + setup(pre, { disableAnnotations, columnVariables = "apply" }) { + const annotationUpdates = /* @__PURE__ */ new Set(); + const applyColumnVariables = columnVariables === "apply"; +@@ -212,6 +213,7 @@ + this.applyAnnotationUpdates(annotationUpdates); + annotationUpdates.clear(); + this.applyColumnUpdates(codeUpdates); ++ if (codeUpdates.size > 0) this.onResize?.(); + codeUpdates.clear(); + } + applyAnnotationUpdates(annotationUpdates) { +diff --git a/package.json b/package.json +--- a/package.json ++++ b/package.json +@@ -58,6 +58,18 @@ + "./worker/worker-portable.js": { + "types": "./dist/worker/worker-portable.d.ts", + "import": "./dist/worker/worker-portable.js" ++ }, ++ "./types": { ++ "types": "./dist/types.d.ts", ++ "import": "./dist/types.js" ++ }, ++ "./utils/getFiletypeFromFileName": { ++ "types": "./dist/utils/getFiletypeFromFileName.d.ts", ++ "import": "./dist/utils/getFiletypeFromFileName.js" ++ }, ++ "./utils/parsePatchFiles": { ++ "types": "./dist/utils/parsePatchFiles.d.ts", ++ "import": "./dist/utils/parsePatchFiles.js" + } + }, + "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff5ba44f197d..17a19d1ffbeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ catalogs: specifier: 1.8.0 version: 1.8.0 '@pierre/diffs': - specifier: 1.3.0-beta.10 - version: 1.3.0-beta.10 + specifier: 1.5.2 + version: 1.5.2 jose: specifier: 6.2.2 version: 6.2.2 @@ -103,7 +103,7 @@ patchedDependencies: '@opencode/client@2.0.23': f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd '@opencode/protocol@2.0.23': 17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685 '@opencode/schema@2.0.23': 650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59 - '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 + '@pierre/diffs@1.5.2': ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d '@react-native-menu/menu@2.0.0': a4866ecfcd44f318043de839d20110c17e14909004900f002ffe12050376f2ba '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 @@ -277,7 +277,7 @@ importers: version: 1.9.1 '@pierre/diffs': specifier: 'catalog:' - version: 1.3.0-beta.10(patch_hash=0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 1.5.2(patch_hash=ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@react-native-ai/apple': specifier: 0.12.0 version: 0.12.0(patch_hash=2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d)(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6)) @@ -502,8 +502,8 @@ importers: specifier: 4.0.1 version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@pierre/trees': - specifier: 1.0.0-beta.4 - version: 1.0.0-beta.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + specifier: 1.0.0-beta.6 + version: 1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0) '@types/react': specifier: ~19.3.0 version: 19.3.0 @@ -666,10 +666,10 @@ importers: version: 1.8.0 '@pierre/diffs': specifier: 'catalog:' - version: 1.3.0-beta.10(patch_hash=0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.5.2(patch_hash=ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@pierre/trees': - specifier: 1.0.0-beta.4 - version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 1.0.0-beta.6 + version: 1.0.0-beta.6(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) '@t3tools/client-runtime': specifier: workspace:* version: link:../../packages/client-runtime @@ -4444,18 +4444,27 @@ packages: resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} engines: {node: '>=14.18.0'} - '@pierre/diffs@1.3.0-beta.10': - resolution: {integrity: sha512-efyFM9GRfI6WkmHJP0CnZBopuM8yCwGqIKbZHoe1D5PV15VDkr7Vpi8EZt40AYrN1km//utQtYhHDSwt2KwjSg==} + '@pierre/diffs@1.5.2': + resolution: {integrity: sha512-QVxIWQEnNoko22k63maERzozd29+FtaUNpRrZiw603WMe5R1mFSED+jMtPkcVP3SDYS1DWg5K7sa7sJbr3xkNQ==} peerDependencies: react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true '@pierre/theme@1.1.0': resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} engines: {vscode: ^1.0.0} - '@pierre/theming@0.0.2': - resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==} + '@pierre/theme@2.0.0': + resolution: {integrity: sha512-yNDd9GYLQl1mEUJR8AneJ5e4ohLIHQd/wZLWr4fagt78vS2RwwZNW530vVgHqXFAyFVcFlRmGUD5ramXH46OXw==} + engines: {vscode: ^1.0.0} + + '@pierre/theming@1.0.0': + resolution: {integrity: sha512-WsdrnhKfjeyXGDikZmN9pkpeZ5S/cl6EE72feiSc0tlynT1tMYqXqouhuv/foK+PY9OEnebOAVRQn3+rAstR8g==} peerDependencies: '@pierre/theme': ^1.1.0 '@shikijs/themes': ^3.0.0 || ^4.0.0 @@ -4474,8 +4483,28 @@ packages: shiki: optional: true - '@pierre/trees@1.0.0-beta.4': - resolution: {integrity: sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag==} + '@pierre/theming@1.0.1': + resolution: {integrity: sha512-WCI5Qd7iprDpISL9fBYOLe8RV53+b7mFNA3bPzl60/2CKCSrsKN8zEcep6Y3BAzvARlmca50zGjDodqPGiTUKA==} + peerDependencies: + '@pierre/theme': ^1.1.0 || ^2.0.0 + '@shikijs/themes': ^3.0.0 || ^4.0.0 + react: ^18.3.1 || ^19.0.0 + react-dom: ^18.3.1 || ^19.0.0 + shiki: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@pierre/theme': + optional: true + '@shikijs/themes': + optional: true + react: + optional: true + react-dom: + optional: true + shiki: + optional: true + + '@pierre/trees@1.0.0-beta.6': + resolution: {integrity: sha512-zxeuSFM9TveM7b5XofweJALCtm/tGYV9HZzdbf7Uf+kBxIlUyz24/EHaGRjB0dsmmfDQl2ETz7AWwJ15lhSnpw==} peerDependencies: react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 @@ -14869,65 +14898,96 @@ snapshots: tslib: 2.8.1 webcrypto-core: 1.9.2 - '@pierre/diffs@1.3.0-beta.10(patch_hash=0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@pierre/diffs@1.5.2(patch_hash=ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@pierre/theme': 1.1.0 - '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0) + '@pierre/theme': 2.0.0 + '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0) '@shikijs/transformers': 4.2.0 diff: 9.0.0 hast-util-to-html: 9.0.5 lru_map: 0.4.1 + shiki: 4.2.0 + optionalDependencies: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - shiki: 4.2.0 transitivePeerDependencies: - '@shikijs/themes' - '@pierre/diffs@1.3.0-beta.10(patch_hash=0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@pierre/diffs@1.5.2(patch_hash=ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@pierre/theme': 1.1.0 - '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) + '@pierre/theme': 2.0.0 + '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) '@shikijs/transformers': 4.2.0 diff: 9.0.0 hast-util-to-html: 9.0.5 lru_map: 0.4.1 + shiki: 4.2.0 + optionalDependencies: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - shiki: 4.2.0 transitivePeerDependencies: - '@shikijs/themes' - '@pierre/theme@1.1.0': {} + '@pierre/theme@1.1.0': + optional: true + + '@pierre/theme@2.0.0': {} - '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0)': + '@pierre/theming@1.0.0(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': optionalDependencies: '@pierre/theme': 1.1.0 + '@shikijs/themes': 4.3.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + shiki: 4.2.0 + + '@pierre/theming@1.0.0(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0)': + optionalDependencies: + '@pierre/theme': 2.0.0 '@shikijs/themes': 4.2.0 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) shiki: 4.2.0 - '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': + '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0)': optionalDependencies: - '@pierre/theme': 1.1.0 + '@pierre/theme': 2.0.0 + '@shikijs/themes': 4.2.0 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + shiki: 4.2.0 + + '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': + optionalDependencies: + '@pierre/theme': 2.0.0 '@shikijs/themes': 4.3.0 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) shiki: 4.2.0 - '@pierre/trees@1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@pierre/trees@1.0.0-beta.6(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': dependencies: + '@pierre/theming': 1.0.0(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) preact: 11.0.0-beta.0 preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@pierre/theme' + - '@shikijs/themes' + - shiki - '@pierre/trees@1.0.0-beta.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@pierre/trees@1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0)': dependencies: + '@pierre/theming': 1.0.0(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0) preact: 11.0.0-beta.0 preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) react: 19.3.0 react-dom: 19.3.0(react@19.3.0) + transitivePeerDependencies: + - '@pierre/theme' + - '@shikijs/themes' + - shiki '@polka/url@1.0.0-next.29': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0e88eb115e7c..92373445f5ed 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -41,7 +41,7 @@ catalog: "@legendapp/list": 3.3.5 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 - "@pierre/diffs": 1.3.0-beta.10 + "@pierre/diffs": 1.5.2 "@tailwindcss/node": 4.3.3 "@tailwindcss/oxide": 4.3.3 "@tailwindcss/vite": 4.3.3 @@ -270,7 +270,9 @@ patchedDependencies: "@opencode/client@2.0.23": patches/@opencode__client@2.0.23.patch "@opencode/protocol@2.0.23": patches/@opencode__protocol@2.0.23.patch "@opencode/schema@2.0.23": patches/@opencode__schema@2.0.23.patch - "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch + # Exports the dependency-light subpaths mobile and the web diff parser import, and keeps + # measured wrapped-row heights above an edit (fileEditorVirtualization.test.ts). + "@pierre/diffs@1.5.2": patches/@pierre%2Fdiffs@1.5.2.patch "@react-native-ai/apple@0.12.0": patches/@react-native-ai__apple@0.12.0.patch "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch