Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/web/src/components/diffs/AnnotatableCodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,13 @@ export function AnnotatableCodeView({
[filesByKey, sectionId, sectionTitle],
);

const handleGutterUtilityClick = useCallback(
(range: SelectedLineRange, context: DiffSelectionContext) => {
setSelectedLines({ id: context.item.id, range });
},
[],
);

const hasOpenComment = draft !== null;
return (
<CodeView<DiffCommentAnnotationGroup>
Expand All @@ -241,6 +248,7 @@ export function AnnotatableCodeView({
...options,
enableGutterUtility: !hasOpenComment,
enableLineSelection: !hasOpenComment,
onGutterUtilityClick: handleGutterUtilityClick,
onLineSelectionEnd: beginComment,
}}
renderHeaderPrefix={(item) =>
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/components/files/FilePreviewPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vite-plus/test";
import {
formatFileCommentRange,
normalizeFileCommentRange,
reconcileFileCommentAnnotations,
remapFileCommentAnnotations,
} from "./fileCommentAnnotations";
import { isMarkdownPreviewFile, setMarkdownTaskChecked } from "./filePreviewMode";
Expand Down Expand Up @@ -52,6 +53,30 @@ describe("file comment annotations", () => {
},
]);
});

it("preserves annotation identity when an edit does not move any annotations", () => {
const annotations = [
{
lineNumber: 7,
metadata: {
entries: [
{
id: "comment-1",
kind: "comment" as const,
startLine: 7,
endLine: 7,
text: "Keep this guarded.",
},
],
},
},
];

expect(reconcileFileCommentAnnotations(annotations, annotations)).toBe(annotations);
expect(reconcileFileCommentAnnotations(annotations, structuredClone(annotations))).toBe(
annotations,
);
});
});

describe("isMarkdownPreviewFile", () => {
Expand Down
24 changes: 16 additions & 8 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -48,11 +48,11 @@ import {
formatFileCommentRange,
nextFileCommentId,
normalizeFileCommentRange,
remapFileCommentAnnotations,
reconcileFileCommentAnnotations,
} 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";
Expand Down Expand Up @@ -344,6 +344,8 @@ function EditableFileSurface({
const addReviewComment = useComposerDraftStore((store) => store.addReviewComment);
const removeReviewComment = useComposerDraftStore((store) => store.removeReviewComment);
const [lineAnnotations, setLineAnnotations] = useState<FileCommentLineAnnotation[]>([]);
const lineAnnotationsRef = useRef(lineAnnotations);
lineAnnotationsRef.current = lineAnnotations;
const [selectionOverride, setSelectionOverride] = useState<FileSelectionOverride | null>(null);
const selectedRange =
selectionOverride?.revealRequestId === revealRequestId ? selectionOverride.range : null;
Expand All @@ -364,14 +366,20 @@ function EditableFileSurface({
const editor = useMemo(
() =>
new Editor<FileCommentAnnotationGroup>({
persistState: true,
persistStateStorage: "inMemory",
onChange: (file, nextLineAnnotations) => {
setProjectFileQueryData(environmentId, cwd, relativePath, file.contents);
saveCoordinator.change(file.contents);
if (nextLineAnnotations) {
const remapped = remapFileCommentAnnotations(
const remapped = reconcileFileCommentAnnotations(
lineAnnotationsRef.current,
nextLineAnnotations as FileCommentLineAnnotation[],
);
setLineAnnotations(remapped);
if (remapped !== lineAnnotationsRef.current) {
lineAnnotationsRef.current = remapped;
setLineAnnotations(remapped);
}
for (const annotation of remapped) {
for (const entry of annotation.metadata.entries) {
if (entry.kind !== "comment") continue;
Expand Down Expand Up @@ -536,7 +544,7 @@ function EditableFileSurface({
);

return (
<EditorProvider editor={editor}>
<EditProvider editor={editor}>
<div ref={surfaceRef} className="flex min-h-0 flex-1">
<Virtualizer
className="file-preview-virtualizer min-h-0 flex-1 overflow-auto"
Expand All @@ -549,7 +557,7 @@ function EditableFileSurface({
file={{
name: relativePath,
contents,
cacheKey: projectFileCacheKey(cwd, relativePath, contents),
cacheKey: projectFileEditorCacheKey(cwd, relativePath),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the file cache key tied to contents

For externally refreshed contents of the same path (for example an agent edits the file on disk and the readFile query updates while this pane stays mounted), this stable cacheKey violates @pierre/diffs' file contract: its renderer caches split lines by file.cacheKey and expects the key to change when contents changes. Local editor keystrokes are covered by the patched editor path, but non-editor updates will reuse the old line cache, so inserted/deleted lines can be omitted or render with stale line counts until the component remounts. Please keep the render cacheKey content-specific and use a separate stable key for persisted editor state.

Useful? React with 👍 / 👎.

Comment thread
cursor[bot] marked this conversation as resolved.
}}
options={{
disableFileHeader: true,
Expand Down Expand Up @@ -586,7 +594,7 @@ function EditableFileSurface({
/>
</Virtualizer>
</div>
</EditorProvider>
</EditProvider>
);
}

Expand Down
35 changes: 35 additions & 0 deletions apps/web/src/components/files/fileCommentAnnotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,38 @@ export function remapFileCommentAnnotations(
},
}));
}

export function reconcileFileCommentAnnotations(
current: FileCommentLineAnnotation[],
next: FileCommentLineAnnotation[],
): FileCommentLineAnnotation[] {
if (next === current) return current;
Comment thread
cursor[bot] marked this conversation as resolved.
const remapped = remapFileCommentAnnotations(next);
if (
remapped.length === current.length &&
remapped.every((annotation, annotationIndex) => {
const currentAnnotation = current[annotationIndex];
if (
currentAnnotation === undefined ||
annotation.lineNumber !== currentAnnotation.lineNumber ||
annotation.metadata.entries.length !== currentAnnotation.metadata.entries.length
) {
return false;
}
return annotation.metadata.entries.every((entry, entryIndex) => {
const currentEntry = currentAnnotation.metadata.entries[entryIndex];
return (
currentEntry !== undefined &&
entry.id === currentEntry.id &&
entry.kind === currentEntry.kind &&
entry.startLine === currentEntry.startLine &&
entry.endLine === currentEntry.endLine &&
entry.text === currentEntry.text
);
});
})
) {
return current;
}
return remapped;
}
13 changes: 12 additions & 1 deletion apps/web/src/components/files/fileContentRevision.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -12,4 +16,11 @@ describe("fileContentRevision", () => {
projectFileCacheKey("/repo", "file.json", "contents"),
);
});

it("keeps editor identity stable while contents change", () => {
const cacheKey = projectFileEditorCacheKey("/repo", "file.json");

expect(cacheKey).toBe(projectFileEditorCacheKey("/repo", "file.json"));
expect(cacheKey).not.toBe(projectFileEditorCacheKey("/repo", "other.json"));
});
});
4 changes: 4 additions & 0 deletions apps/web/src/components/files/fileContentRevision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export function fileContentRevision(contents: string): string {
export function projectFileCacheKey(cwd: string, relativePath: string, contents: string): string {
return `${cwd}:${relativePath}:${fileContentRevision(contents)}`;
}

export function projectFileEditorCacheKey(cwd: string, relativePath: string): string {
return `editor:${cwd}:${relativePath}`;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 @@
Expand Down
Loading
Loading