diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 6ddd38e9d25..e3d921901d4 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -7,7 +7,7 @@ import type { import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; import { Editor } from "@pierre/diffs/editor"; -import { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; +import { EditProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, @@ -52,7 +52,7 @@ import { } from "./fileCommentAnnotations"; import { installFileEditorDismissal } from "./fileEditorDismissal"; import { LocalCommentAnnotation } from "./LocalCommentAnnotation"; -import { projectFileCacheKey } from "./fileContentRevision"; +import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; import { fileBreadcrumbs } from "./filePath"; import { isMarkdownPreviewFile, setMarkdownTaskChecked } from "./filePreviewMode"; import { FileSaveCoordinator } from "./fileSaveCoordinator"; @@ -364,6 +364,8 @@ function EditableFileSurface({ const editor = useMemo( () => new Editor({ + persistState: true, + persistStateStorage: "inMemory", onChange: (file, nextLineAnnotations) => { setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); saveCoordinator.change(file.contents); @@ -536,7 +538,7 @@ function EditableFileSurface({ ); return ( - +
-
+ ); } diff --git a/apps/web/src/components/files/fileContentRevision.test.ts b/apps/web/src/components/files/fileContentRevision.test.ts index db3ba4f5e26..e2ec7f9f1ca 100644 --- a/apps/web/src/components/files/fileContentRevision.test.ts +++ b/apps/web/src/components/files/fileContentRevision.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vite-plus/test"; -import { fileContentRevision, projectFileCacheKey } from "./fileContentRevision"; +import { + fileContentRevision, + projectFileCacheKey, + projectFileEditorCacheKey, +} from "./fileContentRevision"; describe("fileContentRevision", () => { it("changes for same-length edits", () => { @@ -12,4 +16,27 @@ describe("fileContentRevision", () => { projectFileCacheKey("/repo", "file.json", "contents"), ); }); + + it("keeps editor identity stable for locally edited contents", () => { + const cacheKey = projectFileEditorCacheKey("local", "/repo", "file.json", "after", undefined); + + expect( + projectFileEditorCacheKey("local", "/repo", "file.json", "after edit", { + cacheKey, + contents: "after edit", + }), + ).toBe(cacheKey); + }); + + it("rotates editor identity for external contents and environments", () => { + const cacheKey = projectFileEditorCacheKey("local", "/repo", "file.json", "before", undefined); + const editorFile = { cacheKey, contents: "before" }; + + expect( + projectFileEditorCacheKey("local", "/repo", "file.json", "external edit", editorFile), + ).not.toBe(cacheKey); + expect(projectFileEditorCacheKey("remote", "/repo", "file.json", "before", undefined)).not.toBe( + cacheKey, + ); + }); }); diff --git a/apps/web/src/components/files/fileContentRevision.ts b/apps/web/src/components/files/fileContentRevision.ts index 3fa8d07bccb..e51d464925b 100644 --- a/apps/web/src/components/files/fileContentRevision.ts +++ b/apps/web/src/components/files/fileContentRevision.ts @@ -10,3 +10,21 @@ export function fileContentRevision(contents: string): string { export function projectFileCacheKey(cwd: string, relativePath: string, contents: string): string { return `${cwd}:${relativePath}:${fileContentRevision(contents)}`; } + +interface EditorFileIdentity { + readonly cacheKey?: string; + readonly contents: string; +} + +export function projectFileEditorCacheKey( + environmentId: string, + cwd: string, + relativePath: string, + contents: string, + editorFile: EditorFileIdentity | undefined, +): string { + if (editorFile?.contents === contents && editorFile.cacheKey) { + return editorFile.cacheKey; + } + return `editor:${environmentId}:${projectFileCacheKey(cwd, relativePath, contents)}`; +} diff --git a/patches/@pierre%2Fdiffs@1.3.0-beta.5.patch b/patches/@pierre%2Fdiffs@1.3.0-beta.10.patch similarity index 50% rename from patches/@pierre%2Fdiffs@1.3.0-beta.5.patch rename to patches/@pierre%2Fdiffs@1.3.0-beta.10.patch index 59aa02f6d1c..b342d4b5dd1 100644 --- a/patches/@pierre%2Fdiffs@1.3.0-beta.5.patch +++ b/patches/@pierre%2Fdiffs@1.3.0-beta.10.patch @@ -1,38 +1,54 @@ diff --git a/dist/editor/editor.js b/dist/editor/editor.js -index e8013fc6eb6f243a6c912facf3fc0319ac66a8d0..80c82df4cdeb828bd331f5ec2f443d216bedc304 100644 +index ff78e2a..f9df318 100644 --- a/dist/editor/editor.js +++ b/dist/editor/editor.js -@@ -77,15 +77,12 @@ var Editor = class { - this.#options = options; - } - edit(component) { -- const { useTokenTransformer, enableGutterUtility, enableLineSelection, expandUnchanged, diffStyle, lineHoverHighlight,...rest } = component.options; -+ const { useTokenTransformer, expandUnchanged, diffStyle,...rest } = component.options; - const isDiff = component.type === "file-diff"; -- if (useTokenTransformer !== true || enableGutterUtility === true || enableLineSelection === true || lineHoverHighlight !== "disabled" || expandUnchanged !== true && isDiff || diffStyle === "unified" && isDiff) { -+ if (useTokenTransformer !== true || expandUnchanged !== true && isDiff || diffStyle === "unified" && isDiff) { - component.setOptions({ +@@ -146,14 +146,11 @@ var Editor = class { + const file = fileInstance.__getCurrentFile?.(); + if (file !== void 0) requirePersistedCacheKey(file); + } +- const { useTokenTransformer, enableGutterUtility, enableLineSelection, lineHoverHighlight = "disabled", ...rest } = fileInstance.options; +- if (useTokenTransformer !== true || enableGutterUtility === true || enableLineSelection === true || lineHoverHighlight !== "disabled") { ++ const { useTokenTransformer, ...rest } = fileInstance.options; ++ if (useTokenTransformer !== true) { + fileInstance.setOptions({ ...rest, - useTokenTransformer: true, +- useTokenTransformer: true, - enableGutterUtility: false, - enableLineSelection: false, -- lineHoverHighlight: "disabled", - expandUnchanged: true, - diffStyle: "split" +- lineHoverHighlight: "disabled" ++ useTokenTransformer: true }); -@@ -511,6 +508,7 @@ var Editor = class { + fileInstance.rerender(); + } +@@ -908,6 +905,7 @@ var Editor = class { return lineNumber - 1; }; this.#editorEventDisposes.push(addEventListener(gutterEl, "pointerdown", (e) => { + if (this.#fileInstance?.options.enableLineSelection === true) return; - const textDocument = this.#textDocument; - const lineIndex = resolveEditableLine(resolveGutterTarget(e.composedPath()[0])); - if (lineIndex === void 0 || textDocument === void 0) return; + const gutterRow = resolveGutterTarget(e.composedPath()[0]); + if (gutterRow?.dataset.lineType === "change-deletion") { + const code = gutterRow.closest("[data-code]"); +@@ -1522,6 +1520,7 @@ var Editor = class { + if (gutterEl !== void 0) gutterEl.style.gridRow = "span " + gridRow; + } + fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange, didLineCountChange); ++ if (fileInstance.file !== void 0) fileInstance.file.contents = textDocument.getText(); + if (didLineCountChange) fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer); + if (this.#isDiff && (this.#diffSyle === "unified" || didLineCountChange)) this.#resetCache(); + if (newLineAnnotations !== void 0) { +@@ -1788,6 +1787,7 @@ var Editor = class { + } + } + #setSelectedLinesSafe(range, lineNumberOnly = false) { ++ if (this.#fileInstance?.options.controlledSelection === true) return; + try { + this.#fileInstance?.setSelectedLines(range, { + notify: false, diff --git a/dist/react/utils/useFileInstance.js b/dist/react/utils/useFileInstance.js -index cb8e2026fb5d7a19f489c0a2402efbcb7dff3322..510fad6364d4a2214c7dd65fe2b114f1cce0c815 100644 +index e9f62f5..af82a46 100644 --- a/dist/react/utils/useFileInstance.js +++ b/dist/react/utils/useFileInstance.js -@@ -92,10 +92,7 @@ function mergeFileOptions({ options, controlledSelection, contentEditable, hasCu +@@ -91,10 +91,7 @@ function mergeFileOptions({ options, controlledSelection, contentEditable, hasCu }; if (needsEditorOptions) merged = { ...merged, @@ -45,7 +61,7 @@ index cb8e2026fb5d7a19f489c0a2402efbcb7dff3322..510fad6364d4a2214c7dd65fe2b114f1 return merged; } diff --git a/package.json b/package.json -index d1558633de87044b7aa96cff09443db11f163cec..c0b16f0a0bec6fba2026f24f38b2c0a8fa06af7c 100644 +index ff61c90..1e170e5 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,18 @@ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4981a25d3d0..c46d2c6a4dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ catalogs: specifier: 1.8.0 version: 1.8.0 '@pierre/diffs': - specifier: 1.3.0-beta.5 - version: 1.3.0-beta.5 + specifier: 1.3.0-beta.10 + version: 1.3.0-beta.10 '@typescript/native-preview': specifier: 7.0.0-dev.20260604.1 version: 7.0.0-dev.20260604.1 @@ -75,7 +75,7 @@ patchedDependencies: '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 '@legendapp/list@3.2.0': 9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d - '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a + '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 effect@4.0.0-beta.102: 71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488 @@ -222,7 +222,7 @@ importers: version: 1.8.0 '@pierre/diffs': specifier: 'catalog:' - version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-menu/menu': specifier: ^2.0.0 version: 2.0.0(patch_hash=5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -466,7 +466,7 @@ importers: version: 1.15.13 '@pierre/diffs': specifier: 'catalog:' - version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) effect: specifier: 4.0.0-beta.102 version: 4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488) @@ -551,7 +551,7 @@ importers: version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) '@pierre/diffs': specifier: 'catalog:' - version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@pierre/trees': specifier: 1.0.0-beta.4 version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -3584,20 +3584,20 @@ packages: resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} engines: {node: '>=14.18.0'} - '@pierre/diffs@1.3.0-beta.5': - resolution: {integrity: sha512-d7449IY6Phcg9LCRLbPxhsxn6Bv4KoaP/vPyZtGu2uR1SFsSJPQcRoPf8lzyobNGKD0GZGuhgHW5LrOlilFo7w==} + '@pierre/diffs@1.3.0-beta.10': + resolution: {integrity: sha512-efyFM9GRfI6WkmHJP0CnZBopuM8yCwGqIKbZHoe1D5PV15VDkr7Vpi8EZt40AYrN1km//utQtYhHDSwt2KwjSg==} peerDependencies: react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 - '@pierre/theme@1.0.3': - resolution: {integrity: sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA==} + '@pierre/theme@1.1.0': + resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} engines: {vscode: ^1.0.0} - '@pierre/theming@0.0.1': - resolution: {integrity: sha512-1thlEtJbqdyLzc1ZS2KQa1q7FzDGHT4dTEdKHoyQjOMeWWOmbVG5/ndEfOKfAb5Fzkz8cNJrOjFLiZoDH/A03A==} + '@pierre/theming@0.0.2': + resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==} peerDependencies: - '@pierre/theme': ^1.0.0 + '@pierre/theme': ^1.1.0 '@shikijs/themes': ^3.0.0 || ^4.0.0 react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 @@ -6104,6 +6104,10 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} @@ -13569,12 +13573,12 @@ snapshots: tslib: 2.8.1 webcrypto-core: 1.9.2 - '@pierre/diffs@1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@pierre/diffs@1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@pierre/theme': 1.0.3 - '@pierre/theming': 0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@4.2.0) + '@pierre/theme': 1.1.0 + '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@4.2.0) '@shikijs/transformers': 4.2.0 - diff: 8.0.3 + diff: 9.0.0 hast-util-to-html: 9.0.5 lru_map: 0.4.1 react: 19.2.3 @@ -13583,12 +13587,12 @@ snapshots: transitivePeerDependencies: - '@shikijs/themes' - '@pierre/diffs@1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@pierre/diffs@1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@pierre/theme': 1.0.3 - '@pierre/theming': 0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) + '@pierre/theme': 1.1.0 + '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) '@shikijs/transformers': 4.2.0 - diff: 8.0.3 + diff: 9.0.0 hast-util-to-html: 9.0.5 lru_map: 0.4.1 react: 19.2.6 @@ -13597,19 +13601,19 @@ snapshots: transitivePeerDependencies: - '@shikijs/themes' - '@pierre/theme@1.0.3': {} + '@pierre/theme@1.1.0': {} - '@pierre/theming@0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@4.2.0)': + '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@4.2.0)': optionalDependencies: - '@pierre/theme': 1.0.3 + '@pierre/theme': 1.1.0 '@shikijs/themes': 4.2.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) shiki: 4.2.0 - '@pierre/theming@0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': + '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': optionalDependencies: - '@pierre/theme': 1.0.3 + '@pierre/theme': 1.1.0 '@shikijs/themes': 4.3.0 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -16379,6 +16383,8 @@ snapshots: diff@8.0.3: {} + diff@9.0.0: {} + dir-compare@4.2.0: dependencies: minimatch: 3.1.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 42529f56ee3..70f61242053 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -41,7 +41,7 @@ catalog: "@effect/vitest": 4.0.0-beta.102 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 - "@pierre/diffs": 1.3.0-beta.5 + "@pierre/diffs": 1.3.0-beta.10 "@types/node": 24.12.4 "@typescript/native-preview": 7.0.0-dev.20260604.1 effect: 4.0.0-beta.102 @@ -128,7 +128,7 @@ patchedDependencies: "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch "@legendapp/list@3.2.0": patches/@legendapp__list@3.2.0.patch - "@pierre/diffs@1.3.0-beta.5": patches/@pierre%2Fdiffs@1.3.0-beta.5.patch + "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch effect@4.0.0-beta.102: patches/effect@4.0.0-beta.102.patch