chat: better problems integration (#241276)

chat: allow referencing and dragging in diagnostics

- There is a new proposal which adds `ChatReferenceDiagnostic` as a
  prompt reference type
- You can now pick "Problems..." as part of the chat attachment context
- You can drag and drop files and individual diagnostics from the
  Problems view into chat. Previously trying to do this would just
  attach the file.
This commit is contained in:
Connor Peet
2025-02-19 17:38:32 -08:00
committed by GitHub
parent 5312579ab0
commit f9dd5e1a8d
15 changed files with 414 additions and 99 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.98.0",
"distro": "b37092a45bb95e5098cefcf6809fa6973cfd2538",
"distro": "18d6e62cad16da70804e341893ae344d5cdcc6ee",
"author": {
"name": "Microsoft Corporation"
},

View File

@@ -13,7 +13,7 @@ import { ResourceMap } from '../../../base/common/map.js';
import { parse } from '../../../base/common/marshalling.js';
import { Schemas } from '../../../base/common/network.js';
import { isNative, isWeb } from '../../../base/common/platform.js';
import { URI } from '../../../base/common/uri.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { IDialogService } from '../../dialogs/common/dialogs.js';
import { IBaseTextResourceEditorInput, ITextEditorSelection } from '../../editor/common/editor.js';
@@ -23,6 +23,7 @@ import { ByteSize, IFileService } from '../../files/common/files.js';
import { IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js';
import { extractSelection } from '../../opener/common/opener.js';
import { Registry } from '../../registry/common/platform.js';
import { IMarker } from '../../markers/common/markers.js';
//#region Editor / Resources DND
@@ -30,7 +31,8 @@ import { Registry } from '../../registry/common/platform.js';
export const CodeDataTransfers = {
EDITORS: 'CodeEditors',
FILES: 'CodeFiles',
SYMBOLS: 'application/vnd.code.symbols'
SYMBOLS: 'application/vnd.code.symbols',
MARKERS: 'application/vnd.code.diagnostics',
};
export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput {
@@ -414,8 +416,12 @@ export interface DocumentSymbolTransferData {
kind: number;
}
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
const rawSymbolsData = e.dataTransfer?.getData(CodeDataTransfers.SYMBOLS);
function setDataAsJSON(e: DragEvent, kind: string, data: unknown) {
e.dataTransfer?.setData(kind, JSON.stringify(data));
}
function getDataAsJSON<T>(e: DragEvent, kind: string, defaultValue: T): T {
const rawSymbolsData = e.dataTransfer?.getData(kind);
if (rawSymbolsData) {
try {
return JSON.parse(rawSymbolsData);
@@ -424,11 +430,25 @@ export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[
}
}
return [];
return defaultValue;
}
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
return getDataAsJSON(e, CodeDataTransfers.SYMBOLS, []);
}
export function fillInSymbolsDragData(symbolsData: readonly DocumentSymbolTransferData[], e: DragEvent): void {
e.dataTransfer?.setData(CodeDataTransfers.SYMBOLS, JSON.stringify(symbolsData));
setDataAsJSON(e, CodeDataTransfers.SYMBOLS, symbolsData);
}
export type MarkerTransferData = IMarker | { uri: UriComponents };
export function extractMarkerDropData(e: DragEvent): MarkerTransferData[] | undefined {
return getDataAsJSON(e, CodeDataTransfers.MARKERS, undefined);
}
export function fillInMarkersDragData(markerData: MarkerTransferData[], e: DragEvent): void {
setDataAsJSON(e, CodeDataTransfers.MARKERS, markerData);
}
/**

View File

@@ -44,6 +44,9 @@ const _allApiProposals = {
chatReferenceBinaryData: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts',
},
chatReferenceDiagnostic: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts',
},
chatTab: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts',
},

View File

@@ -64,6 +64,15 @@ export namespace MarkerSeverity {
return _displayStrings[a] || '';
}
const _displayStringsPlural: { [value: number]: string } = Object.create(null);
_displayStringsPlural[MarkerSeverity.Error] = localize('sev.errors', "Errors");
_displayStringsPlural[MarkerSeverity.Warning] = localize('sev.warnings', "Warnings");
_displayStringsPlural[MarkerSeverity.Info] = localize('sev.infos', "Infos");
export function toStringPlural(a: MarkerSeverity): string {
return _displayStringsPlural[a] || '';
}
export function fromSeverity(severity: Severity): MarkerSeverity {
switch (severity) {
case Severity.Error: return MarkerSeverity.Error;

View File

@@ -211,7 +211,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels));
const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics));
const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol));
const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol));
const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol));
@@ -1544,6 +1544,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ChatResultFeedbackKind: extHostTypes.ChatResultFeedbackKind,
ChatVariableLevel: extHostTypes.ChatVariableLevel,
ChatCompletionItem: extHostTypes.ChatCompletionItem,
ChatReferenceDiagnostic: extHostTypes.ChatReferenceDiagnostic,
CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall,
CallHierarchyItem: extHostTypes.CallHierarchyItem,
CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall,

View File

@@ -32,6 +32,7 @@ import * as typeConvert from './extHostTypeConverters.js';
import * as extHostTypes from './extHostTypes.js';
import { isChatViewTitleActionContext } from '../../contrib/chat/common/chatActions.js';
import { IChatRelatedFile, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { ExtHostDiagnostics } from './extHostDiagnostics.js';
class ChatAgentResponseStream {
@@ -324,7 +325,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
private readonly _logService: ILogService,
private readonly _commands: ExtHostCommands,
private readonly _documents: ExtHostDocuments,
private readonly _languageModels: ExtHostLanguageModels
private readonly _languageModels: ExtHostLanguageModels,
private readonly _diagnostics: ExtHostDiagnostics,
) {
super();
this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2);
@@ -402,7 +404,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const { request, location, history } = await this._createRequest(requestDto, context, detector.extension);
const model = await this.getModelForRequest(request, detector.extension);
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference'));
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference'), this.getDiagnosticsWhenEnabled(detector.extension));
return detector.provider.provideParticipantDetection(
extRequest,
@@ -486,7 +488,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables);
const model = await this.getModelForRequest(request, agent.extension);
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference'));
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference'), this.getDiagnosticsWhenEnabled(agent.extension));
inFlightRequest = { requestId: requestDto.requestId, extRequest };
this._inFlightRequests.add(inFlightRequest);
@@ -538,6 +540,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
}
}
private getDiagnosticsWhenEnabled(extension: Readonly<IRelaxedExtensionDescription>) {
if (!isProposedApiEnabled(extension, 'chatReferenceDiagnostic')) {
return [];
}
return this._diagnostics.getDiagnostics();
}
private async prepareHistoryTurns(extension: Readonly<IRelaxedExtensionDescription>, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> {
const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = [];
@@ -551,7 +560,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const hasReadonlyProposal = isProposedApiEnabled(extension, 'chatReadonlyPromptReference');
const varsWithoutTools = h.request.variables.variables
.filter(v => !v.isTool)
.map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal));
.map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal, this.getDiagnosticsWhenEnabled(extension)));
const toolReferences = h.request.variables.variables
.filter(v => v.isTool)
.map(typeConvert.ChatLanguageModelToolReference.to);

View File

@@ -2761,7 +2761,7 @@ export namespace ChatResponsePart {
}
export namespace ChatAgentRequest {
export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean): vscode.ChatRequest {
export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatRequest {
const toolReferences = request.variables.variables.filter(v => v.isTool);
const variableReferences = request.variables.variables.filter(v => !v.isTool);
return {
@@ -2770,7 +2770,7 @@ export namespace ChatAgentRequest {
attempt: request.attempt ?? 0,
enableCommandDetection: request.enableCommandDetection ?? true,
isParticipantDetected: request.isParticipantDetected ?? false,
references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal)),
references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal, diagnostics)),
toolReferences: toolReferences.map(ChatLanguageModelToolReference.to),
location: ChatLocation.to(request.location),
acceptedConfirmationData: request.acceptedConfirmationData,
@@ -2814,19 +2814,48 @@ export namespace ChatLocation {
}
export namespace ChatPromptReference {
export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean): vscode.ChatPromptReference {
const value = variable.value;
export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatPromptReference {
let value: vscode.ChatPromptReference['value'] = variable.value;
if (!value) {
throw new Error('Invalid value reference');
}
if (isUriComponents(value)) {
value = URI.revive(value);
} else if (value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri)) {
value = Location.to(revive(value));
} else if (variable.isImage) {
value = new types.ChatReferenceBinaryData(
variable.mimeType ?? 'image/png',
() => Promise.resolve(new Uint8Array(Object.values(variable.value as number[]))),
variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined
);
} else if (variable.kind === 'diagnostic') {
const filterSeverity = variable.filterSeverity && DiagnosticSeverity.to(variable.filterSeverity);
const filterUri = variable.filterUri && URI.revive(variable.filterUri).toString();
value = new types.ChatReferenceDiagnostic(diagnostics.map(([uri, d]): [vscode.Uri, vscode.Diagnostic[]] => {
if (variable.filterUri && uri.toString() !== filterUri) {
return [uri, []];
}
return [uri, d.filter(d => {
if (filterSeverity && d.severity > filterSeverity) {
return false;
}
if (variable.filterRange && !editorRange.Range.areIntersectingOrTouching(variable.filterRange, Range.from(d.range))) {
return false;
}
return true;
})];
}).filter(([, d]) => d.length > 0));
}
return {
id: variable.id,
name: variable.name,
range: variable.range && [variable.range.start, variable.range.endExclusive],
value: isUriComponents(value) ? URI.revive(value) :
value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ?
Location.to(revive(value)) : variable.isImage ? new types.ChatReferenceBinaryData(variable.mimeType ?? 'image/png', () => Promise.resolve(new Uint8Array(Object.values(value))), variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined) : value,
value,
modelDescription: variable.modelDescription,
isReadonly: hasReadonlyProposal ? variable.isMarkedReadonly : undefined,
};

View File

@@ -4716,6 +4716,10 @@ export class ChatReferenceBinaryData implements vscode.ChatReferenceBinaryData {
}
}
export class ChatReferenceDiagnostic implements vscode.ChatReferenceDiagnostic {
constructor(public readonly diagnostics: [vscode.Uri, vscode.Diagnostic[]][]) { }
}
export enum LanguageModelChatMessageRole {
User = 1,
Assistant = 2,

View File

@@ -646,18 +646,24 @@ export class ResourceListDnDHandler<T> implements IListDragAndDrop<T> {
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const resources: URI[] = [];
for (const element of (data as ElementsDragAndDropData<T>).elements) {
const elements = (data as ElementsDragAndDropData<T>).elements;
for (const element of elements) {
const resource = this.toResource(element);
if (resource) {
resources.push(resource);
}
}
this.onWillDragElements(elements, originalEvent);
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));
}
}
protected onWillDragElements(elements: readonly T[], originalEvent: DragEvent): void {
// noop
}
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
return false;
}

View File

@@ -49,14 +49,14 @@ import { SearchContext } from '../../../search/common/constants.js';
import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js';
import { ChatRequestAgentPart } from '../../common/chatParserTypes.js';
import { IChatVariablesService } from '../../common/chatVariables.js';
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js';
import { imageToHash, isImage } from '../chatPasteProviders.js';
import { isQuickChat } from '../chatWidget.js';
import { createFolderQuickPick } from '../contrib/chatDynamicVariables.js';
import { createFolderQuickPick, createMarkersQuickPick } from '../contrib/chatDynamicVariables.js';
import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js';
import { resizeImage } from '../imageUtils.js';
import { CHAT_CATEGORY } from './chatActions.js';
@@ -75,7 +75,7 @@ export function registerChatContextActions() {
*/
type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem |
IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem |
IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem | IFolderQuickPickItem;
IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem;
/**
* These are the types that we can get out of the quick pick
@@ -103,6 +103,12 @@ function isIFolderSearchResultQuickPickItem(obj: unknown): obj is IFolderResultQ
&& (obj as IFolderResultQuickPickItem).kind === 'folder-search-result');
}
function isIDiagnosticsQuickPickItemWithFilter(obj: unknown): obj is IDiagnosticsQuickPickItemWithFilter {
return (
typeof obj === 'object'
&& (obj as IDiagnosticsQuickPickItemWithFilter).kind === 'diagnostic-filter');
}
function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource {
return (
typeof obj === 'object'
@@ -210,6 +216,19 @@ interface IScreenShotQuickPickItem extends IQuickPickItem {
icon?: ThemeIcon;
}
interface IDiagnosticsQuickPickItem extends IQuickPickItem {
kind: 'diagnostic';
id: string;
icon?: ThemeIcon;
}
interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem {
kind: 'diagnostic-filter';
id: string;
filter: IDiagnosticVariableEntryFilterData;
icon?: ThemeIcon;
}
/**
* Quick pick item for prompt instructions attachment.
*/
@@ -499,6 +518,14 @@ export class AttachContextAction extends Action2 {
isFile: false,
isDirectory: true,
});
} else if (isIDiagnosticsQuickPickItemWithFilter(pick)) {
toAttach.push({
id: pick.id,
name: pick.label,
value: pick.filter,
kind: 'diagnostic',
...pick.filter,
});
} else if (isIQuickPickItemWithResource(pick) && pick.resource) {
if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) {
// checks if the file is an image
@@ -750,6 +777,13 @@ export class AttachContextAction extends Action2 {
id: 'folder',
});
quickPickItems.push({
kind: 'diagnostic',
label: localize('chatContext.diagnstic', 'Problem...'),
iconClass: ThemeIcon.asClassName(Codicon.error),
id: 'diagnostic'
});
if (widget.location === ChatAgentLocation.Notebook) {
quickPickItems.push({
kind: 'command',
@@ -824,16 +858,33 @@ export class AttachContextAction extends Action2 {
}), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', context?.placeholder);
}
private async _showDiagnosticsPick(instantiationService: IInstantiationService): Promise<IDiagnosticsQuickPickItemWithFilter | undefined> {
const filter = await instantiationService.invokeFunction(accessor => createMarkersQuickPick(accessor));
if (!filter) {
return undefined;
}
return {
kind: 'diagnostic-filter',
id: IDiagnosticVariableEntryFilterData.id(filter),
label: IDiagnosticVariableEntryFilterData.label(filter),
filter,
};
}
private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, instantiationService: IInstantiationService, query: string = '', placeholder?: string) {
const providerOptions: AnythingQuickAccessProviderRunOptions = {
handleAccept: async (item: IChatContextQuickPickItem, isBackgroundAccept: boolean) => {
handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => {
let item: IChatContextQuickPickItem | undefined = inputItem;
if ('kind' in item && item.kind === 'folder') {
const folderItem = await this._showFolders(instantiationService);
if (!folderItem) {
this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder);
return;
}
item = folderItem;
item = await this._showFolders(instantiationService);
} else if ('kind' in item && item.kind === 'diagnostic') {
item = await this._showDiagnosticsPick(instantiationService);
}
if (!item) {
this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder);
return;
}
if ('prefix' in item) {

View File

@@ -11,21 +11,23 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { Mimes } from '../../../../base/common/mime.js';
import { basename, joinPath } from '../../../../base/common/resources.js';
import { Mutable } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { SymbolKinds } from '../../../../editor/common/languages.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { localize } from '../../../../nls.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractSymbolDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js';
import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractMarkerDropData, extractSymbolDropData, IDraggedResourceEditorInput, MarkerTransferData } from '../../../../platform/dnd/browser/dnd.js';
import { FileType, IFileService, IFileSystemProvider } from '../../../../platform/files/common/files.js';
import { MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
import { isUntitledResourceEditorInput } from '../../../common/editor.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js';
import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../common/chatModel.js';
import { IChatRequestVariableEntry, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry } from '../common/chatModel.js';
import { ChatAttachmentModel } from './chatAttachmentModel.js';
import { IChatInputStyles } from './chatInputPart.js';
import { resizeImage } from './imageUtils.js';
@@ -35,7 +37,8 @@ enum ChatDragAndDropType {
FILE_EXTERNAL,
FOLDER,
IMAGE,
SYMBOL
SYMBOL,
MARKER,
}
export class ChatDragAndDrop extends Themable {
@@ -169,6 +172,8 @@ export class ChatDragAndDrop extends Themable {
return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined;
} else if (containsDragType(e, CodeDataTransfers.SYMBOLS)) {
return ChatDragAndDropType.SYMBOL;
} else if (containsDragType(e, CodeDataTransfers.MARKERS)) {
return ChatDragAndDropType.MARKER;
} else if (containsDragType(e, DataTransfers.FILES)) {
return ChatDragAndDropType.FILE_EXTERNAL;
} else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) {
@@ -193,6 +198,7 @@ export class ChatDragAndDrop extends Themable {
case ChatDragAndDropType.FOLDER: return localize('folder', 'Folder');
case ChatDragAndDropType.IMAGE: return localize('image', 'Image');
case ChatDragAndDropType.SYMBOL: return localize('symbol', 'Symbol');
case ChatDragAndDropType.MARKER: return localize('problem', 'Problem');
}
}
@@ -224,6 +230,11 @@ export class ChatDragAndDrop extends Themable {
return [];
}
const markerData = extractMarkerDropData(e);
if (markerData) {
return this.resolveMarkerAttachContext(markerData);
}
if (containsDragType(e, CodeDataTransfers.SYMBOLS)) {
const data = extractSymbolDropData(e);
return this.resolveSymbolsAttachContext(data);
@@ -304,6 +315,33 @@ export class ChatDragAndDrop extends Themable {
});
}
private resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] {
return markers.map((marker): IDiagnosticVariableEntry => {
const filter: Mutable<IDiagnosticVariableEntryFilterData> = {};
if (!('severity' in marker)) {
filter.filterUri = URI.revive(marker.uri);
filter.filterSeverity = MarkerSeverity.Warning;
} else {
filter.filterUri = URI.revive(marker.resource);
filter.filterSeverity = marker.severity;
filter.filterRange = {
startLineNumber: marker.startLineNumber,
startColumn: marker.startColumn,
endLineNumber: marker.endLineNumber,
endColumn: marker.endColumn
};
}
return {
kind: 'diagnostic',
id: IDiagnosticVariableEntryFilterData.id(filter),
name: IDiagnosticVariableEntryFilterData.label(filter),
value: filter,
...filter,
};
});
}
private setOverlay(target: HTMLElement, type: ChatDragAndDropType | undefined): void {
// Remove any previous overlay text
this.overlayText?.remove();

View File

@@ -4,11 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { coalesce } from '../../../../../base/common/arrays.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { isCancellationError } from '../../../../../base/common/errors.js';
import * as glob from '../../../../../base/common/glob.js';
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { ResourceSet } from '../../../../../base/common/map.js';
import { basename, dirname, joinPath, relativePath } from '../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { IRange, Range } from '../../../../../editor/common/core/range.js';
import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js';
@@ -22,20 +26,19 @@ import { FileType, IFileService } from '../../../../../platform/files/common/fil
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
import { PromptsConfig } from '../../../../../platform/prompts/common/config.js';
import { IQuickAccessOptions } from '../../../../../platform/quickinput/common/quickAccess.js';
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { getExcludes, ISearchConfiguration, IFileQuery, QueryType, ISearchComplete, ISearchService } from '../../../../services/search/common/search.js';
import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js';
import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js';
import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js';
import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js';
import * as glob from '../../../../../base/common/glob.js';
import { IChatWidget } from '../chat.js';
import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js';
import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { PromptsConfig } from '../../../../../platform/prompts/common/config.js';
export const dynamicVariableDecorationType = 'chat-dynamic-variable';
@@ -645,3 +648,57 @@ export class AddDynamicVariableAction extends Action2 {
}
}
registerAction2(AddDynamicVariableAction);
export async function createMarkersQuickPick(accessor: ServicesAccessor): Promise<IDiagnosticVariableEntryFilterData | undefined> {
const markers = accessor.get(IMarkerService).read();
if (!markers.length) {
return;
}
const uriIdentityService = accessor.get(IUriIdentityService);
const labelService = accessor.get(ILabelService);
markers.sort((a, b) => uriIdentityService.extUri.compare(a.resource, b.resource) || b.severity - a.severity);
const severities = new Set<MarkerSeverity>();
type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData };
const items: (MarkerPickItem | IQuickPickSeparator)[] = [];
for (const marker of markers) {
if (!uriIdentityService.extUri.isEqual(marker.resource, (items.at(-1) as MarkerPickItem)?.resource)) {
items.push({ type: 'separator', label: labelService.getUriLabel(marker.resource, { relative: true }) });
}
severities.add(marker.severity);
items.push({
type: 'item',
resource: marker.resource,
label: marker.message,
description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn),
entry: { filterUri: marker.resource, filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn } }
});
}
if (items.length === 2) { // single error in a URI
return (items[1] as MarkerPickItem).entry;
}
if (items.length > 2) {
if (severities.has(MarkerSeverity.Error)) {
items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Errors'), entry: { filterSeverity: MarkerSeverity.Error } });
}
if (severities.has(MarkerSeverity.Warning)) {
items.unshift({ type: 'item', label: localize('markers.panel.allWarnings', 'All Warnings'), entry: { filterSeverity: MarkerSeverity.Warning } });
}
if (severities.has(MarkerSeverity.Info)) {
items.unshift({ type: 'item', label: localize('markers.panel.allInfos', 'All Infos'), entry: { filterSeverity: MarkerSeverity.Info } });
}
}
const quickInputService = accessor.get(IQuickInputService);
const quickPick = quickInputService.createQuickPick({ useSeparators: true });
quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...');
quickPick.items = items;
return quickInputService.pick(items, { canPickMany: false }).then(v => v?.entry);
}

View File

@@ -21,6 +21,7 @@ import { IRange } from '../../../../editor/common/core/range.js';
import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js';
import { localize } from '../../../../nls.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';
import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent, reviveSerializedAgent } from './chatAgents.js';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js';
@@ -87,7 +88,39 @@ export interface ILinkVariableEntry extends Omit<IBaseChatRequestVariableEntry,
readonly value: URI;
}
export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | ILinkVariableEntry | IBaseChatRequestVariableEntry;
export interface IDiagnosticVariableEntryFilterData {
readonly filterUri?: URI;
readonly filterSeverity?: MarkerSeverity;
readonly filterRange?: IRange;
}
export namespace IDiagnosticVariableEntryFilterData {
export function id(data: IDiagnosticVariableEntryFilterData) {
return [data.filterUri, data.filterSeverity, data.filterRange?.startLineNumber].join(':');
}
export function label(data: IDiagnosticVariableEntryFilterData) {
let labelStr: string;
if (data.filterSeverity) {
const sev = data.filterRange ? MarkerSeverity.toString(data.filterSeverity) : MarkerSeverity.toStringPlural(data.filterSeverity);
labelStr = data.filterUri
? localize('chat.attachment.problems.severity', "{0} in {1}", sev, basename(data.filterUri))
: localize('chat.attachment.problems.severity2', "All {0}", sev);
} else {
labelStr = data.filterUri
? localize('chat.attachment.problems.severity3', "Problems in {0}", basename(data.filterUri))
: localize('chat.attachment.problems.severity4', "All Problems");
}
return labelStr;
}
}
export interface IDiagnosticVariableEntry extends Omit<IBaseChatRequestVariableEntry, 'kind'>, IDiagnosticVariableEntryFilterData {
readonly kind: 'diagnostic';
}
export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | ILinkVariableEntry | IBaseChatRequestVariableEntry | IDiagnosticVariableEntry;
export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry {
return obj.kind === 'implicit';
@@ -101,6 +134,10 @@ export function isLinkVariableEntry(obj: IChatRequestVariableEntry): obj is ILin
return obj.kind === 'link';
}
export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry {
return obj.kind === 'diagnostic';
}
export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry {
const entry = obj as IChatRequestVariableEntry;
return typeof entry === 'object' &&

View File

@@ -5,57 +5,59 @@
import './media/markers.css';
import { URI } from '../../../../base/common/uri.js';
import * as dom from '../../../../base/browser/dom.js';
import { IAction, Separator } from '../../../../base/common/actions.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js';
import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement, MarkerTableItem } from './markersModel.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { MarkersFilters, IMarkersFiltersChangeEvent } from './markersViewActions.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import Messages from './messages.js';
import { RangeHighlightDecorations } from '../../../browser/codeeditor.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { localize } from '../../../../nls.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer, ITreeEvent } from '../../../../base/browser/ui/tree/tree.js';
import { Relay, Event } from '../../../../base/common/event.js';
import { WorkbenchObjectTree, IListService, IWorkbenchObjectTreeOptions, IOpenEvent } from '../../../../platform/list/browser/listService.js';
import { FilterOptions } from './markersFilterOptions.js';
import { IExpression } from '../../../../base/common/glob.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersWidgetAccessibilityProvider, MarkersViewModel } from './markersTreeViewer.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { MenuId } from '../../../../platform/actions/common/actions.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { StandardKeyboardEvent, IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
import { ResourceLabels } from '../../../browser/labels.js';
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { MementoObject, Memento } from '../../../common/memento.js';
import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js';
import { IViewDescriptorService } from '../../../common/views.js';
import { IOpenerService, withSelection } from '../../../../platform/opener/common/opener.js';
import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { groupBy } from '../../../../base/common/arrays.js';
import { ResourceMap } from '../../../../base/common/map.js';
import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';
import { IMarkersView } from './markers.js';
import { ResourceListDnDHandler } from '../../../browser/dnd.js';
import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
import { ITableContextMenuEvent, ITableEvent } from '../../../../base/browser/ui/table/table.js';
import { MarkersTable } from './markersTable.js';
import { Markers, MarkersContextKeys, MarkersViewMode } from '../common/markers.js';
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
import { ITreeContextMenuEvent, ITreeElement, ITreeEvent, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js';
import { IAction, Separator } from '../../../../base/common/actions.js';
import { groupBy } from '../../../../base/common/arrays.js';
import { Event, Relay } from '../../../../base/common/event.js';
import { IExpression } from '../../../../base/common/glob.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../base/common/map.js';
import { deepClone } from '../../../../base/common/objects.js';
import { isDefined } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { localize } from '../../../../nls.js';
import { MenuId } from '../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { fillInMarkersDragData, MarkerTransferData } from '../../../../platform/dnd/browser/dnd.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
import { IListService, IOpenEvent, IWorkbenchObjectTreeOptions, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js';
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { IOpenerService, withSelection } from '../../../../platform/opener/common/opener.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
import { RangeHighlightDecorations } from '../../../browser/codeeditor.js';
import { ResourceListDnDHandler } from '../../../browser/dnd.js';
import { ResourceLabels } from '../../../browser/labels.js';
import { FilterViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js';
import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';
import { Memento, MementoObject } from '../../../common/memento.js';
import { IViewDescriptorService } from '../../../common/views.js';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
import { Markers, MarkersContextKeys, MarkersViewMode } from '../common/markers.js';
import { IMarkersView } from './markers.js';
import { FilterOptions } from './markersFilterOptions.js';
import { compareMarkersByUri, Marker, MarkerChangesEvent, MarkerElement, MarkersModel, MarkerTableItem, RelatedInformation, ResourceMarkers } from './markersModel.js';
import { MarkersTable } from './markersTable.js';
import { Filter, FilterData, MarkerRenderer, MarkersViewModel, MarkersWidgetAccessibilityProvider, RelatedInformationRenderer, ResourceMarkersRenderer, VirtualDelegate } from './markersTreeViewer.js';
import { IMarkersFiltersChangeEvent, MarkersFilters } from './markersViewActions.js';
import Messages from './messages.js';
function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable<ITreeElement<MarkerElement>> {
return Iterable.map(resourceMarkers.markers, m => {
@@ -495,18 +497,7 @@ export class MarkersView extends FilterViewPane implements IMarkersView {
filter: this.filter,
accessibilityProvider: this.widgetAccessibilityProvider,
identityProvider: this.widgetIdentityProvider,
dnd: this.instantiationService.createInstance(ResourceListDnDHandler, (element) => {
if (element instanceof ResourceMarkers) {
return element.resource;
}
if (element instanceof Marker) {
return withSelection(element.resource, element.range);
}
if (element instanceof RelatedInformation) {
return withSelection(element.raw.resource, element.raw);
}
return null;
}),
dnd: this.instantiationService.createInstance(MarkersListDnDHandler),
expandOnlyOnTwistieClick: (e: MarkerElement) => e instanceof Marker && e.relatedInformation.length > 0,
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
selectionNavigation: true,
@@ -1078,3 +1069,40 @@ class MarkersTree extends WorkbenchObjectTree<MarkerElement, FilterData> impleme
super.layout(height, width);
}
}
class MarkersListDnDHandler extends ResourceListDnDHandler<MarkerElement | MarkerTableItem> {
constructor(
@IInstantiationService instantiationService: IInstantiationService
) {
super(element => {
if (element instanceof MarkerTableItem) {
return withSelection(element.resource, element.range);
} else if (element instanceof ResourceMarkers) {
return element.resource;
} else if (element instanceof Marker) {
return withSelection(element.resource, element.range);
} else if (element instanceof RelatedInformation) {
return withSelection(element.raw.resource, element.raw);
}
return null;
}, instantiationService);
}
protected override onWillDragElements(elements: (MarkerElement | MarkerTableItem)[], originalEvent: DragEvent) {
const data = elements.map((e): MarkerTransferData | undefined => {
if (e instanceof RelatedInformation || e instanceof Marker) {
return e.marker;
}
if (e instanceof ResourceMarkers) {
return { uri: e.resource };
}
return undefined;
}).filter(isDefined);
if (!data.length) {
return;
}
fillInMarkersDragData(data, originalEvent);
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
export interface ChatPromptReference {
/**
* The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future.
*/
readonly value: string | Uri | Location | ChatReferenceDiagnostic | unknown;
}
export class ChatReferenceDiagnostic {
/**
* All attached diagnostics. An array of uri-diagnostics tuples or an empty array.
*/
readonly diagnostics: [Uri, Diagnostic[]][];
protected constructor(diagnostics: [Uri, Diagnostic[]][]);
}
}