Skip to content

Commit

Permalink
feat: enable attaching symbols to chat via @ (#213347)
Browse files Browse the repository at this point in the history
* feat: enable attaching symbols to chat via `@`

* Oop
  • Loading branch information
joyceerhl authored May 24, 2024
1 parent c1ebab9 commit 7bb6755
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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(() => {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
24 changes: 19 additions & 5 deletions src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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();

Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 [];
Expand Down Expand Up @@ -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
});
Expand Down
50 changes: 38 additions & 12 deletions src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand All @@ -44,6 +46,7 @@ export interface IFileQuickPickItem extends IQuickPickItem {
}

export interface IDynamicVariableQuickPickItem extends IQuickPickItem {
kind: 'dynamic';
id: string;
name?: string;
value: unknown;
Expand All @@ -54,6 +57,7 @@ export interface IDynamicVariableQuickPickItem extends IQuickPickItem {
}

export interface IStaticVariableQuickPickItem extends IQuickPickItem {
kind: 'static';
id: string;
name: string;
value: unknown;
Expand Down Expand Up @@ -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[]) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand All @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/chatInputPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/chat/common/chatModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface IChatRequestVariableEntry {
value: IChatRequestVariableValue;
references?: IChatContentReference[];
isDynamic?: boolean;
isFile?: boolean;
}

export interface IChatRequestVariableData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7bb6755

Please sign in to comment.