Skip to content

Commit 5139c31

Browse files
committed
feat(amazonq): auto-reject edit suggestions when cursor moves >25 lines away
1 parent 7567ec8 commit 5139c31

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "auto-reject edit suggestions when cursor moves >25 lines away"
4+
}

packages/amazonq/src/app/inline/EditRendering/displayImage.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { EditSuggestionState } from '../editSuggestionState'
1616
import type { AmazonQInlineCompletionItemProvider } from '../completion'
1717
import { vsCodeState } from 'aws-core-vscode/codewhisperer'
1818

19+
const autoRejectEditCursorDistance = 25
20+
1921
export class EditDecorationManager {
2022
private imageDecorationType: vscode.TextEditorDecorationType
2123
private removedCodeDecorationType: vscode.TextEditorDecorationType
@@ -346,6 +348,19 @@ export async function displaySvgDecoration(
346348
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
347349
}
348350
})
351+
const cursorChangeListener = vscode.window.onDidChangeTextEditorSelection((e) => {
352+
if (!EditSuggestionState.isEditSuggestionActive()) {
353+
return
354+
}
355+
if (e.textEditor !== editor) {
356+
return
357+
}
358+
const currentPosition = e.selections[0].active
359+
const distance = Math.abs(currentPosition.line - startLine)
360+
if (distance > autoRejectEditCursorDistance) {
361+
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
362+
}
363+
})
349364
await decorationManager.displayEditSuggestion(
350365
editor,
351366
svgImage,
@@ -371,6 +386,7 @@ export async function displaySvgDecoration(
371386

372387
await decorationManager.clearDecorations(editor)
373388
documentChangeListener.dispose()
389+
cursorChangeListener.dispose()
374390
const params: LogInlineCompletionSessionResultsParams = {
375391
sessionId: session.sessionId,
376392
completionSessionResult: {
@@ -405,6 +421,7 @@ export async function displaySvgDecoration(
405421
getLogger().info('Edit suggestion rejected')
406422
await decorationManager.clearDecorations(editor)
407423
documentChangeListener.dispose()
424+
cursorChangeListener.dispose()
408425
const params: LogInlineCompletionSessionResultsParams = {
409426
sessionId: session.sessionId,
410427
completionSessionResult: {

packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts

Lines changed: 221 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import * as vscode from 'vscode'
77
import * as sinon from 'sinon'
88
import assert from 'assert'
9-
import { EditDecorationManager } from '../../../../../src/app/inline/EditRendering/displayImage'
9+
import { EditDecorationManager, displaySvgDecoration } from '../../../../../src/app/inline/EditRendering/displayImage'
10+
import { EditSuggestionState } from '../../../../../src/app/inline/editSuggestionState'
1011

1112
describe('EditDecorationManager', function () {
1213
let sandbox: sinon.SinonSandbox
@@ -174,3 +175,222 @@ describe('EditDecorationManager', function () {
174175
sinon.assert.calledWith(editorStub.setDecorations.secondCall, manager['removedCodeDecorationType'], [])
175176
})
176177
})
178+
179+
describe('displaySvgDecoration cursor distance auto-reject', function () {
180+
let sandbox: sinon.SinonSandbox
181+
let editorStub: sinon.SinonStubbedInstance<vscode.TextEditor>
182+
let documentStub: sinon.SinonStubbedInstance<vscode.TextDocument>
183+
let windowStub: sinon.SinonStub
184+
let commandsStub: sinon.SinonStub
185+
let editSuggestionStateStub: sinon.SinonStub
186+
let onDidChangeTextEditorSelectionStub: sinon.SinonStub
187+
let selectionChangeListener: (e: vscode.TextEditorSelectionChangeEvent) => void
188+
189+
beforeEach(function () {
190+
sandbox = sinon.createSandbox()
191+
192+
documentStub = {
193+
getText: sandbox.stub().returns('Original code content'),
194+
uri: vscode.Uri.file('/test/file.ts'),
195+
lineAt: sandbox.stub().returns({
196+
text: 'Line text content',
197+
range: new vscode.Range(0, 0, 0, 18),
198+
rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 19),
199+
firstNonWhitespaceCharacterIndex: 0,
200+
isEmptyOrWhitespace: false,
201+
}),
202+
} as unknown as sinon.SinonStubbedInstance<vscode.TextDocument>
203+
204+
editorStub = {
205+
document: documentStub,
206+
setDecorations: sandbox.stub(),
207+
} as unknown as sinon.SinonStubbedInstance<vscode.TextEditor>
208+
209+
// Mock vscode.window.onDidChangeTextEditorSelection
210+
onDidChangeTextEditorSelectionStub = sandbox.stub()
211+
onDidChangeTextEditorSelectionStub.returns({ dispose: sandbox.stub() })
212+
windowStub = sandbox.stub(vscode.window, 'onDidChangeTextEditorSelection')
213+
windowStub.callsFake((callback) => {
214+
selectionChangeListener = callback
215+
return { dispose: sandbox.stub() }
216+
})
217+
218+
// Mock vscode.commands.executeCommand
219+
commandsStub = sandbox.stub(vscode.commands, 'executeCommand')
220+
221+
// Mock EditSuggestionState
222+
editSuggestionStateStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive')
223+
editSuggestionStateStub.returns(true)
224+
225+
// Mock other required dependencies
226+
sandbox.stub(vscode.workspace, 'onDidChangeTextDocument').returns({ dispose: sandbox.stub() })
227+
})
228+
229+
afterEach(function () {
230+
sandbox.restore()
231+
})
232+
233+
it('should not reject when cursor moves less than 25 lines away', async function () {
234+
const startLine = 50
235+
await displaySvgDecoration(
236+
editorStub as unknown as vscode.TextEditor,
237+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
238+
startLine,
239+
'new code',
240+
[],
241+
{} as any,
242+
{} as any,
243+
{ itemId: 'test', insertText: 'patch' } as any
244+
)
245+
246+
// Simulate cursor moving 24 lines away (should not reject)
247+
const position = new vscode.Position(startLine + 24, 0)
248+
const selection = new vscode.Selection(position, position)
249+
const selectionChangeEvent = {
250+
textEditor: editorStub,
251+
selections: [selection],
252+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
253+
} as vscode.TextEditorSelectionChangeEvent
254+
255+
selectionChangeListener(selectionChangeEvent)
256+
257+
sinon.assert.notCalled(commandsStub)
258+
})
259+
260+
it('should not reject when cursor moves exactly 25 lines away', async function () {
261+
const startLine = 50
262+
await displaySvgDecoration(
263+
editorStub as unknown as vscode.TextEditor,
264+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
265+
startLine,
266+
'new code',
267+
[],
268+
{} as any,
269+
{} as any,
270+
{ itemId: 'test', insertText: 'patch' } as any
271+
)
272+
273+
// Simulate cursor moving exactly 25 lines away (should not reject)
274+
const position = new vscode.Position(startLine + 25, 0)
275+
const selection = new vscode.Selection(position, position)
276+
const selectionChangeEvent = {
277+
textEditor: editorStub,
278+
selections: [selection],
279+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
280+
} as vscode.TextEditorSelectionChangeEvent
281+
282+
selectionChangeListener(selectionChangeEvent)
283+
284+
sinon.assert.notCalled(commandsStub)
285+
})
286+
287+
it('should reject when cursor moves more than 25 lines away', async function () {
288+
const startLine = 50
289+
await displaySvgDecoration(
290+
editorStub as unknown as vscode.TextEditor,
291+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
292+
startLine,
293+
'new code',
294+
[],
295+
{} as any,
296+
{} as any,
297+
{ itemId: 'test', insertText: 'patch' } as any
298+
)
299+
300+
// Simulate cursor moving 26 lines away (should reject)
301+
const position = new vscode.Position(startLine + 26, 0)
302+
const selection = new vscode.Selection(position, position)
303+
const selectionChangeEvent = {
304+
textEditor: editorStub,
305+
selections: [selection],
306+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
307+
} as vscode.TextEditorSelectionChangeEvent
308+
309+
selectionChangeListener(selectionChangeEvent)
310+
311+
sinon.assert.calledOnceWithExactly(commandsStub, 'aws.amazonq.inline.rejectEdit')
312+
})
313+
314+
it('should reject when cursor moves more than 25 lines before the edit', async function () {
315+
const startLine = 50
316+
await displaySvgDecoration(
317+
editorStub as unknown as vscode.TextEditor,
318+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
319+
startLine,
320+
'new code',
321+
[],
322+
{} as any,
323+
{} as any,
324+
{ itemId: 'test', insertText: 'patch' } as any
325+
)
326+
327+
// Simulate cursor moving 26 lines before the edit (should reject)
328+
const position = new vscode.Position(startLine - 26, 0)
329+
const selection = new vscode.Selection(position, position)
330+
const selectionChangeEvent = {
331+
textEditor: editorStub,
332+
selections: [selection],
333+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
334+
} as vscode.TextEditorSelectionChangeEvent
335+
336+
selectionChangeListener(selectionChangeEvent)
337+
338+
sinon.assert.calledOnceWithExactly(commandsStub, 'aws.amazonq.inline.rejectEdit')
339+
})
340+
341+
it('should not reject when edit is near beginning of file and cursor cannot move far enough', async function () {
342+
const startLine = 10 // Edit near beginning of file
343+
await displaySvgDecoration(
344+
editorStub as unknown as vscode.TextEditor,
345+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
346+
startLine,
347+
'new code',
348+
[],
349+
{} as any,
350+
{} as any,
351+
{ itemId: 'test', insertText: 'patch' } as any
352+
)
353+
354+
// Simulate cursor at line 0 (can't go further back)
355+
const position = new vscode.Position(0, 0)
356+
const selection = new vscode.Selection(position, position)
357+
const selectionChangeEvent = {
358+
textEditor: editorStub,
359+
selections: [selection],
360+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
361+
} as vscode.TextEditorSelectionChangeEvent
362+
363+
selectionChangeListener(selectionChangeEvent)
364+
365+
sinon.assert.notCalled(commandsStub)
366+
})
367+
368+
it('should not reject when edit suggestion is not active', async function () {
369+
editSuggestionStateStub.returns(false)
370+
371+
const startLine = 50
372+
await displaySvgDecoration(
373+
editorStub as unknown as vscode.TextEditor,
374+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
375+
startLine,
376+
'new code',
377+
[],
378+
{} as any,
379+
{} as any,
380+
{ itemId: 'test', insertText: 'patch' } as any
381+
)
382+
383+
// Simulate cursor moving far away
384+
const position = new vscode.Position(startLine + 100, 0)
385+
const selection = new vscode.Selection(position, position)
386+
const selectionChangeEvent = {
387+
textEditor: editorStub,
388+
selections: [selection],
389+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
390+
} as vscode.TextEditorSelectionChangeEvent
391+
392+
selectionChangeListener(selectionChangeEvent)
393+
394+
sinon.assert.notCalled(commandsStub)
395+
})
396+
})

0 commit comments

Comments
 (0)