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
57 changes: 50 additions & 7 deletions packages/diffs/src/renderers/DiffHunksRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,21 @@ export class DiffHunksRenderer<LAnnotation = undefined> {
* Enter edit-session mode: hunk updates preserve the current region
* skeleton instead of recomputing hunks, and rendering happens locally
* with the token transformer forced on (worker-pool requests/results are
* suspended for this renderer). Called on every editor attach, including
* a re-attach after recycle.
* suspended for this renderer). An empty additions document gets one row so
* the editor has a line for its caret. Called on every editor attach,
* including a re-attach after recycle.
*/
public beginEditSession(): void {
this.editSessionActive = true;
const diff = this.diffCache;
if (diff != null && !diff.isPartial && diff.additionLines.length === 0) {
Object.assign(
diff,
recomputeEmptyDocumentDiff(diff, this.options.parseDiffOptions)
);
this.markEditSessionPass(diff);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid dirtying attach-only empty sessions

When an editor is attached to a diff whose new file starts empty and is then detached without any user edit, this shim sets editSessionDirty. The detach path calls finishEditSessionForDiff, which recomputes through the edit pipeline and preserves the synthetic [''] addition line, so a previously deletion-only/empty-new diff keeps rendering a green blank addition row after editing ends. Only mark the session dirty after an actual document change, or restore the original zero-line addition shape on session exit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 4aa6f2a, empty caret-host rows are now removed during session exit, with split and unified regression coverage.

this.clearRenderCache();
}
}

/** Leave edit-session mode. The exit recompute is the host's concern. */
Expand Down Expand Up @@ -652,7 +662,7 @@ export class DiffHunksRenderer<LAnnotation = undefined> {
);
result.code.additionLines[0] = createPlainAdditionLineElement(
0,
textDocument
textDocument.getLineText(0)
);
this.markEditSessionPass(diff);
} else if (this.editSessionActive) {
Expand Down Expand Up @@ -1110,6 +1120,34 @@ export class DiffHunksRenderer<LAnnotation = undefined> {
expandedHunks: forcePlainText ? true : undefined,
collapsedContextThreshold,
});
if (
this.editSessionActive &&
diff.additionLines.length === 1 &&
diff.additionLines[0] === '' &&
result.code.additionLines[0] == null
) {
let fallbackLine: DiffLineMetadata | undefined;
iterateOverDiff({
diff,
diffStyle: 'both',
expandedHunks: forcePlainText ? true : undefined,
collapsedContextThreshold,
callback: ({ additionLine }) => {
if (additionLine?.lineIndex !== 0) return;
fallbackLine = additionLine;
return true;
},
});
if (fallbackLine == null) {
throw new Error('DiffHunksRenderer: missing empty addition line');
}
result.code.additionLines[0] = createPlainAdditionLineElement(
0,
'',
fallbackLine.unifiedLineIndex,
fallbackLine.splitLineIndex
);
}
return { result, options };
}

Expand Down Expand Up @@ -2339,21 +2377,26 @@ function realignAdditionHastLines(
realigned[index] ??= hastLines[index];
}
for (let index = prefix; index < nextLines.length; index++) {
realigned[index] ??= createPlainAdditionLineElement(index, textDocument);
realigned[index] ??= createPlainAdditionLineElement(
index,
textDocument.getLineText(index)
);
}
return realigned;
}

function createPlainAdditionLineElement(
lineIndex: number,
textDocument: DiffsTextDocument
lineText: string,
unifiedLineIndex = lineIndex,
splitLineIndex = lineIndex
): HASTElement {
return {
type: 'element',
tagName: 'div',
properties: {
'data-line': lineIndex + 1,
'data-line-index': `${lineIndex},${lineIndex}`,
'data-line-index': `${unifiedLineIndex},${splitLineIndex}`,
'data-line-type': 'context',
},
children: [
Expand All @@ -2366,7 +2409,7 @@ function createPlainAdditionLineElement(
children: [
{
type: 'text',
value: textDocument.getLineText(lineIndex),
value: lineText,
},
],
},
Expand Down
9 changes: 8 additions & 1 deletion packages/diffs/src/utils/editSessionHunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { parseDiffFromFile } from './parseDiffFromFile';
import {
offsetHunkContent,
preserveTrailingEditorBlankLine,
recomputeDiffHunks,
recomputeDiffHunksForEdit,
recomputeDiffRenderLineCounts,
recomputeHunkRenderLineCounts,
Expand Down Expand Up @@ -419,7 +420,13 @@ export function finishEditSessionForDiff(
return false;
}
diff.editSessionDirty = undefined;
Object.assign(diff, recomputeDiffHunksForEdit(diff, parseDiffOptions));
// The empty editor row only hosts a caret; it is not file content after exit.
Object.assign(
diff,
diff.additionLines.length <= 1 && diff.additionLines.join('') === ''
? recomputeDiffHunks(diff, parseDiffOptions)
: recomputeDiffHunksForEdit(diff, parseDiffOptions)
);
return true;
}

Expand Down
71 changes: 70 additions & 1 deletion packages/diffs/test/editorDiffEmptyDocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function countEditableLineEls(content: HTMLElement): number {
interface DiffEditorFixture {
container: HTMLElement;
editor: Editor<undefined>;
fileDiff: FileDiff<undefined>;
cleanup(): Promise<void>;
}

Expand Down Expand Up @@ -96,6 +97,7 @@ async function createDiffEditorFixture(
return {
container,
editor,
fileDiff,
async cleanup() {
// Drain any pending highlighter/sync callbacks before tearing down the DOM
// so a late re-attach does not run against a destroyed document.
Expand All @@ -122,8 +124,75 @@ function replaceAll(editor: Editor<undefined>, newText: string): void {
);
}

describe('diff editor: select-all then delete', () => {
describe('diff editor: empty document', () => {
for (const diffStyle of ['split', 'unified'] as const) {
test(`renders line 1 and a caret when the new file starts empty (${diffStyle})`, async () => {
const fixture = await createDiffEditorFixture(diffStyle, 'removed\n', '');
const { editor, container } = fixture;

try {
const content = findAdditionContent(container);
expect(content).toBeDefined();
if (content == null) return;
expect(countEditableLineEls(content)).toBe(1);
expect(
[...content.children].some(
(child) => (child as HTMLElement).dataset.line === '1'
)
).toBe(true);
const editableLine = [...content.children].find((child) => {
const el = child as HTMLElement;
return (
el.dataset.line === '1' && el.dataset.lineType !== 'change-deletion'
);
}) as HTMLElement | undefined;
expect(editableLine?.dataset.lineIndex).toBe('1,0');

editor.setSelections([
{
start: { line: 0, character: 0 },
end: { line: 0, character: 0 },
direction: 'none',
},
]);
expect(
container.shadowRoot?.querySelector('[data-caret]') != null
).toBe(true);
} finally {
await fixture.cleanup();
}
});

test(`restores the zero-line diff after an attach-only session (${diffStyle})`, async () => {
const fixture = await createDiffEditorFixture(
diffStyle,
'removed 1\nremoved 2\n',
''
);

try {
expect(fixture.fileDiff.fileDiff?.additionLines).toEqual(['']);

fixture.editor.cleanUp();
for (let attempt = 0; attempt < 40; attempt++) {
const content = findAdditionContent(fixture.container);
if (
fixture.fileDiff.fileDiff?.additionLines.length === 0 &&
(content == null || countEditableLineEls(content) === 0)
) {
break;
}
await wait(0);
}

expect(fixture.fileDiff.fileDiff?.additionLines).toEqual([]);
const content = findAdditionContent(fixture.container);
expect(content == null ? 0 : countEditableLineEls(content)).toBe(0);
} finally {
await fixture.cleanup();
}
});

test(`keeps an editable line, accepts typing, and undoes (${diffStyle})`, async () => {
const fixture = await createDiffEditorFixture(
diffStyle,
Expand Down