| 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,7 +131,7 @@ |
| 131 | }, | 131 | }, |
| 132 | "devDependencies": { | 132 | "devDependencies": { |
| 133 | "@effect/vitest": "catalog:", | 133 | "@effect/vitest": "catalog:", |
| 134 | "@pierre/trees": "1.0.0-beta.4", | 134 | "@pierre/trees": "1.0.0-beta.6", |
| 135 | "@types/react": "~19.3.0", | 135 | "@types/react": "~19.3.0", |
| 136 | "@types/react-dom": "~19.2.3", | 136 | "@types/react-dom": "~19.2.3", |
| 137 | "babel-preset-expo": "~58.0.8", | 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,7 +2,6 @@ import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; |
| 2 | import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types"; | 2 | import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types"; |
| 3 | import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; | 3 | import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; |
| 4 | import type { ReviewDiffPreviewSource } from "@t3tools/contracts"; | 4 | import type { ReviewDiffPreviewSource } from "@t3tools/contracts"; |
| 5 | import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; | ||
| 6 | import * as Arr from "effect/Array"; | 5 | import * as Arr from "effect/Array"; |
| 7 | import { pipe } from "effect/Function"; | 6 | import { pipe } from "effect/Function"; |
| 8 | import * as Order from "effect/Order"; | 7 | import * as Order from "effect/Order"; |
| ... | @@ -374,8 +373,8 @@ function buildRenderableRows(file: FileDiffMetadata): ReadonlyArray<ReviewRender | ... | @@ -374,8 +373,8 @@ function buildRenderableRows(file: FileDiffMetadata): ReadonlyArray<ReviewRender |
| 374 | } | 373 | } |
| 375 | 374 | ||
| 376 | function mapRenderableFile(file: FileDiffMetadata): ReviewRenderableFile { | 375 | function mapRenderableFile(file: FileDiffMetadata): ReviewRenderableFile { |
| 377 | const path = unquoteGitPatchPath(file.name || file.prevName || ""); | 376 | const path = file.name || file.prevName || ""; |
| 378 | const previousPath = file.prevName ? unquoteGitPatchPath(file.prevName) : null; | 377 | const previousPath = file.prevName || null; |
| 379 | const additions = file.hunks.reduce((total, hunk) => total + hunk.additionLines, 0); | 378 | const additions = file.hunks.reduce((total, hunk) => total + hunk.additionLines, 0); |
| 380 | const deletions = file.hunks.reduce((total, hunk) => total + hunk.deletionLines, 0); | 379 | const deletions = file.hunks.reduce((total, hunk) => total + hunk.deletionLines, 0); |
| 381 | const cacheKey = file.cacheKey ?? `${previousPath ?? "none"}:${path}:${file.type}`; | 380 | const cacheKey = file.cacheKey ?? `${previousPath ?? "none"}:${path}:${file.type}`; |
apps/web/package.json+1-1| ... | @@ -25,7 +25,7 @@ | ... | @@ -25,7 +25,7 @@ |
| 25 | "@legendapp/list": "catalog:", | 25 | "@legendapp/list": "catalog:", |
| 26 | "@noble/hashes": "catalog:", | 26 | "@noble/hashes": "catalog:", |
| 27 | "@pierre/diffs": "catalog:", | 27 | "@pierre/diffs": "catalog:", |
| 28 | "@pierre/trees": "1.0.0-beta.4", | 28 | "@pierre/trees": "1.0.0-beta.6", |
| 29 | "@t3tools/client-runtime": "workspace:*", | 29 | "@t3tools/client-runtime": "workspace:*", |
| 30 | "@t3tools/contracts": "workspace:*", | 30 | "@t3tools/contracts": "workspace:*", |
| 31 | "@t3tools/shared": "workspace:*", | 31 | "@t3tools/shared": "workspace:*", |
apps/web/src/components/diffs/AnnotatableCodeView.tsx+1-1| ... | @@ -35,7 +35,7 @@ interface DiffCommentAnnotationGroup { | ... | @@ -35,7 +35,7 @@ interface DiffCommentAnnotationGroup { |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | type DiffCommentLineAnnotation = DiffLineAnnotation<DiffCommentAnnotationGroup>; | 37 | type DiffCommentLineAnnotation = DiffLineAnnotation<DiffCommentAnnotationGroup>; |
| 38 | export type AnnotatableCodeViewHandle = CodeViewHandle<DiffCommentAnnotationGroup>; | 38 | export type AnnotatableCodeViewHandle = CodeViewHandle<DiffCommentAnnotationGroup, undefined>; |
| 39 | const EMPTY_REVIEW_COMMENTS: ReadonlyArray<ReviewCommentContext> = []; | 39 | const EMPTY_REVIEW_COMMENTS: ReadonlyArray<ReviewCommentContext> = []; |
| 40 | 40 | ||
| 41 | function annotationSide(range: SelectedLineRange): AnnotationSide { | 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,7 +98,7 @@ vi.mock("@pierre/diffs/worker/worker.js?worker", async () => { |
| 98 | 98 | ||
| 99 | vi.mock("@pierre/diffs/react", async (importOriginal) => ({ | 99 | vi.mock("@pierre/diffs/react", async (importOriginal) => ({ |
| 100 | ...(await importOriginal<typeof import("@pierre/diffs/react")>()), | 100 | ...(await importOriginal<typeof import("@pierre/diffs/react")>()), |
| 101 | CodeView: (props: CodeViewProps) => { | 101 | CodeView: (props: CodeViewProps<undefined, undefined>) => { |
| 102 | return props.items?.map((item) => | 102 | return props.items?.map((item) => |
| 103 | item.type === "file" ? <FileOutput key={item.id} file={item.file} /> : null, | 103 | item.type === "file" ? <FileOutput key={item.id} file={item.file} /> : null, |
| 104 | ); | 104 | ); |
| ... | @@ -115,6 +115,7 @@ function FileOutput({ file }: { file: FileContents }) { | ... | @@ -115,6 +115,7 @@ function FileOutput({ file }: { file: FileContents }) { |
| 115 | testState.renderPools.push(pool); | 115 | testState.renderPools.push(pool); |
| 116 | const renderer = new FileRenderer( | 116 | const renderer = new FileRenderer( |
| 117 | { theme: "pierre-dark", preferredHighlighter: "shiki-wasm" }, | 117 | { theme: "pierre-dark", preferredHighlighter: "shiki-wasm" }, |
| 118 | undefined, | ||
| 118 | render, | 119 | render, |
| 119 | pool, | 120 | pool, |
| 120 | ); | 121 | ); |
| ... | @@ -301,8 +302,10 @@ describe("code-view worker lifecycle", () => { | ... | @@ -301,8 +302,10 @@ describe("code-view worker lifecycle", () => { |
| 301 | expect(pool.getStats().totalWorkers).toBe(2); | 302 | expect(pool.getStats().totalWorkers).toBe(2); |
| 302 | expect(testState.terminations).toHaveLength(0); | 303 | expect(testState.terminations).toHaveLength(0); |
| 303 | await act(async () => renderer!.update(renderViews(0))); | 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 | await vi.advanceTimersByTimeAsync(30_000); | 307 | await vi.advanceTimersByTimeAsync(30_000); |
| 305 | await pending; | 308 | await timedOut; |
| 306 | await Promise.all(testState.terminations); | 309 | await Promise.all(testState.terminations); |
| 307 | await act(async () => { | 310 | await act(async () => { |
| 308 | for (const deliver of testState.heldResponses) deliver(); | 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,16 +261,16 @@ const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS} |
| 261 | `; | 261 | `; |
| 262 | 262 | ||
| 263 | export type StyledDiffCodeViewOptions<LAnnotation> = Omit< | 263 | export type StyledDiffCodeViewOptions<LAnnotation> = Omit< |
| 264 | NonNullable<CodeViewProps<LAnnotation>["options"]>, | 264 | NonNullable<CodeViewProps<LAnnotation, undefined>["options"]>, |
| 265 | "unsafeCSS" | "itemMetrics" | "layout" | 265 | "unsafeCSS" | "itemMetrics" | "layout" |
| 266 | >; | 266 | >; |
| 267 | 267 | ||
| 268 | type StyledDiffCodeViewProps<LAnnotation> = ( | 268 | type StyledDiffCodeViewProps<LAnnotation> = ( |
| 269 | | Omit<ControlledCodeViewProps<LAnnotation>, "options"> | 269 | | Omit<ControlledCodeViewProps<LAnnotation, undefined>, "options"> |
| 270 | | Omit<UncontrolledCodeViewProps<LAnnotation>, "options"> | 270 | | Omit<UncontrolledCodeViewProps<LAnnotation, undefined>, "options"> |
| 271 | ) & { | 271 | ) & { |
| 272 | readonly options?: StyledDiffCodeViewOptions<LAnnotation>; | 272 | readonly options?: StyledDiffCodeViewOptions<LAnnotation>; |
| 273 | readonly viewerRef?: Ref<CodeViewHandle<LAnnotation>>; | 273 | readonly viewerRef?: Ref<CodeViewHandle<LAnnotation, undefined>>; |
| 274 | /** | 274 | /** |
| 275 | * Appended to the shared stylesheet inside the viewer's shadow root, for a surface that has | 275 | * Appended to the shared stylesheet inside the viewer's shadow root, for a surface that has |
| 276 | * to restyle chrome the viewer owns — such as replacing its per-file line counts. | 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,9 +11,23 @@ import { |
| 11 | isWorkspaceImagePreviewPath, | 11 | isWorkspaceImagePreviewPath, |
| 12 | isWorkspaceVideoPreviewPath, | 12 | isWorkspaceVideoPreviewPath, |
| 13 | } from "@t3tools/shared/filePreview"; | 13 | } from "@t3tools/shared/filePreview"; |
| 14 | import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; | 14 | import { |
| 15 | import { Editor } from "@pierre/diffs/editor"; | 15 | DEFAULT_TOKENIZE_MAX_LENGTH, |
| 16 | import { EditProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; | 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 | import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; | 31 | import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; |
| 18 | import { | 32 | import { |
| 19 | isAtomCommandInterrupted, | 33 | isAtomCommandInterrupted, |
| ... | @@ -80,7 +94,7 @@ import { | ... | @@ -80,7 +94,7 @@ import { |
| 80 | import SourceFilePreview from "./ReadOnlySourcePreview"; | 94 | import SourceFilePreview from "./ReadOnlySourcePreview"; |
| 81 | import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; | 95 | import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; |
| 82 | import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; | 96 | import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; |
| 83 | import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; | 97 | import { projectFileCacheKey } from "./fileContentRevision"; |
| 84 | import { | 98 | import { |
| 85 | filePreviewReadErrorMessage, | 99 | filePreviewReadErrorMessage, |
| 86 | isMarkdownPreviewFile, | 100 | isMarkdownPreviewFile, |
| ... | @@ -91,6 +105,7 @@ import { | ... | @@ -91,6 +105,7 @@ import { |
| 91 | import { useFileSaveCoordinator } from "./useFileSaveCoordinator"; | 105 | import { useFileSaveCoordinator } from "./useFileSaveCoordinator"; |
| 92 | import { | 106 | import { |
| 93 | getOptimisticProjectFileQueryData, | 107 | getOptimisticProjectFileQueryData, |
| 108 | getProjectFileContents, | ||
| 94 | setProjectFileQueryData, | 109 | setProjectFileQueryData, |
| 95 | useProjectFileQuery, | 110 | useProjectFileQuery, |
| 96 | } from "./projectFilesQueryState"; | 111 | } from "./projectFilesQueryState"; |
| ... | @@ -117,7 +132,12 @@ const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; | ... | @@ -117,7 +132,12 @@ const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; |
| 117 | const RENDER_MARKDOWN_STORAGE_KEY = "t3code.renderMarkdown"; | 132 | const RENDER_MARKDOWN_STORAGE_KEY = "t3code.renderMarkdown"; |
| 118 | const RENDER_BROWSER_FILE_STORAGE_KEY = "t3code.renderBrowserFile"; | 133 | const RENDER_BROWSER_FILE_STORAGE_KEY = "t3code.renderBrowserFile"; |
| 119 | const RENDER_TABLE_STORAGE_KEY = "t3code.renderTable"; | 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 | function WorkspaceImagePreview(props: { | 142 | function WorkspaceImagePreview(props: { |
| 123 | readonly environmentId: EnvironmentId; | 143 | readonly environmentId: EnvironmentId; |
| ... | @@ -547,6 +567,67 @@ function useFileLineReveal( | ... | @@ -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 | interface EditableFileSurfaceProps { | 631 | interface EditableFileSurfaceProps { |
| 551 | environmentId: EnvironmentId; | 632 | environmentId: EnvironmentId; |
| 552 | cwd: string; | 633 | cwd: string; |
| ... | @@ -597,46 +678,65 @@ function EditableFileSurface({ | ... | @@ -597,46 +678,65 @@ function EditableFileSurface({ |
| 597 | relativePath, | 678 | relativePath, |
| 598 | onPendingChange, | 679 | onPendingChange, |
| 599 | }); | 680 | }); |
| 600 | const editor = useMemo( | 681 | // The editor owns the draft, so its own edits echoing back through the file |
| 601 | () => | 682 | // query keep the file identity. Only a change from elsewhere replaces it. |
| 602 | new Editor<FileCommentAnnotationGroup>({ | 683 | const [externalFile, setExternalFile] = useState(() => |
| 603 | persistState: true, | 684 | editableFileContents(environmentId, cwd, relativePath, contents), |
| 604 | persistStateStorage: "inMemory", | 685 | ); |
| 605 | onChange: (file, nextLineAnnotations) => { | 686 | const [editedContents, setEditedContents] = useState<string | null>(null); |
| 606 | setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); | 687 | if (contents !== (editedContents ?? externalFile.contents)) { |
| 607 | saveCoordinator.change(file.contents); | 688 | setExternalFile(editableFileContents(environmentId, cwd, relativePath, contents)); |
| 608 | if (nextLineAnnotations) { | 689 | setEditedContents(null); |
| 609 | const remapped = remapFileCommentAnnotations( | 690 | } |
| 610 | nextLineAnnotations as FileCommentLineAnnotation[], | 691 | const { ready: editable, onPostRender: onEditablePostRender } = |
| 611 | ); | 692 | useEditableAfterHighlight(externalFile); |
| 612 | setLineAnnotations(remapped); | 693 | const editorRef = useRef<Editor<"file", FileCommentAnnotationGroup, undefined> | null>(null); |
| 613 | for (const annotation of remapped) { | 694 | const editorOptions = useMemo<EditorOptions<"file", FileCommentAnnotationGroup, undefined>>( |
| 614 | for (const entry of annotation.metadata.entries) { | 695 | () => ({ |
| 615 | if (entry.kind !== "comment") continue; | 696 | onAttach: (editor) => { |
| 616 | addReviewComment( | 697 | editorRef.current = editor; |
| 617 | composerDraftTarget, | 698 | }, |
| 618 | buildFileReviewComment({ | 699 | onComplete: () => { |
| 619 | id: entry.id, | 700 | editorRef.current = null; |
| 620 | filePath: relativePath, | 701 | }, |
| 621 | startLine: entry.startLine, | 702 | }), |
| 622 | endLine: entry.endLine, | 703 | [], |
| 623 | text: entry.text, | ||
| 624 | contents: file.contents, | ||
| 625 | }), | ||
| 626 | ); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | } | ||
| 630 | }, | ||
| 631 | }), | ||
| 632 | [addReviewComment, composerDraftTarget, cwd, environmentId, relativePath, saveCoordinator], | ||
| 633 | ); | 704 | ); |
| 634 | 705 | ||
| 635 | useEffect( | 706 | // Mirrors the draft out to the save queue, the optimistic file query and the |
| 636 | () => () => { | 707 | // composer's review comments. |
| 637 | editor.cleanUp(); | 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 | const removeAnnotationEntry = useCallback( | 742 | const removeAnnotationEntry = useCallback( |
| ... | @@ -695,47 +795,44 @@ function EditableFileSurface({ | ... | @@ -695,47 +795,44 @@ function EditableFileSurface({ |
| 695 | ], | 795 | ], |
| 696 | ); | 796 | ); |
| 697 | 797 | ||
| 698 | const beginComment = useCallback( | 798 | const beginComment = useCallback((range: SelectedLineRange) => { |
| 699 | (range: SelectedLineRange) => { | 799 | editorRef.current?.setSelections([]); |
| 700 | editor.setSelections([]); | 800 | editorRef.current?.blur(); |
| 701 | editor.blur(); | 801 | const { startLine, endLine } = normalizeFileCommentRange(range); |
| 702 | const { startLine, endLine } = normalizeFileCommentRange(range); | 802 | const draftEntry: FileCommentAnnotationEntry = { |
| 703 | const draftEntry: FileCommentAnnotationEntry = { | 803 | id: nextFileCommentId(), |
| 704 | id: nextFileCommentId(), | 804 | kind: "draft", |
| 705 | kind: "draft", | 805 | startLine, |
| 706 | startLine, | 806 | endLine, |
| 707 | endLine, | 807 | text: "", |
| 708 | text: "", | 808 | }; |
| 709 | }; | 809 | setLineAnnotations((current) => { |
| 710 | setLineAnnotations((current) => { | 810 | const withoutDraft = current.flatMap((annotation) => { |
| 711 | const withoutDraft = current.flatMap((annotation) => { | 811 | const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); |
| 712 | const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); | 812 | return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; |
| 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 | ); | ||
| 735 | }); | 813 | }); |
| 736 | }, | 814 | const existingIndex = withoutDraft.findIndex( |
| 737 | [editor], | 815 | (annotation) => annotation.lineNumber === endLine, |
| 738 | ); | 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 | const hasOpenCommentForm = lineAnnotations.some((annotation) => | 836 | const hasOpenCommentForm = lineAnnotations.some((annotation) => |
| 740 | annotation.metadata.entries.some((entry) => entry.kind === "draft"), | 837 | annotation.metadata.entries.some((entry) => entry.kind === "draft"), |
| 741 | ); | 838 | ); |
| ... | @@ -744,11 +841,11 @@ function EditableFileSurface({ | ... | @@ -744,11 +841,11 @@ function EditableFileSurface({ |
| 744 | if (!root) return; | 841 | if (!root) return; |
| 745 | return installFileEditorDismissal({ | 842 | return installFileEditorDismissal({ |
| 746 | root, | 843 | root, |
| 747 | editor, | 844 | editor: { setSelections: (selections) => editorRef.current?.setSelections(selections) }, |
| 748 | isBlocked: () => hasOpenCommentForm, | 845 | isBlocked: () => hasOpenCommentForm, |
| 749 | onDismiss: () => setSelectedRange(null), | 846 | onDismiss: () => setSelectedRange(null), |
| 750 | }); | 847 | }); |
| 751 | }, [editor, hasOpenCommentForm, setSelectedRange]); | 848 | }, [hasOpenCommentForm, setSelectedRange]); |
| 752 | const handleLineSelectionEnd = useCallback( | 849 | const handleLineSelectionEnd = useCallback( |
| 753 | (range: SelectedLineRange | null) => { | 850 | (range: SelectedLineRange | null) => { |
| 754 | setSelectedRange(range); | 851 | setSelectedRange(range); |
| ... | @@ -762,6 +859,7 @@ function EditableFileSurface({ | ... | @@ -762,6 +859,7 @@ function EditableFileSurface({ |
| 762 | const handlePostRender = useCallback<FilePostRender>( | 859 | const handlePostRender = useCallback<FilePostRender>( |
| 763 | (fileContainer, instance, phase) => { | 860 | (fileContainer, instance, phase) => { |
| 764 | onPostRender(fileContainer, instance, phase); | 861 | onPostRender(fileContainer, instance, phase); |
| 862 | onEditablePostRender(instance.file, phase); | ||
| 765 | 863 | ||
| 766 | if (selectionFrameRef.current !== null) { | 864 | if (selectionFrameRef.current !== null) { |
| 767 | cancelAnimationFrame(selectionFrameRef.current); | 865 | cancelAnimationFrame(selectionFrameRef.current); |
| ... | @@ -775,11 +873,11 @@ function EditableFileSurface({ | ... | @@ -775,11 +873,11 @@ function EditableFileSurface({ |
| 775 | instance.setSelectedLines(selectedRange, { notify: false }); | 873 | instance.setSelectedLines(selectedRange, { notify: false }); |
| 776 | }); | 874 | }); |
| 777 | }, | 875 | }, |
| 778 | [onPostRender, selectedRange], | 876 | [onEditablePostRender, onPostRender, selectedRange], |
| 779 | ); | 877 | ); |
| 780 | 878 | ||
| 781 | return ( | 879 | return ( |
| 782 | <EditProvider editor={editor}> | 880 | <EditProvider createEditor={createFileEditor}> |
| 783 | <div ref={surfaceRef} className="flex min-h-0 flex-1"> | 881 | <div ref={surfaceRef} className="flex min-h-0 flex-1"> |
| 784 | <Virtualizer | 882 | <Virtualizer |
| 785 | className="file-preview-virtualizer min-h-0 flex-1 overflow-auto" | 883 | className="file-preview-virtualizer min-h-0 flex-1 overflow-auto" |
| ... | @@ -789,17 +887,10 @@ function EditableFileSurface({ | ... | @@ -789,17 +887,10 @@ function EditableFileSurface({ |
| 789 | }} | 887 | }} |
| 790 | > | 888 | > |
| 791 | <File<FileCommentAnnotationGroup> | 889 | <File<FileCommentAnnotationGroup> |
| 792 | file={{ | 890 | file={externalFile} |
| 793 | name: relativePath, | 891 | edit={editable} |
| 794 | contents, | 892 | editorOptions={editorOptions} |
| 795 | cacheKey: projectFileEditorCacheKey( | 893 | onEditChange={handleEditChange} |
| 796 | environmentId, | ||
| 797 | cwd, | ||
| 798 | relativePath, | ||
| 799 | contents, | ||
| 800 | editor.getFile(), | ||
| 801 | ), | ||
| 802 | }} | ||
| 803 | options={{ | 894 | options={{ |
| 804 | disableFileHeader: true, | 895 | disableFileHeader: true, |
| 805 | enableGutterUtility: !hasOpenCommentForm, | 896 | enableGutterUtility: !hasOpenCommentForm, |
| ... | @@ -832,7 +923,6 @@ function EditableFileSurface({ | ... | @@ -832,7 +923,6 @@ function EditableFileSurface({ |
| 832 | </div> | 923 | </div> |
| 833 | )} | 924 | )} |
| 834 | className="min-h-full" | 925 | className="min-h-full" |
| 835 | contentEditable | ||
| 836 | /> | 926 | /> |
| 837 | </Virtualizer> | 927 | </Virtualizer> |
| 838 | </div> | 928 | </div> |
apps/web/src/components/files/ReadOnlySourcePreview.tsx+1-1| ... | @@ -17,7 +17,7 @@ export default function ReadOnlySourcePreview(props: { | ... | @@ -17,7 +17,7 @@ export default function ReadOnlySourcePreview(props: { |
| 17 | readonly name: string; | 17 | readonly name: string; |
| 18 | readonly text: string; | 18 | readonly text: string; |
| 19 | readonly cacheKey?: string; | 19 | readonly cacheKey?: string; |
| 20 | readonly onPostRender?: FileOptions<unknown>["onPostRender"]; | 20 | readonly onPostRender?: FileOptions<unknown, undefined>["onPostRender"]; |
| 21 | }) { | 21 | }) { |
| 22 | const { resolvedTheme } = useTheme(); | 22 | const { resolvedTheme } = useTheme(); |
| 23 | const wordWrap = useClientSettings((settings) => settings.wordWrap); | 23 | const wordWrap = useClientSettings((settings) => settings.wordWrap); |
apps/web/src/components/files/fileContentRevision.test.ts+1-24| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 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 | describe("file cache identity", () => { | 5 | describe("file cache identity", () => { |
| 6 | it("changes for same-length edits", () => { | 6 | it("changes for same-length edits", () => { |
| ... | @@ -8,27 +8,4 @@ describe("file cache identity", () => { | ... | @@ -8,27 +8,4 @@ describe("file cache identity", () => { |
| 8 | projectFileCacheKey("/repo", "file.json", "nodeVeasdrs"), | 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,21 +10,3 @@ function fileContentRevision(contents: string): string { |
| 10 | export function projectFileCacheKey(cwd: string, relativePath: string, contents: string): string { | 10 | export function projectFileCacheKey(cwd: string, relativePath: string, contents: string): string { |
| 11 | return `${cwd}:${relativePath}:${fileContentRevision(contents)}`; | 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,7 +4,7 @@ import { |
| 4 | Virtualizer, | 4 | Virtualizer, |
| 5 | type FileContents, | 5 | type FileContents, |
| 6 | } from "@pierre/diffs"; | 6 | } from "@pierre/diffs"; |
| 7 | import { Editor, TextDocument } from "@pierre/diffs/editor"; | 7 | import { Editor, TextDocument } from "@pierre/diffs/edit"; |
| 8 | import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite-plus/test"; | 8 | import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 9 | 9 | ||
| 10 | const renderingManagerUrl = new URL( | 10 | const renderingManagerUrl = new URL( |
| ... | @@ -22,10 +22,21 @@ class MeasuredElement { | ... | @@ -22,10 +22,21 @@ class MeasuredElement { |
| 22 | children: MeasuredElement[] = []; | 22 | children: MeasuredElement[] = []; |
| 23 | dataset: Record<string, string> = {}; | 23 | dataset: Record<string, string> = {}; |
| 24 | nextElementSibling: MeasuredElement | null = null; | 24 | nextElementSibling: MeasuredElement | null = null; |
| 25 | shadowRoot: MeasuredElement | null = null; | ||
| 25 | width = 283; | 26 | width = 283; |
| 26 | 27 | ||
| 27 | constructor(readonly height = 0) {} | 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 | getBoundingClientRect() { | 40 | getBoundingClientRect() { |
| 30 | MeasuredElement.geometryReads += 1; | 41 | MeasuredElement.geometryReads += 1; |
| 31 | return { top: 0, height: this.height, width: this.width }; | 42 | return { top: 0, height: this.height, width: this.width }; |
| ... | @@ -108,8 +119,10 @@ class LayoutVirtualizer extends Virtualizer { | ... | @@ -108,8 +119,10 @@ class LayoutVirtualizer extends Virtualizer { |
| 108 | 119 | ||
| 109 | class MeasuredFile extends VirtualizedFile { | 120 | class MeasuredFile extends VirtualizedFile { |
| 110 | override top = 0; | 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 | this.editor = editor; | 126 | this.editor = editor; |
| 114 | return () => { | 127 | return () => { |
| 115 | this.editor = undefined; | 128 | this.editor = undefined; |
| ... | @@ -117,7 +130,9 @@ class MeasuredFile extends VirtualizedFile { | ... | @@ -117,7 +130,9 @@ class MeasuredFile extends VirtualizedFile { |
| 117 | } | 130 | } |
| 118 | 131 | ||
| 119 | async initialize(file: FileContents) { | 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 | await this.fileRenderer.initializeHighlighter(); | 136 | await this.fileRenderer.initializeHighlighter(); |
| 122 | expect( | 137 | expect( |
| 123 | this.fileRenderer.renderFile(file, { | 138 | this.fileRenderer.renderFile(file, { |
| ... | @@ -180,7 +195,7 @@ class MeasuredFile extends VirtualizedFile { | ... | @@ -180,7 +195,7 @@ class MeasuredFile extends VirtualizedFile { |
| 180 | } | 195 | } |
| 181 | 196 | ||
| 182 | const instances: MeasuredFile[] = []; | 197 | const instances: MeasuredFile[] = []; |
| 183 | const editors: Editor<undefined>[] = []; | 198 | const editors: Editor<"file", undefined, undefined>[] = []; |
| 184 | 199 | ||
| 185 | beforeAll(async () => { | 200 | beforeAll(async () => { |
| 186 | await getSharedHighlighter({ | 201 | await getSharedHighlighter({ |
| ... | @@ -225,7 +240,7 @@ async function makeFixture( | ... | @@ -225,7 +240,7 @@ async function makeFixture( |
| 225 | cacheKey: `wrapped:${overflow}`, | 240 | cacheKey: `wrapped:${overflow}`, |
| 226 | lang: "text", | 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 | const instance = new MeasuredFile( | 244 | const instance = new MeasuredFile( |
| 230 | { | 245 | { |
| 231 | overflow, | 246 | overflow, |
| ... | @@ -274,7 +289,7 @@ describe("wrapped editor document changes", () => { | ... | @@ -274,7 +289,7 @@ describe("wrapped editor document changes", () => { |
| 274 | const before = instance.getLinePosition(previousLastLine); | 289 | const before = instance.getLinePosition(previousLastLine); |
| 275 | expect(before).toEqual({ top: 120328, height: 60 }); | 290 | expect(before).toEqual({ top: 120328, height: 60 }); |
| 276 | const viewport = { top: before!.top - 100, bottom: before!.top + 80 }; | 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 | append(); | 294 | append(); |
| 280 | 295 | ||
| ... | @@ -282,7 +297,7 @@ describe("wrapped editor document changes", () => { | ... | @@ -282,7 +297,7 @@ describe("wrapped editor document changes", () => { |
| 282 | expect(instance.getLinePosition(previousLastLine)).toEqual({ top: before!.top, height: 20 }); | 297 | expect(instance.getLinePosition(previousLastLine)).toEqual({ top: before!.top, height: 20 }); |
| 283 | expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120348, height: 20 }); | 298 | expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120348, height: 20 }); |
| 284 | expect(instance.getVirtualizedHeight()).toBe(120376); | 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 | it("invalidates changed and shifted rows after an insertion in the middle", async () => { | 303 | it("invalidates changed and shifted rows after an insertion in the middle", async () => { |
| ... | @@ -373,7 +388,7 @@ describe("wrapped editor document changes", () => { | ... | @@ -373,7 +388,7 @@ describe("wrapped editor document changes", () => { |
| 373 | const { instance, file, append } = await makeFixture(); | 388 | const { instance, file, append } = await makeFixture(); |
| 374 | append(); | 389 | append(); |
| 375 | instance.setMetrics({ hunkLineCount: 50, lineHeight: 24, diffHeaderHeight: 44, spacing: 8 }); | 390 | instance.setMetrics({ hunkLineCount: 50, lineHeight: 24, diffHeaderHeight: 44, spacing: 8 }); |
| 376 | instance.prepareCodeViewItem(file, 0); | 391 | instance.updateCodeViewLayout(file, 0); |
| 377 | expect(instance.getLinePosition(6001)).toEqual({ top: 144008, height: 24 }); | 392 | expect(instance.getLinePosition(6001)).toEqual({ top: 144008, height: 24 }); |
| 378 | }); | 393 | }); |
| 379 | 394 | ||
| ... | @@ -381,7 +396,7 @@ describe("wrapped editor document changes", () => { | ... | @@ -381,7 +396,7 @@ describe("wrapped editor document changes", () => { |
| 381 | const { instance, file, append } = await makeFixture(); | 396 | const { instance, file, append } = await makeFixture(); |
| 382 | append(); | 397 | append(); |
| 383 | instance.setLineAnnotations([{ lineNumber: 10, metadata: undefined }]); | 398 | instance.setLineAnnotations([{ lineNumber: 10, metadata: undefined }]); |
| 384 | instance.prepareCodeViewItem(file, 0); | 399 | instance.updateCodeViewLayout(file, 0); |
| 385 | expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 }); | 400 | expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 }); |
| 386 | }); | 401 | }); |
| 387 | }); | 402 | }); |
| ... | @@ -476,11 +491,11 @@ describe("wrapped measurement widths", () => { | ... | @@ -476,11 +491,11 @@ describe("wrapped measurement widths", () => { |
| 476 | "document", | 491 | "document", |
| 477 | Object.assign(new EditorElement(), { createElement: () => new EditorElement() }), | 492 | Object.assign(new EditorElement(), { createElement: () => new EditorElement() }), |
| 478 | ); | 493 | ); |
| 479 | const first = new Editor<undefined>(); | 494 | const first = new Editor("file"); |
| 480 | editors.push(first); | 495 | editors.push(first); |
| 481 | first.edit(instance); | 496 | first.edit(instance); |
| 482 | first.cleanUp(); | 497 | first.cleanUp(); |
| 483 | const second = new Editor<undefined>(); | 498 | const second = new Editor("file"); |
| 484 | editors.push(second); | 499 | editors.push(second); |
| 485 | second.edit(instance); | 500 | second.edit(instance); |
| 486 | instance.resizeContent(482.25); | 501 | instance.resizeContent(482.25); |
| ... | @@ -531,12 +546,16 @@ class EditorElement extends MeasuredElement { | ... | @@ -531,12 +546,16 @@ class EditorElement extends MeasuredElement { |
| 531 | style: Record<string, string> = {}; | 546 | style: Record<string, string> = {}; |
| 532 | parentElement: EditorElement | null = null; | 547 | parentElement: EditorElement | null = null; |
| 533 | 548 | ||
| 534 | appendChild(child: EditorElement) { | 549 | override appendChild(child: EditorElement) { |
| 535 | child.parentElement = this; | 550 | child.parentElement = this; |
| 536 | this.children.push(child); | 551 | this.children.push(child); |
| 537 | return child; | 552 | return child; |
| 538 | } | 553 | } |
| 539 | 554 | ||
| 555 | append(child: EditorElement) { | ||
| 556 | this.appendChild(child); | ||
| 557 | } | ||
| 558 | |||
| 540 | prepend(child: EditorElement) { | 559 | prepend(child: EditorElement) { |
| 541 | child.parentElement = this; | 560 | child.parentElement = this; |
| 542 | this.children.unshift(child); | 561 | this.children.unshift(child); |
| ... | @@ -630,14 +649,15 @@ async function makeEditorFixture(lineCount: number) { | ... | @@ -630,14 +649,15 @@ async function makeEditorFixture(lineCount: number) { |
| 630 | langs: ["text"], | 649 | langs: ["text"], |
| 631 | preferredHighlighter: "shiki-wasm", | 650 | preferredHighlighter: "shiki-wasm", |
| 632 | }); | 651 | }); |
| 633 | const editor = new Editor<undefined>(); | 652 | const editor = new Editor("file"); |
| 634 | editors.push(editor); | 653 | editors.push(editor); |
| 635 | editor.edit(instance); | 654 | editor.edit(instance); |
| 636 | editor.__syncRenderView(highlighter, measuredElement(host), file, undefined, { | 655 | editor.__syncRenderView({ |
| 637 | startingLine: 0, | 656 | highlighter, |
| 638 | totalLines: 1, | 657 | fileContainer: measuredElement(host), |
| 639 | bufferBefore: 0, | 658 | file, |
| 640 | bufferAfter: 0, | 659 | lineAnnotations: undefined, |
| 660 | renderRange: { startingLine: 0, totalLines: 1, bufferBefore: 0, bufferAfter: 0 }, | ||
| 641 | }); | 661 | }); |
| 642 | const append = (count: number) => { | 662 | const append = (count: number) => { |
| 643 | const lines = editor.getText().split("\n"); | 663 | const lines = editor.getText().split("\n"); |
apps/web/src/components/files/projectFilesQueryState.ts+12| ... | @@ -89,6 +89,18 @@ export function getOptimisticProjectFileQueryData( | ... | @@ -89,6 +89,18 @@ export function getOptimisticProjectFileQueryData( |
| 89 | return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null; | 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 | export function confirmProjectFileQueryData( | 104 | export function confirmProjectFileQueryData( |
| 93 | environmentId: EnvironmentId, | 105 | environmentId: EnvironmentId, |
| 94 | cwd: string, | 106 | cwd: string, |
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+3-1| ... | @@ -254,7 +254,9 @@ function PullRequestCodeTab({ | ... | @@ -254,7 +254,9 @@ function PullRequestCodeTab({ |
| 254 | readonly slices: ReadonlyArray<DiffSlice>; | 254 | readonly slices: ReadonlyArray<DiffSlice>; |
| 255 | }>({ key: "", cursor: null, slices: NO_SLICES }); | 255 | }>({ key: "", cursor: null, slices: NO_SLICES }); |
| 256 | const parseCache = useRef(new Map<string, RenderablePatch>()); | 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 | const referenceKey = pullRequestReviewKey(reference); | 261 | const referenceKey = pullRequestReviewKey(reference); |
| 260 | const commit = selectedCommitOid; | 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,6 +402,10 @@ describe("a file whose name a patch header cannot carry plainly", () => { |
| 402 | expect(pathOf(quotedPatch("line\\nfile.txt"))).toBe("line\nfile.txt"); | 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 | it("reads the octal a host with core.quotePath on writes for a name outside ASCII", () => { | 409 | it("reads the octal a host with core.quotePath on writes for a name outside ASCII", () => { |
| 406 | expect(pathOf(quotedPatch("caf\\303\\251/r\\303\\251sum\\303\\251.ts"))).toBe("café/résumé.ts"); | 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,7 +1,6 @@ |
| 1 | import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; | 1 | import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; |
| 2 | import { parseDiffFromFile } from "@pierre/diffs"; | 2 | import { parseDiffFromFile } from "@pierre/diffs"; |
| 3 | import type { FileDiffMetadata } from "@pierre/diffs/types"; | 3 | import type { FileDiffMetadata } from "@pierre/diffs/types"; |
| 4 | import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; | ||
| 5 | 4 | ||
| 6 | const DIFF_THEME_NAMES = { | 5 | const DIFF_THEME_NAMES = { |
| 7 | light: "pierre-light", | 6 | light: "pierre-light", |
| ... | @@ -201,17 +200,11 @@ export function getRenderablePatch( | ... | @@ -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 | 203 | * The file's own name. Git quotes and escapes a name holding a tab, a newline, a quote or a |
| 205 | * newline, a quote or a backslash quoted and escaped, and the parser hands one of those back still | 204 | * backslash, and the parser decodes it, so this is the name the host knows the file by. |
| 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. | ||
| 208 | */ | 205 | */ |
| 209 | function fileDiffPath(raw: string): string { | ||
| 210 | return unquoteGitPatchPath(raw); | ||
| 211 | } | ||
| 212 | |||
| 213 | export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { | 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,7 +212,7 @@ export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { |
| 219 | * path, and the hosts that resolve a diff position against both sides need both names. | 212 | * path, and the hosts that resolve a diff position against both sides need both names. |
| 220 | */ | 213 | */ |
| 221 | export function resolveFileDiffPreviousPath(fileDiff: FileDiffMetadata): string { | 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,7 +190,7 @@ export default defineConfig(() => { |
| 190 | "@clerk/clerk-js", | 190 | "@clerk/clerk-js", |
| 191 | "@clerk/react/internal", | 191 | "@clerk/react/internal", |
| 192 | "@pierre/diffs", | 192 | "@pierre/diffs", |
| 193 | "@pierre/diffs/editor", | 193 | "@pierre/diffs/edit", |
| 194 | "@pierre/diffs/react", | 194 | "@pierre/diffs/react", |
| 195 | "@pierre/diffs/worker/worker.js", | 195 | "@pierre/diffs/worker/worker.js", |
| 196 | "effect/Array", | 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,8 +25,8 @@ catalogs: |
| 25 | specifier: 1.8.0 | 25 | specifier: 1.8.0 |
| 26 | version: 1.8.0 | 26 | version: 1.8.0 |
| 27 | '@pierre/diffs': | 27 | '@pierre/diffs': |
| 28 | specifier: 1.3.0-beta.10 | 28 | specifier: 1.5.2 |
| 29 | version: 1.3.0-beta.10 | 29 | version: 1.5.2 |
| 30 | jose: | 30 | jose: |
| 31 | specifier: 6.2.2 | 31 | specifier: 6.2.2 |
| 32 | version: 6.2.2 | 32 | version: 6.2.2 |
| ... | @@ -103,7 +103,7 @@ patchedDependencies: | ... | @@ -103,7 +103,7 @@ patchedDependencies: |
| 103 | '@opencode/client@2.0.23': f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd | 103 | '@opencode/client@2.0.23': f725d6e6d9af0d56dc6ed71b863518fbfc751af1773cff2311209d2c404cf4cd |
| 104 | '@opencode/protocol@2.0.23': 17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685 | 104 | '@opencode/protocol@2.0.23': 17a82f6c2c98fd0371e786c211a4fbdaaba403577a25733e5e99a0c25a16a685 |
| 105 | '@opencode/schema@2.0.23': 650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59 | 105 | '@opencode/schema@2.0.23': 650f1b8602789407c010861284272dcb6bd6de01ec6a75bf3c0904a0e7596d59 |
| 106 | '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 | 106 | '@pierre/diffs@1.5.2': ba1766e9669d5699e31f0dde5a52f5d47f389f49a286de54489a409ba2527289 |
| 107 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d | 107 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d |
| 108 | '@react-native-menu/menu@2.0.0': a4866ecfcd44f318043de839d20110c17e14909004900f002ffe12050376f2ba | 108 | '@react-native-menu/menu@2.0.0': a4866ecfcd44f318043de839d20110c17e14909004900f002ffe12050376f2ba |
| 109 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 | 109 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 |
| ... | @@ -277,7 +277,7 @@ importers: | ... | @@ -277,7 +277,7 @@ importers: |
| 277 | version: 1.9.1 | 277 | version: 1.9.1 |
| 278 | '@pierre/diffs': | 278 | '@pierre/diffs': |
| 279 | specifier: 'catalog:' | 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 | '@react-native-ai/apple': | 281 | '@react-native-ai/apple': |
| 282 | specifier: 0.12.0 | 282 | specifier: 0.12.0 |
| 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)) | 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,8 +502,8 @@ importers: |
| 502 | specifier: 4.0.1 | 502 | specifier: 4.0.1 |
| 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) | 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 | '@pierre/trees': | 504 | '@pierre/trees': |
| 505 | specifier: 1.0.0-beta.4 | 505 | specifier: 1.0.0-beta.6 |
| 506 | version: 1.0.0-beta.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) | 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 | '@types/react': | 507 | '@types/react': |
| 508 | specifier: ~19.3.0 | 508 | specifier: ~19.3.0 |
| 509 | version: 19.3.0 | 509 | version: 19.3.0 |
| ... | @@ -666,10 +666,10 @@ importers: | ... | @@ -666,10 +666,10 @@ importers: |
| 666 | version: 1.8.0 | 666 | version: 1.8.0 |
| 667 | '@pierre/diffs': | 667 | '@pierre/diffs': |
| 668 | specifier: 'catalog:' | 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 | '@pierre/trees': | 670 | '@pierre/trees': |
| 671 | specifier: 1.0.0-beta.4 | 671 | specifier: 1.0.0-beta.6 |
| 672 | version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.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 | '@t3tools/client-runtime': | 673 | '@t3tools/client-runtime': |
| 674 | specifier: workspace:* | 674 | specifier: workspace:* |
| 675 | version: link:../../packages/client-runtime | 675 | version: link:../../packages/client-runtime |
| ... | @@ -4444,18 +4444,27 @@ packages: | ... | @@ -4444,18 +4444,27 @@ packages: |
| 4444 | resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} | 4444 | resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} |
| 4445 | engines: {node: '>=14.18.0'} | 4445 | engines: {node: '>=14.18.0'} |
| 4446 | 4446 | ||
| 4447 | '@pierre/diffs@1.3.0-beta.10': | 4447 | '@pierre/diffs@1.5.2': |
| 4448 | resolution: {integrity: sha512-efyFM9GRfI6WkmHJP0CnZBopuM8yCwGqIKbZHoe1D5PV15VDkr7Vpi8EZt40AYrN1km//utQtYhHDSwt2KwjSg==} | 4448 | resolution: {integrity: sha512-QVxIWQEnNoko22k63maERzozd29+FtaUNpRrZiw603WMe5R1mFSED+jMtPkcVP3SDYS1DWg5K7sa7sJbr3xkNQ==} |
| 4449 | peerDependencies: | 4449 | peerDependencies: |
| 4450 | react: ^18.3.1 || ^19.0.0 | 4450 | react: ^18.3.1 || ^19.0.0 |
| 4451 | react-dom: ^18.3.1 || ^19.0.0 | 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 | '@pierre/theme@1.1.0': | 4458 | '@pierre/theme@1.1.0': |
| 4454 | resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} | 4459 | resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} |
| 4455 | engines: {vscode: ^1.0.0} | 4460 | engines: {vscode: ^1.0.0} |
| 4456 | 4461 | ||
| 4457 | '@pierre/theming@0.0.2': | 4462 | '@pierre/theme@2.0.0': |
| 4458 | resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==} | 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 | peerDependencies: | 4468 | peerDependencies: |
| 4460 | '@pierre/theme': ^1.1.0 | 4469 | '@pierre/theme': ^1.1.0 |
| 4461 | '@shikijs/themes': ^3.0.0 || ^4.0.0 | 4470 | '@shikijs/themes': ^3.0.0 || ^4.0.0 |
| ... | @@ -4474,8 +4483,28 @@ packages: | ... | @@ -4474,8 +4483,28 @@ packages: |
| 4474 | shiki: | 4483 | shiki: |
| 4475 | optional: true | 4484 | optional: true |
| 4476 | 4485 | ||
| 4477 | '@pierre/trees@1.0.0-beta.4': | 4486 | '@pierre/theming@1.0.1': |
| 4478 | resolution: {integrity: sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag==} | 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 | peerDependencies: | 4508 | peerDependencies: |
| 4480 | react: ^18.3.1 || ^19.0.0 | 4509 | react: ^18.3.1 || ^19.0.0 |
| 4481 | react-dom: ^18.3.1 || ^19.0.0 | 4510 | react-dom: ^18.3.1 || ^19.0.0 |
| ... | @@ -14869,65 +14898,96 @@ snapshots: | ... | @@ -14869,65 +14898,96 @@ snapshots: |
| 14869 | tslib: 2.8.1 | 14898 | tslib: 2.8.1 |
| 14870 | webcrypto-core: 1.9.2 | 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 | dependencies: | 14902 | dependencies: |
| 14874 | '@pierre/theme': 1.1.0 | 14903 | '@pierre/theme': 2.0.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) | 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 | '@shikijs/transformers': 4.2.0 | 14905 | '@shikijs/transformers': 4.2.0 |
| 14877 | diff: 9.0.0 | 14906 | diff: 9.0.0 |
| 14878 | hast-util-to-html: 9.0.5 | 14907 | hast-util-to-html: 9.0.5 |
| 14879 | lru_map: 0.4.1 | 14908 | lru_map: 0.4.1 |
| 14909 | shiki: 4.2.0 | ||
| 14910 | optionalDependencies: | ||
| 14880 | react: 19.3.0 | 14911 | react: 19.3.0 |
| 14881 | react-dom: 19.3.0(react@19.3.0) | 14912 | react-dom: 19.3.0(react@19.3.0) |
| 14882 | shiki: 4.2.0 | ||
| 14883 | transitivePeerDependencies: | 14913 | transitivePeerDependencies: |
| 14884 | - '@shikijs/themes' | 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 | dependencies: | 14917 | dependencies: |
| 14888 | '@pierre/theme': 1.1.0 | 14918 | '@pierre/theme': 2.0.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) | 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 | '@shikijs/transformers': 4.2.0 | 14920 | '@shikijs/transformers': 4.2.0 |
| 14891 | diff: 9.0.0 | 14921 | diff: 9.0.0 |
| 14892 | hast-util-to-html: 9.0.5 | 14922 | hast-util-to-html: 9.0.5 |
| 14893 | lru_map: 0.4.1 | 14923 | lru_map: 0.4.1 |
| 14924 | shiki: 4.2.0 | ||
| 14925 | optionalDependencies: | ||
| 14894 | react: 19.2.6 | 14926 | react: 19.2.6 |
| 14895 | react-dom: 19.2.6(react@19.2.6) | 14927 | react-dom: 19.2.6(react@19.2.6) |
| 14896 | shiki: 4.2.0 | ||
| 14897 | transitivePeerDependencies: | 14928 | transitivePeerDependencies: |
| 14898 | - '@shikijs/themes' | 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 | optionalDependencies: | 14937 | optionalDependencies: |
| 14904 | '@pierre/theme': 1.1.0 | 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 | '@shikijs/themes': 4.2.0 | 14947 | '@shikijs/themes': 4.2.0 |
| 14906 | react: 19.3.0 | 14948 | react: 19.3.0 |
| 14907 | react-dom: 19.3.0(react@19.3.0) | 14949 | react-dom: 19.3.0(react@19.3.0) |
| 14908 | shiki: 4.2.0 | 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 | optionalDependencies: | 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 | '@shikijs/themes': 4.3.0 | 14963 | '@shikijs/themes': 4.3.0 |
| 14914 | react: 19.2.6 | 14964 | react: 19.2.6 |
| 14915 | react-dom: 19.2.6(react@19.2.6) | 14965 | react-dom: 19.2.6(react@19.2.6) |
| 14916 | shiki: 4.2.0 | 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 | dependencies: | 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 | preact: 11.0.0-beta.0 | 14971 | preact: 11.0.0-beta.0 |
| 14921 | preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) | 14972 | preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) |
| 14922 | react: 19.2.6 | 14973 | react: 19.2.6 |
| 14923 | react-dom: 19.2.6(react@19.2.6) | 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 | dependencies: | 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 | preact: 11.0.0-beta.0 | 14983 | preact: 11.0.0-beta.0 |
| 14928 | preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) | 14984 | preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) |
| 14929 | react: 19.3.0 | 14985 | react: 19.3.0 |
| 14930 | react-dom: 19.3.0(react@19.3.0) | 14986 | react-dom: 19.3.0(react@19.3.0) |
| 14987 | transitivePeerDependencies: | ||
| 14988 | - '@pierre/theme' | ||
| 14989 | - '@shikijs/themes' | ||
| 14990 | - shiki | ||
| 14931 | 14991 | ||
| 14932 | '@polka/url@1.0.0-next.29': {} | 14992 | '@polka/url@1.0.0-next.29': {} |
| 14933 | 14993 |
pnpm-workspace.yaml+4-2| ... | @@ -41,7 +41,7 @@ catalog: | ... | @@ -41,7 +41,7 @@ catalog: |
| 41 | "@legendapp/list": 3.3.5 | 41 | "@legendapp/list": 3.3.5 |
| 42 | "@noble/curves": 1.9.1 | 42 | "@noble/curves": 1.9.1 |
| 43 | "@noble/hashes": 1.8.0 | 43 | "@noble/hashes": 1.8.0 |
| 44 | "@pierre/diffs": 1.3.0-beta.10 | 44 | "@pierre/diffs": 1.5.2 |
| 45 | "@tailwindcss/node": 4.3.3 | 45 | "@tailwindcss/node": 4.3.3 |
| 46 | "@tailwindcss/oxide": 4.3.3 | 46 | "@tailwindcss/oxide": 4.3.3 |
| 47 | "@tailwindcss/vite": 4.3.3 | 47 | "@tailwindcss/vite": 4.3.3 |
| ... | @@ -270,7 +270,9 @@ patchedDependencies: | ... | @@ -270,7 +270,9 @@ patchedDependencies: |
| 270 | "@opencode/client@2.0.23": patches/@opencode__client@2.0.23.patch | 270 | "@opencode/client@2.0.23": patches/@opencode__client@2.0.23.patch |
| 271 | "@opencode/protocol@2.0.23": patches/@opencode__protocol@2.0.23.patch | 271 | "@opencode/protocol@2.0.23": patches/@opencode__protocol@2.0.23.patch |
| 272 | "@opencode/schema@2.0.23": patches/@opencode__schema@2.0.23.patch | 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 | "@react-native-ai/apple@0.12.0": patches/@react-native-ai__apple@0.12.0.patch | 276 | "@react-native-ai/apple@0.12.0": patches/@react-native-ai__apple@0.12.0.patch |
| 275 | "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch | 277 | "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch |
| 276 | "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch | 278 | "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch |