Skip to content

Commit 387de91

Browse files
Copilotjoshspicer
andcommitted
Add code lens provider for TODO items to show Start Coding Agent Session
Co-authored-by: joshspicer <23246594+joshspicer@users.noreply.github.com>
1 parent 179582d commit 387de91

3 files changed

Lines changed: 231 additions & 135 deletions

File tree

src/issues/issueFeatureRegistrar.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,12 @@ export class IssueFeatureRegistrar extends Disposable {
575575
this._register(
576576
vscode.languages.registerHoverProvider('*', new UserHoverProvider(this.manager, this.telemetry)),
577577
);
578+
const todoProvider = new IssueTodoProvider(this.context, this.copilotRemoteAgentManager);
578579
this._register(
579-
vscode.languages.registerCodeActionsProvider('*', new IssueTodoProvider(this.context, this.copilotRemoteAgentManager), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }),
580+
vscode.languages.registerCodeActionsProvider('*', todoProvider, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }),
581+
);
582+
this._register(
583+
vscode.languages.registerCodeLensProvider('*', todoProvider),
580584
);
581585
});
582586
}

src/issues/issueTodoProvider.ts

Lines changed: 138 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,138 @@
1-
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) Microsoft Corporation. All rights reserved.
3-
* Licensed under the MIT License. See License.txt in the project root for license information.
4-
*--------------------------------------------------------------------------------------------*/
5-
6-
import * as vscode from 'vscode';
7-
import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys';
8-
import { escapeRegExp } from '../common/utils';
9-
import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent';
10-
import { ISSUE_OR_URL_EXPRESSION } from '../github/utils';
11-
import { MAX_LINE_LENGTH } from './util';
12-
13-
export class IssueTodoProvider implements vscode.CodeActionProvider {
14-
private expression: RegExp | undefined;
15-
16-
constructor(
17-
context: vscode.ExtensionContext,
18-
private copilotRemoteAgentManager: CopilotRemoteAgentManager
19-
) {
20-
context.subscriptions.push(
21-
vscode.workspace.onDidChangeConfiguration(() => {
22-
this.updateTriggers();
23-
}),
24-
);
25-
this.updateTriggers();
26-
}
27-
28-
private updateTriggers() {
29-
const triggers = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE).get(CREATE_ISSUE_TRIGGERS, []);
30-
this.expression = triggers.length > 0 ? new RegExp(triggers.map(trigger => escapeRegExp(trigger)).join('|')) : undefined;
31-
}
32-
33-
async provideCodeActions(
34-
document: vscode.TextDocument,
35-
range: vscode.Range | vscode.Selection,
36-
context: vscode.CodeActionContext,
37-
_token: vscode.CancellationToken,
38-
): Promise<vscode.CodeAction[]> {
39-
if (this.expression === undefined || (context.only && context.only !== vscode.CodeActionKind.QuickFix)) {
40-
return [];
41-
}
42-
const codeActions: vscode.CodeAction[] = [];
43-
let lineNumber = range.start.line;
44-
do {
45-
const line = document.lineAt(lineNumber).text;
46-
const truncatedLine = line.substring(0, MAX_LINE_LENGTH);
47-
const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION);
48-
if (!matches) {
49-
const match = truncatedLine.match(this.expression);
50-
const search = match?.index ?? -1;
51-
if (search >= 0 && match) {
52-
// Create GitHub Issue action
53-
const createIssueAction: vscode.CodeAction = new vscode.CodeAction(
54-
vscode.l10n.t('Create GitHub Issue'),
55-
vscode.CodeActionKind.QuickFix,
56-
);
57-
createIssueAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)];
58-
const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/);
59-
const insertIndex =
60-
search +
61-
(indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length);
62-
createIssueAction.command = {
63-
title: vscode.l10n.t('Create GitHub Issue'),
64-
command: 'issue.createIssueFromSelection',
65-
arguments: [{ document, lineNumber, line, insertIndex, range }],
66-
};
67-
codeActions.push(createIssueAction);
68-
69-
// Start Coding Agent Session action (if copilot manager is available)
70-
if (this.copilotRemoteAgentManager) {
71-
const startAgentAction: vscode.CodeAction = new vscode.CodeAction(
72-
vscode.l10n.t('Start Coding Agent Session'),
73-
vscode.CodeActionKind.QuickFix,
74-
);
75-
startAgentAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)];
76-
startAgentAction.command = {
77-
title: vscode.l10n.t('Start Coding Agent Session'),
78-
command: 'issue.startCodingAgentFromTodo',
79-
arguments: [{ document, lineNumber, line, insertIndex, range }],
80-
};
81-
codeActions.push(startAgentAction);
82-
}
83-
break;
84-
}
85-
}
86-
lineNumber++;
87-
} while (range.end.line >= lineNumber);
88-
return codeActions;
89-
}
90-
}
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys';
8+
import { escapeRegExp } from '../common/utils';
9+
import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent';
10+
import { ISSUE_OR_URL_EXPRESSION } from '../github/utils';
11+
import { MAX_LINE_LENGTH } from './util';
12+
13+
export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.CodeLensProvider {
14+
private expression: RegExp | undefined;
15+
16+
constructor(
17+
context: vscode.ExtensionContext,
18+
private copilotRemoteAgentManager: CopilotRemoteAgentManager
19+
) {
20+
context.subscriptions.push(
21+
vscode.workspace.onDidChangeConfiguration(() => {
22+
this.updateTriggers();
23+
}),
24+
);
25+
this.updateTriggers();
26+
}
27+
28+
private updateTriggers() {
29+
const triggers = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE).get(CREATE_ISSUE_TRIGGERS, []);
30+
this.expression = triggers.length > 0 ? new RegExp(triggers.map(trigger => escapeRegExp(trigger)).join('|')) : undefined;
31+
}
32+
33+
async provideCodeActions(
34+
document: vscode.TextDocument,
35+
range: vscode.Range | vscode.Selection,
36+
context: vscode.CodeActionContext,
37+
_token: vscode.CancellationToken,
38+
): Promise<vscode.CodeAction[]> {
39+
if (this.expression === undefined || (context.only && context.only !== vscode.CodeActionKind.QuickFix)) {
40+
return [];
41+
}
42+
const codeActions: vscode.CodeAction[] = [];
43+
let lineNumber = range.start.line;
44+
do {
45+
const line = document.lineAt(lineNumber).text;
46+
const truncatedLine = line.substring(0, MAX_LINE_LENGTH);
47+
const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION);
48+
if (!matches) {
49+
const match = truncatedLine.match(this.expression);
50+
const search = match?.index ?? -1;
51+
if (search >= 0 && match) {
52+
// Create GitHub Issue action
53+
const createIssueAction: vscode.CodeAction = new vscode.CodeAction(
54+
vscode.l10n.t('Create GitHub Issue'),
55+
vscode.CodeActionKind.QuickFix,
56+
);
57+
createIssueAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)];
58+
const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/);
59+
const insertIndex =
60+
search +
61+
(indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length);
62+
createIssueAction.command = {
63+
title: vscode.l10n.t('Create GitHub Issue'),
64+
command: 'issue.createIssueFromSelection',
65+
arguments: [{ document, lineNumber, line, insertIndex, range }],
66+
};
67+
codeActions.push(createIssueAction);
68+
69+
// Start Coding Agent Session action (if copilot manager is available)
70+
if (this.copilotRemoteAgentManager) {
71+
const startAgentAction: vscode.CodeAction = new vscode.CodeAction(
72+
vscode.l10n.t('Start Coding Agent Session'),
73+
vscode.CodeActionKind.QuickFix,
74+
);
75+
startAgentAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)];
76+
startAgentAction.command = {
77+
title: vscode.l10n.t('Start Coding Agent Session'),
78+
command: 'issue.startCodingAgentFromTodo',
79+
arguments: [{ document, lineNumber, line, insertIndex, range }],
80+
};
81+
codeActions.push(startAgentAction);
82+
}
83+
break;
84+
}
85+
}
86+
lineNumber++;
87+
} while (range.end.line >= lineNumber);
88+
return codeActions;
89+
}
90+
91+
async provideCodeLenses(
92+
document: vscode.TextDocument,
93+
_token: vscode.CancellationToken,
94+
): Promise<vscode.CodeLens[]> {
95+
if (this.expression === undefined) {
96+
return [];
97+
}
98+
99+
const codeLenses: vscode.CodeLens[] = [];
100+
for (let lineNumber = 0; lineNumber < document.lineCount; lineNumber++) {
101+
const line = document.lineAt(lineNumber).text;
102+
const truncatedLine = line.substring(0, MAX_LINE_LENGTH);
103+
const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION);
104+
105+
if (!matches) {
106+
const match = truncatedLine.match(this.expression);
107+
const search = match?.index ?? -1;
108+
if (search >= 0 && match) {
109+
const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/);
110+
const insertIndex =
111+
search +
112+
(indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length);
113+
114+
const range = new vscode.Range(lineNumber, search, lineNumber, search + match[0].length);
115+
116+
// Only show "Start Coding Agent Session" code lens if copilot manager is available
117+
if (this.copilotRemoteAgentManager) {
118+
const startAgentCodeLens = new vscode.CodeLens(range, {
119+
title: vscode.l10n.t('Start Coding Agent Session'),
120+
command: 'issue.startCodingAgentFromTodo',
121+
arguments: [{ document, lineNumber, line, insertIndex, range }],
122+
});
123+
codeLenses.push(startAgentCodeLens);
124+
}
125+
}
126+
}
127+
}
128+
return codeLenses;
129+
}
130+
131+
resolveCodeLens(
132+
codeLens: vscode.CodeLens,
133+
_token: vscode.CancellationToken,
134+
): vscode.ProviderResult<vscode.CodeLens> {
135+
// Code lens is already resolved in provideCodeLenses
136+
return codeLens;
137+
}
138+
}
Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,89 @@
1-
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) Microsoft Corporation. All rights reserved.
3-
* Licensed under the MIT License. See License.txt in the project root for license information.
4-
*--------------------------------------------------------------------------------------------*/
5-
6-
import { default as assert } from 'assert';
7-
import * as vscode from 'vscode';
8-
import { IssueTodoProvider } from '../../issues/issueTodoProvider';
9-
10-
describe('IssueTodoProvider', function () {
11-
it('should provide both actions when CopilotRemoteAgentManager is available', async function () {
12-
const mockContext = {
13-
subscriptions: []
14-
} as any as vscode.ExtensionContext;
15-
16-
const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager
17-
18-
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
19-
20-
// Create a mock document with TODO comment
21-
const document = {
22-
lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }),
23-
lineCount: 4
24-
} as vscode.TextDocument;
25-
26-
const range = new vscode.Range(1, 0, 1, 20);
27-
const context = {
28-
only: vscode.CodeActionKind.QuickFix
29-
} as vscode.CodeActionContext;
30-
31-
const actions = await provider.provideCodeActions(document, range, context, new vscode.CancellationTokenSource().token);
32-
33-
assert.strictEqual(actions.length, 2);
34-
35-
// Find the actions
36-
const createIssueAction = actions.find(a => a.title === 'Create GitHub Issue');
37-
const startAgentAction = actions.find(a => a.title === 'Start Coding Agent Session');
38-
39-
assert.ok(createIssueAction, 'Should have Create GitHub Issue action');
40-
assert.ok(startAgentAction, 'Should have Start Coding Agent Session action');
41-
42-
assert.strictEqual(createIssueAction?.command?.command, 'issue.createIssueFromSelection');
43-
assert.strictEqual(startAgentAction?.command?.command, 'issue.startCodingAgentFromTodo');
44-
});
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { default as assert } from 'assert';
7+
import * as vscode from 'vscode';
8+
import { IssueTodoProvider } from '../../issues/issueTodoProvider';
9+
10+
describe('IssueTodoProvider', function () {
11+
it('should provide both actions when CopilotRemoteAgentManager is available', async function () {
12+
const mockContext = {
13+
subscriptions: []
14+
} as any as vscode.ExtensionContext;
15+
16+
const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager
17+
18+
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
19+
20+
// Create a mock document with TODO comment
21+
const document = {
22+
lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }),
23+
lineCount: 4
24+
} as vscode.TextDocument;
25+
26+
const range = new vscode.Range(1, 0, 1, 20);
27+
const context = {
28+
only: vscode.CodeActionKind.QuickFix
29+
} as vscode.CodeActionContext;
30+
31+
const actions = await provider.provideCodeActions(document, range, context, new vscode.CancellationTokenSource().token);
32+
33+
assert.strictEqual(actions.length, 2);
34+
35+
// Find the actions
36+
const createIssueAction = actions.find(a => a.title === 'Create GitHub Issue');
37+
const startAgentAction = actions.find(a => a.title === 'Start Coding Agent Session');
38+
39+
assert.ok(createIssueAction, 'Should have Create GitHub Issue action');
40+
assert.ok(startAgentAction, 'Should have Start Coding Agent Session action');
41+
42+
assert.strictEqual(createIssueAction?.command?.command, 'issue.createIssueFromSelection');
43+
assert.strictEqual(startAgentAction?.command?.command, 'issue.startCodingAgentFromTodo');
44+
});
45+
46+
it('should provide code lens for TODO comments when CopilotRemoteAgentManager is available', async function () {
47+
const mockContext = {
48+
subscriptions: []
49+
} as any as vscode.ExtensionContext;
50+
51+
const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager
52+
53+
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
54+
55+
// Create a mock document with TODO comment
56+
const document = {
57+
lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }),
58+
lineCount: 4
59+
} as vscode.TextDocument;
60+
61+
const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token);
62+
63+
assert.strictEqual(codeLenses.length, 1);
64+
65+
const codeLens = codeLenses[0];
66+
assert.ok(codeLens.command, 'Code lens should have a command');
67+
assert.strictEqual(codeLens.command.title, 'Start Coding Agent Session');
68+
assert.strictEqual(codeLens.command.command, 'issue.startCodingAgentFromTodo');
69+
assert.strictEqual(codeLens.range.start.line, 1);
70+
});
71+
72+
it('should not provide code lens when CopilotRemoteAgentManager is not available', async function () {
73+
const mockContext = {
74+
subscriptions: []
75+
} as any as vscode.ExtensionContext;
76+
77+
const provider = new IssueTodoProvider(mockContext, undefined);
78+
79+
// Create a mock document with TODO comment
80+
const document = {
81+
lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }),
82+
lineCount: 4
83+
} as vscode.TextDocument;
84+
85+
const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token);
86+
87+
assert.strictEqual(codeLenses.length, 0, 'Should not provide code lens when CopilotRemoteAgentManager is not available');
88+
});
4589
});

0 commit comments

Comments
 (0)