Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4428,6 +4428,7 @@
"number",
"null"
],
"default": 10,
"markdownDescription": "%github.copilot.config.inlineEdits.triggerOnEditorChangeAfterSeconds%",
"tags": [
"advanced",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ suite('InlineEditTriggerer', () => {
const doc2 = createTextDocument(undefined, Uri.file('file2.py'));

nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1;
nextEditProvider.lastOutcome = NesOutcome.Accepted;

// Configure to trigger on document switch
void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30);
Expand Down Expand Up @@ -504,6 +505,7 @@ suite('InlineEditTriggerer', () => {
const doc2 = createTextDocument(undefined, Uri.file('file2.py'));

nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1;
nextEditProvider.lastOutcome = NesOutcome.Accepted;

const triggerAfterSeconds = 30;
// Configure to trigger on document switch
Expand Down Expand Up @@ -930,6 +932,7 @@ suite('InlineEditTriggerer', () => {
const doc2 = createTextDocument(undefined, Uri.file('file2.py'), 'line1\nline2');

nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1;
nextEditProvider.lastOutcome = NesOutcome.Accepted;
void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30);

// Edit doc1 and trigger
Expand Down Expand Up @@ -1148,6 +1151,7 @@ suite('InlineEditTriggerer', () => {
const doc2 = createTextDocument(undefined, Uri.file('file2.py'));

nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1;
nextEditProvider.lastOutcome = NesOutcome.Accepted;
void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30);

// Fire an undo change — this should still update lastEditTimestamp
Expand Down Expand Up @@ -1216,25 +1220,31 @@ suite('InlineEditTriggerer', () => {
assert.strictEqual(firedEvents.length, 0, 'Should not fire on doc switch during rejection cooldown');
});

test('Same-line cooldown is bypassed when triggerOnActiveEditorChange is set', () => {
const { document, textEditor } = createTextDocument();
test('Same-line cooldown is bypassed after switching away and back', () => {
const doc1 = createTextDocument(undefined, Uri.file('file1.py'));
const doc2 = createTextDocument(undefined, Uri.file('file2.py'));
nextEditProvider.lastRejectionTime = Date.now() - TRIGGER_INLINE_EDIT_REJECTION_COOLDOWN - 1;

// Enable triggerOnActiveEditorChange — this bypasses same-line cooldown
void configurationService.setConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, 30);

triggerTextChange(document);
triggerTextSelectionChange(textEditor, new Selection(0, 5, 0, 5));
// Edit doc1 and trigger on line 0
triggerTextChange(doc1.document);
triggerTextSelectionChange(doc1.textEditor, new Selection(0, 5, 0, 5));

const initialCount = firedEvents.length;
assert.isAtLeast(initialCount, 1, 'First trigger should fire');

// Same line, different column — normally would be in cooldown,
// but triggerOnActiveEditorChange is set so cooldown is bypassed
triggerTextSelectionChange(textEditor, new Selection(0, 10, 0, 10));
// Same line — cooldown blocks
triggerTextSelectionChange(doc1.textEditor, new Selection(0, 10, 0, 10));
assert.strictEqual(firedEvents.length, initialCount, 'Same-line cooldown should block');

// Switch to doc2
triggerTextChange(doc2.document);
triggerTextSelectionChange(doc2.textEditor, new Selection(0, 0, 0, 0));
const countAfterDoc2 = firedEvents.length;

assert.isAtLeast(firedEvents.length, initialCount + 1,
'Same-line cooldown should be bypassed when triggerOnActiveEditorChange is set');
// Switch back to doc1, same line — cooldown should be cleared by the doc switch
triggerTextSelectionChange(doc1.textEditor, new Selection(0, 10, 0, 10));
assert.isAtLeast(firedEvents.length, countAfterDoc2 + 1,
'Same-line cooldown should be bypassed after switching away and back');
});

test('Output pane documents are ignored for selection changes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export class InlineEditTriggerer extends Disposable {
return;
}

// When the user switches to a different file and comes back, clear same-line
// cooldowns so the triggerer fires again on the same line.
if (!isSameDoc) {
mostRecentChange.lineNumberTriggers.clear();
}

const hadRecentEdit = this._hasRecentEdit(mostRecentChange);
if (!hadRecentEdit || !this._hasRecentTrigger()) {
// The edit is too old or the provider was not triggered recently (we might be
Expand Down Expand Up @@ -221,17 +227,14 @@ export class InlineEditTriggerer extends Disposable {
/**
* Returns true if the same-line cooldown is active and we should skip triggering.
*
* The cooldown is bypassed when:
* - `triggerOnActiveEditorChange` is configured, OR
* - we're in a notebook cell and the current document differs from the one that
* originally triggered the change (user moved to a different cell).
* The cooldown is bypassed when we're in a notebook cell and the current document
* differs from the one that originally triggered the change (user moved to a
* different cell).
*
* When the user switches to a different file and comes back, line triggers are
* cleared (see {@link _handleSelectionChange}), so the cooldown naturally resets.
*/
private _isSameLineCooldownActive(mostRecentChange: LastChange, selectionLine: number, currentDocument: vscode.TextDocument): boolean {
const triggerOnActiveEditorChange = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineEditsTriggerOnEditorChangeAfterSeconds, this._expService);
if (triggerOnActiveEditorChange) {
return false; // cooldown bypassed
}

// In a notebook, if the user moved to a different cell, bypass the cooldown
if (isNotebookCell(currentDocument.uri) && currentDocument !== mostRecentChange.documentTrigger) {
return false; // cooldown bypassed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ export namespace ConfigKey {
/** Maximum number of tool calls the execution subagent can make */
export const ExecutionSubagentToolCallLimit = defineSetting<number>('chat.executionSubagent.toolCallLimit', ConfigType.ExperimentBased, 10);

export const InlineEditsTriggerOnEditorChangeAfterSeconds = defineAndMigrateExpSetting<number | undefined>('chat.advanced.inlineEdits.triggerOnEditorChangeAfterSeconds', 'chat.inlineEdits.triggerOnEditorChangeAfterSeconds', undefined);
export const InlineEditsTriggerOnEditorChangeAfterSeconds = defineAndMigrateExpSetting<number | undefined>('chat.advanced.inlineEdits.triggerOnEditorChangeAfterSeconds', 'chat.inlineEdits.triggerOnEditorChangeAfterSeconds', 10);
export const InlineEditsNextCursorPredictionDisplayLine = defineAndMigrateExpSetting<boolean>('chat.advanced.inlineEdits.nextCursorPrediction.displayLine', 'chat.inlineEdits.nextCursorPrediction.displayLine', true);
export const InlineEditsNextCursorPredictionCurrentFileMaxTokens = defineAndMigrateExpSetting<number>('chat.advanced.inlineEdits.nextCursorPrediction.currentFileMaxTokens', 'chat.inlineEdits.nextCursorPrediction.currentFileMaxTokens', 3000);
export const InlineEditsRenameSymbolSuggestions = defineSetting<boolean>('chat.inlineEdits.renameSymbolSuggestions', ConfigType.ExperimentBased, true);
Expand Down Expand Up @@ -777,7 +777,7 @@ export namespace ConfigKey {
export const InlineEditsSpeculativeRequestsAutoExpandEditWindowLines = defineTeamInternalSetting<SpeculativeRequestsAutoExpandEditWindowLines>('chat.advanced.inlineEdits.speculativeRequestsAutoExpandEditWindowLines', ConfigType.ExperimentBased, SpeculativeRequestsAutoExpandEditWindowLines.Off, SpeculativeRequestsAutoExpandEditWindowLines.VALIDATOR);
export const InlineEditsExtraDebounceInlineSuggestion = defineTeamInternalSetting<number>('chat.advanced.inlineEdits.extraDebounceInlineSuggestion', ConfigType.ExperimentBased, 0);
export const InlineEditsDebounceOnSelectionChange = defineTeamInternalSetting<number | undefined>('chat.advanced.inlineEdits.debounceOnSelectionChange', ConfigType.ExperimentBased, undefined);
export const InlineEditsTriggerOnEditorChangeStrategy = defineTeamInternalSetting<triggerOptions.DocumentSwitchTriggerStrategy>('chat.advanced.inlineEdits.triggerOnEditorChangeStrategy', ConfigType.ExperimentBased, triggerOptions.DocumentSwitchTriggerStrategy.Always, triggerOptions.DocumentSwitchTriggerStrategy.VALIDATOR);
export const InlineEditsTriggerOnEditorChangeStrategy = defineTeamInternalSetting<triggerOptions.DocumentSwitchTriggerStrategy>('chat.advanced.inlineEdits.triggerOnEditorChangeStrategy', ConfigType.ExperimentBased, triggerOptions.DocumentSwitchTriggerStrategy.AfterAcceptance, triggerOptions.DocumentSwitchTriggerStrategy.VALIDATOR);
export const InlineEditsProviderId = defineTeamInternalSetting<string | undefined>('chat.advanced.inlineEdits.providerId', ConfigType.ExperimentBased, undefined);
export const InlineEditsUnification = defineTeamInternalSetting<boolean>('chat.advanced.inlineEdits.unification', ConfigType.ExperimentBased, false);
export const InlineEditsNextCursorPredictionModelName = defineTeamInternalSetting<string | undefined>('chat.advanced.inlineEdits.nextCursorPrediction.modelName', ConfigType.ExperimentBased, 'copilot-suggestions-himalia-001');
Expand Down
Loading