paste transform followup (#17624)

Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
This commit is contained in:
Jacob Richman
2026-01-27 06:19:54 -08:00
committed by GitHub
parent 0dc69bd364
commit 362384112e
12 changed files with 539 additions and 440 deletions

View File

@@ -6,18 +6,30 @@
import { getBoundingBox, type DOMElement } from 'ink';
import type React from 'react';
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
import { useCallback, useRef } from 'react';
import {
useMouse,
type MouseEvent,
type MouseEventName,
} from '../contexts/MouseContext.js';
export const useMouseClick = (
containerRef: React.RefObject<DOMElement | null>,
handler: (event: MouseEvent, relativeX: number, relativeY: number) => void,
options: { isActive?: boolean; button?: 'left' | 'right' } = {},
options: {
isActive?: boolean;
button?: 'left' | 'right';
name?: MouseEventName;
} = {},
) => {
const { isActive = true, button = 'left' } = options;
const { isActive = true, button = 'left', name } = options;
const handlerRef = useRef(handler);
handlerRef.current = handler;
useMouse(
const onMouse = useCallback(
(event: MouseEvent) => {
const eventName = button === 'left' ? 'left-press' : 'right-release';
const eventName =
name ?? (button === 'left' ? 'left-press' : 'right-release');
if (event.name === eventName && containerRef.current) {
const { x, y, width, height } = getBoundingBox(containerRef.current);
// Terminal mouse events are 1-based, Ink layout is 0-based.
@@ -33,10 +45,12 @@ export const useMouseClick = (
relativeY >= 0 &&
relativeY < height
) {
handler(event, relativeX, relativeY);
handlerRef.current(event, relativeX, relativeY);
}
}
},
{ isActive },
[containerRef, button, name],
);
useMouse(onMouse, { isActive });
};

View File

@@ -1,148 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useMouseDoubleClick } from './useMouseDoubleClick.js';
import * as MouseContext from '../contexts/MouseContext.js';
import type { MouseEvent } from '../contexts/MouseContext.js';
import type { DOMElement } from 'ink';
describe('useMouseDoubleClick', () => {
const mockHandler = vi.fn();
const mockContainerRef = {
current: {} as DOMElement,
};
// Mock getBoundingBox from ink
vi.mock('ink', async () => {
const actual = await vi.importActual('ink');
return {
...actual,
getBoundingBox: () => ({ x: 0, y: 0, width: 80, height: 24 }),
};
});
let mouseCallback: (event: MouseEvent) => void;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
// Mock useMouse to capture the callback
vi.spyOn(MouseContext, 'useMouse').mockImplementation((callback) => {
mouseCallback = callback;
});
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
it('should detect double-click within threshold', async () => {
renderHook(() => useMouseDoubleClick(mockContainerRef, mockHandler));
const event1: MouseEvent = {
name: 'left-press',
col: 10,
row: 5,
shift: false,
meta: false,
ctrl: false,
button: 'left',
};
const event2: MouseEvent = {
name: 'left-press',
col: 10,
row: 5,
shift: false,
meta: false,
ctrl: false,
button: 'left',
};
await act(async () => {
mouseCallback(event1);
vi.advanceTimersByTime(200);
mouseCallback(event2);
});
expect(mockHandler).toHaveBeenCalledWith(event2, 9, 4);
});
it('should NOT detect double-click if time exceeds threshold', async () => {
renderHook(() => useMouseDoubleClick(mockContainerRef, mockHandler));
const event1: MouseEvent = {
name: 'left-press',
col: 10,
row: 5,
shift: false,
meta: false,
ctrl: false,
button: 'left',
};
const event2: MouseEvent = {
name: 'left-press',
col: 10,
row: 5,
shift: false,
meta: false,
ctrl: false,
button: 'left',
};
await act(async () => {
mouseCallback(event1);
vi.advanceTimersByTime(500); // Threshold is 400ms
mouseCallback(event2);
});
expect(mockHandler).not.toHaveBeenCalled();
});
it('should NOT detect double-click if distance exceeds tolerance', async () => {
renderHook(() => useMouseDoubleClick(mockContainerRef, mockHandler));
const event1: MouseEvent = {
name: 'left-press',
col: 10,
row: 5,
shift: false,
meta: false,
ctrl: false,
button: 'left',
};
const event2: MouseEvent = {
name: 'left-press',
col: 15,
row: 10,
shift: false,
meta: false,
ctrl: false,
button: 'left',
};
await act(async () => {
mouseCallback(event1);
vi.advanceTimersByTime(200);
mouseCallback(event2);
});
expect(mockHandler).not.toHaveBeenCalled();
});
it('should respect isActive option', () => {
renderHook(() =>
useMouseDoubleClick(mockContainerRef, mockHandler, { isActive: false }),
);
expect(MouseContext.useMouse).toHaveBeenCalledWith(expect.any(Function), {
isActive: false,
});
});
});

View File

@@ -1,72 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { getBoundingBox, type DOMElement } from 'ink';
import type React from 'react';
import { useRef, useCallback } from 'react';
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
const DOUBLE_CLICK_THRESHOLD_MS = 400;
const DOUBLE_CLICK_DISTANCE_TOLERANCE = 2;
export const useMouseDoubleClick = (
containerRef: React.RefObject<DOMElement | null>,
handler: (event: MouseEvent, relativeX: number, relativeY: number) => void,
options: { isActive?: boolean } = {},
) => {
const { isActive = true } = options;
const handlerRef = useRef(handler);
handlerRef.current = handler;
const lastClickRef = useRef<{
time: number;
col: number;
row: number;
} | null>(null);
const onMouse = useCallback(
(event: MouseEvent) => {
if (event.name !== 'left-press' || !containerRef.current) return;
const now = Date.now();
const lastClick = lastClickRef.current;
// Check if this is a valid double-click
if (
lastClick &&
now - lastClick.time < DOUBLE_CLICK_THRESHOLD_MS &&
Math.abs(event.col - lastClick.col) <=
DOUBLE_CLICK_DISTANCE_TOLERANCE &&
Math.abs(event.row - lastClick.row) <= DOUBLE_CLICK_DISTANCE_TOLERANCE
) {
// Double-click detected
const { x, y, width, height } = getBoundingBox(containerRef.current);
// Terminal mouse events are 1-based, Ink layout is 0-based.
const mouseX = event.col - 1;
const mouseY = event.row - 1;
const relativeX = mouseX - x;
const relativeY = mouseY - y;
if (
relativeX >= 0 &&
relativeX < width &&
relativeY >= 0 &&
relativeY < height
) {
handlerRef.current(event, relativeX, relativeY);
}
lastClickRef.current = null; // Reset after double-click
} else {
// First click, record it
lastClickRef.current = { time: now, col: event.col, row: event.row };
}
},
[containerRef],
);
useMouse(onMouse, { isActive });
};

View File

@@ -68,7 +68,7 @@ const createMockTextBufferState = (
visualToTransformedMap: [],
},
pastedContent: {},
expandedPasteInfo: new Map(),
expandedPaste: null,
...partial,
};
};