Skip to content
Merged
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
115 changes: 80 additions & 35 deletions packages/diffs/src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2496,14 +2496,16 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {
// whole document. #suppressNativeSelectionSync then stops the resulting
// selectionchange from reading that shorter range back over #selections
// before the delete runs.
const renderedLines = this.#getRenderedEditableLineRange();
if (renderedLines !== undefined) {
const renderedLineRanges = this.#getRenderedEditableLineRanges();
const firstRenderedLine = renderedLineRanges[0]?.[0];
const lastRenderedLine = renderedLineRanges.at(-1)?.[1];
if (firstRenderedLine !== undefined && lastRenderedLine !== undefined) {
this.#suppressNativeSelectionSync = true;
this.#setWindowSelection({
start: { line: renderedLines.first, character: 0 },
start: { line: firstRenderedLine, character: 0 },
end: {
line: renderedLines.last,
character: textDocument.getLineLength(renderedLines.last),
line: lastRenderedLine,
character: textDocument.getLineLength(lastRenderedLine),
},
direction: DirectionForward,
});
Expand Down Expand Up @@ -3750,6 +3752,15 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {

const textDocument = this.#textDocument;
if (this.#matches !== undefined && textDocument !== undefined) {
const matches = this.#matches;
const renderRange = this.#renderRange;
const shouldCullMatches =
(renderRange !== undefined &&
Number.isFinite(renderRange.totalLines)) ||
(this.#isDiff && this.#fileInstance?.options.expandUnchanged !== true);
const renderedLineRanges = shouldCullMatches
? this.#getRenderedEditableLineRanges()
: undefined;
const primarySelection = this.#selections?.at(-1);
const primaryStartOffset =
primarySelection !== undefined
Expand All @@ -3759,19 +3770,59 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {
primarySelection !== undefined
? textDocument.offsetAt(primarySelection.end)
: -1;
for (const [startOffset, endOffset] of this.#matches) {
const range: Range = {
start: textDocument.positionAt(startOffset),
end: textDocument.positionAt(endOffset),
};
const isFocused =
primaryStartOffset === startOffset && primaryEndOffset === endOffset;
this.#renderSelection(
renderCtx,
'match',
range,
isFocused ? 'focus' : undefined
);
let firstMatchIndex = 0;
const rangeCount = renderedLineRanges?.length ?? 1;
for (let rangeIndex = 0; rangeIndex < rangeCount; rangeIndex++) {
let visibleEndOffset = Infinity;
const lineRange = renderedLineRanges?.[rangeIndex];
if (lineRange !== undefined) {
const [firstLine, lastLine] = lineRange;
const visibleStartOffset = textDocument.offsetAt({
line: firstLine,
character: 0,
});
const endLine = lastLine + 1;
visibleEndOffset =
endLine < textDocument.lineCount
? textDocument.offsetAt({ line: endLine, character: 0 })
: textDocument.offsetAt({
line: endLine - 1,
character: textDocument.getLineLength(endLine - 1),
});

// Matches are sorted and non-overlapping, so narrow the rendered
// offset window before resolving any match positions through the treap.
let low = firstMatchIndex;
let high = matches.length;
while (low < high) {
const middle = low + Math.floor((high - low) / 2);
if (matches[middle][1] <= visibleStartOffset) {
low = middle + 1;
} else {
high = middle;
}
}
firstMatchIndex = low;
}
for (; firstMatchIndex < matches.length; firstMatchIndex++) {
const [startOffset, endOffset] = matches[firstMatchIndex];
if (startOffset >= visibleEndOffset) {
break;
}
const range: Range = {
start: textDocument.positionAt(startOffset),
end: textDocument.positionAt(endOffset),
};
const isFocused =
primaryStartOffset === startOffset &&
primaryEndOffset === endOffset;
this.#renderSelection(
renderCtx,
'match',
range,
isFocused ? 'focus' : undefined
);
}
}
}

Expand Down Expand Up @@ -4898,18 +4949,14 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {
return undefined;
}

// Returns the first and last document lines that have an editable row in the
// current (virtualized) render window, or undefined when none are rendered.
// Used by select-all to anchor a native selection: only rendered lines
// resolve to DOM nodes, so the native range must stay within what is on
// screen even though the document selection spans the whole file.
#getRenderedEditableLineRange(): { first: number; last: number } | undefined {
// Returns contiguous document-line ranges that have editable rows in the
// current render window. Collapsed diff sections create gaps between ranges.
#getRenderedEditableLineRanges(): [first: number, last: number][] {
const contentElement = this.#contentElement;
if (contentElement === undefined) {
return undefined;
return [];
}
let first: number | undefined;
let last: number | undefined;
const ranges: [first: number, last: number][] = [];
for (const child of contentElement.children) {
const el = child as HTMLElement;
const lineType = el.dataset.lineType;
Expand All @@ -4922,16 +4969,14 @@ export class Editor<LAnnotation> implements DiffsEditor<LAnnotation> {
continue;
}
const line = lineNumber - 1;
if (first === undefined || line < first) {
first = line;
}
if (last === undefined || line > last) {
last = line;
const current = ranges.at(-1);
if (current !== undefined && line <= current[1] + 1) {
current[1] = Math.max(current[1], line);
} else {
ranges.push([line, line]);
}
}
return first === undefined || last === undefined
? undefined
: { first, last };
return ranges;
}

#getLineElement(line: number): HTMLElement | undefined {
Expand Down
60 changes: 59 additions & 1 deletion packages/diffs/test/editorCollapsedEdit.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { afterAll, describe, expect, test } from 'bun:test';
import { afterAll, describe, expect, spyOn, test } from 'bun:test';

import { FileDiff } from '../src/components/FileDiff';
import { DEFAULT_THEMES } from '../src/constants';
import { Editor } from '../src/editor/editor';
import { PieceTable } from '../src/editor/pieceTable';
import { disposeHighlighter } from '../src/highlighter/shared_highlighter';
import { installDom, wait } from './domHarness';

Expand Down Expand Up @@ -203,6 +204,63 @@ describe('diff editor: collapsed regions during edit', () => {
}
});

test('resolves search positions only for rendered sides of collapsed gaps', async () => {
const fixture = await createCollapsedEditFixture();
const { container, editor } = fixture;
try {
editor.setOptions({ matchBrackets: false });
editor.setSelections([
{
start: { line: 9, character: 0 },
end: { line: 9, character: 4 },
direction: 'forward',
},
]);
const content = findAdditionContent(container);
expect(content).not.toBeUndefined();
content!.dispatchEvent(
new window.KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
code: 'KeyF',
composed: true,
key: 'f',
metaKey: true,
})
);
await wait(0);
const input = container.shadowRoot?.querySelector<HTMLInputElement>(
'[data-search-panel] input[data-search]'
);
expect(input).not.toBeNull();
input!.value = 'line';
input!.dispatchEvent(new window.Event('input', { bubbles: true }));

const visibleMatchCount =
container.shadowRoot?.querySelectorAll('[data-match-range]').length ??
0;
expect(visibleMatchCount).toBeGreaterThan(0);
expect(visibleMatchCount).toBeLessThan(60);

const positionAt = spyOn(PieceTable.prototype, 'positionAt');
try {
editor.setSelections([
{
start: { line: 9, character: 0 },
end: { line: 9, character: 0 },
direction: 'none',
},
]);

expect(positionAt).toHaveBeenCalledTimes(visibleMatchCount * 2);
} finally {
positionAt.mockRestore();
}
} finally {
await fixture.cleanup();
}
});

test('typing below a collapsed gap patches the row without duplicates', async () => {
const fixture = await createCollapsedEditFixture();
const { container, editor } = fixture;
Expand Down
46 changes: 46 additions & 0 deletions packages/diffs/test/editorVirtualizedEdit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { afterAll, describe, expect, spyOn, test } from 'bun:test';
import { File } from '../src/components/File';
import { DEFAULT_THEMES } from '../src/constants';
import { Editor } from '../src/editor/editor';
import { PieceTable } from '../src/editor/pieceTable';
import { disposeHighlighter } from '../src/highlighter/shared_highlighter';
import type { FileContents, RenderRange } from '../src/types';
import { installDom, wait } from './domHarness';
Expand Down Expand Up @@ -305,6 +306,51 @@ describe('Editor edits at the bottom of a virtualized window', () => {
});
});

describe('Editor search matches in a virtualized window', () => {
test('resolves positions only for matches in the rendered window', async () => {
const range = makeRange(900, 7);
const { cleanup, content, editor, fileContainer } =
await createWindowedEditor(2000, range);
try {
content.dispatchEvent(
new window.KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
code: 'KeyF',
composed: true,
key: 'f',
metaKey: true,
})
);
await wait(0);
const input = fileContainer.shadowRoot?.querySelector<HTMLInputElement>(
'[data-search-panel] input[data-search]'
);
expect(input).not.toBeNull();
input!.value = 'line';
input!.dispatchEvent(new window.Event('input', { bubbles: true }));

editor.setOptions({ matchBrackets: false });
const positionAt = spyOn(PieceTable.prototype, 'positionAt');
try {
editor.setSelections([collapsedCaret(903)]);

// There is one match per document line. A repaint resolves only both
// endpoints of the seven rendered matches, not all 2,000 matches.
expect(positionAt).toHaveBeenCalledTimes(range.totalLines * 2);
expect(
fileContainer.shadowRoot?.querySelectorAll('[data-match-range]')
.length
).toBe(range.totalLines);
} finally {
positionAt.mockRestore();
}
} finally {
cleanup();
}
});
});

describe('Editor virtualized line lookup', () => {
test('does not scan the content subtree for offscreen selected lines', async () => {
const { cleanup, content, editor } = await createWindowedEditor(
Expand Down