Fix #2922: Prevent @ concatenation to valid paths in shellmode. (#2932)

This commit is contained in:
Daniel Sibaja 2025-07-05 16:20:12 -06:00 committed by GitHub
parent b564d4a088
commit 2b8a565f89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 78 additions and 8 deletions

View File

@ -310,6 +310,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
stdin, stdin,
setRawMode, setRawMode,
isValidPath, isValidPath,
shellModeActive,
}); });
const handleExit = useCallback( const handleExit = useCallback(

View File

@ -439,6 +439,60 @@ describe('useTextBuffer', () => {
}); });
}); });
describe('Shell Mode Behavior', () => {
it('should not prepend @ to valid file paths when shellModeActive is true', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: () => true,
shellModeActive: true,
}),
);
const filePath = '/path/to/a/valid/file.txt';
act(() => result.current.insert(filePath));
expect(getBufferState(result).text).toBe(filePath); // No @ prefix
});
it('should not prepend @ to quoted paths when shellModeActive is true', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: () => true,
shellModeActive: true,
}),
);
const quotedFilePath = "'/path/to/a/valid/file.txt'";
act(() => result.current.insert(quotedFilePath));
expect(getBufferState(result).text).toBe(quotedFilePath); // No @ prefix, keeps quotes
});
it('should behave normally with invalid paths when shellModeActive is true', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: () => false,
shellModeActive: true,
}),
);
const notAPath = 'this is just some text';
act(() => result.current.insert(notAPath));
expect(getBufferState(result).text).toBe(notAPath);
});
it('should behave normally with short text when shellModeActive is true', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: () => true,
shellModeActive: true,
}),
);
const shortText = 'ls';
act(() => result.current.insert(shortText));
expect(getBufferState(result).text).toBe(shortText); // No @ prefix for short text
});
});
describe('Cursor Movement', () => { describe('Cursor Movement', () => {
it('move: left/right should work within and across visual lines (due to wrapping)', () => { it('move: left/right should work within and across visual lines (due to wrapping)', () => {
// Text: "long line1next line2" (20 chars) // Text: "long line1next line2" (20 chars)
@ -722,6 +776,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: 'h', sequence: 'h',
}), }),
); );
@ -731,6 +786,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: 'i', sequence: 'i',
}), }),
); );
@ -747,6 +803,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\r', sequence: '\r',
}), }),
); );
@ -768,6 +825,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\x7f', sequence: '\x7f',
}), }),
); );
@ -863,6 +921,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\x1b[D', sequence: '\x1b[D',
}), }),
); // cursor [0,1] ); // cursor [0,1]
@ -873,6 +932,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: '\x1b[C', sequence: '\x1b[C',
}), }),
); // cursor [0,2] ); // cursor [0,2]
@ -887,10 +947,11 @@ describe('useTextBuffer', () => {
// Simulate pasting by calling handleInput with a string longer than 1 char // Simulate pasting by calling handleInput with a string longer than 1 char
act(() => act(() =>
result.current.handleInput({ result.current.handleInput({
name: undefined, name: '',
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: textWithAnsi, sequence: textWithAnsi,
}), }),
); );
@ -907,6 +968,7 @@ describe('useTextBuffer', () => {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: true, shift: true,
paste: false,
sequence: '\r', sequence: '\r',
}), }),
); // Simulates Shift+Enter in VSCode terminal ); // Simulates Shift+Enter in VSCode terminal
@ -1096,10 +1158,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
const textWithAnsi = '\x1B[31mHello\x1B[0m'; const textWithAnsi = '\x1B[31mHello\x1B[0m';
act(() => act(() =>
result.current.handleInput({ result.current.handleInput({
name: undefined, name: '',
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: textWithAnsi, sequence: textWithAnsi,
}), }),
); );
@ -1113,10 +1176,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
const textWithControlChars = 'H\x07e\x08l\x0Bl\x0Co'; // BELL, BACKSPACE, VT, FF const textWithControlChars = 'H\x07e\x08l\x0Bl\x0Co'; // BELL, BACKSPACE, VT, FF
act(() => act(() =>
result.current.handleInput({ result.current.handleInput({
name: undefined, name: '',
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: textWithControlChars, sequence: textWithControlChars,
}), }),
); );
@ -1130,10 +1194,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
const textWithMixed = '\u001B[4mH\u001B[0mello'; const textWithMixed = '\u001B[4mH\u001B[0mello';
act(() => act(() =>
result.current.handleInput({ result.current.handleInput({
name: undefined, name: '',
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: textWithMixed, sequence: textWithMixed,
}), }),
); );
@ -1147,10 +1212,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
const validText = 'Hello World\nThis is a test.'; const validText = 'Hello World\nThis is a test.';
act(() => act(() =>
result.current.handleInput({ result.current.handleInput({
name: undefined, name: '',
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: validText, sequence: validText,
}), }),
); );
@ -1164,10 +1230,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
const pastedText = '\u001B[4mPasted\u001B[4m Text'; const pastedText = '\u001B[4mPasted\u001B[4m Text';
act(() => act(() =>
result.current.handleInput({ result.current.handleInput({
name: undefined, name: '',
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: false, shift: false,
paste: false,
sequence: pastedText, sequence: pastedText,
}), }),
); );

View File

@ -73,6 +73,7 @@ interface UseTextBufferProps {
setRawMode?: (mode: boolean) => void; // For external editor setRawMode?: (mode: boolean) => void; // For external editor
onChange?: (text: string) => void; // Callback for when text changes onChange?: (text: string) => void; // Callback for when text changes
isValidPath: (path: string) => boolean; isValidPath: (path: string) => boolean;
shellModeActive?: boolean; // Whether the text buffer is in shell mode
} }
interface UndoHistoryEntry { interface UndoHistoryEntry {
@ -960,6 +961,7 @@ export function useTextBuffer({
setRawMode, setRawMode,
onChange, onChange,
isValidPath, isValidPath,
shellModeActive = false,
}: UseTextBufferProps): TextBuffer { }: UseTextBufferProps): TextBuffer {
const initialState = useMemo((): TextBufferState => { const initialState = useMemo((): TextBufferState => {
const lines = initialText.split('\n'); const lines = initialText.split('\n');
@ -1028,7 +1030,7 @@ export function useTextBuffer({
} }
const minLengthToInferAsDragDrop = 3; const minLengthToInferAsDragDrop = 3;
if (ch.length >= minLengthToInferAsDragDrop) { if (ch.length >= minLengthToInferAsDragDrop && !shellModeActive) {
let potentialPath = ch; let potentialPath = ch;
if ( if (
potentialPath.length > 2 && potentialPath.length > 2 &&
@ -1060,7 +1062,7 @@ export function useTextBuffer({
dispatch({ type: 'insert', payload: currentText }); dispatch({ type: 'insert', payload: currentText });
} }
}, },
[isValidPath], [isValidPath, shellModeActive],
); );
const newline = useCallback((): void => { const newline = useCallback((): void => {