feat: enable attaching symbols to chat via @ (#213347)

* feat: enable attaching symbols to chat via `@`

* Oop
This commit is contained in:
Joyce Er 2024-05-24 01:24:21 -07:00 committed by GitHub
parent c1ebab91fb
commit 7bb6755241
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 25 deletions

View file

@ -12,7 +12,7 @@ import { IRange } from 'vs/editor/common/core/range';
import { IDiffEditor, IEditor, ScrollType } from 'vs/editor/common/editorCommon';
import { IModelDeltaDecoration, ITextModel, OverviewRulerLane } from 'vs/editor/common/model';
import { overviewRulerRangeHighlight } from 'vs/editor/common/core/editorColorRegistry';
import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess';
import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess';
import { IKeyMods, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { status } from 'vs/base/browser/ui/aria/aria';
@ -52,7 +52,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
//#region Provider methods
provide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable {
provide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
const disposables = new DisposableStore();
// Apply options if any
@ -63,7 +63,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
// Provide based on current active editor
const pickerDisposable = disposables.add(new MutableDisposable());
pickerDisposable.value = this.doProvide(picker, token);
pickerDisposable.value = this.doProvide(picker, token, runOptions);
// Re-create whenever the active editor changes
disposables.add(this.onDidActiveTextEditorControlChange(() => {
@ -78,7 +78,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
return disposables;
}
private doProvide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable {
private doProvide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
const disposables = new DisposableStore();
// With text control
@ -113,7 +113,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
disposables.add(toDisposable(() => this.clearDecorations(editor)));
// Ask subclass for entries
disposables.add(this.provideWithTextEditor(context, picker, token));
disposables.add(this.provideWithTextEditor(context, picker, token, runOptions));
}
// Without text control
@ -134,7 +134,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
/**
* Subclasses to implement to provide picks for the picker when an editor is active.
*/
protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable;
protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IQuickPickItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable;
/**
* Subclasses to implement to provide picks for the picker when no editor is active.

View file

@ -22,16 +22,26 @@ import { IQuickInputButton, IQuickPick, IQuickPickItem, IQuickPickSeparator } fr
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { Position } from 'vs/editor/common/core/position';
import { findLast } from 'vs/base/common/arraysFind';
import { IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess';
import { URI } from 'vs/base/common/uri';
export interface IGotoSymbolQuickPickItem extends IQuickPickItem {
kind: SymbolKind;
index: number;
score?: number;
uri?: URI;
symbolName?: string;
range?: { decoration: IRange; selection: IRange };
}
export interface IGotoSymbolQuickAccessProviderOptions extends IEditorNavigationQuickAccessOptions {
openSideBySideDirection?: () => undefined | 'right' | 'down';
/**
* A handler to invoke when an item is accepted for
* this particular showing of the quick access.
* @param item The item that was accepted.
*/
readonly handleAccept?: (item: IQuickPickItem) => void;
}
export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider {
@ -59,7 +69,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
return Disposable.None;
}
protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
const editor = context.editor;
const model = this.getModel(editor);
if (!model) {
@ -68,7 +78,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
// Provide symbols from model if available in registry
if (this._languageFeaturesService.documentSymbolProvider.has(model)) {
return this.doProvideWithEditorSymbols(context, model, picker, token);
return this.doProvideWithEditorSymbols(context, model, picker, token, runOptions);
}
// Otherwise show an entry for a model without registry
@ -127,7 +137,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
return symbolProviderRegistryPromise.p;
}
private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable {
const editor = context.editor;
const disposables = new DisposableStore();
@ -137,6 +147,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
if (item && item.range) {
this.gotoLocation(context, { range: item.range.selection, keyMods: picker.keyMods, preserveFocus: event.inBackground });
runOptions?.handleAccept?.(item);
if (!event.inBackground) {
picker.hide();
}
@ -171,7 +183,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
picker.busy = true;
try {
const query = prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim());
const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token);
const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token, model);
if (token.isCancellationRequested) {
return;
}
@ -218,7 +230,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
return disposables;
}
protected async doGetSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
protected async doGetSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken, model: ITextModel): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
const symbols = await symbolsPromise;
if (token.isCancellationRequested) {
return [];
@ -326,6 +338,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
selection: Range.collapseToStart(symbol.selectionRange),
decoration: symbol.range
},
uri: model.uri,
symbolName: symbolLabel,
strikethrough: deprecated,
buttons
});

View file

@ -7,10 +7,12 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Schemas } from 'vs/base/common/network';
import { IRange } from 'vs/editor/common/core/range';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { Command } from 'vs/editor/common/languages';
import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess';
import { localize, localize2 } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
@ -20,7 +22,6 @@ import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/q
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments';
import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel';
@ -32,9 +33,10 @@ export function registerChatContextActions() {
registerAction2(AttachContextAction);
}
export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem;
export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem;
export interface IFileQuickPickItem extends IQuickPickItem {
kind: 'file';
id: string;
name: string;
value: URI;
@ -44,6 +46,7 @@ export interface IFileQuickPickItem extends IQuickPickItem {
}
export interface IDynamicVariableQuickPickItem extends IQuickPickItem {
kind: 'dynamic';
id: string;
name?: string;
value: unknown;
@ -54,6 +57,7 @@ export interface IDynamicVariableQuickPickItem extends IQuickPickItem {
}
export interface IStaticVariableQuickPickItem extends IQuickPickItem {
kind: 'static';
id: string;
name: string;
value: unknown;
@ -92,8 +96,14 @@ class AttachContextAction extends Action2 {
});
}
private _getFileContextId(item: { resource: URI }) {
return item.resource.toString();
private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) {
if ('resource' in item) {
return item.resource.toString();
}
return item.uri.toString() + (item.range.startLineNumber !== item.range.endLineNumber ?
`:${item.range.startLineNumber}-${item.range.endLineNumber}` :
`:${item.range.startLineNumber}`);
}
private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) {
@ -121,14 +131,27 @@ class AttachContextAction extends Action2 {
id: this._getFileContextId(pick),
value: pick.resource,
name: pick.label,
isFile: true,
isDynamic: true
});
} else if ('symbolName' in pick && pick.uri && pick.range) {
// Symbol
toAttach.push({
...pick,
range: undefined,
id: this._getFileContextId({ uri: pick.uri, range: pick.range.decoration }),
value: { uri: pick.uri, range: pick.range.decoration },
fullName: pick.label,
name: pick.symbolName!,
isDynamic: true
});
} else {
// All other dynamic variables and static variables
toAttach.push({
...pick,
id: pick.id,
value: pick.value,
range: undefined,
id: pick.id ?? '',
value: 'value' in pick ? pick.value : undefined,
fullName: pick.label,
name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label,
icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined
@ -186,12 +209,11 @@ class AttachContextAction extends Action2 {
}
if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) {
quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' });
}
quickInputService.quickAccess.show('', {
enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX],
enabledProviderPrefixes: [
AnythingQuickAccessProvider.PREFIX,
AbstractGotoSymbolQuickAccessProvider.PREFIX
],
placeholder: localize('chatContext.attach.placeholder', 'Search attachments'),
providerOptions: <AnythingQuickAccessProviderRunOptions>{
handleAccept: (item: IChatContextQuickPickItem) => {
@ -208,7 +230,11 @@ class AttachContextAction extends Action2 {
&& !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly
}
if (!('command' in item)) {
if (item && typeof item === 'object' && 'uri' in item && item.uri && item.range) {
return !attachedContext.has(this._getFileContextId({ uri: item.uri, range: item.range.decoration }));
}
if (!('command' in item) && item.id) {
return !attachedContext.has(item.id);
}

View file

@ -442,7 +442,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons'));
const label = this._contextResourceLabels.create(widget, { supportIcons: true });
const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
if (file) {
if (file && attachment.isFile) {
label.setFile(file, {
fileKind: FileKind.FILE,
hidePath: true,

View file

@ -33,6 +33,7 @@ export interface IChatRequestVariableEntry {
value: IChatRequestVariableValue;
references?: IChatContentReference[];
isDynamic?: boolean;
isFile?: boolean;
}
export interface IChatRequestVariableData {

View file

@ -118,7 +118,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess
return [];
}
return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token);
return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token, model);
}
//#endregion