Fix bugs breaking drag and drop of files. (#4887)

Co-authored-by: matt korwel <matt.korwel@gmail.com>
This commit is contained in:
Jacob Richman
2025-07-25 20:26:13 +00:00
committed by GitHub
parent 13b0971291
commit de96887789
3 changed files with 32 additions and 27 deletions

View File

@@ -164,7 +164,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const handleInput = useCallback( const handleInput = useCallback(
(key: Key) => { (key: Key) => {
if (!focus) { /// We want to handle paste even when not focused to support drag and drop.
if (!focus && !key.paste) {
return; return;
} }
@@ -349,7 +350,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
], ],
); );
useKeypress(handleInput, { isActive: focus }); useKeypress(handleInput, { isActive: true });
const linesToRender = buffer.viewportVisualLines; const linesToRender = buffer.viewportVisualLines;
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] = const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =

View File

@@ -407,7 +407,7 @@ describe('useTextBuffer', () => {
useTextBuffer({ viewport, isValidPath: () => true }), useTextBuffer({ viewport, isValidPath: () => true }),
); );
const filePath = '/path/to/a/valid/file.txt'; const filePath = '/path/to/a/valid/file.txt';
act(() => result.current.insert(filePath)); act(() => result.current.insert(filePath, { paste: true }));
expect(getBufferState(result).text).toBe(`@${filePath} `); expect(getBufferState(result).text).toBe(`@${filePath} `);
}); });
@@ -416,7 +416,7 @@ describe('useTextBuffer', () => {
useTextBuffer({ viewport, isValidPath: () => false }), useTextBuffer({ viewport, isValidPath: () => false }),
); );
const notAPath = 'this is just some long text'; const notAPath = 'this is just some long text';
act(() => result.current.insert(notAPath)); act(() => result.current.insert(notAPath, { paste: true }));
expect(getBufferState(result).text).toBe(notAPath); expect(getBufferState(result).text).toBe(notAPath);
}); });
@@ -425,7 +425,7 @@ describe('useTextBuffer', () => {
useTextBuffer({ viewport, isValidPath: () => true }), useTextBuffer({ viewport, isValidPath: () => true }),
); );
const filePath = "'/path/to/a/valid/file.txt'"; const filePath = "'/path/to/a/valid/file.txt'";
act(() => result.current.insert(filePath)); act(() => result.current.insert(filePath, { paste: true }));
expect(getBufferState(result).text).toBe(`@/path/to/a/valid/file.txt `); expect(getBufferState(result).text).toBe(`@/path/to/a/valid/file.txt `);
}); });
@@ -434,7 +434,7 @@ describe('useTextBuffer', () => {
useTextBuffer({ viewport, isValidPath: () => true }), useTextBuffer({ viewport, isValidPath: () => true }),
); );
const shortText = 'ab'; const shortText = 'ab';
act(() => result.current.insert(shortText)); act(() => result.current.insert(shortText, { paste: true }));
expect(getBufferState(result).text).toBe(shortText); expect(getBufferState(result).text).toBe(shortText);
}); });
}); });
@@ -449,7 +449,7 @@ describe('useTextBuffer', () => {
}), }),
); );
const filePath = '/path/to/a/valid/file.txt'; const filePath = '/path/to/a/valid/file.txt';
act(() => result.current.insert(filePath)); act(() => result.current.insert(filePath, { paste: true }));
expect(getBufferState(result).text).toBe(filePath); // No @ prefix expect(getBufferState(result).text).toBe(filePath); // No @ prefix
}); });
@@ -462,7 +462,7 @@ describe('useTextBuffer', () => {
}), }),
); );
const quotedFilePath = "'/path/to/a/valid/file.txt'"; const quotedFilePath = "'/path/to/a/valid/file.txt'";
act(() => result.current.insert(quotedFilePath)); act(() => result.current.insert(quotedFilePath, { paste: true }));
expect(getBufferState(result).text).toBe(quotedFilePath); // No @ prefix, keeps quotes expect(getBufferState(result).text).toBe(quotedFilePath); // No @ prefix, keeps quotes
}); });
@@ -475,7 +475,7 @@ describe('useTextBuffer', () => {
}), }),
); );
const notAPath = 'this is just some text'; const notAPath = 'this is just some text';
act(() => result.current.insert(notAPath)); act(() => result.current.insert(notAPath, { paste: true }));
expect(getBufferState(result).text).toBe(notAPath); expect(getBufferState(result).text).toBe(notAPath);
}); });
@@ -488,7 +488,7 @@ describe('useTextBuffer', () => {
}), }),
); );
const shortText = 'ls'; const shortText = 'ls';
act(() => result.current.insert(shortText)); act(() => result.current.insert(shortText, { paste: true }));
expect(getBufferState(result).text).toBe(shortText); // No @ prefix for short text expect(getBufferState(result).text).toBe(shortText); // No @ prefix for short text
}); });
}); });
@@ -849,6 +849,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\x7f', sequence: '\x7f',
}); });
result.current.handleInput({ result.current.handleInput({
@@ -856,6 +857,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\x7f', sequence: '\x7f',
}); });
result.current.handleInput({ result.current.handleInput({
@@ -863,6 +865,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\x7f', sequence: '\x7f',
}); });
}); });
@@ -990,9 +993,9 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
// Simulate pasting the long text multiple times // Simulate pasting the long text multiple times
act(() => { act(() => {
result.current.insert(longText); result.current.insert(longText, { paste: true });
result.current.insert(longText); result.current.insert(longText, { paste: true });
result.current.insert(longText); result.current.insert(longText, { paste: true });
}); });
const state = getBufferState(result); const state = getBufferState(result);

View File

@@ -1023,21 +1023,22 @@ export function useTextBuffer({
}, [visualCursor, visualScrollRow, viewport]); }, [visualCursor, visualScrollRow, viewport]);
const insert = useCallback( const insert = useCallback(
(ch: string): void => { (ch: string, { paste = false }: { paste?: boolean } = {}): void => {
if (/[\n\r]/.test(ch)) { if (/[\n\r]/.test(ch)) {
dispatch({ type: 'insert', payload: ch }); dispatch({ type: 'insert', payload: ch });
return; return;
} }
const minLengthToInferAsDragDrop = 3; const minLengthToInferAsDragDrop = 3;
if (ch.length >= minLengthToInferAsDragDrop && !shellModeActive) {
let potentialPath = ch;
if ( if (
potentialPath.length > 2 && ch.length >= minLengthToInferAsDragDrop &&
potentialPath.startsWith("'") && !shellModeActive &&
potentialPath.endsWith("'") paste
) { ) {
potentialPath = ch.slice(1, -1); let potentialPath = ch.trim();
const quoteMatch = potentialPath.match(/^'(.*)'$/);
if (quoteMatch) {
potentialPath = quoteMatch[1];
} }
potentialPath = potentialPath.trim(); potentialPath = potentialPath.trim();
@@ -1203,7 +1204,7 @@ export function useTextBuffer({
backspace(); backspace();
else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del(); else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del();
else if (input && !key.ctrl && !key.meta) { else if (input && !key.ctrl && !key.meta) {
insert(input); insert(input, { paste: key.paste });
} }
}, },
[newline, move, deleteWordLeft, deleteWordRight, backspace, del, insert], [newline, move, deleteWordLeft, deleteWordRight, backspace, del, insert],
@@ -1306,7 +1307,7 @@ export interface TextBuffer {
/** /**
* Insert a single character or string without newlines. * Insert a single character or string without newlines.
*/ */
insert: (ch: string) => void; insert: (ch: string, opts?: { paste?: boolean }) => void;
newline: () => void; newline: () => void;
backspace: () => void; backspace: () => void;
del: () => void; del: () => void;