Skip to content

Commit 39bf4cf

Browse files
authored
chat: add #problems reference to core (microsoft#242127)
- Users can reference #problems in core now - It will ask the user to pick a file, or all files, when there's >1 file with errors. ([demo](https://memes.peet.io/img/25-02-8fbc8580-5f13-4714-b9d3-231edb53bdf7.mp4)) - Tweaked the icon to match the error icon in the markers view
1 parent 7d3d01e commit 39bf4cf

File tree

9 files changed

+162
-46
lines changed

9 files changed

+162
-46
lines changed

src/vs/platform/severityIcon/browser/media/severityIcon.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
.text-search-provider-messages .providerMessage .codicon.codicon-error,
99
.extensions-viewlet > .extensions .codicon.codicon-error,
1010
.extension-editor .codicon.codicon-error,
11-
.preferences-editor .codicon.codicon-error {
11+
.preferences-editor .codicon.codicon-error,
12+
.chat-attached-context-attachment .codicon.codicon-error {
1213
color: var(--vscode-problemsErrorIcon-foreground);
1314
}
1415

src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ export class AttachContextAction extends Action2 {
852852
});
853853

854854
const filter = await instantiationService.invokeFunction(accessor =>
855-
createMarkersQuickPick(accessor, items => onBackgroundAccept(items.map(convert))));
855+
createMarkersQuickPick(accessor, 'problem', items => onBackgroundAccept(items.map(convert))));
856856
return filter && convert(filter);
857857
}
858858

src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,7 @@ export class ChatDragAndDrop extends Themable {
383383
filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);
384384
}
385385

386-
return {
387-
...IDiagnosticVariableEntryFilterData.toEntry(filter),
388-
...filter,
389-
};
386+
return IDiagnosticVariableEntryFilterData.toEntry(filter);
390387
});
391388
}
392389

src/vs/workbench/contrib/chat/browser/chatVariables.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { coalesce } from '../../../../base/common/arrays.js';
7-
import { ThemeIcon } from '../../../../base/common/themables.js';
87
import { URI } from '../../../../base/common/uri.js';
98
import { Location } from '../../../../editor/common/languages.js';
109
import { IViewsService } from '../../../services/views/common/viewsService.js';
@@ -29,10 +28,8 @@ export class ChatVariablesService implements IChatVariablesService {
2928

3029
prompt.parts
3130
.forEach((part, i) => {
32-
if (part instanceof ChatRequestDynamicVariablePart) {
33-
resolvedVariables[i] = { id: part.id, name: part.referenceText, range: part.range, value: part.data, fullName: part.fullName, icon: part.icon, isFile: part.isFile, isDirectory: part.isDirectory };
34-
} else if (part instanceof ChatRequestToolPart) {
35-
resolvedVariables[i] = { id: part.toolId, name: part.toolName, range: part.range, value: undefined, isTool: true, icon: ThemeIcon.isThemeIcon(part.icon) ? part.icon : undefined, fullName: part.displayName };
31+
if (part instanceof ChatRequestDynamicVariablePart || part instanceof ChatRequestToolPart) {
32+
resolvedVariables[i] = part.toVariableEntry();
3633
}
3734
});
3835

src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { coalesce } from '../../../../../base/common/arrays.js';
6+
import { coalesce, groupBy } from '../../../../../base/common/arrays.js';
7+
import { assertNever } from '../../../../../base/common/assert.js';
78
import { CancellationToken } from '../../../../../base/common/cancellation.js';
89
import { Codicon } from '../../../../../base/common/codicons.js';
910
import { isCancellationError } from '../../../../../base/common/errors.js';
@@ -35,7 +36,7 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm
3536
import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js';
3637
import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js';
3738
import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js';
38-
import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js';
39+
import { IChatRequestProblemsVariable, IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js';
3940
import { IChatWidget } from '../chat.js';
4041
import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js';
4142
import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js';
@@ -650,62 +651,84 @@ export class AddDynamicVariableAction extends Action2 {
650651
}
651652
registerAction2(AddDynamicVariableAction);
652653

653-
export async function createMarkersQuickPick(accessor: ServicesAccessor, onBackgroundAccept: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise<IDiagnosticVariableEntryFilterData | undefined> {
654+
export async function createMarkersQuickPick(accessor: ServicesAccessor, level: 'problem' | 'file', onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise<IDiagnosticVariableEntryFilterData | undefined> {
654655
const markers = accessor.get(IMarkerService).read();
655656
if (!markers.length) {
656657
return;
657658
}
658659

659660
const uriIdentityService = accessor.get(IUriIdentityService);
660661
const labelService = accessor.get(ILabelService);
661-
markers.sort((a, b) => uriIdentityService.extUri.compare(a.resource, b.resource) || b.severity - a.severity);
662+
const grouped = groupBy(markers, (a, b) => uriIdentityService.extUri.compare(a.resource, b.resource));
662663

663664
const severities = new Set<MarkerSeverity>();
664665
type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData };
665666
const items: (MarkerPickItem | IQuickPickSeparator)[] = [];
666-
for (const marker of markers) {
667-
if (!uriIdentityService.extUri.isEqual(marker.resource, (items.at(-1) as MarkerPickItem)?.resource)) {
668-
items.push({ type: 'separator', label: labelService.getUriLabel(marker.resource, { relative: true }) });
667+
668+
let pickCount = 0;
669+
for (const group of grouped) {
670+
const resource = group[0].resource;
671+
if (level === 'problem') {
672+
items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) });
673+
for (const marker of group) {
674+
pickCount++;
675+
severities.add(marker.severity);
676+
items.push({
677+
type: 'item',
678+
resource: marker.resource,
679+
label: marker.message,
680+
description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn),
681+
entry: IDiagnosticVariableEntryFilterData.fromMarker(marker),
682+
});
683+
}
684+
} else if (level === 'file') {
685+
const entry = { filterUri: resource };
686+
pickCount++;
687+
items.push({
688+
type: 'item',
689+
resource,
690+
label: IDiagnosticVariableEntryFilterData.label(entry),
691+
description: group[0].message + (group.length > 1 ? localize('problemsMore', '+ {0} more', group.length - 1) : ''),
692+
entry,
693+
});
694+
for (const marker of group) {
695+
severities.add(marker.severity);
696+
}
697+
} else {
698+
assertNever(level);
669699
}
700+
}
670701

671-
severities.add(marker.severity);
672-
items.push({
673-
type: 'item',
674-
resource: marker.resource,
675-
label: marker.message,
676-
description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn),
677-
entry: IDiagnosticVariableEntryFilterData.fromMarker(marker),
678-
});
702+
if (pickCount < 2) { // single error in a URI
703+
return items.find((i): i is MarkerPickItem => i.type === 'item')?.entry;
679704
}
680705

681-
if (items.length === 2) { // single error in a URI
682-
return (items[1] as MarkerPickItem).entry;
706+
if (level === 'file') {
707+
items.unshift({ type: 'separator', label: localize('markers.panel.files', 'Files') });
683708
}
684709

685-
if (items.length > 2) {
686-
if (severities.has(MarkerSeverity.Error)) {
687-
items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Errors'), entry: { filterSeverity: MarkerSeverity.Error } });
688-
}
689-
if (severities.has(MarkerSeverity.Warning)) {
690-
items.unshift({ type: 'item', label: localize('markers.panel.allWarnings', 'All Warnings'), entry: { filterSeverity: MarkerSeverity.Warning } });
691-
}
692-
if (severities.has(MarkerSeverity.Info)) {
693-
items.unshift({ type: 'item', label: localize('markers.panel.allInfos', 'All Infos'), entry: { filterSeverity: MarkerSeverity.Info } });
694-
}
710+
if (severities.has(MarkerSeverity.Error)) {
711+
items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Errors'), entry: { filterSeverity: MarkerSeverity.Error } });
712+
}
713+
if (severities.has(MarkerSeverity.Warning)) {
714+
items.unshift({ type: 'item', label: localize('markers.panel.allWarnings', 'All Warnings'), entry: { filterSeverity: MarkerSeverity.Warning } });
715+
}
716+
if (severities.has(MarkerSeverity.Info)) {
717+
items.unshift({ type: 'item', label: localize('markers.panel.allInfos', 'All Infos'), entry: { filterSeverity: MarkerSeverity.Info } });
695718
}
696719

697720

698721
const quickInputService = accessor.get(IQuickInputService);
699722
const quickPick = quickInputService.createQuickPick<MarkerPickItem>({ useSeparators: true });
700-
quickPick.canAcceptInBackground = true;
723+
quickPick.canAcceptInBackground = !onBackgroundAccept;
701724
quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...');
702725
quickPick.items = items;
703726

704727
return new Promise<IDiagnosticVariableEntryFilterData | undefined>(resolve => {
705728
quickPick.onDidHide(() => resolve(undefined));
706729
quickPick.onDidAccept(ev => {
707730
if (ev.inBackground) {
708-
onBackgroundAccept(quickPick.selectedItems.map(i => i.entry));
731+
onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry));
709732
} else {
710733
resolve(quickPick.selectedItems[0]?.entry);
711734
quickPick.dispose();
@@ -715,3 +738,53 @@ export async function createMarkersQuickPick(accessor: ServicesAccessor, onBackg
715738
}).finally(() => quickPick.dispose());
716739
}
717740

741+
export class SelectAndInsertProblemAction extends Action2 {
742+
static readonly Name = 'problems';
743+
static readonly ID = 'workbench.action.chat.selectAndInsertProblems';
744+
745+
constructor() {
746+
super({
747+
id: SelectAndInsertProblemAction.ID,
748+
title: '' // not displayed
749+
});
750+
}
751+
752+
async run(accessor: ServicesAccessor, ...args: any[]) {
753+
const logService = accessor.get(ILogService);
754+
const context = args[0];
755+
if (!isSelectAndInsertActionContext(context)) {
756+
return;
757+
}
758+
759+
const doCleanup = () => {
760+
// Failed, remove the dangling `problem`
761+
context.widget.inputEditor.executeEdits('chatInsertProblems', [{ range: context.range, text: `` }]);
762+
};
763+
764+
const pick = await createMarkersQuickPick(accessor, 'file');
765+
if (!pick) {
766+
doCleanup();
767+
return;
768+
}
769+
770+
const editor = context.widget.inputEditor;
771+
const originalRange = context.range;
772+
const insertText = `#${SelectAndInsertProblemAction.Name}:${pick.filterUri ? basename(pick.filterUri) : MarkerSeverity.toString(pick.filterSeverity!)}`;
773+
774+
const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length);
775+
const success = editor.executeEdits('chatInsertProblems', [{ range: varRange, text: insertText + ' ' }]);
776+
if (!success) {
777+
logService.trace(`SelectAndInsertProblemsAction: failed to insert "${insertText}"`);
778+
doCleanup();
779+
return;
780+
}
781+
782+
context.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID)?.addReference({
783+
id: 'vscode.problems',
784+
prefix: SelectAndInsertProblemAction.Name,
785+
range: varRange,
786+
data: { id: 'vscode.problems', filter: pick } satisfies IChatRequestProblemsVariable,
787+
});
788+
}
789+
}
790+
registerAction2(SelectAndInsertProblemAction);

src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com
2727
import { IFileService } from '../../../../../platform/files/common/files.js';
2828
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
2929
import { ILabelService } from '../../../../../platform/label/common/label.js';
30+
import { IMarkerService } from '../../../../../platform/markers/common/markers.js';
3031
import { Registry } from '../../../../../platform/registry/common/platform.js';
3132
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
3233
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js';
@@ -44,7 +45,7 @@ import { ILanguageModelToolsService } from '../../common/languageModelToolsServi
4445
import { ChatEditingSessionSubmitAction, ChatSubmitAction } from '../actions/chatExecuteActions.js';
4546
import { IChatWidget, IChatWidgetService } from '../chat.js';
4647
import { ChatInputPart } from '../chatInputPart.js';
47-
import { ChatDynamicVariableModel, getTopLevelFolders, searchFolders, SelectAndInsertFolderAction, SelectAndInsertFileAction, SelectAndInsertSymAction } from './chatDynamicVariables.js';
48+
import { ChatDynamicVariableModel, SelectAndInsertFileAction, SelectAndInsertFolderAction, SelectAndInsertProblemAction, SelectAndInsertSymAction, getTopLevelFolders, searchFolders } from './chatDynamicVariables.js';
4849

4950
class SlashCommandCompletions extends Disposable {
5051
constructor(
@@ -467,6 +468,7 @@ class BuiltinDynamicCompletions extends Disposable {
467468
@IEditorService private readonly editorService: IEditorService,
468469
@IConfigurationService private readonly configurationService: IConfigurationService,
469470
@IFileService private readonly fileService: IFileService,
471+
@IMarkerService markerService: IMarkerService,
470472
) {
471473
super();
472474

@@ -598,6 +600,30 @@ class BuiltinDynamicCompletions extends Disposable {
598600
return result;
599601
});
600602

603+
// Problems completions, we just attach all problems in this case
604+
this.registerVariableCompletions(SelectAndInsertProblemAction.Name, ({ widget, range, position, model }, token) => {
605+
const stats = markerService.getStatistics();
606+
if (!stats.errors && !stats.warnings) {
607+
return null;
608+
}
609+
610+
const result: CompletionList = { suggestions: [] };
611+
612+
const completedText = `${chatVariableLeader}${SelectAndInsertProblemAction.Name}:`;
613+
const afterTextRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + completedText.length);
614+
result.suggestions.push({
615+
label: `${chatVariableLeader}${SelectAndInsertProblemAction.Name}`,
616+
insertText: completedText,
617+
documentation: localize('pickProblemsLabel', "Problems in your workspace"),
618+
range,
619+
kind: CompletionItemKind.Text,
620+
command: { id: SelectAndInsertProblemAction.ID, title: SelectAndInsertProblemAction.ID, arguments: [{ widget, range: afterTextRange }] },
621+
sortText: 'z'
622+
});
623+
624+
return result;
625+
});
626+
601627
this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg)));
602628

603629
this.queryBuilder = this.instantiationService.createInstance(QueryBuilder);

src/vs/workbench/contrib/chat/common/chatModel.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export interface IDiagnosticVariableEntryFilterData {
104104
}
105105

106106
export namespace IDiagnosticVariableEntryFilterData {
107-
export const icon = Codicon.warning;
107+
export const icon = Codicon.error;
108108

109109
export function fromMarker(marker: IMarker): IDiagnosticVariableEntryFilterData {
110110
return {
@@ -115,14 +115,15 @@ export namespace IDiagnosticVariableEntryFilterData {
115115
};
116116
}
117117

118-
export function toEntry(data: IDiagnosticVariableEntryFilterData) {
118+
export function toEntry(data: IDiagnosticVariableEntryFilterData): IDiagnosticVariableEntry {
119119
return {
120120
id: id(data),
121121
name: label(data),
122122
icon,
123123
value: data,
124124
kind: 'diagnostic' as const,
125125
range: data.filterRange ? new OffsetRange(data.filterRange.startLineNumber, data.filterRange.endLineNumber) : undefined,
126+
...data,
126127
};
127128
}
128129

src/vs/workbench/contrib/chat/common/chatParserTypes.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
88
import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js';
99
import { IRange } from '../../../../editor/common/core/range.js';
1010
import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js';
11+
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js';
1112
import { IChatSlashData } from './chatSlashCommands.js';
12-
import { IChatRequestVariableValue } from './chatVariables.js';
13+
import { IChatRequestProblemsVariable, IChatRequestVariableValue } from './chatVariables.js';
1314
import { IToolData } from './languageModelToolsService.js';
1415

1516
// These are in a separate file to avoid circular dependencies with the dependencies of the parser
@@ -84,6 +85,10 @@ export class ChatRequestToolPart implements IParsedChatRequestPart {
8485
get promptText(): string {
8586
return this.text;
8687
}
88+
89+
toVariableEntry(): IChatRequestVariableEntry {
90+
return { id: this.toolId, name: this.toolName, range: this.range, value: undefined, isTool: true, icon: ThemeIcon.isThemeIcon(this.icon) ? this.icon : undefined, fullName: this.displayName };
91+
}
8792
}
8893

8994
/**
@@ -152,6 +157,14 @@ export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart {
152157
get promptText(): string {
153158
return this.text;
154159
}
160+
161+
toVariableEntry(): IChatRequestVariableEntry {
162+
if (this.id === 'vscode.problems') {
163+
return IDiagnosticVariableEntryFilterData.toEntry((this.data as IChatRequestProblemsVariable).filter);
164+
}
165+
166+
return { id: this.id, name: this.referenceText, range: this.range, value: this.data, fullName: this.fullName, icon: this.icon, isFile: this.isFile, isDirectory: this.isDirectory };
167+
}
155168
}
156169

157170
export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsedChatRequest {

src/vs/workbench/contrib/chat/common/chatVariables.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { IRange } from '../../../../editor/common/core/range.js';
1010
import { Location } from '../../../../editor/common/languages.js';
1111
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
1212
import { ChatAgentLocation } from './chatAgents.js';
13-
import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from './chatModel.js';
13+
import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js';
1414
import { IParsedChatRequest } from './chatParserTypes.js';
1515
import { IChatContentReference, IChatProgressMessage } from './chatService.js';
1616

@@ -24,7 +24,15 @@ export interface IChatVariableData {
2424
canTakeArgument?: boolean;
2525
}
2626

27-
export type IChatRequestVariableValue = string | URI | Location | unknown | Uint8Array;
27+
export interface IChatRequestProblemsVariable {
28+
id: 'vscode.problems';
29+
filter: IDiagnosticVariableEntryFilterData;
30+
}
31+
32+
export const isIChatRequestProblemsVariable = (obj: unknown): obj is IChatRequestProblemsVariable =>
33+
typeof obj === 'object' && obj !== null && 'id' in obj && (obj as IChatRequestProblemsVariable).id === 'vscode.problems';
34+
35+
export type IChatRequestVariableValue = string | URI | Location | unknown | Uint8Array | IChatRequestProblemsVariable;
2836

2937
export type IChatVariableResolverProgress =
3038
| IChatContentReference

0 commit comments

Comments
 (0)