Skip to content

Commit a871a19

Browse files
committed
only show TODO code lens on lines that start with a comment token
1 parent e43d70e commit a871a19

File tree

5 files changed

+122
-15
lines changed

5 files changed

+122
-15
lines changed

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,19 @@
616616
],
617617
"description": "%githubIssues.createIssueTriggers.description%"
618618
},
619+
"githubIssues.createIssueCommentPrefixes": {
620+
"type": "array",
621+
"items": {
622+
"type": "string",
623+
"description": "%githubIssues.createIssueCommentPrefixes.items%"
624+
},
625+
"default": [
626+
"//",
627+
"#",
628+
"--"
629+
],
630+
"description": "%githubIssues.createIssueCommentPrefixes.description%"
631+
},
619632
"githubPullRequests.codingAgent.codeLens": {
620633
"type": "boolean",
621634
"default": true,

package.nls.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,21 @@
103103
"githubPullRequests.experimental.useQuickChat.description": "Controls whether the Copilot \"Summarize\" commands in the Pull Requests, Issues, and Notifications views will use quick chat. Only has an effect if `#githubPullRequests.experimental.chat#` is enabled.",
104104
"githubPullRequests.webviewRefreshInterval.description": "The interval, in seconds, at which the pull request and issues webviews are refreshed when the webview is the active tab.",
105105
"githubIssues.ignoreMilestones.description": "An array of milestones titles to never show issues from.",
106-
"githubIssues.createIssueTriggers.description": "Strings that will cause the 'Create issue from comment' code action to show.",
106+
"githubIssues.createIssueTriggers.description": {
107+
"message": "Trigger tokens found after a token in `#githubIssues.createIssueCommentPrefixes#` will show the 'Create issue from comment' code action. These tokens also enable the 'Delegate to Coding Agent' code lens if `#githubPullRequests.codingAgent.codeLens#` is enabled.",
108+
"comment": [
109+
"{Locked='`...`'}",
110+
"Do not translate what's inside of the `...`. It is a setting id."
111+
]
112+
},
113+
"githubIssues.createIssueCommentPrefixes.description": {
114+
"message": "Comment prefixes (e.g. //, #, --) that must immediately precede a trigger token from `#githubIssues.createIssueTriggers#` to activate the issue actions / code lens.",
115+
"comment": [
116+
"{Locked='`#githubIssues.createIssueTriggers#`'}",
117+
"Do not translate what's inside of the `...`. It is a setting id."
118+
]
119+
},
120+
"githubIssues.createIssueCommentPrefixes.items": "Comment prefix used to detect issue trigger tokens (e.g. //, #, --).",
107121
"githubIssues.createIssueTriggers.items": "String that enables the 'Create issue from comment' code action. Should not contain whitespace.",
108122
"githubPullRequests.codingAgent.codeLens.description": "Show CodeLens actions above TODO comments for delegating to coding agent.",
109123
"githubIssues.createInsertFormat.description": "Controls whether an issue number (ex. #1234) or a full url (ex. https://github.com/owner/name/issues/1234) is inserted when the Create Issue code action is run.",

src/common/settingKeys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const WORKING_ISSUE_FORMAT_SCM = 'workingIssueFormatScm';
5353
export const IGNORE_COMPLETION_TRIGGER = 'ignoreCompletionTrigger';
5454
export const ISSUE_COMPLETION_FORMAT_SCM = 'issueCompletionFormatScm';
5555
export const CREATE_ISSUE_TRIGGERS = 'createIssueTriggers';
56+
// Comment prefixes that, when followed by a trigger token, cause issue actions to appear
57+
export const CREATE_ISSUE_COMMENT_PREFIXES = 'createIssueCommentPrefixes';
5658
export const DEFAULT = 'default';
5759
export const IGNORE_MILESTONES = 'ignoreMilestones';
5860
export const ALLOW_FETCH = 'allowFetch';

src/issues/issueTodoProvider.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
import * as vscode from 'vscode';
77
import { MAX_LINE_LENGTH } from './util';
8-
import { CODING_AGENT, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../common/settingKeys';
8+
import { CODING_AGENT, CREATE_ISSUE_COMMENT_PREFIXES, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../common/settingKeys';
99
import { escapeRegExp } from '../common/utils';
1010
import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent';
1111
import { ISSUE_OR_URL_EXPRESSION } from '../github/utils';
1212

1313
export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.CodeLensProvider {
1414
private expression: RegExp | undefined;
15+
private triggerTokens: string[] = [];
16+
private prefixTokens: string[] = [];
1517

1618
constructor(
1719
context: vscode.ExtensionContext,
@@ -26,26 +28,42 @@ export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.Code
2628
}
2729

2830
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+
const issuesConfig = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE);
32+
this.triggerTokens = issuesConfig.get<string[]>(CREATE_ISSUE_TRIGGERS, []);
33+
this.prefixTokens = issuesConfig.get<string[]>(CREATE_ISSUE_COMMENT_PREFIXES, []);
34+
if (this.triggerTokens.length === 0 || this.prefixTokens.length === 0) {
35+
this.expression = undefined;
36+
return;
37+
}
38+
// Build a regex that captures the trigger word so we can highlight just that portion
39+
// ^\s*(?:prefix1|prefix2)\s*(trigger1|trigger2)\b
40+
const prefixesSource = this.prefixTokens.map(p => escapeRegExp(p)).join('|');
41+
const triggersSource = this.triggerTokens.map(t => escapeRegExp(t)).join('|');
42+
this.expression = new RegExp(`^\\s*(?:${prefixesSource})\\s*(${triggersSource})\\b`);
3143
}
3244

3345
private findTodoInLine(line: string): { match: RegExpMatchArray; search: number; insertIndex: number } | undefined {
46+
if (!this.expression) {
47+
return undefined;
48+
}
3449
const truncatedLine = line.substring(0, MAX_LINE_LENGTH);
35-
const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION);
36-
if (matches) {
50+
// If the line already contains an issue reference or URL, skip
51+
if (ISSUE_OR_URL_EXPRESSION.test(truncatedLine)) {
3752
return undefined;
3853
}
39-
const match = truncatedLine.match(this.expression!);
40-
const search = match?.index ?? -1;
41-
if (search >= 0 && match) {
42-
const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/);
43-
const insertIndex =
44-
search +
45-
(indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression!)![0].length);
46-
return { match, search, insertIndex };
54+
const match = this.expression.exec(truncatedLine);
55+
if (!match) {
56+
return undefined;
4757
}
48-
return undefined;
58+
// match[1] is the captured trigger token
59+
const fullMatch = match[0];
60+
const trigger = match[1];
61+
// Find start of trigger within full line for highlighting
62+
const triggerStartInFullMatch = fullMatch.lastIndexOf(trigger); // safe since trigger appears once at end
63+
const search = match.index + triggerStartInFullMatch;
64+
const insertIndex = search + trigger.length;
65+
// Return a RegExpMatchArray-like structure; reuse match
66+
return { match, search, insertIndex };
4967
}
5068

5169
async provideCodeActions(

src/test/issues/issueTodoProvider.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ describe('IssueTodoProvider', function () {
1515

1616
const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager
1717

18+
// Mock configuration for triggers and prefixes
19+
const originalGetConfiguration = vscode.workspace.getConfiguration;
20+
vscode.workspace.getConfiguration = (section?: string) => {
21+
if (section === 'githubIssues') {
22+
return {
23+
get: (key: string, defaultValue?: any) => {
24+
if (key === 'createIssueTriggers') { return ['TODO']; }
25+
if (key === 'createIssueCommentPrefixes') { return ['//']; }
26+
return defaultValue;
27+
}
28+
} as any;
29+
}
30+
return originalGetConfiguration(section);
31+
};
32+
1833
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
1934

2035
// Create a mock document with TODO comment
@@ -50,6 +65,19 @@ describe('IssueTodoProvider', function () {
5065

5166
const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager
5267

68+
const originalGetConfiguration = vscode.workspace.getConfiguration;
69+
vscode.workspace.getConfiguration = (section?: string) => {
70+
if (section === 'githubIssues') {
71+
return {
72+
get: (key: string, defaultValue?: any) => {
73+
if (key === 'createIssueTriggers') { return ['TODO']; }
74+
if (key === 'createIssueCommentPrefixes') { return ['//', '#']; }
75+
return defaultValue;
76+
}
77+
} as any;
78+
}
79+
return originalGetConfiguration(section);
80+
};
5381
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
5482

5583
// Create a mock document with TODO comment
@@ -128,4 +156,36 @@ describe('IssueTodoProvider', function () {
128156
vscode.workspace.getConfiguration = originalGetConfiguration;
129157
}
130158
});
159+
160+
it('should not trigger on line without comment prefix', async function () {
161+
const mockContext = { subscriptions: [] } as any as vscode.ExtensionContext;
162+
const mockCopilotManager = {} as any;
163+
164+
const originalGetConfiguration = vscode.workspace.getConfiguration;
165+
vscode.workspace.getConfiguration = (section?: string) => {
166+
if (section === 'githubIssues') {
167+
return {
168+
get: (key: string, defaultValue?: any) => {
169+
if (key === 'createIssueTriggers') { return ['DEBUG_RUN']; }
170+
if (key === 'createIssueCommentPrefixes') { return ['//']; }
171+
return defaultValue;
172+
}
173+
} as any;
174+
}
175+
return originalGetConfiguration(section);
176+
};
177+
178+
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
179+
180+
const testLine = "\tregisterTouchBarEntry(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png'));";
181+
const document = {
182+
lineAt: (_line: number) => ({ text: testLine }),
183+
lineCount: 1
184+
} as vscode.TextDocument;
185+
186+
const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token);
187+
assert.strictEqual(codeLenses.length, 0, 'Should not create CodeLens for trigger inside code without prefix');
188+
189+
vscode.workspace.getConfiguration = originalGetConfiguration; // restore
190+
});
131191
});

0 commit comments

Comments
 (0)