mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-23 20:40:41 -07:00
paste transform followup (#17624)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
This commit is contained in:
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -68,7 +68,7 @@ const createMockTextBufferState = (
|
||||
visualToTransformedMap: [],
|
||||
},
|
||||
pastedContent: {},
|
||||
expandedPasteInfo: new Map(),
|
||||
expandedPaste: null,
|
||||
...partial,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user