Skip to content

Commit fb21f0b

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

File tree

3 files changed

+175
-18
lines changed

3 files changed

+175
-18
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: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,30 @@
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'
11+
12+
// Shared helper function to create common stubs
13+
function createCommonStubs(sandbox: sinon.SinonSandbox) {
14+
const documentStub = {
15+
getText: sandbox.stub().returns('Original code content'),
16+
uri: vscode.Uri.file('/test/file.ts'),
17+
lineAt: sandbox.stub().returns({
18+
text: 'Line text content',
19+
range: new vscode.Range(0, 0, 0, 18),
20+
rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 19),
21+
firstNonWhitespaceCharacterIndex: 0,
22+
isEmptyOrWhitespace: false,
23+
}),
24+
} as unknown as sinon.SinonStubbedInstance<vscode.TextDocument>
25+
26+
const editorStub = {
27+
document: documentStub,
28+
setDecorations: sandbox.stub(),
29+
} as unknown as sinon.SinonStubbedInstance<vscode.TextEditor>
30+
31+
return { documentStub, editorStub }
32+
}
1033

1134
describe('EditDecorationManager', function () {
1235
let sandbox: sinon.SinonSandbox
@@ -25,23 +48,13 @@ describe('EditDecorationManager', function () {
2548
dispose: sandbox.stub(),
2649
} as unknown as sinon.SinonStubbedInstance<vscode.TextEditorDecorationType>
2750

28-
documentStub = {
29-
getText: sandbox.stub().returns('Original code content'),
30-
lineCount: 5,
31-
lineAt: sandbox.stub().returns({
32-
text: 'Line text content',
33-
range: new vscode.Range(0, 0, 0, 18),
34-
rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 19),
35-
firstNonWhitespaceCharacterIndex: 0,
36-
isEmptyOrWhitespace: false,
37-
}),
38-
} as unknown as sinon.SinonStubbedInstance<vscode.TextDocument>
39-
40-
editorStub = {
41-
document: documentStub,
42-
setDecorations: sandbox.stub(),
43-
edit: sandbox.stub().resolves(true),
44-
} as unknown as sinon.SinonStubbedInstance<vscode.TextEditor>
51+
const commonStubs = createCommonStubs(sandbox)
52+
documentStub = commonStubs.documentStub
53+
editorStub = commonStubs.editorStub
54+
55+
// Add additional properties needed for this test suite
56+
documentStub.lineCount = 5
57+
editorStub.edit = sandbox.stub().resolves(true)
4558

4659
windowStub = sandbox.stub(vscode.window)
4760
windowStub.createTextEditorDecorationType.returns(decorationTypeStub as any)
@@ -174,3 +187,126 @@ describe('EditDecorationManager', function () {
174187
sinon.assert.calledWith(editorStub.setDecorations.secondCall, manager['removedCodeDecorationType'], [])
175188
})
176189
})
190+
191+
describe('displaySvgDecoration cursor distance auto-reject', function () {
192+
let sandbox: sinon.SinonSandbox
193+
let editorStub: sinon.SinonStubbedInstance<vscode.TextEditor>
194+
let documentStub: sinon.SinonStubbedInstance<vscode.TextDocument>
195+
let windowStub: sinon.SinonStub
196+
let commandsStub: sinon.SinonStub
197+
let editSuggestionStateStub: sinon.SinonStub
198+
let onDidChangeTextEditorSelectionStub: sinon.SinonStub
199+
let selectionChangeListener: (e: vscode.TextEditorSelectionChangeEvent) => void
200+
201+
// Helper function to setup displaySvgDecoration
202+
async function setupDisplaySvgDecoration(startLine: number) {
203+
return await displaySvgDecoration(
204+
editorStub as unknown as vscode.TextEditor,
205+
vscode.Uri.parse('data:image/svg+xml;base64,test'),
206+
startLine,
207+
'new code',
208+
[],
209+
{} as any,
210+
{} as any,
211+
{ itemId: 'test', insertText: 'patch' } as any
212+
)
213+
}
214+
215+
// Helper function to create selection change event
216+
function createSelectionChangeEvent(line: number): vscode.TextEditorSelectionChangeEvent {
217+
const position = new vscode.Position(line, 0)
218+
const selection = new vscode.Selection(position, position)
219+
return {
220+
textEditor: editorStub,
221+
selections: [selection],
222+
kind: vscode.TextEditorSelectionChangeKind.Mouse,
223+
} as vscode.TextEditorSelectionChangeEvent
224+
}
225+
226+
beforeEach(function () {
227+
sandbox = sinon.createSandbox()
228+
229+
const commonStubs = createCommonStubs(sandbox)
230+
documentStub = commonStubs.documentStub
231+
editorStub = commonStubs.editorStub
232+
233+
// Mock vscode.window.onDidChangeTextEditorSelection
234+
onDidChangeTextEditorSelectionStub = sandbox.stub()
235+
onDidChangeTextEditorSelectionStub.returns({ dispose: sandbox.stub() })
236+
windowStub = sandbox.stub(vscode.window, 'onDidChangeTextEditorSelection')
237+
windowStub.callsFake((callback) => {
238+
selectionChangeListener = callback
239+
return { dispose: sandbox.stub() }
240+
})
241+
242+
// Mock vscode.commands.executeCommand
243+
commandsStub = sandbox.stub(vscode.commands, 'executeCommand')
244+
245+
// Mock EditSuggestionState
246+
editSuggestionStateStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive')
247+
editSuggestionStateStub.returns(true)
248+
249+
// Mock other required dependencies
250+
sandbox.stub(vscode.workspace, 'onDidChangeTextDocument').returns({ dispose: sandbox.stub() })
251+
})
252+
253+
afterEach(function () {
254+
sandbox.restore()
255+
})
256+
257+
it('should not reject when cursor moves less than 25 lines away', async function () {
258+
const startLine = 50
259+
await setupDisplaySvgDecoration(startLine)
260+
261+
selectionChangeListener(createSelectionChangeEvent(startLine + 24))
262+
263+
sinon.assert.notCalled(commandsStub)
264+
})
265+
266+
it('should not reject when cursor moves exactly 25 lines away', async function () {
267+
const startLine = 50
268+
await setupDisplaySvgDecoration(startLine)
269+
270+
selectionChangeListener(createSelectionChangeEvent(startLine + 25))
271+
272+
sinon.assert.notCalled(commandsStub)
273+
})
274+
275+
it('should reject when cursor moves more than 25 lines away', async function () {
276+
const startLine = 50
277+
await setupDisplaySvgDecoration(startLine)
278+
279+
selectionChangeListener(createSelectionChangeEvent(startLine + 26))
280+
281+
sinon.assert.calledOnceWithExactly(commandsStub, 'aws.amazonq.inline.rejectEdit')
282+
})
283+
284+
it('should reject when cursor moves more than 25 lines before the edit', async function () {
285+
const startLine = 50
286+
await setupDisplaySvgDecoration(startLine)
287+
288+
selectionChangeListener(createSelectionChangeEvent(startLine - 26))
289+
290+
sinon.assert.calledOnceWithExactly(commandsStub, 'aws.amazonq.inline.rejectEdit')
291+
})
292+
293+
it('should not reject when edit is near beginning of file and cursor cannot move far enough', async function () {
294+
const startLine = 10
295+
await setupDisplaySvgDecoration(startLine)
296+
297+
selectionChangeListener(createSelectionChangeEvent(0))
298+
299+
sinon.assert.notCalled(commandsStub)
300+
})
301+
302+
it('should not reject when edit suggestion is not active', async function () {
303+
editSuggestionStateStub.returns(false)
304+
305+
const startLine = 50
306+
await setupDisplaySvgDecoration(startLine)
307+
308+
selectionChangeListener(createSelectionChangeEvent(startLine + 100))
309+
310+
sinon.assert.notCalled(commandsStub)
311+
})
312+
})

0 commit comments

Comments
 (0)