From 179582d9a20c1cee29edd5ef1f2cbee993d56f4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:42:29 +0000 Subject: [PATCH 1/2] Initial plan From 387de913399df5697b23cf911f6598ccf1465cff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:15:32 +0000 Subject: [PATCH 2/2] Add code lens provider for TODO items to show Start Coding Agent Session Co-authored-by: joshspicer <23246594+joshspicer@users.noreply.github.com> --- src/issues/issueFeatureRegistrar.ts | 6 +- src/issues/issueTodoProvider.ts | 228 +++++++++++++--------- src/test/issues/issueTodoProvider.test.ts | 132 ++++++++----- 3 files changed, 231 insertions(+), 135 deletions(-) diff --git a/src/issues/issueFeatureRegistrar.ts b/src/issues/issueFeatureRegistrar.ts index cf62dad245..bfb6a5b62e 100644 --- a/src/issues/issueFeatureRegistrar.ts +++ b/src/issues/issueFeatureRegistrar.ts @@ -575,8 +575,12 @@ export class IssueFeatureRegistrar extends Disposable { this._register( vscode.languages.registerHoverProvider('*', new UserHoverProvider(this.manager, this.telemetry)), ); + const todoProvider = new IssueTodoProvider(this.context, this.copilotRemoteAgentManager); this._register( - vscode.languages.registerCodeActionsProvider('*', new IssueTodoProvider(this.context, this.copilotRemoteAgentManager), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }), + vscode.languages.registerCodeActionsProvider('*', todoProvider, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }), + ); + this._register( + vscode.languages.registerCodeLensProvider('*', todoProvider), ); }); } diff --git a/src/issues/issueTodoProvider.ts b/src/issues/issueTodoProvider.ts index fbc676e539..8f52b4fed9 100644 --- a/src/issues/issueTodoProvider.ts +++ b/src/issues/issueTodoProvider.ts @@ -1,90 +1,138 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys'; -import { escapeRegExp } from '../common/utils'; -import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; -import { ISSUE_OR_URL_EXPRESSION } from '../github/utils'; -import { MAX_LINE_LENGTH } from './util'; - -export class IssueTodoProvider implements vscode.CodeActionProvider { - private expression: RegExp | undefined; - - constructor( - context: vscode.ExtensionContext, - private copilotRemoteAgentManager: CopilotRemoteAgentManager - ) { - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration(() => { - this.updateTriggers(); - }), - ); - this.updateTriggers(); - } - - private updateTriggers() { - const triggers = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE).get(CREATE_ISSUE_TRIGGERS, []); - this.expression = triggers.length > 0 ? new RegExp(triggers.map(trigger => escapeRegExp(trigger)).join('|')) : undefined; - } - - async provideCodeActions( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - context: vscode.CodeActionContext, - _token: vscode.CancellationToken, - ): Promise { - if (this.expression === undefined || (context.only && context.only !== vscode.CodeActionKind.QuickFix)) { - return []; - } - const codeActions: vscode.CodeAction[] = []; - let lineNumber = range.start.line; - do { - const line = document.lineAt(lineNumber).text; - const truncatedLine = line.substring(0, MAX_LINE_LENGTH); - const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION); - if (!matches) { - const match = truncatedLine.match(this.expression); - const search = match?.index ?? -1; - if (search >= 0 && match) { - // Create GitHub Issue action - const createIssueAction: vscode.CodeAction = new vscode.CodeAction( - vscode.l10n.t('Create GitHub Issue'), - vscode.CodeActionKind.QuickFix, - ); - createIssueAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; - const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/); - const insertIndex = - search + - (indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length); - createIssueAction.command = { - title: vscode.l10n.t('Create GitHub Issue'), - command: 'issue.createIssueFromSelection', - arguments: [{ document, lineNumber, line, insertIndex, range }], - }; - codeActions.push(createIssueAction); - - // Start Coding Agent Session action (if copilot manager is available) - if (this.copilotRemoteAgentManager) { - const startAgentAction: vscode.CodeAction = new vscode.CodeAction( - vscode.l10n.t('Start Coding Agent Session'), - vscode.CodeActionKind.QuickFix, - ); - startAgentAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; - startAgentAction.command = { - title: vscode.l10n.t('Start Coding Agent Session'), - command: 'issue.startCodingAgentFromTodo', - arguments: [{ document, lineNumber, line, insertIndex, range }], - }; - codeActions.push(startAgentAction); - } - break; - } - } - lineNumber++; - } while (range.end.line >= lineNumber); - return codeActions; - } -} +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { escapeRegExp } from '../common/utils'; +import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; +import { ISSUE_OR_URL_EXPRESSION } from '../github/utils'; +import { MAX_LINE_LENGTH } from './util'; + +export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.CodeLensProvider { + private expression: RegExp | undefined; + + constructor( + context: vscode.ExtensionContext, + private copilotRemoteAgentManager: CopilotRemoteAgentManager + ) { + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(() => { + this.updateTriggers(); + }), + ); + this.updateTriggers(); + } + + private updateTriggers() { + const triggers = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE).get(CREATE_ISSUE_TRIGGERS, []); + this.expression = triggers.length > 0 ? new RegExp(triggers.map(trigger => escapeRegExp(trigger)).join('|')) : undefined; + } + + async provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + _token: vscode.CancellationToken, + ): Promise { + if (this.expression === undefined || (context.only && context.only !== vscode.CodeActionKind.QuickFix)) { + return []; + } + const codeActions: vscode.CodeAction[] = []; + let lineNumber = range.start.line; + do { + const line = document.lineAt(lineNumber).text; + const truncatedLine = line.substring(0, MAX_LINE_LENGTH); + const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION); + if (!matches) { + const match = truncatedLine.match(this.expression); + const search = match?.index ?? -1; + if (search >= 0 && match) { + // Create GitHub Issue action + const createIssueAction: vscode.CodeAction = new vscode.CodeAction( + vscode.l10n.t('Create GitHub Issue'), + vscode.CodeActionKind.QuickFix, + ); + createIssueAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; + const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/); + const insertIndex = + search + + (indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length); + createIssueAction.command = { + title: vscode.l10n.t('Create GitHub Issue'), + command: 'issue.createIssueFromSelection', + arguments: [{ document, lineNumber, line, insertIndex, range }], + }; + codeActions.push(createIssueAction); + + // Start Coding Agent Session action (if copilot manager is available) + if (this.copilotRemoteAgentManager) { + const startAgentAction: vscode.CodeAction = new vscode.CodeAction( + vscode.l10n.t('Start Coding Agent Session'), + vscode.CodeActionKind.QuickFix, + ); + startAgentAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; + startAgentAction.command = { + title: vscode.l10n.t('Start Coding Agent Session'), + command: 'issue.startCodingAgentFromTodo', + arguments: [{ document, lineNumber, line, insertIndex, range }], + }; + codeActions.push(startAgentAction); + } + break; + } + } + lineNumber++; + } while (range.end.line >= lineNumber); + return codeActions; + } + + async provideCodeLenses( + document: vscode.TextDocument, + _token: vscode.CancellationToken, + ): Promise { + if (this.expression === undefined) { + return []; + } + + const codeLenses: vscode.CodeLens[] = []; + for (let lineNumber = 0; lineNumber < document.lineCount; lineNumber++) { + const line = document.lineAt(lineNumber).text; + const truncatedLine = line.substring(0, MAX_LINE_LENGTH); + const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION); + + if (!matches) { + const match = truncatedLine.match(this.expression); + const search = match?.index ?? -1; + if (search >= 0 && match) { + const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/); + const insertIndex = + search + + (indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length); + + const range = new vscode.Range(lineNumber, search, lineNumber, search + match[0].length); + + // Only show "Start Coding Agent Session" code lens if copilot manager is available + if (this.copilotRemoteAgentManager) { + const startAgentCodeLens = new vscode.CodeLens(range, { + title: vscode.l10n.t('Start Coding Agent Session'), + command: 'issue.startCodingAgentFromTodo', + arguments: [{ document, lineNumber, line, insertIndex, range }], + }); + codeLenses.push(startAgentCodeLens); + } + } + } + } + return codeLenses; + } + + resolveCodeLens( + codeLens: vscode.CodeLens, + _token: vscode.CancellationToken, + ): vscode.ProviderResult { + // Code lens is already resolved in provideCodeLenses + return codeLens; + } +} diff --git a/src/test/issues/issueTodoProvider.test.ts b/src/test/issues/issueTodoProvider.test.ts index ffd27e1064..e7d1329193 100644 --- a/src/test/issues/issueTodoProvider.test.ts +++ b/src/test/issues/issueTodoProvider.test.ts @@ -1,45 +1,89 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { default as assert } from 'assert'; -import * as vscode from 'vscode'; -import { IssueTodoProvider } from '../../issues/issueTodoProvider'; - -describe('IssueTodoProvider', function () { - it('should provide both actions when CopilotRemoteAgentManager is available', async function () { - const mockContext = { - subscriptions: [] - } as any as vscode.ExtensionContext; - - const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager - - const provider = new IssueTodoProvider(mockContext, mockCopilotManager); - - // Create a mock document with TODO comment - const document = { - lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }), - lineCount: 4 - } as vscode.TextDocument; - - const range = new vscode.Range(1, 0, 1, 20); - const context = { - only: vscode.CodeActionKind.QuickFix - } as vscode.CodeActionContext; - - const actions = await provider.provideCodeActions(document, range, context, new vscode.CancellationTokenSource().token); - - assert.strictEqual(actions.length, 2); - - // Find the actions - const createIssueAction = actions.find(a => a.title === 'Create GitHub Issue'); - const startAgentAction = actions.find(a => a.title === 'Start Coding Agent Session'); - - assert.ok(createIssueAction, 'Should have Create GitHub Issue action'); - assert.ok(startAgentAction, 'Should have Start Coding Agent Session action'); - - assert.strictEqual(createIssueAction?.command?.command, 'issue.createIssueFromSelection'); - assert.strictEqual(startAgentAction?.command?.command, 'issue.startCodingAgentFromTodo'); - }); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { default as assert } from 'assert'; +import * as vscode from 'vscode'; +import { IssueTodoProvider } from '../../issues/issueTodoProvider'; + +describe('IssueTodoProvider', function () { + it('should provide both actions when CopilotRemoteAgentManager is available', async function () { + const mockContext = { + subscriptions: [] + } as any as vscode.ExtensionContext; + + const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager + + const provider = new IssueTodoProvider(mockContext, mockCopilotManager); + + // Create a mock document with TODO comment + const document = { + lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }), + lineCount: 4 + } as vscode.TextDocument; + + const range = new vscode.Range(1, 0, 1, 20); + const context = { + only: vscode.CodeActionKind.QuickFix + } as vscode.CodeActionContext; + + const actions = await provider.provideCodeActions(document, range, context, new vscode.CancellationTokenSource().token); + + assert.strictEqual(actions.length, 2); + + // Find the actions + const createIssueAction = actions.find(a => a.title === 'Create GitHub Issue'); + const startAgentAction = actions.find(a => a.title === 'Start Coding Agent Session'); + + assert.ok(createIssueAction, 'Should have Create GitHub Issue action'); + assert.ok(startAgentAction, 'Should have Start Coding Agent Session action'); + + assert.strictEqual(createIssueAction?.command?.command, 'issue.createIssueFromSelection'); + assert.strictEqual(startAgentAction?.command?.command, 'issue.startCodingAgentFromTodo'); + }); + + it('should provide code lens for TODO comments when CopilotRemoteAgentManager is available', async function () { + const mockContext = { + subscriptions: [] + } as any as vscode.ExtensionContext; + + const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager + + const provider = new IssueTodoProvider(mockContext, mockCopilotManager); + + // Create a mock document with TODO comment + const document = { + lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }), + lineCount: 4 + } as vscode.TextDocument; + + const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); + + assert.strictEqual(codeLenses.length, 1); + + const codeLens = codeLenses[0]; + assert.ok(codeLens.command, 'Code lens should have a command'); + assert.strictEqual(codeLens.command.title, 'Start Coding Agent Session'); + assert.strictEqual(codeLens.command.command, 'issue.startCodingAgentFromTodo'); + assert.strictEqual(codeLens.range.start.line, 1); + }); + + it('should not provide code lens when CopilotRemoteAgentManager is not available', async function () { + const mockContext = { + subscriptions: [] + } as any as vscode.ExtensionContext; + + const provider = new IssueTodoProvider(mockContext, undefined); + + // Create a mock document with TODO comment + const document = { + lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }), + lineCount: 4 + } as vscode.TextDocument; + + const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); + + assert.strictEqual(codeLenses.length, 0, 'Should not provide code lens when CopilotRemoteAgentManager is not available'); + }); }); \ No newline at end of file