| author | |
| committer | |
| log | f8ed2a039b7deaa324beda29b44cc74c846311f7 |
| tree | 0cf2e65a08e52357bed3325526f9435b9b24d4de |
| parent | d021f57bf3a500e419e800c3eb957568bed8f713 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>22 files changed, 505 insertions(+), 951 deletions(-)
apps/mobile/package.json+1-1| ... | ... | @@ -131,7 +131,7 @@ |
| 131 | 131 | }, |
| 132 | 132 | "devDependencies": { |
| 133 | 133 | "@effect/vitest": "catalog:", |
| 134 | "@pierre/trees": "1.0.0-beta.4", | |
| 134 | "@pierre/trees": "1.0.0-beta.6", | |
| 135 | 135 | "@types/react": "~19.3.0", |
| 136 | 136 | "@types/react-dom": "~19.2.3", |
| 137 | 137 | "babel-preset-expo": "~58.0.8", |
apps/mobile/src/features/review/reviewModel.ts+2-3| ... | ... | @@ -2,7 +2,6 @@ import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; |
| 2 | 2 | import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types"; |
| 3 | 3 | import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; |
| 4 | 4 | import type { ReviewDiffPreviewSource } from "@t3tools/contracts"; |
| 5 | import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; | |
| 6 | 5 | import * as Arr from "effect/Array"; |
| 7 | 6 | import { pipe } from "effect/Function"; |
| 8 | 7 | import * as Order from "effect/Order"; |
| ... | ... | @@ -374,8 +373,8 @@ function buildRenderableRows(file: FileDiffMetadata): ReadonlyArray<ReviewRender |
| 374 | 373 | } |
| 375 | 374 | |
| 376 | 375 | function mapRenderableFile(file: FileDiffMetadata): ReviewRenderableFile { |
| 377 | const path = unquoteGitPatchPath(file.name || file.prevName || ""); | |
| 378 | const previousPath = file.prevName ? unquoteGitPatchPath(file.prevName) : null; | |
| 376 | const path = file.name || file.prevName || ""; | |
| 377 | const previousPath = file.prevName || null; | |
| 379 | 378 | const additions = file.hunks.reduce((total, hunk) => total + hunk.additionLines, 0); |
| 380 | 379 | const deletions = file.hunks.reduce((total, hunk) => total + hunk.deletionLines, 0); |
| 381 | 380 | const cacheKey = file.cacheKey ?? `${previousPath ?? "none"}:${path}:${file.type}`; |
apps/web/package.json+1-1| ... | ... | @@ -25,7 +25,7 @@ |
| 25 | 25 | "@legendapp/list": "catalog:", |
| 26 | 26 | "@noble/hashes": "catalog:", |
| 27 | 27 | "@pierre/diffs": "catalog:", |
| 28 | "@pierre/trees": "1.0.0-beta.4", | |
| 28 | "@pierre/trees": "1.0.0-beta.6", | |
| 29 | 29 | "@t3tools/client-runtime": "workspace:*", |
| 30 | 30 | "@t3tools/contracts": "workspace:*", |
| 31 | 31 | "@t3tools/shared": "workspace:*", |
apps/web/src/components/diffs/AnnotatableCodeView.tsx+1-1| ... | ... | @@ -35,7 +35,7 @@ interface DiffCommentAnnotationGroup { |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | 37 | type DiffCommentLineAnnotation = DiffLineAnnotation<DiffCommentAnnotationGroup>; |
| 38 | export type AnnotatableCodeViewHandle = CodeViewHandle<DiffCommentAnnotationGroup>; | |
| 38 | export type AnnotatableCodeViewHandle = CodeViewHandle<DiffCommentAnnotationGroup, undefined>; | |
| 39 | 39 | const EMPTY_REVIEW_COMMENTS: ReadonlyArray<ReviewCommentContext> = []; |
| 40 | 40 | |
| 41 | 41 | function annotationSide(range: SelectedLineRange): AnnotationSide { |
apps/web/src/components/diffs/StyledDiffCodeView.test.tsx+5-2| ... | ... | @@ -98,7 +98,7 @@ vi.mock("@pierre/diffs/worker/worker.js?worker", async () => { |
| 98 | 98 | |
| 99 | 99 | vi.mock("@pierre/diffs/react", async (importOriginal) => ({ |
| 100 | 100 | ...(await importOriginal<typeof import("@pierre/diffs/react")>()), |
| 101 | CodeView: (props: CodeViewProps) => { | |
| 101 | CodeView: (props: CodeViewProps<undefined, undefined>) => { | |
| 102 | 102 | return props.items?.map((item) => |
| 103 | 103 | item.type === "file" ? <FileOutput key={item.id} file={item.file} /> : null, |
| 104 | 104 | ); |
| ... | ... | @@ -115,6 +115,7 @@ function FileOutput({ file }: { file: FileContents }) { |
| 115 | 115 | testState.renderPools.push(pool); |
| 116 | 116 | const renderer = new FileRenderer( |
| 117 | 117 | { theme: "pierre-dark", preferredHighlighter: "shiki-wasm" }, |
| 118 | undefined, | |
| 118 | 119 | render, |
| 119 | 120 | pool, |
| 120 | 121 | ); |
| ... | ... | @@ -301,8 +302,10 @@ describe("code-view worker lifecycle", () => { |
| 301 | 302 | expect(pool.getStats().totalWorkers).toBe(2); |
| 302 | 303 | expect(testState.terminations).toHaveLength(0); |
| 303 | 304 | await act(async () => renderer!.update(renderViews(0))); |
| 305 | // Pierre gives up on a held startup after 10 seconds; the idle timer then tears the pool down. | |
| 306 | const timedOut = expect(pending).rejects.toThrow("worker initialization timed out"); | |
| 304 | 307 | await vi.advanceTimersByTimeAsync(30_000); |
| 305 | await pending; | |
| 308 | await timedOut; | |
| 306 | 309 | await Promise.all(testState.terminations); |
| 307 | 310 | await act(async () => { |
| 308 | 311 | for (const deliver of testState.heldResponses) deliver(); |
apps/web/src/components/diffs/StyledDiffCodeView.tsx+4-4| ... | ... | @@ -261,16 +261,16 @@ const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS} |
| 261 | 261 | `; |
| 262 | 262 | |
| 263 | 263 | export type StyledDiffCodeViewOptions<LAnnotation> = Omit< |
| 264 | NonNullable<CodeViewProps<LAnnotation>["options"]>, | |
| 264 | NonNullable<CodeViewProps<LAnnotation, undefined>["options"]>, | |
| 265 | 265 | "unsafeCSS" | "itemMetrics" | "layout" |
| 266 | 266 | >; |
| 267 | 267 | |
| 268 | 268 | type StyledDiffCodeViewProps<LAnnotation> = ( |
| 269 | | Omit<ControlledCodeViewProps<LAnnotation>, "options"> | |
| 270 | | Omit<UncontrolledCodeViewProps<LAnnotation>, "options"> | |
| 269 | | Omit<ControlledCodeViewProps<LAnnotation, undefined>, "options"> | |
| 270 | | Omit<UncontrolledCodeViewProps<LAnnotation, undefined>, "options"> | |
| 271 | 271 | ) & { |
| 272 | 272 | readonly options?: StyledDiffCodeViewOptions<LAnnotation>; |
| 273 | readonly viewerRef?: Ref<CodeViewHandle<LAnnotation>>; | |
| 273 | readonly viewerRef?: Ref<CodeViewHandle<LAnnotation, undefined>>; | |
| 274 | 274 | /** |
| 275 | 275 | * Appended to the shared stylesheet inside the viewer's shadow root, for a surface that has |
| 276 | 276 | * to restyle chrome the viewer owns — such as replacing its per-file line counts. |
apps/web/src/components/files/FilePreviewPanel.tsx+188-98| ... | ... | @@ -11,9 +11,23 @@ import { |
| 11 | 11 | isWorkspaceImagePreviewPath, |
| 12 | 12 | isWorkspaceVideoPreviewPath, |
| 13 | 13 | } from "@t3tools/shared/filePreview"; |
| 14 | import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; | |
| 15 | import { Editor } from "@pierre/diffs/editor"; | |
| 16 | import { EditProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; | |
| 14 | import { | |
| 15 | DEFAULT_TOKENIZE_MAX_LENGTH, | |
| 16 | VirtualizedFile, | |
| 17 | getFiletypeFromFileName, | |
| 18 | type File as FileInstance, | |
| 19 | type FileContents, | |
| 20 | type PostRenderPhase, | |
| 21 | type SelectedLineRange, | |
| 22 | } from "@pierre/diffs"; | |
| 23 | import { | |
| 24 | Editor, | |
| 25 | type EditorChangeEvent, | |
| 26 | type EditorFactory, | |
| 27 | type EditorOptions, | |
| 28 | } from "@pierre/diffs/edit"; | |
| 29 | import type { WorkerPoolManager } from "@pierre/diffs/worker"; | |
| 30 | import { EditProvider, File, Virtualizer, useWorkerPool } from "@pierre/diffs/react"; | |
| 17 | 31 | import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; |
| 18 | 32 | import { |
| 19 | 33 | isAtomCommandInterrupted, |
| ... | ... | @@ -80,7 +94,7 @@ import { |
| 80 | 94 | import SourceFilePreview from "./ReadOnlySourcePreview"; |
| 81 | 95 | import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; |
| 82 | 96 | import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; |
| 83 | import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; | |
| 97 | import { projectFileCacheKey } from "./fileContentRevision"; | |
| 84 | 98 | import { |
| 85 | 99 | filePreviewReadErrorMessage, |
| 86 | 100 | isMarkdownPreviewFile, |
| ... | ... | @@ -91,6 +105,7 @@ import { |
| 91 | 105 | import { useFileSaveCoordinator } from "./useFileSaveCoordinator"; |
| 92 | 106 | import { |
| 93 | 107 | getOptimisticProjectFileQueryData, |
| 108 | getProjectFileContents, | |
| 94 | 109 | setProjectFileQueryData, |
| 95 | 110 | useProjectFileQuery, |
| 96 | 111 | } from "./projectFilesQueryState"; |
| ... | ... | @@ -117,7 +132,12 @@ const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; |
| 117 | 132 | const RENDER_MARKDOWN_STORAGE_KEY = "t3code.renderMarkdown"; |
| 118 | 133 | const RENDER_BROWSER_FILE_STORAGE_KEY = "t3code.renderBrowserFile"; |
| 119 | 134 | const RENDER_TABLE_STORAGE_KEY = "t3code.renderTable"; |
| 120 | type FilePostRender = NonNullable<FileOptions<unknown>["onPostRender"]>; | |
| 135 | // Shared by the read-only and annotated surfaces, so it is generic over annotation metadata. | |
| 136 | type FilePostRender = <LAnnotation>( | |
| 137 | fileContainer: HTMLElement, | |
| 138 | instance: FileInstance<LAnnotation, undefined>, | |
| 139 | phase: PostRenderPhase, | |
| 140 | ) => void; | |
| 121 | 141 | |
| 122 | 142 | function WorkspaceImagePreview(props: { |
| 123 | 143 | readonly environmentId: EnvironmentId; |
| ... | ... | @@ -547,6 +567,67 @@ function useFileLineReveal( |
| 547 | 567 | ); |
| 548 | 568 | } |
| 549 | 569 | |
| 570 | const createFileEditor: EditorFactory<FileCommentAnnotationGroup, undefined> = ( | |
| 571 | editorType, | |
| 572 | options, | |
| 573 | editStateKey, | |
| 574 | ) => new Editor(editorType, options, editStateKey); | |
| 575 | ||
| 576 | function editableFileContents( | |
| 577 | environmentId: EnvironmentId, | |
| 578 | cwd: string, | |
| 579 | relativePath: string, | |
| 580 | contents: string, | |
| 581 | ): FileContents { | |
| 582 | return { | |
| 583 | name: relativePath, | |
| 584 | contents, | |
| 585 | cacheKey: `editor:${environmentId}:${projectFileCacheKey(cwd, relativePath, contents)}`, | |
| 586 | }; | |
| 587 | } | |
| 588 | ||
| 589 | function needsWorkerHighlight(workerPool: WorkerPoolManager | undefined, file: FileContents) { | |
| 590 | if (workerPool?.isWorkingPool() !== true) return false; | |
| 591 | if ((file.lang ?? getFiletypeFromFileName(file.name)) === "text") return false; | |
| 592 | let lines = 1; | |
| 593 | for ( | |
| 594 | let index = file.contents.indexOf("\n"); | |
| 595 | index !== -1; | |
| 596 | index = file.contents.indexOf("\n", index + 1) | |
| 597 | ) { | |
| 598 | lines += 1; | |
| 599 | } | |
| 600 | return lines <= DEFAULT_TOKENIZE_MAX_LENGTH; | |
| 601 | } | |
| 602 | ||
| 603 | /** | |
| 604 | * Pierre highlights an active edit session on the main thread, so each version | |
| 605 | * of the file becomes editable only once it has rendered the worker's | |
| 606 | * highlight. A failed worker highlight falls back to main-thread highlighting. | |
| 607 | */ | |
| 608 | function useEditableAfterHighlight(file: FileContents) { | |
| 609 | const workerPool = useWorkerPool(); | |
| 610 | const [highlightedFile, setHighlightedFile] = useState<FileContents | null>(null); | |
| 611 | const needsHighlight = useMemo(() => needsWorkerHighlight(workerPool, file), [file, workerPool]); | |
| 612 | const ready = !needsHighlight || highlightedFile === file; | |
| 613 | ||
| 614 | useEffect(() => { | |
| 615 | if (ready || workerPool === undefined) return; | |
| 616 | workerPool.primeFileHighlightCache(file).catch(() => setHighlightedFile(file)); | |
| 617 | }, [file, ready, workerPool]); | |
| 618 | ||
| 619 | const onPostRender = useCallback( | |
| 620 | (renderedFile: FileContents | undefined, phase: PostRenderPhase) => { | |
| 621 | if (ready || phase === "unmount" || renderedFile?.cacheKey !== file.cacheKey) return; | |
| 622 | // The pool caches a result just before the instance renders it, so a | |
| 623 | // render that sees the cache has painted highlighted rows. | |
| 624 | if (workerPool?.getFileResultCache(file) !== undefined) setHighlightedFile(file); | |
| 625 | }, | |
| 626 | [file, ready, workerPool], | |
| 627 | ); | |
| 628 | return { ready, onPostRender }; | |
| 629 | } | |
| 630 | ||
| 550 | 631 | interface EditableFileSurfaceProps { |
| 551 | 632 | environmentId: EnvironmentId; |
| 552 | 633 | cwd: string; |
| ... | ... | @@ -597,46 +678,65 @@ function EditableFileSurface({ |
| 597 | 678 | relativePath, |
| 598 | 679 | onPendingChange, |
| 599 | 680 | }); |
| 600 | const editor = useMemo( | |
| 601 | () => | |
| 602 | new Editor<FileCommentAnnotationGroup>({ | |
| 603 | persistState: true, | |
| 604 | persistStateStorage: "inMemory", | |
| 605 | onChange: (file, nextLineAnnotations) => { | |
| 606 | setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); | |
| 607 | saveCoordinator.change(file.contents); | |
| 608 | if (nextLineAnnotations) { | |
| 609 | const remapped = remapFileCommentAnnotations( | |
| 610 | nextLineAnnotations as FileCommentLineAnnotation[], | |
| 611 | ); | |
| 612 | setLineAnnotations(remapped); | |
| 613 | for (const annotation of remapped) { | |
| 614 | for (const entry of annotation.metadata.entries) { | |
| 615 | if (entry.kind !== "comment") continue; | |
| 616 | addReviewComment( | |
| 617 | composerDraftTarget, | |
| 618 | buildFileReviewComment({ | |
| 619 | id: entry.id, | |
| 620 | filePath: relativePath, | |
| 621 | startLine: entry.startLine, | |
| 622 | endLine: entry.endLine, | |
| 623 | text: entry.text, | |
| 624 | contents: file.contents, | |
| 625 | }), | |
| 626 | ); | |
| 627 | } | |
| 628 | } | |
| 629 | } | |
| 630 | }, | |
| 631 | }), | |
| 632 | [addReviewComment, composerDraftTarget, cwd, environmentId, relativePath, saveCoordinator], | |
| 681 | // The editor owns the draft, so its own edits echoing back through the file | |
| 682 | // query keep the file identity. Only a change from elsewhere replaces it. | |
| 683 | const [externalFile, setExternalFile] = useState(() => | |
| 684 | editableFileContents(environmentId, cwd, relativePath, contents), | |
| 685 | ); | |
| 686 | const [editedContents, setEditedContents] = useState<string | null>(null); | |
| 687 | if (contents !== (editedContents ?? externalFile.contents)) { | |
| 688 | setExternalFile(editableFileContents(environmentId, cwd, relativePath, contents)); | |
| 689 | setEditedContents(null); | |
| 690 | } | |
| 691 | const { ready: editable, onPostRender: onEditablePostRender } = | |
| 692 | useEditableAfterHighlight(externalFile); | |
| 693 | const editorRef = useRef<Editor<"file", FileCommentAnnotationGroup, undefined> | null>(null); | |
| 694 | const editorOptions = useMemo<EditorOptions<"file", FileCommentAnnotationGroup, undefined>>( | |
| 695 | () => ({ | |
| 696 | onAttach: (editor) => { | |
| 697 | editorRef.current = editor; | |
| 698 | }, | |
| 699 | onComplete: () => { | |
| 700 | editorRef.current = null; | |
| 701 | }, | |
| 702 | }), | |
| 703 | [], | |
| 633 | 704 | ); |
| 634 | 705 | |
| 635 | useEffect( | |
| 636 | () => () => { | |
| 637 | editor.cleanUp(); | |
| 706 | // Mirrors the draft out to the save queue, the optimistic file query and the | |
| 707 | // composer's review comments. | |
| 708 | const handleEditChange = useCallback( | |
| 709 | ({ | |
| 710 | file, | |
| 711 | lineAnnotations: nextLineAnnotations, | |
| 712 | }: EditorChangeEvent<"file", FileCommentAnnotationGroup, undefined>) => { | |
| 713 | // Adopting an external change reports it as an edit; it is already on disk. | |
| 714 | if (file.contents === getProjectFileContents(environmentId, cwd, relativePath)) return; | |
| 715 | setEditedContents(file.contents); | |
| 716 | setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); | |
| 717 | saveCoordinator.change(file.contents); | |
| 718 | if (!nextLineAnnotations) return; | |
| 719 | const remapped = remapFileCommentAnnotations(nextLineAnnotations); | |
| 720 | // The editor hands back the array it was given until an edit moves an annotation. | |
| 721 | setLineAnnotations((current) => (current === nextLineAnnotations ? current : remapped)); | |
| 722 | for (const annotation of remapped) { | |
| 723 | for (const entry of annotation.metadata.entries) { | |
| 724 | if (entry.kind !== "comment") continue; | |
| 725 | addReviewComment( | |
| 726 | composerDraftTarget, | |
| 727 | buildFileReviewComment({ | |
| 728 | id: entry.id, | |
| 729 | filePath: relativePath, | |
| 730 | startLine: entry.startLine, | |
| 731 | endLine: entry.endLine, | |
| 732 | text: entry.text, | |
| 733 | contents: file.contents, | |
| 734 | }), | |
| 735 | ); | |
| 736 | } | |
| 737 | } | |
| 638 | 738 | }, |
| 639 | [editor], | |
| 739 | [addReviewComment, composerDraftTarget, cwd, environmentId, relativePath, saveCoordinator], | |
| 640 | 740 | ); |
| 641 | 741 | |
| 642 | 742 | const removeAnnotationEntry = useCallback( |
| ... | ... | @@ -695,47 +795,44 @@ function EditableFileSurface({ |
| 695 | 795 | ], |
| 696 | 796 | ); |
| 697 | 797 | |
| 698 | const beginComment = useCallback( | |
| 699 | (range: SelectedLineRange) => { | |
| 700 | editor.setSelections([]); | |
| 701 | editor.blur(); | |
| 702 | const { startLine, endLine } = normalizeFileCommentRange(range); | |
| 703 | const draftEntry: FileCommentAnnotationEntry = { | |
| 704 | id: nextFileCommentId(), | |
| 705 | kind: "draft", | |
| 706 | startLine, | |
| 707 | endLine, | |
| 708 | text: "", | |
| 709 | }; | |
| 710 | setLineAnnotations((current) => { | |
| 711 | const withoutDraft = current.flatMap((annotation) => { | |
| 712 | const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); | |
| 713 | return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; | |
| 714 | }); | |
| 715 | const existingIndex = withoutDraft.findIndex( | |
| 716 | (annotation) => annotation.lineNumber === endLine, | |
| 717 | ); | |
| 718 | if (existingIndex < 0) { | |
| 719 | return [ | |
| 720 | ...withoutDraft, | |
| 721 | { | |
| 722 | lineNumber: endLine, | |
| 723 | metadata: { entries: [draftEntry] }, | |
| 724 | }, | |
| 725 | ]; | |
| 726 | } | |
| 727 | return withoutDraft.map((annotation, index) => | |
| 728 | index === existingIndex | |
| 729 | ? { | |
| 730 | ...annotation, | |
| 731 | metadata: { entries: [...annotation.metadata.entries, draftEntry] }, | |
| 732 | } | |
| 733 | : annotation, | |
| 734 | ); | |
| 798 | const beginComment = useCallback((range: SelectedLineRange) => { | |
| 799 | editorRef.current?.setSelections([]); | |
| 800 | editorRef.current?.blur(); | |
| 801 | const { startLine, endLine } = normalizeFileCommentRange(range); | |
| 802 | const draftEntry: FileCommentAnnotationEntry = { | |
| 803 | id: nextFileCommentId(), | |
| 804 | kind: "draft", | |
| 805 | startLine, | |
| 806 | endLine, | |
| 807 | text: "", | |
| 808 | }; | |
| 809 | setLineAnnotations((current) => { | |
| 810 | const withoutDraft = current.flatMap((annotation) => { | |
| 811 | const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); | |
| 812 | return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; | |
| 735 | 813 | }); |
| 736 | }, | |
| 737 | [editor], | |
| 738 | ); | |
| 814 | const existingIndex = withoutDraft.findIndex( | |
| 815 | (annotation) => annotation.lineNumber === endLine, | |
| 816 | ); | |
| 817 | if (existingIndex < 0) { | |
| 818 | return [ | |
| 819 | ...withoutDraft, | |
| 820 | { | |
| 821 | lineNumber: endLine, | |
| 822 | metadata: { entries: [draftEntry] }, | |
| 823 | }, | |
| 824 | ]; | |
| 825 | } | |
| 826 | return withoutDraft.map((annotation, index) => | |
| 827 | index === existingIndex | |
| 828 | ? { | |
| 829 | ...annotation, | |
| 830 | metadata: { entries: [...annotation.metadata.entries, draftEntry] }, | |
| 831 | } | |
| 832 | : annotation, | |
| 833 | ); | |
| 834 | }); | |
| 835 | }, []); | |
| 739 | 836 | const hasOpenCommentForm = lineAnnotations.some((annotation) => |
| 740 | 837 | annotation.metadata.entries.some((entry) => entry.kind === "draft"), |
| 741 | 838 | ); |
| ... | ... | @@ -744,11 +841,11 @@ function EditableFileSurface({ |
| 744 | 841 | if (!root) return; |
| 745 | 842 | return installFileEditorDismissal({ |
| 746 | 843 | root, |
| 747 | editor, | |
| 844 | editor: { setSelections: (selections) => editorRef.current?.setSelections(selections) }, | |
| 748 | 845 | isBlocked: () => hasOpenCommentForm, |
| 749 | 846 | onDismiss: () => setSelectedRange(null), |
| 750 | 847 | }); |
| 751 | }, [editor, hasOpenCommentForm, setSelectedRange]); | |
| 848 | }, [hasOpenCommentForm, setSelectedRange]); | |
| 752 | 849 | const handleLineSelectionEnd = useCallback( |
| 753 | 850 | (range: SelectedLineRange | null) => { |
| 754 | 851 | setSelectedRange(range); |
| ... | ... | @@ -762,6 +859,7 @@ function EditableFileSurface({ |
| 762 | 859 | const handlePostRender = useCallback<FilePostRender>( |
| 763 | 860 | (fileContainer, instance, phase) => { |
| 764 | 861 | onPostRender(fileContainer, instance, phase); |
| 862 | onEditablePostRender(instance.file, phase); | |
| 765 | 863 | |
| 766 | 864 | if (selectionFrameRef.current !== null) { |
| 767 | 865 | cancelAnimationFrame(selectionFrameRef.current); |
| ... | ... | @@ -775,11 +873,11 @@ function EditableFileSurface({ |
| 775 | 873 | instance.setSelectedLines(selectedRange, { notify: false }); |
| 776 | 874 | }); |
| 777 | 875 | }, |
| 778 | [onPostRender, selectedRange], | |
| 876 | [onEditablePostRender, onPostRender, selectedRange], | |
| 779 | 877 | ); |
| 780 | 878 | |
| 781 | 879 | return ( |
| 782 | <EditProvider editor={editor}> | |
| 880 | <EditProvider createEditor={createFileEditor}> | |
| 783 | 881 | <div ref={surfaceRef} className="flex min-h-0 flex-1"> |
| 784 | 882 | <Virtualizer |
| 785 | 883 | className="file-preview-virtualizer min-h-0 flex-1 overflow-auto" |
| ... | ... | @@ -789,17 +887,10 @@ function EditableFileSurface({ |
| 789 | 887 | }} |
| 790 | 888 | > |
| 791 | 889 | <File<FileCommentAnnotationGroup> |
| 792 | file={{ | |
| 793 | name: relativePath, | |
| 794 | contents, | |
| 795 | cacheKey: projectFileEditorCacheKey( | |
| 796 | environmentId, | |
| 797 | cwd, | |
| 798 | relativePath, | |
| 799 | contents, | |
| 800 | editor.getFile(), | |
| 801 | ), | |
| 802 | }} | |
| 890 | file={externalFile} | |
| 891 | edit={editable} | |
| 892 | editorOptions={editorOptions} | |
| 893 | onEditChange={handleEditChange} | |
| 803 | 894 | options={{ |
| 804 | 895 | disableFileHeader: true, |
| 805 | 896 | enableGutterUtility: !hasOpenCommentForm, |
| ... | ... | @@ -832,7 +923,6 @@ function EditableFileSurface({ |
| 832 | 923 | </div> |
| 833 | 924 | )} |
| 834 | 925 | className="min-h-full" |
| 835 | contentEditable | |
| 836 | 926 | /> |
| 837 | 927 | </Virtualizer> |
| 838 | 928 | </div> |
apps/web/src/components/files/ReadOnlySourcePreview.tsx+1-1| ... | ... | @@ -17,7 +17,7 @@ export default function ReadOnlySourcePreview(props: { |
| 17 | 17 | readonly name: string; |
| 18 | 18 | readonly text: string; |
| 19 | 19 | readonly cacheKey?: string; |
| 20 | readonly onPostRender?: FileOptions<unknown>["onPostRender"]; | |
| 20 | readonly onPostRender?: FileOptions<unknown, undefined>["onPostRender"]; | |
| 21 | 21 | }) { |
| 22 | 22 | const { resolvedTheme } = useTheme(); |
| 23 | 23 | const wordWrap = useClientSettings((settings) => settings.wordWrap); |
apps/web/src/components/files/fileContentRevision.test.ts+1-24| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; | |
| 3 | import { projectFileCacheKey } from "./fileContentRevision"; | |
| 4 | 4 | |
| 5 | 5 | describe("file cache identity", () => { |
| 6 | 6 | it("changes for same-length edits", () => { |
| ... | ... | @@ -8,27 +8,4 @@ describe("file cache identity", () => { |
| 8 | 8 | projectFileCacheKey("/repo", "file.json", "nodeVeasdrs"), |
| 9 | 9 | ); |
| 10 | 10 | }); |
| 11 | ||
| 12 | it("keeps editor identity stable for locally edited contents", () => { | |
| 13 | const cacheKey = projectFileEditorCacheKey("local", "/repo", "file.json", "after", undefined); | |
| 14 | ||
| 15 | expect( | |
| 16 | projectFileEditorCacheKey("local", "/repo", "file.json", "after edit", { | |
| 17 | cacheKey, | |
| 18 | contents: "after edit", | |
| 19 | }), | |
| 20 | ).toBe(cacheKey); | |
| 21 | }); | |
| 22 | ||
| 23 | it("rotates editor identity for external contents and environments", () => { | |
| 24 | const cacheKey = projectFileEditorCacheKey("local", "/repo", "file.json", "before", undefined); | |
| 25 | const editorFile = { cacheKey, contents: "before" }; | |
| 26 | ||
| 27 | expect( | |
| 28 | projectFileEditorCacheKey("local", "/repo", "file.json", "external edit", editorFile), | |
| 29 | ).not.toBe(cacheKey); | |
| 30 | expect(projectFileEditorCacheKey("remote", "/repo", "file.json", "before", undefined)).not.toBe( | |
| 31 | cacheKey, | |
| 32 | ); | |
| 33 | }); | |
| 34 | 11 | }); |
apps/web/src/components/files/fileContentRevision.ts-18| ... | ... | @@ -10,21 +10,3 @@ function fileContentRevision(contents: string): string { |
| 10 | 10 | export function projectFileCacheKey(cwd: string, relativePath: string, contents: string): string { |
| 11 | 11 | return `${cwd}:${relativePath}:${fileContentRevision(contents)}`; |
| 12 | 12 | } |
| 13 | ||
| 14 | interface EditorFileIdentity { | |
| 15 | readonly cacheKey?: string; | |
| 16 | readonly contents: string; | |
| 17 | } | |
| 18 | ||
| 19 | export function projectFileEditorCacheKey( | |
| 20 | environmentId: string, | |
| 21 | cwd: string, | |
| 22 | relativePath: string, | |
| 23 | contents: string, | |
| 24 | editorFile: EditorFileIdentity | undefined, | |
| 25 | ): string { | |
| 26 | if (editorFile?.contents === contents && editorFile.cacheKey) { | |
| 27 | return editorFile.cacheKey; | |
| 28 | } | |
| 29 | return `editor:${environmentId}:${projectFileCacheKey(cwd, relativePath, contents)}`; | |
| 30 | } |
apps/web/src/components/files/fileEditorHighlight.test.ts deleted-299| ... | ... | @@ -1,299 +0,0 @@ |
| 1 | import { | |
| 2 | FileRenderer, | |
| 3 | getSharedHighlighter, | |
| 4 | type BaseCodeOptions, | |
| 5 | type DiffsHighlighter, | |
| 6 | type FileContents, | |
| 7 | type HighlightedToken, | |
| 8 | type RenderRange, | |
| 9 | } from "@pierre/diffs"; | |
| 10 | import { TextDocument } from "@pierre/diffs/editor"; | |
| 11 | import { WorkerPoolManager, type WorkerRequest, type WorkerResponse } from "@pierre/diffs/worker"; | |
| 12 | import * as NodeWorkerThreads from "node:worker_threads"; | |
| 13 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 14 | ||
| 15 | type DocumentChange = NonNullable<ReturnType<TextDocument<unknown>["applyEdits"]>>; | |
| 16 | interface Tokenizer { | |
| 17 | readonly themeType: "light" | "dark"; | |
| 18 | tokenize(change: DocumentChange, range: RenderRange): Map<number, HighlightedToken[]>; | |
| 19 | cleanUp(): void; | |
| 20 | } | |
| 21 | ||
| 22 | // This dependency-internal tokenizer is the one used by Editor.#rerender. | |
| 23 | const tokenizerUrl = new URL("./editor/tokenizer.js", import.meta.resolve("@pierre/diffs")); | |
| 24 | const { EditorTokenizer } = (await import(/* @vite-ignore */ tokenizerUrl.href)) as { | |
| 25 | EditorTokenizer: new (options: { | |
| 26 | codeOptions: BaseCodeOptions; | |
| 27 | highlighter: DiffsHighlighter; | |
| 28 | textDocument: TextDocument<unknown>; | |
| 29 | setStyle: (style: string) => void; | |
| 30 | onDeferTokenize: (lines: Map<number, HighlightedToken[]>, theme: "light" | "dark") => void; | |
| 31 | }) => Tokenizer; | |
| 32 | }; | |
| 33 | ||
| 34 | const workerModule = import.meta.resolve("@pierre/diffs/worker/worker.js"); | |
| 35 | const source = Array.from( | |
| 36 | { length: 7_000 }, | |
| 37 | (_, index) => | |
| 38 | `export const section${index} = <p>Long wrapped source line ${index} for the file editor.</p>;`, | |
| 39 | ).join("\n"); | |
| 40 | const options = { | |
| 41 | theme: "pierre-dark", | |
| 42 | themeType: "dark", | |
| 43 | preferredHighlighter: "shiki-wasm", | |
| 44 | useTokenTransformer: true, | |
| 45 | overflow: "wrap", | |
| 46 | disableFileHeader: true, | |
| 47 | } as const; | |
| 48 | const range: RenderRange = { | |
| 49 | startingLine: 6_950, | |
| 50 | totalLines: 150, | |
| 51 | bufferBefore: 0, | |
| 52 | bufferAfter: 0, | |
| 53 | }; | |
| 54 | ||
| 55 | interface HeldResponse { | |
| 56 | data: WorkerResponse; | |
| 57 | deliver: () => void; | |
| 58 | } | |
| 59 | let responses: HeldResponse[]; | |
| 60 | let responseWaiters: ((response: HeldResponse) => void)[]; | |
| 61 | let terminationPromises: Promise<number>[]; | |
| 62 | let pool: WorkerPoolManager; | |
| 63 | let renderer: FileRenderer; | |
| 64 | let tokenizer: Tokenizer; | |
| 65 | let file: FileContents; | |
| 66 | let document: TextDocument<unknown>; | |
| 67 | const animationFrames = new Set<ReturnType<typeof setImmediate>>(); | |
| 68 | ||
| 69 | function nextResponse(): Promise<HeldResponse> { | |
| 70 | const response = responses.shift(); | |
| 71 | return response | |
| 72 | ? Promise.resolve(response) | |
| 73 | : new Promise((resolve) => responseWaiters.push(resolve)); | |
| 74 | } | |
| 75 | ||
| 76 | class WorkerTransport { | |
| 77 | private readonly worker = new NodeWorkerThreads.Worker( | |
| 78 | `const { parentPort, workerData } = require("node:worker_threads"); | |
| 79 | globalThis.self = { | |
| 80 | addEventListener(type, listener) { | |
| 81 | if (type === "message") parentPort.on("message", data => listener({ data })); | |
| 82 | if (type === "error") process.on("uncaughtException", listener); | |
| 83 | } | |
| 84 | }; | |
| 85 | globalThis.postMessage = data => parentPort.postMessage(data); | |
| 86 | import(workerData.moduleUrl);`, | |
| 87 | { eval: true, workerData: { moduleUrl: workerModule }, execArgv: [] }, | |
| 88 | ); | |
| 89 | ||
| 90 | addEventListener( | |
| 91 | type: "message" | "error", | |
| 92 | listener: (event: { data: WorkerResponse } | Error) => void, | |
| 93 | ) { | |
| 94 | if (type === "error") { | |
| 95 | this.worker.on("error", listener); | |
| 96 | return; | |
| 97 | } | |
| 98 | this.worker.on("message", (data: WorkerResponse) => { | |
| 99 | const response = { data, deliver: () => listener({ data }) }; | |
| 100 | if (data.type !== "success" || data.requestType !== "file") { | |
| 101 | response.deliver(); | |
| 102 | return; | |
| 103 | } | |
| 104 | const waiter = responseWaiters.shift(); | |
| 105 | if (waiter) waiter(response); | |
| 106 | else responses.push(response); | |
| 107 | }); | |
| 108 | } | |
| 109 | ||
| 110 | postMessage(message: WorkerRequest) { | |
| 111 | this.worker.postMessage(message, []); | |
| 112 | } | |
| 113 | ||
| 114 | terminate() { | |
| 115 | terminationPromises.push(this.worker.terminate()); | |
| 116 | } | |
| 117 | } | |
| 118 | ||
| 119 | function applyChange(change: DocumentChange) { | |
| 120 | // Keep the installed editor's non-DOM order, including the existing contents patch. | |
| 121 | renderer.updateRenderCache(tokenizer.tokenize(change, range), tokenizer.themeType); | |
| 122 | file.contents = document.getText(); | |
| 123 | if (change.lineDelta !== 0) renderer.applyDocumentChange(document); | |
| 124 | } | |
| 125 | ||
| 126 | function append(text: string) { | |
| 127 | const position = document.positionAt(document.getText().length); | |
| 128 | const change = document.applyEdits([ | |
| 129 | { range: { start: position, end: position }, newText: text }, | |
| 130 | ]); | |
| 131 | expect(change).toBeDefined(); | |
| 132 | applyChange(change!); | |
| 133 | } | |
| 134 | ||
| 135 | function undo() { | |
| 136 | const change = document.undo()?.[0]; | |
| 137 | expect(change).toBeDefined(); | |
| 138 | applyChange(change!); | |
| 139 | } | |
| 140 | ||
| 141 | function renderContents() { | |
| 142 | const result = renderer.renderFile(file, range); | |
| 143 | expect(result?.totalLines).toBe(document.lineCount); | |
| 144 | return renderer.renderFullHTML(result!); | |
| 145 | } | |
| 146 | ||
| 147 | beforeEach(async () => { | |
| 148 | responses = []; | |
| 149 | responseWaiters = []; | |
| 150 | terminationPromises = []; | |
| 151 | vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { | |
| 152 | const frame = setImmediate(() => { | |
| 153 | animationFrames.delete(frame); | |
| 154 | callback(0); | |
| 155 | }); | |
| 156 | animationFrames.add(frame); | |
| 157 | return frame; | |
| 158 | }); | |
| 159 | vi.stubGlobal("cancelAnimationFrame", (frame: ReturnType<typeof setImmediate>) => { | |
| 160 | animationFrames.delete(frame); | |
| 161 | clearImmediate(frame); | |
| 162 | }); | |
| 163 | vi.stubGlobal("window", { matchMedia: () => ({ matches: true }) }); | |
| 164 | pool = new WorkerPoolManager( | |
| 165 | // Adapt browser transport only; Pierre's real worker produces each response. | |
| 166 | { workerFactory: () => new WorkerTransport() as unknown as globalThis.Worker, poolSize: 1 }, | |
| 167 | options, | |
| 168 | ); | |
| 169 | await pool.initialize(["tsx"]); | |
| 170 | const highlighter = await getSharedHighlighter({ | |
| 171 | themes: ["pierre-dark"], | |
| 172 | langs: ["tsx"], | |
| 173 | preferredHighlighter: "shiki-wasm", | |
| 174 | }); | |
| 175 | file = { name: "wrapped.tsx", contents: source, cacheKey: "editable-file" }; | |
| 176 | document = new TextDocument(file.name, source, "tsx"); | |
| 177 | renderer = new FileRenderer(options, () => {}, pool); | |
| 178 | tokenizer = new EditorTokenizer({ | |
| 179 | codeOptions: options, | |
| 180 | highlighter, | |
| 181 | textDocument: document, | |
| 182 | setStyle: () => {}, | |
| 183 | onDeferTokenize: (lines, theme) => renderer.updateRenderCache(lines, theme), | |
| 184 | }); | |
| 185 | renderContents(); | |
| 186 | }); | |
| 187 | ||
| 188 | async function cleanUpFixture() { | |
| 189 | tokenizer?.cleanUp(); | |
| 190 | renderer?.cleanUp(); | |
| 191 | pool?.terminate(); | |
| 192 | await Promise.all(terminationPromises); | |
| 193 | // Pool termination can queue a final broadcast after its workers have exited. | |
| 194 | for (const frame of animationFrames) clearImmediate(frame); | |
| 195 | animationFrames.clear(); | |
| 196 | vi.unstubAllGlobals(); | |
| 197 | } | |
| 198 | ||
| 199 | afterEach(cleanUpFixture); | |
| 200 | ||
| 201 | describe("editable file highlighting", () => { | |
| 202 | it("cleans up an already terminated worker pool", async () => { | |
| 203 | (await nextResponse()).deliver(); | |
| 204 | expect(pool.getStats().totalWorkers).toBe(1); | |
| 205 | pool.terminate(); | |
| 206 | await Promise.all(terminationPromises); | |
| 207 | expect(pool.getStats().totalWorkers).toBe(0); | |
| 208 | ||
| 209 | const animationFrame = globalThis.requestAnimationFrame; | |
| 210 | const cancelFrame = globalThis.cancelAnimationFrame; | |
| 211 | const window = globalThis.window; | |
| 212 | try { | |
| 213 | await cleanUpFixture(); | |
| 214 | // Deliver the real Immediate queue after cleanup has removed the browser globals. | |
| 215 | await new Promise<void>((resolve) => setImmediate(resolve)); | |
| 216 | } finally { | |
| 217 | vi.stubGlobal("requestAnimationFrame", animationFrame); | |
| 218 | vi.stubGlobal("cancelAnimationFrame", cancelFrame); | |
| 219 | vi.stubGlobal("window", window); | |
| 220 | } | |
| 221 | }); | |
| 222 | ||
| 223 | it("still accepts an asynchronous highlight when the file has not changed", async () => { | |
| 224 | expect(renderContents()).not.toContain('style="color:'); | |
| 225 | (await nextResponse()).deliver(); | |
| 226 | expect(renderContents()).toContain('style="color:'); | |
| 227 | expect(pool.getFileResultCache(file)).toBeDefined(); | |
| 228 | }); | |
| 229 | ||
| 230 | it.each([1, 60])( | |
| 231 | "ignores a dispatched highlight after %i Enter edits and highlights the new version", | |
| 232 | async (count) => { | |
| 233 | const oldResponse = await nextResponse(); | |
| 234 | for (let index = 0; index < count; index += 1) append("\n"); | |
| 235 | append("export const EDITED_MARKER = 1;"); | |
| 236 | oldResponse.deliver(); | |
| 237 | expect(renderContents()).toContain("EDITED_MARKER"); | |
| 238 | const currentResponse = await nextResponse(); | |
| 239 | currentResponse.deliver(); | |
| 240 | expect(renderContents()).toContain("EDITED_MARKER"); | |
| 241 | const firstLines = renderer.renderFile(file, { ...range, startingLine: 0, totalLines: 20 }); | |
| 242 | expect(renderer.renderFullHTML(firstLines!)).toContain('style="color:'); | |
| 243 | expect(document.lineCount).toBe(7_000 + count); | |
| 244 | }, | |
| 245 | ); | |
| 246 | ||
| 247 | it("does not replace a same-line edit with stale tokens", async () => { | |
| 248 | const oldResponse = await nextResponse(); | |
| 249 | append(" EDITED_MARKER"); | |
| 250 | oldResponse.deliver(); | |
| 251 | expect(renderContents()).toContain("EDITED_MARKER"); | |
| 252 | expect(document.lineCount).toBe(7_000); | |
| 253 | (await nextResponse()).deliver(); | |
| 254 | expect(renderContents()).toContain("EDITED_MARKER"); | |
| 255 | }); | |
| 256 | ||
| 257 | it("keeps undo edits after an older highlight arrives", async () => { | |
| 258 | const oldResponse = await nextResponse(); | |
| 259 | append("\nexport const RETAINED_MARKER = 1;"); | |
| 260 | append("\nexport const UNDONE_MARKER = 2;"); | |
| 261 | undo(); | |
| 262 | oldResponse.deliver(); | |
| 263 | const html = renderContents(); | |
| 264 | expect(html).toContain("RETAINED_MARKER"); | |
| 265 | expect(html).not.toContain("UNDONE_MARKER"); | |
| 266 | (await nextResponse()).deliver(); | |
| 267 | expect(renderContents()).toContain("RETAINED_MARKER"); | |
| 268 | undo(); | |
| 269 | expect(document.getText()).toBe(source); | |
| 270 | expect(renderContents()).not.toContain("RETAINED_MARKER"); | |
| 271 | const redone = document.redo()?.[0]; | |
| 272 | expect(redone).toBeDefined(); | |
| 273 | applyChange(redone!); | |
| 274 | expect(renderContents()).toContain("RETAINED_MARKER"); | |
| 275 | }); | |
| 276 | ||
| 277 | it("evicts the pre-edit shared cache without losing already-highlighted lines", async () => { | |
| 278 | (await nextResponse()).deliver(); | |
| 279 | expect(pool.getFileResultCache(file)).toBeDefined(); | |
| 280 | append("\nexport const EDITED_MARKER = 1;"); | |
| 281 | expect(pool.getFileResultCache(file)).toBeUndefined(); | |
| 282 | expect(renderContents()).toContain("EDITED_MARKER"); | |
| 283 | const firstLines = renderer.renderFile(file, { ...range, startingLine: 0, totalLines: 20 }); | |
| 284 | expect(renderer.renderFullHTML(firstLines!)).toContain('style="color:'); | |
| 285 | }); | |
| 286 | ||
| 287 | it("reopens the edited file with the same cache key while an old response is pending", async () => { | |
| 288 | const oldResponse = await nextResponse(); | |
| 289 | append("\nexport const REOPENED_MARKER = 1;"); | |
| 290 | renderer.cleanUp(); | |
| 291 | renderer = new FileRenderer(options, () => {}, pool); | |
| 292 | file = { ...file }; | |
| 293 | expect(renderContents()).toContain("REOPENED_MARKER"); | |
| 294 | oldResponse.deliver(); | |
| 295 | (await nextResponse()).deliver(); | |
| 296 | expect(renderContents()).toContain("REOPENED_MARKER"); | |
| 297 | expect(renderContents()).toContain('style="color:'); | |
| 298 | }); | |
| 299 | }); |
apps/web/src/components/files/fileEditorLanguageReadiness.test.ts deleted-199| ... | ... | @@ -1,199 +0,0 @@ |
| 1 | import { | |
| 2 | FileRenderer, | |
| 3 | disposeHighlighter, | |
| 4 | getSharedHighlighter, | |
| 5 | type BaseCodeOptions, | |
| 6 | type DiffsHighlighter, | |
| 7 | type FileContents, | |
| 8 | type HighlightedToken, | |
| 9 | } from "@pierre/diffs"; | |
| 10 | import { TextDocument } from "@pierre/diffs/editor"; | |
| 11 | import { WorkerPoolManager, type WorkerRequest, type WorkerResponse } from "@pierre/diffs/worker"; | |
| 12 | import * as NodeWorkerThreads from "node:worker_threads"; | |
| 13 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 14 | ||
| 15 | type DocumentChange = NonNullable<ReturnType<TextDocument<unknown>["applyEdits"]>>; | |
| 16 | interface Tokenizer { | |
| 17 | tokenize(change: DocumentChange): Map<number, HighlightedToken[]>; | |
| 18 | cleanUp(): void; | |
| 19 | } | |
| 20 | ||
| 21 | const tokenizerUrl = new URL("./editor/tokenizer.js", import.meta.resolve("@pierre/diffs")); | |
| 22 | const { EditorTokenizer } = (await import(/* @vite-ignore */ tokenizerUrl.href)) as { | |
| 23 | EditorTokenizer: new (options: { | |
| 24 | codeOptions: BaseCodeOptions; | |
| 25 | highlighter: DiffsHighlighter; | |
| 26 | textDocument: TextDocument<unknown>; | |
| 27 | setStyle: (style: string) => void; | |
| 28 | onDeferTokenize: () => void; | |
| 29 | }) => Tokenizer; | |
| 30 | }; | |
| 31 | ||
| 32 | const workerModule = import.meta.resolve("@pierre/diffs/worker/worker.js"); | |
| 33 | const options = { | |
| 34 | theme: "pierre-dark", | |
| 35 | themeType: "dark", | |
| 36 | preferredHighlighter: "shiki-wasm", | |
| 37 | useTokenTransformer: true, | |
| 38 | } as const; | |
| 39 | const source = "export const View = () => <div>Ready</div>;"; | |
| 40 | let pool: WorkerPoolManager; | |
| 41 | let renderer: FileRenderer; | |
| 42 | let terminationPromises: Promise<number>[]; | |
| 43 | const pendingAnimationFrames = new Set<ReturnType<typeof setImmediate>>(); | |
| 44 | ||
| 45 | class WorkerTransport { | |
| 46 | private readonly worker = new NodeWorkerThreads.Worker( | |
| 47 | `const { parentPort, workerData } = require("node:worker_threads"); | |
| 48 | globalThis.self = { | |
| 49 | addEventListener(type, listener) { | |
| 50 | if (type === "message") parentPort.on("message", data => listener({ data })); | |
| 51 | if (type === "error") process.on("uncaughtException", listener); | |
| 52 | } | |
| 53 | }; | |
| 54 | globalThis.postMessage = data => parentPort.postMessage(data); | |
| 55 | import(workerData.moduleUrl);`, | |
| 56 | { eval: true, workerData: { moduleUrl: workerModule }, execArgv: [] }, | |
| 57 | ); | |
| 58 | ||
| 59 | addEventListener( | |
| 60 | type: "message" | "error", | |
| 61 | listener: (event: { data: WorkerResponse } | Error) => void, | |
| 62 | ) { | |
| 63 | if (type === "error") this.worker.on("error", listener); | |
| 64 | else this.worker.on("message", (data: WorkerResponse) => listener({ data })); | |
| 65 | } | |
| 66 | ||
| 67 | postMessage(message: WorkerRequest) { | |
| 68 | this.worker.postMessage(message, []); | |
| 69 | } | |
| 70 | ||
| 71 | terminate() { | |
| 72 | terminationPromises.push(this.worker.terminate()); | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | function firstEnter(highlighter: DiffsHighlighter, file: FileContents, language: string) { | |
| 77 | const document = new TextDocument(file.name, file.contents, language); | |
| 78 | const tokenizer = new EditorTokenizer({ | |
| 79 | codeOptions: options, | |
| 80 | highlighter, | |
| 81 | textDocument: document, | |
| 82 | setStyle: () => {}, | |
| 83 | onDeferTokenize: () => {}, | |
| 84 | }); | |
| 85 | try { | |
| 86 | const end = document.positionAt(file.contents.length); | |
| 87 | const change = document.applyEdits([{ range: { start: end, end }, newText: "\n" }]); | |
| 88 | expect(change).toBeDefined(); | |
| 89 | // This is the synchronous first edit, before the tokenizer's debounced prebuild. | |
| 90 | const dirtyLines = tokenizer.tokenize(change!); | |
| 91 | expect([...dirtyLines.keys()]).toEqual([0, 1]); | |
| 92 | expect(document.getText()).toBe(`${file.contents}\n`); | |
| 93 | } finally { | |
| 94 | tokenizer.cleanUp(); | |
| 95 | } | |
| 96 | } | |
| 97 | ||
| 98 | beforeEach(async () => { | |
| 99 | terminationPromises = []; | |
| 100 | vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { | |
| 101 | const handle = setImmediate(() => { | |
| 102 | pendingAnimationFrames.delete(handle); | |
| 103 | callback(0); | |
| 104 | }); | |
| 105 | pendingAnimationFrames.add(handle); | |
| 106 | return handle; | |
| 107 | }); | |
| 108 | vi.stubGlobal("cancelAnimationFrame", (handle: ReturnType<typeof setImmediate>) => { | |
| 109 | pendingAnimationFrames.delete(handle); | |
| 110 | clearImmediate(handle); | |
| 111 | }); | |
| 112 | vi.stubGlobal("window", { matchMedia: () => ({ matches: true }) }); | |
| 113 | await disposeHighlighter(); | |
| 114 | pool = new WorkerPoolManager( | |
| 115 | // Adapt transport only. The installed Pierre worker resolves and highlights the file. | |
| 116 | { workerFactory: () => new WorkerTransport() as unknown as globalThis.Worker, poolSize: 1 }, | |
| 117 | options, | |
| 118 | ); | |
| 119 | await pool.initialize(); | |
| 120 | renderer = new FileRenderer(options, undefined, pool); | |
| 121 | }); | |
| 122 | ||
| 123 | afterEach(async () => { | |
| 124 | renderer?.cleanUp(); | |
| 125 | pool?.terminate(); | |
| 126 | await Promise.all(terminationPromises); | |
| 127 | await disposeHighlighter(); | |
| 128 | // Drain the pool's final state broadcast before removing the animation frame stubs. | |
| 129 | await new Promise<void>((resolve) => setImmediate(resolve)); | |
| 130 | for (const handle of pendingAnimationFrames) clearImmediate(handle); | |
| 131 | pendingAnimationFrames.clear(); | |
| 132 | vi.unstubAllGlobals(); | |
| 133 | }); | |
| 134 | ||
| 135 | describe("editable file language readiness", () => { | |
| 136 | it.each(["hydrate", "renderFile"] as const)( | |
| 137 | "%s prepares the inferred language before the first edit of a worker-highlighted file", | |
| 138 | async (method) => { | |
| 139 | const file = { name: "cold.tsx", contents: source, cacheKey: "cold-tsx" }; | |
| 140 | await pool.primeFileHighlightCache(file); | |
| 141 | expect(pool.getFileResultCache(file)).toBeDefined(); | |
| 142 | const mainHighlighter = await getSharedHighlighter({ | |
| 143 | themes: ["pierre-dark"], | |
| 144 | langs: ["text"], | |
| 145 | }); | |
| 146 | expect(mainHighlighter.getLoadedLanguages()).not.toContain("tsx"); | |
| 147 | renderer[method](file); | |
| 148 | // Read-only worker rendering must not load editor grammars on the main thread. | |
| 149 | expect(mainHighlighter.getLoadedLanguages()).not.toContain("tsx"); | |
| 150 | const highlighter = await renderer.initializeHighlighter(); | |
| 151 | firstEnter(highlighter, file, "tsx"); | |
| 152 | }, | |
| 153 | ); | |
| 154 | ||
| 155 | it.each(["hydrate", "renderFile"] as const)( | |
| 156 | "%s respects an explicit language when the filename suggests plain text", | |
| 157 | async (method) => { | |
| 158 | const file: FileContents = { | |
| 159 | name: "source.txt", | |
| 160 | lang: "tsx", | |
| 161 | contents: source, | |
| 162 | cacheKey: "explicit-tsx", | |
| 163 | }; | |
| 164 | await pool.primeFileHighlightCache(file); | |
| 165 | renderer[method](file); | |
| 166 | firstEnter(await renderer.initializeHighlighter(), file, "tsx"); | |
| 167 | }, | |
| 168 | ); | |
| 169 | ||
| 170 | it("loads a newly opened language after reusing a worker-backed renderer", async () => { | |
| 171 | const previousFile: FileContents = { | |
| 172 | name: "previous.ts", | |
| 173 | contents: "export const value = 1;", | |
| 174 | cacheKey: "previous-ts", | |
| 175 | }; | |
| 176 | await getSharedHighlighter({ themes: ["pierre-dark"], langs: ["typescript"] }); | |
| 177 | renderer.renderFile(previousFile); | |
| 178 | firstEnter(await renderer.initializeHighlighter(), previousFile, "typescript"); | |
| 179 | const nextFile = { name: "next.tsx", contents: source, cacheKey: "next-tsx" }; | |
| 180 | renderer.renderFile(nextFile); | |
| 181 | firstEnter(await renderer.initializeHighlighter(), nextFile, "tsx"); | |
| 182 | }); | |
| 183 | ||
| 184 | it("prepares a hydrated non-worker file even when its theme was already loaded", async () => { | |
| 185 | renderer.cleanUp(); | |
| 186 | renderer = new FileRenderer(options); | |
| 187 | const file = { name: "local.tsx", contents: source, cacheKey: "local-tsx" }; | |
| 188 | renderer.hydrate(file); | |
| 189 | firstEnter(await renderer.initializeHighlighter(), file, "tsx"); | |
| 190 | }); | |
| 191 | ||
| 192 | it("keeps plain text editable without loading an unrelated grammar", async () => { | |
| 193 | const file = { name: "notes.txt", contents: "Plain text", cacheKey: "plain-text" }; | |
| 194 | renderer.renderFile(file); | |
| 195 | const highlighter = await renderer.initializeHighlighter(); | |
| 196 | firstEnter(highlighter, file, "text"); | |
| 197 | expect(highlighter.getLoadedLanguages()).not.toContain("tsx"); | |
| 198 | }); | |
| 199 | }); |
apps/web/src/components/files/fileEditorVirtualization.test.ts+38-18| ... | ... | @@ -4,7 +4,7 @@ import { |
| 4 | 4 | Virtualizer, |
| 5 | 5 | type FileContents, |
| 6 | 6 | } from "@pierre/diffs"; |
| 7 | import { Editor, TextDocument } from "@pierre/diffs/editor"; | |
| 7 | import { Editor, TextDocument } from "@pierre/diffs/edit"; | |
| 8 | 8 | import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 9 | 9 | |
| 10 | 10 | const renderingManagerUrl = new URL( |
| ... | ... | @@ -22,10 +22,21 @@ class MeasuredElement { |
| 22 | 22 | children: MeasuredElement[] = []; |
| 23 | 23 | dataset: Record<string, string> = {}; |
| 24 | 24 | nextElementSibling: MeasuredElement | null = null; |
| 25 | shadowRoot: MeasuredElement | null = null; | |
| 25 | 26 | width = 283; |
| 26 | 27 | |
| 27 | 28 | constructor(readonly height = 0) {} |
| 28 | 29 | |
| 30 | attachShadow() { | |
| 31 | this.shadowRoot ??= new MeasuredElement(); | |
| 32 | return this.shadowRoot; | |
| 33 | } | |
| 34 | ||
| 35 | appendChild(child: MeasuredElement) { | |
| 36 | this.children.push(child); | |
| 37 | return child; | |
| 38 | } | |
| 39 | ||
| 29 | 40 | getBoundingClientRect() { |
| 30 | 41 | MeasuredElement.geometryReads += 1; |
| 31 | 42 | return { top: 0, height: this.height, width: this.width }; |
| ... | ... | @@ -108,8 +119,10 @@ class LayoutVirtualizer extends Virtualizer { |
| 108 | 119 | |
| 109 | 120 | class MeasuredFile extends VirtualizedFile { |
| 110 | 121 | override top = 0; |
| 122 | // Rows are measured by hand here; the virtualizer only needs to reconcile them. | |
| 123 | override onRender = () => false; | |
| 111 | 124 | |
| 112 | override attachEditor(editor: Parameters<VirtualizedFile["attachEditor"]>[0]) { | |
| 125 | override __attachEditor(editor: Parameters<VirtualizedFile["__attachEditor"]>[0]) { | |
| 113 | 126 | this.editor = editor; |
| 114 | 127 | return () => { |
| 115 | 128 | this.editor = undefined; |
| ... | ... | @@ -117,7 +130,9 @@ class MeasuredFile extends VirtualizedFile { |
| 117 | 130 | } |
| 118 | 131 | |
| 119 | 132 | async initialize(file: FileContents) { |
| 120 | this.prepareCodeViewItem(file, 0); | |
| 133 | this.updateCodeViewLayout(file, 0); | |
| 134 | // Document changes require the session an attached editor installs. | |
| 135 | (this as unknown as { installEditSession(file: FileContents): void }).installEditSession(file); | |
| 121 | 136 | await this.fileRenderer.initializeHighlighter(); |
| 122 | 137 | expect( |
| 123 | 138 | this.fileRenderer.renderFile(file, { |
| ... | ... | @@ -180,7 +195,7 @@ class MeasuredFile extends VirtualizedFile { |
| 180 | 195 | } |
| 181 | 196 | |
| 182 | 197 | const instances: MeasuredFile[] = []; |
| 183 | const editors: Editor<undefined>[] = []; | |
| 198 | const editors: Editor<"file", undefined, undefined>[] = []; | |
| 184 | 199 | |
| 185 | 200 | beforeAll(async () => { |
| 186 | 201 | await getSharedHighlighter({ |
| ... | ... | @@ -225,7 +240,7 @@ async function makeFixture( |
| 225 | 240 | cacheKey: `wrapped:${overflow}`, |
| 226 | 241 | lang: "text", |
| 227 | 242 | }; |
| 228 | const document = new TextDocument(file.name, contents, "text"); | |
| 243 | const document = new TextDocument<"file", undefined>(file.name, contents, "text"); | |
| 229 | 244 | const instance = new MeasuredFile( |
| 230 | 245 | { |
| 231 | 246 | overflow, |
| ... | ... | @@ -274,7 +289,7 @@ describe("wrapped editor document changes", () => { |
| 274 | 289 | const before = instance.getLinePosition(previousLastLine); |
| 275 | 290 | expect(before).toEqual({ top: 120328, height: 60 }); |
| 276 | 291 | const viewport = { top: before!.top - 100, bottom: before!.top + 80 }; |
| 277 | expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2156 }); | |
| 292 | expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2096 }); | |
| 278 | 293 | |
| 279 | 294 | append(); |
| 280 | 295 | |
| ... | ... | @@ -282,7 +297,7 @@ describe("wrapped editor document changes", () => { |
| 282 | 297 | expect(instance.getLinePosition(previousLastLine)).toEqual({ top: before!.top, height: 20 }); |
| 283 | 298 | expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120348, height: 20 }); |
| 284 | 299 | expect(instance.getVirtualizedHeight()).toBe(120376); |
| 285 | expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2136 }); | |
| 300 | expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2096 }); | |
| 286 | 301 | }); |
| 287 | 302 | |
| 288 | 303 | it("invalidates changed and shifted rows after an insertion in the middle", async () => { |
| ... | ... | @@ -373,7 +388,7 @@ describe("wrapped editor document changes", () => { |
| 373 | 388 | const { instance, file, append } = await makeFixture(); |
| 374 | 389 | append(); |
| 375 | 390 | instance.setMetrics({ hunkLineCount: 50, lineHeight: 24, diffHeaderHeight: 44, spacing: 8 }); |
| 376 | instance.prepareCodeViewItem(file, 0); | |
| 391 | instance.updateCodeViewLayout(file, 0); | |
| 377 | 392 | expect(instance.getLinePosition(6001)).toEqual({ top: 144008, height: 24 }); |
| 378 | 393 | }); |
| 379 | 394 | |
| ... | ... | @@ -381,7 +396,7 @@ describe("wrapped editor document changes", () => { |
| 381 | 396 | const { instance, file, append } = await makeFixture(); |
| 382 | 397 | append(); |
| 383 | 398 | instance.setLineAnnotations([{ lineNumber: 10, metadata: undefined }]); |
| 384 | instance.prepareCodeViewItem(file, 0); | |
| 399 | instance.updateCodeViewLayout(file, 0); | |
| 385 | 400 | expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 }); |
| 386 | 401 | }); |
| 387 | 402 | }); |
| ... | ... | @@ -476,11 +491,11 @@ describe("wrapped measurement widths", () => { |
| 476 | 491 | "document", |
| 477 | 492 | Object.assign(new EditorElement(), { createElement: () => new EditorElement() }), |
| 478 | 493 | ); |
| 479 | const first = new Editor<undefined>(); | |
| 494 | const first = new Editor("file"); | |
| 480 | 495 | editors.push(first); |
| 481 | 496 | first.edit(instance); |
| 482 | 497 | first.cleanUp(); |
| 483 | const second = new Editor<undefined>(); | |
| 498 | const second = new Editor("file"); | |
| 484 | 499 | editors.push(second); |
| 485 | 500 | second.edit(instance); |
| 486 | 501 | instance.resizeContent(482.25); |
| ... | ... | @@ -531,12 +546,16 @@ class EditorElement extends MeasuredElement { |
| 531 | 546 | style: Record<string, string> = {}; |
| 532 | 547 | parentElement: EditorElement | null = null; |
| 533 | 548 | |
| 534 | appendChild(child: EditorElement) { | |
| 549 | override appendChild(child: EditorElement) { | |
| 535 | 550 | child.parentElement = this; |
| 536 | 551 | this.children.push(child); |
| 537 | 552 | return child; |
| 538 | 553 | } |
| 539 | 554 | |
| 555 | append(child: EditorElement) { | |
| 556 | this.appendChild(child); | |
| 557 | } | |
| 558 | ||
| 540 | 559 | prepend(child: EditorElement) { |
| 541 | 560 | child.parentElement = this; |
| 542 | 561 | this.children.unshift(child); |
| ... | ... | @@ -630,14 +649,15 @@ async function makeEditorFixture(lineCount: number) { |
| 630 | 649 | langs: ["text"], |
| 631 | 650 | preferredHighlighter: "shiki-wasm", |
| 632 | 651 | }); |
| 633 | const editor = new Editor<undefined>(); | |
| 652 | const editor = new Editor("file"); | |
| 634 | 653 | editors.push(editor); |
| 635 | 654 | editor.edit(instance); |
| 636 | editor.__syncRenderView(highlighter, measuredElement(host), file, undefined, { | |
| 637 | startingLine: 0, | |
| 638 | totalLines: 1, | |
| 639 | bufferBefore: 0, | |
| 640 | bufferAfter: 0, | |
| 655 | editor.__syncRenderView({ | |
| 656 | highlighter, | |
| 657 | fileContainer: measuredElement(host), | |
| 658 | file, | |
| 659 | lineAnnotations: undefined, | |
| 660 | renderRange: { startingLine: 0, totalLines: 1, bufferBefore: 0, bufferAfter: 0 }, | |
| 641 | 661 | }); |
| 642 | 662 | const append = (count: number) => { |
| 643 | 663 | const lines = editor.getText().split("\n"); |
apps/web/src/components/files/projectFilesQueryState.ts+12| ... | ... | @@ -89,6 +89,18 @@ export function getOptimisticProjectFileQueryData( |
| 89 | 89 | return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null; |
| 90 | 90 | } |
| 91 | 91 | |
| 92 | /** The contents the Files panel shows, read outside React so it is current within a frame. */ | |
| 93 | export function getProjectFileContents( | |
| 94 | environmentId: EnvironmentId, | |
| 95 | cwd: string, | |
| 96 | relativePath: string, | |
| 97 | ): string | undefined { | |
| 98 | const optimistic = getOptimisticProjectFileQueryData(environmentId, cwd, relativePath); | |
| 99 | if (optimistic) return optimistic.contents; | |
| 100 | const result = appAtomRegistry.get(getProjectFileQueryAtom(environmentId, cwd, relativePath)); | |
| 101 | return Option.getOrUndefined(AsyncResult.value(result))?.contents; | |
| 102 | } | |
| 103 | ||
| 92 | 104 | export function confirmProjectFileQueryData( |
| 93 | 105 | environmentId: EnvironmentId, |
| 94 | 106 | cwd: string, |
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+3-1| ... | ... | @@ -254,7 +254,9 @@ function PullRequestCodeTab({ |
| 254 | 254 | readonly slices: ReadonlyArray<DiffSlice>; |
| 255 | 255 | }>({ key: "", cursor: null, slices: NO_SLICES }); |
| 256 | 256 | const parseCache = useRef(new Map<string, RenderablePatch>()); |
| 257 | const [viewer, setViewer] = useState<CodeViewHandle<ReviewAnnotationGroup> | null>(null); | |
| 257 | const [viewer, setViewer] = useState<CodeViewHandle<ReviewAnnotationGroup, undefined> | null>( | |
| 258 | null, | |
| 259 | ); | |
| 258 | 260 | |
| 259 | 261 | const referenceKey = pullRequestReviewKey(reference); |
| 260 | 262 | const commit = selectedCommitOid; |
apps/web/src/lib/diffRendering.test.ts+4| ... | ... | @@ -402,6 +402,10 @@ describe("a file whose name a patch header cannot carry plainly", () => { |
| 402 | 402 | expect(pathOf(quotedPatch("line\\nfile.txt"))).toBe("line\nfile.txt"); |
| 403 | 403 | }); |
| 404 | 404 | |
| 405 | it("keeps a backslash the name really has", () => { | |
| 406 | expect(pathOf(quotedPatch("back\\\\slash.txt"))).toBe("back\\slash.txt"); | |
| 407 | }); | |
| 408 | ||
| 405 | 409 | it("reads the octal a host with core.quotePath on writes for a name outside ASCII", () => { |
| 406 | 410 | expect(pathOf(quotedPatch("caf\\303\\251/r\\303\\251sum\\303\\251.ts"))).toBe("café/résumé.ts"); |
| 407 | 411 | }); |
apps/web/src/lib/diffRendering.ts+4-11| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; |
| 2 | 2 | import { parseDiffFromFile } from "@pierre/diffs"; |
| 3 | 3 | import type { FileDiffMetadata } from "@pierre/diffs/types"; |
| 4 | import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; | |
| 5 | 4 | |
| 6 | 5 | const DIFF_THEME_NAMES = { |
| 7 | 6 | light: "pierre-light", |
| ... | ... | @@ -201,17 +200,11 @@ export function getRenderablePatch( |
| 201 | 200 | } |
| 202 | 201 | |
| 203 | 202 | /** |
| 204 | * What the patch called the file, as the file's own name. Git writes a name holding a tab, a | |
| 205 | * newline, a quote or a backslash quoted and escaped, and the parser hands one of those back still | |
| 206 | * escaped. A viewed mark, a review comment and a file's contents are all asked for by this path, | |
| 207 | * and the host knows the file only under the name it really has. | |
| 203 | * The file's own name. Git quotes and escapes a name holding a tab, a newline, a quote or a | |
| 204 | * backslash, and the parser decodes it, so this is the name the host knows the file by. | |
| 208 | 205 | */ |
| 209 | function fileDiffPath(raw: string): string { | |
| 210 | return unquoteGitPatchPath(raw); | |
| 211 | } | |
| 212 | ||
| 213 | 206 | export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { |
| 214 | return fileDiffPath(fileDiff.name ?? fileDiff.prevName ?? ""); | |
| 207 | return fileDiff.name ?? fileDiff.prevName ?? ""; | |
| 215 | 208 | } |
| 216 | 209 | |
| 217 | 210 | /** |
| ... | ... | @@ -219,7 +212,7 @@ export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { |
| 219 | 212 | * path, and the hosts that resolve a diff position against both sides need both names. |
| 220 | 213 | */ |
| 221 | 214 | export function resolveFileDiffPreviousPath(fileDiff: FileDiffMetadata): string { |
| 222 | return fileDiffPath(fileDiff.prevName ?? fileDiff.name ?? ""); | |
| 215 | return fileDiff.prevName ?? fileDiff.name ?? ""; | |
| 223 | 216 | } |
| 224 | 217 | |
| 225 | 218 | /** |
apps/web/vite.config.ts+1-1| ... | ... | @@ -190,7 +190,7 @@ export default defineConfig(() => { |
| 190 | 190 | "@clerk/clerk-js", |
| 191 | 191 | "@clerk/react/internal", |
| 192 | 192 | "@pierre/diffs", |
| 193 | "@pierre/diffs/editor", | |
| 193 | "@pierre/diffs/edit", | |
| 194 | 194 | "@pierre/diffs/react", |
| 195 | 195 | "@pierre/diffs/worker/worker.js", |
| 196 | 196 | "effect/Array", |
patches/@pierre%2Fdiffs@1.3.0-beta.10.patch deleted-238| ... | ... | @@ -1,238 +0,0 @@ |
| 1 | diff --git a/dist/components/VirtualizedFile.d.ts b/dist/components/VirtualizedFile.d.ts | |
| 2 | --- a/dist/components/VirtualizedFile.d.ts | |
| 3 | +++ b/dist/components/VirtualizedFile.d.ts | |
| 4 | @@ -42,7 +42,7 @@ declare class VirtualizedFile<LAnnotation = undefined> extends File<LAnnotation> { | |
| 5 | private computeApproximateSize; | |
| 6 | setVisibility(visible: boolean): void; | |
| 7 | rerender(): void; | |
| 8 | - applyDocumentChange(textDocument: DiffsTextDocument, newLineAnnotations?: LineAnnotation<LAnnotation>[], shouldUpdateBuffer?: boolean): void; | |
| 9 | + applyDocumentChange(textDocument: DiffsTextDocument, newLineAnnotations?: LineAnnotation<LAnnotation>[], shouldUpdateBuffer?: boolean, startLine?: number): void; | |
| 10 | protected renderPreparedFile({ | |
| 11 | fileContainer, | |
| 12 | file, | |
| 13 | diff --git a/dist/components/VirtualizedFile.js b/dist/components/VirtualizedFile.js | |
| 14 | --- a/dist/components/VirtualizedFile.js | |
| 15 | +++ b/dist/components/VirtualizedFile.js | |
| 16 | @@ -20,6 +20,7 @@ | |
| 17 | 	cache = { | |
| 18 | 		heights: /* @__PURE__ */ new Map(), | |
| 19 | 		checkpoints: [], | |
| 20 | +		codeWidth: void 0, | |
| 21 | 		fileAnnotationHeight: 0 | |
| 22 | 	}; | |
| 23 | 	isVisible = false; | |
| 24 | @@ -31,6 +32,8 @@ | |
| 25 | 		super(options, workerManager, isContainerManaged); | |
| 26 | 		this.virtualizer = virtualizer; | |
| 27 | 		this.metrics = metrics; | |
| 28 | +		const simpleVirtualizer = this.getSimpleVirtualizer(); | |
| 29 | +		if (simpleVirtualizer != null) this.resizeManager.onResize = () => simpleVirtualizer.requestHeightReconcile(this); | |
| 30 | 	} | |
| 31 | 	setMetrics(metrics, force = false) { | |
| 32 | 		if (!force && areObjectsEqual(this.metrics, metrics)) return; | |
| 33 | @@ -70,10 +73,12 @@ | |
| 34 | 		if (this.isAdvancedMode()) throw new Error("VirtualizedFile.setThemeType cannot be used inside CodeView. Update CodeView options instead."); | |
| 35 | 		super.setThemeType(themeType); | |
| 36 | 	} | |
| 37 | -	resetLayoutCache(recompute = false, resetRenderRange = true) { | |
| 38 | +	resetLayoutCache(recompute = false, resetRenderRange = true, startLine = 0) { | |
| 39 | 		this.layoutDirty = true; | |
| 40 | -		this.cache.fileAnnotationHeight = 0; | |
| 41 | -		if (this.cache.heights.size > 0) this.cache.heights.clear(); | |
| 42 | +		if (startLine === 0) this.cache.fileAnnotationHeight = 0; | |
| 43 | +		// Dropping unchanged wrapped rows moves the viewport before they can be remeasured. | |
| 44 | +		if (startLine === 0) this.cache.heights.clear(); | |
| 45 | +		else for (const lineIndex of this.cache.heights.keys()) if (lineIndex >= startLine) this.cache.heights.delete(lineIndex); | |
| 46 | 		if (this.cache.checkpoints.length > 0) this.cache.checkpoints.length = 0; | |
| 47 | 		if (this.renderRange != null && resetRenderRange) this.renderRange = void 0; | |
| 48 | 		if (recompute && this.isSimpleMode()) this.computeApproximateSize(); | |
| 49 | @@ -91,6 +96,13 @@ | |
| 50 | 		if (this.code == null) return hasHeightChange; | |
| 51 | 		const content = this.code.children[1]; | |
| 52 | 		if (!(content instanceof HTMLElement)) return hasHeightChange; | |
| 53 | +		const codeWidth = this.code.getBoundingClientRect().width; | |
| 54 | +		if (!(codeWidth > 0)) return hasHeightChange; | |
| 55 | +		if (this.cache.codeWidth != null && this.cache.codeWidth !== codeWidth) { | |
| 56 | +			this.resetLayoutCache(false, false); | |
| 57 | +			hasHeightChange = true; | |
| 58 | +		} | |
| 59 | +		this.cache.codeWidth = codeWidth; | |
| 60 | 		const hasFileAnnotations = includesFileAnnotations(this.lineAnnotations); | |
| 61 | 		if (this.renderRange != null && hasFileAnnotations && shouldRenderFileAnnotations(this.renderRange)) { | |
| 62 | 			const nextFileAnnotationHeight = measureFileAnnotationHeight(content) ?? 0; | |
| 63 | @@ -287,11 +299,11 @@ | |
| 64 | 		this.forceRenderOverride = true; | |
| 65 | 		this.virtualizer.instanceChanged(this, false); | |
| 66 | 	} | |
| 67 | -	applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false) { | |
| 68 | +	applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false, startLine = 0) { | |
| 69 | 		const previousRenderRange = this.renderRange; | |
| 70 | 		super.applyDocumentChange(textDocument, newLineAnnotations); | |
| 71 | 		this.getSimpleVirtualizer()?.markDOMDirty(); | |
| 72 | -		this.resetLayoutCache(this.isSimpleMode(), false); | |
| 73 | +		this.resetLayoutCache(this.isSimpleMode(), false, startLine); | |
| 74 | 		if (shouldUpdateBuffer && previousRenderRange !== void 0 && this.file !== void 0) { | |
| 75 | 			const windowSpecs = this.virtualizer.getWindowSpecs(); | |
| 76 | 			const renderRange = this.computeRenderRangeFromWindow(this.file, this.top ?? 0, windowSpecs); | |
| 77 | diff --git a/dist/editor/editor.js b/dist/editor/editor.js | |
| 78 | index ff78e2a..f9df318 100644 | |
| 79 | --- a/dist/editor/editor.js | |
| 80 | +++ b/dist/editor/editor.js | |
| 81 | @@ -146,14 +146,11 @@ var Editor = class { | |
| 82 | 			const file = fileInstance.__getCurrentFile?.(); | |
| 83 | 			if (file !== void 0) requirePersistedCacheKey(file); | |
| 84 | 		} | |
| 85 | -		const { useTokenTransformer, enableGutterUtility, enableLineSelection, lineHoverHighlight = "disabled", ...rest } = fileInstance.options; | |
| 86 | -		if (useTokenTransformer !== true || enableGutterUtility === true || enableLineSelection === true || lineHoverHighlight !== "disabled") { | |
| 87 | +		const { useTokenTransformer, ...rest } = fileInstance.options; | |
| 88 | +		if (useTokenTransformer !== true) { | |
| 89 | 			fileInstance.setOptions({ | |
| 90 | 				...rest, | |
| 91 | -				useTokenTransformer: true, | |
| 92 | -				enableGutterUtility: false, | |
| 93 | -				enableLineSelection: false, | |
| 94 | -				lineHoverHighlight: "disabled" | |
| 95 | +				useTokenTransformer: true | |
| 96 | 			}); | |
| 97 | 			fileInstance.rerender(); | |
| 98 | 		} | |
| 99 | @@ -908,6 +905,7 @@ var Editor = class { | |
| 100 | 				return lineNumber - 1; | |
| 101 | 			}; | |
| 102 | 			this.#editorEventDisposes.push(addEventListener(gutterEl, "pointerdown", (e) => { | |
| 103 | +				if (this.#fileInstance?.options.enableLineSelection === true) return; | |
| 104 | 				const gutterRow = resolveGutterTarget(e.composedPath()[0]); | |
| 105 | 				if (gutterRow?.dataset.lineType === "change-deletion") { | |
| 106 | 					const code = gutterRow.closest("[data-code]"); | |
| 107 | @@ -1522,6 +1520,12 @@ var Editor = class { | |
| 108 | 			if (gutterEl !== void 0) gutterEl.style.gridRow = "span " + gridRow; | |
| 109 | 		} | |
| 110 | 		fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange, didLineCountChange); | |
| 111 | +		if (fileInstance.file !== void 0) fileInstance.file.contents = textDocument.getText(); | |
| 112 | -		if (didLineCountChange) fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer); | |
| 113 | +		if (didLineCountChange) { | |
| 114 | +			const previousLineCount = change.lineCount - change.lineDelta; | |
| 115 | +			// A wider or narrower line-number gutter can rewrap unchanged rows. | |
| 116 | +			const layoutStartLine = String(previousLineCount).length === String(change.lineCount).length ? change.startLine : 0; | |
| 117 | +			fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer, layoutStartLine); | |
| 118 | +		} | |
| 119 | 		if (this.#isDiff && (this.#diffSyle === "unified" || didLineCountChange)) this.#resetCache(); | |
| 120 | 		if (newLineAnnotations !== void 0) { | |
| 121 | @@ -1788,6 +1787,7 @@ var Editor = class { | |
| 122 | 		} | |
| 123 | 	} | |
| 124 | 	#setSelectedLinesSafe(range, lineNumberOnly = false) { | |
| 125 | +		if (this.#fileInstance?.options.controlledSelection === true) return; | |
| 126 | 		try { | |
| 127 | 			this.#fileInstance?.setSelectedLines(range, { | |
| 128 | 				notify: false, | |
| 129 | diff --git a/dist/managers/ResizeManager.d.ts b/dist/managers/ResizeManager.d.ts | |
| 130 | --- a/dist/managers/ResizeManager.d.ts | |
| 131 | +++ b/dist/managers/ResizeManager.d.ts | |
| 132 | @@ -5,6 +5,8 @@ | |
| 133 | columnVariables?: ResizeManagerColumnVariableMode; | |
| 134 | } | |
| 135 | declare class ResizeManager { | |
| 136 | + /** Schedule owner measurement after an observed code or gutter size change. */ | |
| 137 | + onResize?: () => void; | |
| 138 | private static resizeObserver; | |
| 139 | private static managersByElement; | |
| 140 | private static getResizeObserver; | |
| 141 | diff --git a/dist/managers/ResizeManager.js b/dist/managers/ResizeManager.js | |
| 142 | --- a/dist/managers/ResizeManager.js | |
| 143 | +++ b/dist/managers/ResizeManager.js | |
| 144 | @@ -19,6 +19,7 @@ | |
| 145 | 		for (const [manager, managerEntries] of entriesByManager) manager.handleResizeEntries(managerEntries); | |
| 146 | 	} | |
| 147 | 	observedNodes = /* @__PURE__ */ new Map(); | |
| 148 | +	onResize; | |
| 149 | 	setup(pre, { disableAnnotations, columnVariables = "apply" }) { | |
| 150 | 		const annotationUpdates = /* @__PURE__ */ new Set(); | |
| 151 | 		const applyColumnVariables = columnVariables === "apply"; | |
| 152 | @@ -212,6 +213,7 @@ | |
| 153 | 		this.applyAnnotationUpdates(annotationUpdates); | |
| 154 | 		annotationUpdates.clear(); | |
| 155 | 		this.applyColumnUpdates(codeUpdates); | |
| 156 | +		if (codeUpdates.size > 0) this.onResize?.(); | |
| 157 | 		codeUpdates.clear(); | |
| 158 | 	} | |
| 159 | 	applyAnnotationUpdates(annotationUpdates) { | |
| 160 | diff --git a/dist/react/utils/useFileInstance.js b/dist/react/utils/useFileInstance.js | |
| 161 | index e9f62f5..af82a46 100644 | |
| 162 | --- a/dist/react/utils/useFileInstance.js | |
| 163 | +++ b/dist/react/utils/useFileInstance.js | |
| 164 | @@ -91,10 +91,7 @@ function mergeFileOptions({ options, controlledSelection, contentEditable, hasCu | |
| 165 | 	}; | |
| 166 | 	if (needsEditorOptions) merged = { | |
| 167 | 		...merged, | |
| 168 | -		useTokenTransformer: true, | |
| 169 | -		enableGutterUtility: false, | |
| 170 | -		enableLineSelection: false, | |
| 171 | -		lineHoverHighlight: "disabled" | |
| 172 | +		useTokenTransformer: true | |
| 173 | 	}; | |
| 174 | 	return merged; | |
| 175 | } | |
| 176 | diff --git a/dist/renderers/FileRenderer.js b/dist/renderers/FileRenderer.js | |
| 177 | --- a/dist/renderers/FileRenderer.js | |
| 178 | +++ b/dist/renderers/FileRenderer.js | |
| 179 | @@ -107,10 +107,10 @@ | |
| 180 | 			result: massiveFile ? void 0 : cache?.result, | |
| 181 | 			renderRange: void 0 | |
| 182 | 		}; | |
| 183 | +		this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); | |
| 184 | 		if (this.workerManager?.isWorkingPool() === true) { | |
| 185 | 			if (this.renderCache.result == null && !massiveFile) this.workerManager.highlightFileAST(this, file); | |
| 186 | 		} else if (this.highlighter == null) { | |
| 187 | -			this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); | |
| 188 | 			this.initializeHighlighter(); | |
| 189 | 		} | |
| 190 | 	} | |
| 191 | @@ -163,6 +163,8 @@ | |
| 192 | 		if (this.renderCache == null) return; | |
| 193 | 		const { file, result } = this.renderCache; | |
| 194 | 		if (result == null) return; | |
| 195 | +		this.workerManager?.cleanUpTasks(this); | |
| 196 | +		if (file.cacheKey != null) this.workerManager?.evictFileFromCache(file.cacheKey); | |
| 197 | 		const lineCache = this.lineCache != null && isLineCacheForFile(this.lineCache, file) ? this.lineCache : void 0; | |
| 198 | 		for (const [line, tokens] of dirtyLines) { | |
| 199 | 			if (lineCache != null && line < lineCache.lines.length) { | |
| 200 | @@ -268,6 +270,7 @@ | |
| 201 | 		const forcePlainText = !hasContent || isFilePlainText(file) || isFileMassive(lines.length, this.getTokenizeMaxLength()); | |
| 202 | 		const newContent = !areFilesEqual(file, this.renderCache.file); | |
| 203 | 		const newRenderRange = !areRenderRangesEqual(this.renderCache.renderRange, renderRange); | |
| 204 | +		this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); | |
| 205 | 		if (this.workerManager?.isWorkingPool() === true) { | |
| 206 | 			if (forcePlainText || this.renderCache.result == null || !this.renderCache.highlighted && (newContent || newRenderRange)) { | |
| 207 | 				this.renderCache.file = file; | |
| 208 | @@ -278,7 +281,6 @@ | |
| 209 | 			} | |
| 210 | 			if (!forcePlainText && hasContent && (!this.renderCache.highlighted || forceHighlight)) this.workerManager.highlightFileAST(this, file); | |
| 211 | 		} else { | |
| 212 | -			this.computedLang = file.lang ?? getFiletypeFromFileName(file.name); | |
| 213 | 			const hasThemes = this.highlighter != null && areThemesAttached(options.theme); | |
| 214 | 			const hasLangs = this.highlighter != null && areLanguagesAttached(this.computedLang); | |
| 215 | 			const canHighlight = !forcePlainText && hasLangs; | |
| 216 | diff --git a/package.json b/package.json | |
| 217 | index ff61c90..1e170e5 100644 | |
| 218 | --- a/package.json | |
| 219 | +++ b/package.json | |
| 220 | @@ -55,6 +55,18 @@ | |
| 221 | "./worker/worker-portable.js": { | |
| 222 | "types": "./dist/worker/worker-portable.d.ts", | |
| 223 | "import": "./dist/worker/worker-portable.js" | |
| 224 | + }, | |
| 225 | + "./types": { | |
| 226 | + "types": "./dist/types.d.ts", | |
| 227 | + "import": "./dist/types.js" | |
| 228 | + }, | |
| 229 | + "./utils/getFiletypeFromFileName": { | |
| 230 | + "types": "./dist/utils/getFiletypeFromFileName.d.ts", | |
| 231 | + "import": "./dist/utils/getFiletypeFromFileName.js" | |
| 232 | + }, | |
| 233 | + "./utils/parsePatchFiles": { | |
| 234 | + "types": "./dist/utils/parsePatchFiles.d.ts", | |
| 235 | + "import": "./dist/utils/parsePatchFiles.js" | |
| 236 | } | |
| 237 | }, | |
| 238 | "publishConfig": { |
patches/@pierre%2Fdiffs@1.5.2.patch created+146| ... | ... | @@ -0,0 +1,146 @@ |
| 1 | diff --git a/dist/components/VirtualizedFile.d.ts b/dist/components/VirtualizedFile.d.ts | |
| 2 | --- a/dist/components/VirtualizedFile.d.ts | |
| 3 | +++ b/dist/components/VirtualizedFile.d.ts | |
| 4 | @@ -49,7 +49,7 @@ | |
| 5 | setVisibility(visible: boolean): void; | |
| 6 | rerender(): void; | |
| 7 | syncGhostTextRows(): void; | |
| 8 | - applyDocumentChange(textDocument: TextDocument<'file', LAnnotation>, newLineAnnotations?: LineAnnotation<LAnnotation>[], shouldUpdateBuffer?: boolean): void; | |
| 9 | + applyDocumentChange(textDocument: TextDocument<'file', LAnnotation>, newLineAnnotations?: LineAnnotation<LAnnotation>[], shouldUpdateBuffer?: boolean, startLine?: number): void; | |
| 10 | render({ fileContainer, file, forceRender, lineAnnotations, ...props }: FileRenderProps<LAnnotation>): boolean; | |
| 11 | protected finalizeRender(): void; | |
| 12 | private updatePendingRender; | |
| 13 | diff --git a/dist/components/VirtualizedFile.js b/dist/components/VirtualizedFile.js | |
| 14 | --- a/dist/components/VirtualizedFile.js | |
| 15 | +++ b/dist/components/VirtualizedFile.js | |
| 16 | @@ -23,6 +23,7 @@ | |
| 17 | 		heights: /* @__PURE__ */ new Map(), | |
| 18 | 		checkpoints: [], | |
| 19 | 		fileAnnotationHeight: 0, | |
| 20 | +		codeWidth: void 0, | |
| 21 | 		ghostTextRows: NO_GHOST_TEXT_ROWS | |
| 22 | 	}; | |
| 23 | 	pendingRender; | |
| 24 | @@ -35,6 +36,8 @@ | |
| 25 | 		super(options, workerManager, isContainerManaged); | |
| 26 | 		this.virtualizer = virtualizer; | |
| 27 | 		this.metrics = metrics; | |
| 28 | +		const simpleVirtualizer = this.getSimpleVirtualizer(); | |
| 29 | +		if (simpleVirtualizer != null) this.resizeManager.onResize = () => simpleVirtualizer.requestHeightReconcile(this); | |
| 30 | 	} | |
| 31 | 	setMetrics(metrics, force = false) { | |
| 32 | 		const nextMetrics = computeVirtualFileMetrics(metrics); | |
| 33 | @@ -86,10 +89,12 @@ | |
| 34 | 		if (this.isAdvancedMode()) throw new Error("VirtualizedFile.setThemeType cannot be used inside CodeView. Update CodeView options instead."); | |
| 35 | 		super.setThemeType(themeType); | |
| 36 | 	} | |
| 37 | -	resetLayoutCache(recompute = false, resetRenderRange = true) { | |
| 38 | +	resetLayoutCache(recompute = false, resetRenderRange = true, startLine = 0) { | |
| 39 | 		this.layoutDirty = true; | |
| 40 | -		this.cache.fileAnnotationHeight = 0; | |
| 41 | -		if (this.cache.heights.size > 0) this.cache.heights.clear(); | |
| 42 | +		if (startLine === 0) this.cache.fileAnnotationHeight = 0; | |
| 43 | +		// Dropping unchanged wrapped rows moves the viewport before they can be remeasured. | |
| 44 | +		if (startLine === 0) this.cache.heights.clear(); | |
| 45 | +		else for (const lineIndex of this.cache.heights.keys()) if (lineIndex >= startLine) this.cache.heights.delete(lineIndex); | |
| 46 | 		if (this.cache.checkpoints.length > 0) this.cache.checkpoints.length = 0; | |
| 47 | 		this.cache.ghostTextRows = NO_GHOST_TEXT_ROWS; | |
| 48 | 		if (this.renderRange != null && resetRenderRange) this.renderRange = void 0; | |
| 49 | @@ -135,6 +140,13 @@ | |
| 50 | 		if (this.code == null) return hasHeightChange; | |
| 51 | 		const content = this.code.children[1]; | |
| 52 | 		if (!(content instanceof HTMLElement)) return hasHeightChange; | |
| 53 | +		const codeWidth = this.code.getBoundingClientRect().width; | |
| 54 | +		if (!(codeWidth > 0)) return hasHeightChange; | |
| 55 | +		if (this.cache.codeWidth != null && this.cache.codeWidth !== codeWidth) { | |
| 56 | +			this.resetLayoutCache(false, false); | |
| 57 | +			hasHeightChange = true; | |
| 58 | +		} | |
| 59 | +		this.cache.codeWidth = codeWidth; | |
| 60 | 		if (this.renderRange != null && shouldRenderFileAnnotations(this.renderRange)) { | |
| 61 | 			const nextFileAnnotationHeight = measureFileAnnotationHeight(content) ?? 0; | |
| 62 | 			if (nextFileAnnotationHeight !== this.cache.fileAnnotationHeight) { | |
| 63 | @@ -358,12 +370,12 @@ | |
| 64 | 			codeView.instanceChanged(this, true); | |
| 65 | 		} else this.getSimpleVirtualizer()?.requestHeightReconcile(this); | |
| 66 | 	} | |
| 67 | -	applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false) { | |
| 68 | +	applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer = false, startLine = 0) { | |
| 69 | 		const { renderRange: previousRenderRange } = this; | |
| 70 | 		this.getAdvancedVirtualizer()?.capturePendingLayoutAnchor(); | |
| 71 | 		super.applyDocumentChange(textDocument, newLineAnnotations); | |
| 72 | 		this.getSimpleVirtualizer()?.markDOMDirty(); | |
| 73 | -		this.resetLayoutCache(this.isSimpleMode(), false); | |
| 74 | +		this.resetLayoutCache(this.isSimpleMode(), false, startLine); | |
| 75 | 		const file = this.getRenderedFile(); | |
| 76 | 		if (!this.isSimpleMode()) this.computeApproximateSize(true); | |
| 77 | 		else if (shouldUpdateBuffer && previousRenderRange != null && file != null) { | |
| 78 | diff --git a/dist/editor/editor.js b/dist/editor/editor.js | |
| 79 | --- a/dist/editor/editor.js | |
| 80 | +++ b/dist/editor/editor.js | |
| 81 | @@ -1899,7 +1899,11 @@ | |
| 82 | 			changedDocumentLines: this.#isDiff && !didLineCountChange ? getChangedDocumentLines(textDocument, change) : void 0, | |
| 83 | 			documentLineCount: textDocument.lineCount | |
| 84 | 		}); | |
| 85 | -		if (didLineCountChange) applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer); | |
| 86 | +		if (didLineCountChange) { | |
| 87 | +			// A wider or narrower line-number gutter can rewrap unchanged rows. | |
| 88 | +			const layoutStartLine = String(change.previousLineCount).length === String(change.lineCount).length ? change.startLine : 0; | |
| 89 | +			applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer, layoutStartLine); | |
| 90 | +		} | |
| 91 | 		if (didLineCountChange || this.#isDiff && this.#diffSyle === "unified") this.#resetCache(); | |
| 92 | 		if (newLineAnnotations != null) { | |
| 93 | 			this.#lineAnnotations = newLineAnnotations; | |
| 94 | diff --git a/dist/managers/ResizeManager.d.ts b/dist/managers/ResizeManager.d.ts | |
| 95 | --- a/dist/managers/ResizeManager.d.ts | |
| 96 | +++ b/dist/managers/ResizeManager.d.ts | |
| 97 | @@ -5,6 +5,8 @@ | |
| 98 | columnVariables?: ResizeManagerColumnVariableMode; | |
| 99 | } | |
| 100 | declare class ResizeManager { | |
| 101 | + /** Schedule owner measurement after an observed code or gutter size change. */ | |
| 102 | + onResize?: () => void; | |
| 103 | private static resizeObserver; | |
| 104 | private static managersByElement; | |
| 105 | private static getResizeObserver; | |
| 106 | diff --git a/dist/managers/ResizeManager.js b/dist/managers/ResizeManager.js | |
| 107 | --- a/dist/managers/ResizeManager.js | |
| 108 | +++ b/dist/managers/ResizeManager.js | |
| 109 | @@ -19,6 +19,7 @@ | |
| 110 | 		for (const [manager, managerEntries] of entriesByManager) manager.handleResizeEntries(managerEntries); | |
| 111 | 	} | |
| 112 | 	observedNodes = /* @__PURE__ */ new Map(); | |
| 113 | +	onResize; | |
| 114 | 	setup(pre, { disableAnnotations, columnVariables = "apply" }) { | |
| 115 | 		const annotationUpdates = /* @__PURE__ */ new Set(); | |
| 116 | 		const applyColumnVariables = columnVariables === "apply"; | |
| 117 | @@ -212,6 +213,7 @@ | |
| 118 | 		this.applyAnnotationUpdates(annotationUpdates); | |
| 119 | 		annotationUpdates.clear(); | |
| 120 | 		this.applyColumnUpdates(codeUpdates); | |
| 121 | +		if (codeUpdates.size > 0) this.onResize?.(); | |
| 122 | 		codeUpdates.clear(); | |
| 123 | 	} | |
| 124 | 	applyAnnotationUpdates(annotationUpdates) { | |
| 125 | diff --git a/package.json b/package.json | |
| 126 | --- a/package.json | |
| 127 | +++ b/package.json | |
| 128 | @@ -58,6 +58,18 @@ | |
| 129 | "./worker/worker-portable.js": { | |
| 130 | "types": "./dist/worker/worker-portable.d.ts", | |
| 131 | "import": "./dist/worker/worker-portable.js" | |
| 132 | + }, | |
| 133 | + "./types": { | |
| 134 | + "types": "./dist/types.d.ts", | |
| 135 | + "import": "./dist/types.js" | |
| 136 | + }, | |
| 137 | + "./utils/getFiletypeFromFileName": { | |
| 138 | + "types": "./dist/utils/getFiletypeFromFileName.d.ts", | |
| 139 | + "import": "./dist/utils/getFiletypeFromFileName.js" | |
| 140 | + }, | |
| 141 | + "./utils/parsePatchFiles": { | |
| 142 | + "types": "./dist/utils/parsePatchFiles.d.ts", | |
| 143 | + "import": "./dist/utils/parsePatchFiles.js" | |
| 144 | } | |
| 145 | }, | |
| 146 | "publishConfig": { |
pnpm-lock.yaml+89-29| ... | ... | @@ -25,8 +25,8 @@ catalogs: |
| 25 | 25 | specifier: 1.8.0 |
| 26 | 26 | version: 1.8.0 |
| 27 | 27 | '@pierre/diffs': |
| 28 | specifier: 1.3.0-beta.10 | |
| 29 | version: 1.3.0-beta.10 | |
| 28 | specifier: 1.5.2 | |
| 29 | version: 1.5.2 | |
| 30 | 30 | jose: |
| 31 | 31 | specifier: 6.2.2 |
| 32 | 32 | version: 6.2.2 |
| ... | ... | @@ -103,7 +103,7 @@ patchedDependencies: |
| 103 | 103 | '@opencode/client@2.0.23': f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd |
| 104 | 104 | '@opencode/protocol@2.0.23': 17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685 |
| 105 | 105 | '@opencode/schema@2.0.23': 650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59 |
| 106 | '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 | |
| 106 | '@pierre/diffs@1.5.2': ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289 | |
| 107 | 107 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d |
| 108 | 108 | '@react-native-menu/menu@2.0.0': a4866ecfcd44f318043de839d20110c17e14909004900f002ffe12050376f2ba |
| 109 | 109 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 |
| ... | ... | @@ -277,7 +277,7 @@ importers: |
| 277 | 277 | version: 1.9.1 |
| 278 | 278 | '@pierre/diffs': |
| 279 | 279 | specifier: 'catalog:' |
| 280 | 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) | |
| 280 | 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) | |
| 281 | 281 | '@react-native-ai/apple': |
| 282 | 282 | specifier: 0.12.0 |
| 283 | 283 | 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: |
| 502 | 502 | specifier: 4.0.1 |
| 503 | 503 | 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) |
| 504 | 504 | '@pierre/trees': |
| 505 | specifier: 1.0.0-beta.4 | |
| 506 | version: 1.0.0-beta.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) | |
| 505 | specifier: 1.0.0-beta.6 | |
| 506 | 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) | |
| 507 | 507 | '@types/react': |
| 508 | 508 | specifier: ~19.3.0 |
| 509 | 509 | version: 19.3.0 |
| ... | ... | @@ -666,10 +666,10 @@ importers: |
| 666 | 666 | version: 1.8.0 |
| 667 | 667 | '@pierre/diffs': |
| 668 | 668 | specifier: 'catalog:' |
| 669 | 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) | |
| 669 | 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) | |
| 670 | 670 | '@pierre/trees': |
| 671 | specifier: 1.0.0-beta.4 | |
| 672 | version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) | |
| 671 | specifier: 1.0.0-beta.6 | |
| 672 | 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) | |
| 673 | 673 | '@t3tools/client-runtime': |
| 674 | 674 | specifier: workspace:* |
| 675 | 675 | version: link:../../packages/client-runtime |
| ... | ... | @@ -4444,18 +4444,27 @@ packages: |
| 4444 | 4444 | resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} |
| 4445 | 4445 | engines: {node: '>=14.18.0'} |
| 4446 | 4446 | |
| 4447 | '@pierre/diffs@1.3.0-beta.10': | |
| 4448 | resolution: {integrity: sha512-efyFM9GRfI6WkmHJP0CnZBopuM8yCwGqIKbZHoe1D5PV15VDkr7Vpi8EZt40AYrN1km//utQtYhHDSwt2KwjSg==} | |
| 4447 | '@pierre/diffs@1.5.2': | |
| 4448 | resolution: {integrity: sha512-QVxIWQEnNoko22k63maERzozd29+FtaUNpRrZiw603WMe5R1mFSED+jMtPkcVP3SDYS1DWg5K7sa7sJbr3xkNQ==} | |
| 4449 | 4449 | peerDependencies: |
| 4450 | 4450 | react: ^18.3.1 || ^19.0.0 |
| 4451 | 4451 | react-dom: ^18.3.1 || ^19.0.0 |
| 4452 | peerDependenciesMeta: | |
| 4453 | react: | |
| 4454 | optional: true | |
| 4455 | react-dom: | |
| 4456 | optional: true | |
| 4452 | 4457 | |
| 4453 | 4458 | '@pierre/theme@1.1.0': |
| 4454 | 4459 | resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} |
| 4455 | 4460 | engines: {vscode: ^1.0.0} |
| 4456 | 4461 | |
| 4457 | '@pierre/theming@0.0.2': | |
| 4458 | resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==} | |
| 4462 | '@pierre/theme@2.0.0': | |
| 4463 | resolution: {integrity: sha512-yNDd9GYLQl1mEUJR8AneJ5e4ohLIHQd/wZLWr4fagt78vS2RwwZNW530vVgHqXFAyFVcFlRmGUD5ramXH46OXw==} | |
| 4464 | engines: {vscode: ^1.0.0} | |
| 4465 | ||
| 4466 | '@pierre/theming@1.0.0': | |
| 4467 | resolution: {integrity: sha512-WsdrnhKfjeyXGDikZmN9pkpeZ5S/cl6EE72feiSc0tlynT1tMYqXqouhuv/foK+PY9OEnebOAVRQn3+rAstR8g==} | |
| 4459 | 4468 | peerDependencies: |
| 4460 | 4469 | '@pierre/theme': ^1.1.0 |
| 4461 | 4470 | '@shikijs/themes': ^3.0.0 || ^4.0.0 |
| ... | ... | @@ -4474,8 +4483,28 @@ packages: |
| 4474 | 4483 | shiki: |
| 4475 | 4484 | optional: true |
| 4476 | 4485 | |
| 4477 | '@pierre/trees@1.0.0-beta.4': | |
| 4478 | resolution: {integrity: sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag==} | |
| 4486 | '@pierre/theming@1.0.1': | |
| 4487 | resolution: {integrity: sha512-WCI5Qd7iprDpISL9fBYOLe8RV53+b7mFNA3bPzl60/2CKCSrsKN8zEcep6Y3BAzvARlmca50zGjDodqPGiTUKA==} | |
| 4488 | peerDependencies: | |
| 4489 | '@pierre/theme': ^1.1.0 || ^2.0.0 | |
| 4490 | '@shikijs/themes': ^3.0.0 || ^4.0.0 | |
| 4491 | react: ^18.3.1 || ^19.0.0 | |
| 4492 | react-dom: ^18.3.1 || ^19.0.0 | |
| 4493 | shiki: ^3.0.0 || ^4.0.0 | |
| 4494 | peerDependenciesMeta: | |
| 4495 | '@pierre/theme': | |
| 4496 | optional: true | |
| 4497 | '@shikijs/themes': | |
| 4498 | optional: true | |
| 4499 | react: | |
| 4500 | optional: true | |
| 4501 | react-dom: | |
| 4502 | optional: true | |
| 4503 | shiki: | |
| 4504 | optional: true | |
| 4505 | ||
| 4506 | '@pierre/trees@1.0.0-beta.6': | |
| 4507 | resolution: {integrity: sha512-zxeuSFM9TveM7b5XofweJALCtm/tGYV9HZzdbf7Uf+kBxIlUyz24/EHaGRjB0dsmmfDQl2ETz7AWwJ15lhSnpw==} | |
| 4479 | 4508 | peerDependencies: |
| 4480 | 4509 | react: ^18.3.1 || ^19.0.0 |
| 4481 | 4510 | react-dom: ^18.3.1 || ^19.0.0 |
| ... | ... | @@ -14869,65 +14898,96 @@ snapshots: |
| 14869 | 14898 | tslib: 2.8.1 |
| 14870 | 14899 | webcrypto-core: 1.9.2 |
| 14871 | 14900 | |
| 14872 | '@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)': | |
| 14901 | '@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)': | |
| 14873 | 14902 | dependencies: |
| 14874 | '@pierre/theme': 1.1.0 | |
| 14875 | '@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) | |
| 14903 | '@pierre/theme': 2.0.0 | |
| 14904 | '@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) | |
| 14876 | 14905 | '@shikijs/transformers': 4.2.0 |
| 14877 | 14906 | diff: 9.0.0 |
| 14878 | 14907 | hast-util-to-html: 9.0.5 |
| 14879 | 14908 | lru_map: 0.4.1 |
| 14909 | shiki: 4.2.0 | |
| 14910 | optionalDependencies: | |
| 14880 | 14911 | react: 19.3.0 |
| 14881 | 14912 | react-dom: 19.3.0(react@19.3.0) |
| 14882 | shiki: 4.2.0 | |
| 14883 | 14913 | transitivePeerDependencies: |
| 14884 | 14914 | - '@shikijs/themes' |
| 14885 | 14915 | |
| 14886 | '@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)': | |
| 14916 | '@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)': | |
| 14887 | 14917 | dependencies: |
| 14888 | '@pierre/theme': 1.1.0 | |
| 14889 | '@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) | |
| 14918 | '@pierre/theme': 2.0.0 | |
| 14919 | '@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) | |
| 14890 | 14920 | '@shikijs/transformers': 4.2.0 |
| 14891 | 14921 | diff: 9.0.0 |
| 14892 | 14922 | hast-util-to-html: 9.0.5 |
| 14893 | 14923 | lru_map: 0.4.1 |
| 14924 | shiki: 4.2.0 | |
| 14925 | optionalDependencies: | |
| 14894 | 14926 | react: 19.2.6 |
| 14895 | 14927 | react-dom: 19.2.6(react@19.2.6) |
| 14896 | shiki: 4.2.0 | |
| 14897 | 14928 | transitivePeerDependencies: |
| 14898 | 14929 | - '@shikijs/themes' |
| 14899 | 14930 | |
| 14900 | '@pierre/theme@1.1.0': {} | |
| 14931 | '@pierre/theme@1.1.0': | |
| 14932 | optional: true | |
| 14933 | ||
| 14934 | '@pierre/theme@2.0.0': {} | |
| 14901 | 14935 | |
| 14902 | '@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)': | |
| 14936 | '@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)': | |
| 14903 | 14937 | optionalDependencies: |
| 14904 | 14938 | '@pierre/theme': 1.1.0 |
| 14939 | '@shikijs/themes': 4.3.0 | |
| 14940 | react: 19.2.6 | |
| 14941 | react-dom: 19.2.6(react@19.2.6) | |
| 14942 | shiki: 4.2.0 | |
| 14943 | ||
| 14944 | '@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)': | |
| 14945 | optionalDependencies: | |
| 14946 | '@pierre/theme': 2.0.0 | |
| 14905 | 14947 | '@shikijs/themes': 4.2.0 |
| 14906 | 14948 | react: 19.3.0 |
| 14907 | 14949 | react-dom: 19.3.0(react@19.3.0) |
| 14908 | 14950 | shiki: 4.2.0 |
| 14909 | 14951 | |
| 14910 | '@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)': | |
| 14952 | '@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)': | |
| 14911 | 14953 | optionalDependencies: |
| 14912 | '@pierre/theme': 1.1.0 | |
| 14954 | '@pierre/theme': 2.0.0 | |
| 14955 | '@shikijs/themes': 4.2.0 | |
| 14956 | react: 19.3.0 | |
| 14957 | react-dom: 19.3.0(react@19.3.0) | |
| 14958 | shiki: 4.2.0 | |
| 14959 | ||
| 14960 | '@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)': | |
| 14961 | optionalDependencies: | |
| 14962 | '@pierre/theme': 2.0.0 | |
| 14913 | 14963 | '@shikijs/themes': 4.3.0 |
| 14914 | 14964 | react: 19.2.6 |
| 14915 | 14965 | react-dom: 19.2.6(react@19.2.6) |
| 14916 | 14966 | shiki: 4.2.0 |
| 14917 | 14967 | |
| 14918 | '@pierre/trees@1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': | |
| 14968 | '@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)': | |
| 14919 | 14969 | dependencies: |
| 14970 | '@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) | |
| 14920 | 14971 | preact: 11.0.0-beta.0 |
| 14921 | 14972 | preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) |
| 14922 | 14973 | react: 19.2.6 |
| 14923 | 14974 | react-dom: 19.2.6(react@19.2.6) |
| 14975 | transitivePeerDependencies: | |
| 14976 | - '@pierre/theme' | |
| 14977 | - '@shikijs/themes' | |
| 14978 | - shiki | |
| 14924 | 14979 | |
| 14925 | '@pierre/trees@1.0.0-beta.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': | |
| 14980 | '@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)': | |
| 14926 | 14981 | dependencies: |
| 14982 | '@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) | |
| 14927 | 14983 | preact: 11.0.0-beta.0 |
| 14928 | 14984 | preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) |
| 14929 | 14985 | react: 19.3.0 |
| 14930 | 14986 | react-dom: 19.3.0(react@19.3.0) |
| 14987 | transitivePeerDependencies: | |
| 14988 | - '@pierre/theme' | |
| 14989 | - '@shikijs/themes' | |
| 14990 | - shiki | |
| 14931 | 14991 | |
| 14932 | 14992 | '@polka/url@1.0.0-next.29': {} |
| 14933 | 14993 |
pnpm-workspace.yaml+4-2| ... | ... | @@ -41,7 +41,7 @@ catalog: |
| 41 | 41 | "@legendapp/list": 3.3.5 |
| 42 | 42 | "@noble/curves": 1.9.1 |
| 43 | 43 | "@noble/hashes": 1.8.0 |
| 44 | "@pierre/diffs": 1.3.0-beta.10 | |
| 44 | "@pierre/diffs": 1.5.2 | |
| 45 | 45 | "@tailwindcss/node": 4.3.3 |
| 46 | 46 | "@tailwindcss/oxide": 4.3.3 |
| 47 | 47 | "@tailwindcss/vite": 4.3.3 |
| ... | ... | @@ -270,7 +270,9 @@ patchedDependencies: |
| 270 | 270 | "@opencode/client@2.0.23": patches/@opencode__client@2.0.23.patch |
| 271 | 271 | "@opencode/protocol@2.0.23": patches/@opencode__protocol@2.0.23.patch |
| 272 | 272 | "@opencode/schema@2.0.23": patches/@opencode__schema@2.0.23.patch |
| 273 | "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch | |
| 273 | # Exports the dependency-light subpaths mobile and the web diff parser import, and keeps | |
| 274 | # measured wrapped-row heights above an edit (fileEditorVirtualization.test.ts). | |
| 275 | "@pierre/diffs@1.5.2": patches/@pierre%2Fdiffs@1.5.2.patch | |
| 274 | 276 | "@react-native-ai/apple@0.12.0": patches/@react-native-ai__apple@0.12.0.patch |
| 275 | 277 | "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch |
| 276 | 278 | "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch |