fix(editor): handle mixed multi-range Tab indentation
This commit is contained in:
1 parent
f4f894d979
commit
45819df9e3
2 files changed
+86
-11
No files matched your search
@@ -35,7 +35,7 @@ interface MockState {
|
||||
doc: {
|
||||
lineAt: (position: number) => { from: number; text: string };
|
||||
};
|
||||
selection: { main: MockSelection };
|
||||
selection: { main: MockSelection; ranges: MockSelection[] };
|
||||
replaceSelection: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
@@ -117,19 +117,27 @@ function createHarness(options: {
|
||||
) as TabHarness;
|
||||
}
|
||||
|
||||
function createView(text = "SELECT", position = text.length): MockView {
|
||||
const selection: MockSelection = { anchor: position, head: position, from: position, empty: true };
|
||||
function createView(text = "SELECT", position = text.length, selectionOverrides: Partial<MockSelection> = {}, additionalRanges: MockSelection[] = []): MockView {
|
||||
const selection: MockSelection = { anchor: position, head: position, from: position, empty: true, ...selectionOverrides };
|
||||
const state: MockState = {
|
||||
doc: {
|
||||
lineAt: () => ({ from: 0, text }),
|
||||
},
|
||||
selection: { main: selection },
|
||||
selection: { main: selection, ranges: [selection, ...additionalRanges] },
|
||||
replaceSelection: vi.fn((insert: string) => ({ insert })),
|
||||
update: vi.fn((change: unknown, options: unknown) => ({ change, options })),
|
||||
};
|
||||
return { state, dispatch: vi.fn() };
|
||||
}
|
||||
|
||||
function createMultiLineSelectionView(text = "SELECT 1\nSELECT 2"): MockView {
|
||||
return createView(text, 0, { anchor: 0, head: text.length, from: 0, empty: false });
|
||||
}
|
||||
|
||||
function createMixedMultiRangeView(text = "SELECT 1\nSELECT 2"): MockView {
|
||||
return createView(text, text.length, {}, [{ anchor: 0, head: 6, from: 0, empty: false }]);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
@@ -281,6 +289,67 @@ describe("QueryEditor completion Tab keymap", () => {
|
||||
expect(acceptCompletion).toHaveBeenCalledWith(view);
|
||||
});
|
||||
|
||||
it("keeps indenting a multi-line selection instead of letting a completion popup hijack Tab (#5419)", () => {
|
||||
const acceptCompletion = vi.fn(() => true);
|
||||
const indentMore = vi.fn(() => true);
|
||||
const harness = createHarness({ completionStatus: () => "active", acceptCompletion, indentMore });
|
||||
const view = createMultiLineSelectionView();
|
||||
|
||||
expect(harness.handleTab(view)).toBe(true);
|
||||
expect(indentMore).toHaveBeenCalledWith(view);
|
||||
expect(acceptCompletion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not let a pending completion hijack a second Tab press while indenting a selection (#5419)", async () => {
|
||||
vi.useFakeTimers();
|
||||
let status: "active" | "pending" | null = "pending";
|
||||
const acceptCompletion = vi.fn(() => true);
|
||||
const indentMore = vi.fn(() => true);
|
||||
const harness = createHarness({ completionStatus: () => status, acceptCompletion, indentMore });
|
||||
const view = createMultiLineSelectionView();
|
||||
|
||||
expect(harness.handleTab(view)).toBe(true);
|
||||
status = "active";
|
||||
await vi.advanceTimersByTimeAsync(32);
|
||||
|
||||
expect(indentMore).toHaveBeenCalledWith(view);
|
||||
expect(acceptCompletion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("indents mixed multi-range selections instead of accepting an active completion", () => {
|
||||
const completionStatus = vi.fn(() => "active" as const);
|
||||
const acceptCompletion = vi.fn(() => true);
|
||||
const nextSnippetField = vi.fn(() => true);
|
||||
const indentMore = vi.fn(() => true);
|
||||
const harness = createHarness({ completionStatus, acceptCompletion, nextSnippetField, indentMore });
|
||||
const view = createMixedMultiRangeView();
|
||||
|
||||
expect(harness.handleTab(view)).toBe(true);
|
||||
expect(indentMore).toHaveBeenCalledWith(view);
|
||||
expect(completionStatus).not.toHaveBeenCalled();
|
||||
expect(acceptCompletion).not.toHaveBeenCalled();
|
||||
expect(nextSnippetField).not.toHaveBeenCalled();
|
||||
expect(view.state.replaceSelection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not accept or wait for completion when a mixed multi-range selection is active", async () => {
|
||||
vi.useFakeTimers();
|
||||
let status: "active" | "pending" | null = "pending";
|
||||
const completionStatus = vi.fn(() => status);
|
||||
const acceptCompletion = vi.fn(() => true);
|
||||
const harness = createHarness({ completionStatus, acceptCompletion });
|
||||
const view = createMixedMultiRangeView();
|
||||
|
||||
expect(harness.acceptCompletionOrNextSnippetField(view)).toBe(false);
|
||||
status = "active";
|
||||
await vi.advanceTimersByTimeAsync(32);
|
||||
|
||||
expect(completionStatus).not.toHaveBeenCalled();
|
||||
expect(acceptCompletion).not.toHaveBeenCalled();
|
||||
expect(view.dispatch).not.toHaveBeenCalled();
|
||||
expect(view.state.replaceSelection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to normal Tab when pending completion has no candidate", async () => {
|
||||
vi.useFakeTimers();
|
||||
let status: "active" | "pending" | null = "pending";
|
||||
|
||||
Reference in new issue
Block a user