mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
feat(paste): add large paste placeholder with ID reuse and atomic deletion
- Add placeholder mode for pastes >1000 chars OR >10 lines - Show placeholder like '[Pasted Content 1500 chars]' instead of full content - Store full content and expand on submit - Support multiple placeholders with sequential IDs (#2, #3, etc.) - Reuse freed IDs when placeholders are deleted - Delete entire placeholder atomically on backspace - Add 7 unit tests for placeholder functionality Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
c7b681ef5d
commit
21ae35b221
2 changed files with 322 additions and 9 deletions
|
|
@ -2140,6 +2140,230 @@ describe('InputPrompt', () => {
|
|||
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('large paste placeholder', () => {
|
||||
it('should create placeholder for paste > 1000 characters', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Create a paste with 1001 characters
|
||||
const largeContent = 'x'.repeat(1001);
|
||||
|
||||
// Simulate bracketed paste
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Verify placeholder was inserted, not the full content
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
'[Pasted Content 1001 chars]',
|
||||
{ paste: false },
|
||||
);
|
||||
expect(mockBuffer.insert).toHaveBeenCalledTimes(1);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should create placeholder for paste > 10 lines', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Create a paste with 11 lines (each line is short)
|
||||
const multiLineContent = Array(11).fill('line').join('\n');
|
||||
|
||||
// Simulate bracketed paste
|
||||
stdin.write(`\x1b[200~${multiLineContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Verify placeholder was inserted
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\[Pasted Content \d+ chars\]/),
|
||||
{ paste: false },
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use sequential IDs for multiple pastes of same size', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
const largeContent = 'x'.repeat(1001);
|
||||
|
||||
// First paste
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Second paste
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Verify both placeholders were created with correct IDs
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
'[Pasted Content 1001 chars]',
|
||||
{ paste: false },
|
||||
);
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
'[Pasted Content 1001 chars] #2',
|
||||
{ paste: false },
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should expand placeholder to full content on submit', async () => {
|
||||
const largeContent = 'x'.repeat(1001);
|
||||
mockBuffer.text = '[Pasted Content 1001 chars]';
|
||||
mockBuffer.lines = [mockBuffer.text];
|
||||
mockBuffer.cursor = [0, mockBuffer.text.length];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// First paste to set up the placeholder
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Wait for paste protection to expire
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
|
||||
// Submit the input
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
// Verify onSubmit was called with expanded content
|
||||
expect(props.onSubmit).toHaveBeenCalledWith(largeContent);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should delete entire placeholder on backspace', async () => {
|
||||
const placeholderText = '[Pasted Content 1001 chars]';
|
||||
mockBuffer.text = placeholderText;
|
||||
mockBuffer.lines = [placeholderText];
|
||||
mockBuffer.cursor = [0, placeholderText.length];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// First set up a placeholder via paste
|
||||
const largeContent = 'x'.repeat(1001);
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Press backspace to delete the placeholder
|
||||
stdin.write('\x7f'); // backspace character
|
||||
await wait();
|
||||
|
||||
// Verify replaceRangeByOffset was called to delete entire placeholder
|
||||
expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalledWith(
|
||||
0,
|
||||
placeholderText.length,
|
||||
'',
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reuse placeholder ID after deletion', async () => {
|
||||
// Set up mocks that actually update buffer state
|
||||
vi.mocked(mockBuffer.insert).mockImplementation((text: string) => {
|
||||
mockBuffer.text += text;
|
||||
mockBuffer.lines = [mockBuffer.text];
|
||||
mockBuffer.cursor = [0, mockBuffer.text.length];
|
||||
});
|
||||
|
||||
vi.mocked(mockBuffer.replaceRangeByOffset).mockImplementation(
|
||||
(start: number, end: number, replacement: string) => {
|
||||
mockBuffer.text =
|
||||
mockBuffer.text.slice(0, start) +
|
||||
replacement +
|
||||
mockBuffer.text.slice(end);
|
||||
mockBuffer.lines = [mockBuffer.text];
|
||||
mockBuffer.cursor = [0, start];
|
||||
},
|
||||
);
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
const largeContent = 'x'.repeat(1001);
|
||||
|
||||
// First paste - gets ID 1
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Verify first placeholder was inserted
|
||||
expect(mockBuffer.text).toBe('[Pasted Content 1001 chars]');
|
||||
|
||||
// Press backspace to delete the placeholder (cursor is at end of placeholder)
|
||||
stdin.write('\x7f');
|
||||
await wait();
|
||||
|
||||
// Verify the placeholder was deleted (buffer is now empty)
|
||||
expect(mockBuffer.text).toBe('');
|
||||
|
||||
// Second paste - should reuse ID 1 since the first was deleted
|
||||
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Verify the ID was reused (no #2 suffix)
|
||||
const insertCalls = vi.mocked(mockBuffer.insert).mock.calls;
|
||||
const lastCall = insertCalls[insertCalls.length - 1];
|
||||
expect(lastCall[0]).toBe('[Pasted Content 1001 chars]');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle mixed pastes with different character counts', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await wait();
|
||||
|
||||
const content1001 = 'x'.repeat(1001);
|
||||
const content1500 = 'y'.repeat(1500);
|
||||
|
||||
// Paste 1001 chars
|
||||
stdin.write(`\x1b[200~${content1001}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Paste 1500 chars
|
||||
stdin.write(`\x1b[200~${content1500}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Paste 1001 chars again (should get ID #2 for 1001)
|
||||
stdin.write(`\x1b[200~${content1001}\x1b[201~`);
|
||||
await wait();
|
||||
|
||||
// Verify placeholders with correct IDs
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
'[Pasted Content 1001 chars]',
|
||||
{ paste: false },
|
||||
);
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
'[Pasted Content 1500 chars]',
|
||||
{ paste: false },
|
||||
);
|
||||
expect(mockBuffer.insert).toHaveBeenCalledWith(
|
||||
'[Pasted Content 1001 chars] #2',
|
||||
{ paste: false },
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
function clean(str: string | undefined): string {
|
||||
if (!str) return '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue