Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Symbols as synamic variables and add context menu for symbols #234740

Merged
merged 3 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/vs/editor/common/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,45 @@ export namespace SymbolKinds {
}
return icon;
}

const byCompletionKind = new Map<SymbolKind, CompletionItemKind>();
byCompletionKind.set(SymbolKind.File, CompletionItemKind.File);
byCompletionKind.set(SymbolKind.Module, CompletionItemKind.Module);
byCompletionKind.set(SymbolKind.Namespace, CompletionItemKind.Module);
byCompletionKind.set(SymbolKind.Package, CompletionItemKind.Module);
byCompletionKind.set(SymbolKind.Class, CompletionItemKind.Class);
byCompletionKind.set(SymbolKind.Method, CompletionItemKind.Method);
byCompletionKind.set(SymbolKind.Property, CompletionItemKind.Property);
byCompletionKind.set(SymbolKind.Field, CompletionItemKind.Field);
byCompletionKind.set(SymbolKind.Constructor, CompletionItemKind.Constructor);
byCompletionKind.set(SymbolKind.Enum, CompletionItemKind.Enum);
byCompletionKind.set(SymbolKind.Interface, CompletionItemKind.Interface);
byCompletionKind.set(SymbolKind.Function, CompletionItemKind.Function);
byCompletionKind.set(SymbolKind.Variable, CompletionItemKind.Variable);
byCompletionKind.set(SymbolKind.Constant, CompletionItemKind.Constant);
byCompletionKind.set(SymbolKind.String, CompletionItemKind.Text);
byCompletionKind.set(SymbolKind.Number, CompletionItemKind.Value);
byCompletionKind.set(SymbolKind.Boolean, CompletionItemKind.Value);
byCompletionKind.set(SymbolKind.Array, CompletionItemKind.Value);
byCompletionKind.set(SymbolKind.Object, CompletionItemKind.Value);
byCompletionKind.set(SymbolKind.Key, CompletionItemKind.Keyword);
byCompletionKind.set(SymbolKind.Null, CompletionItemKind.Value);
byCompletionKind.set(SymbolKind.EnumMember, CompletionItemKind.EnumMember);
byCompletionKind.set(SymbolKind.Struct, CompletionItemKind.Struct);
byCompletionKind.set(SymbolKind.Event, CompletionItemKind.Event);
byCompletionKind.set(SymbolKind.Operator, CompletionItemKind.Operator);
byCompletionKind.set(SymbolKind.TypeParameter, CompletionItemKind.TypeParameter);
/**
* @internal
*/
export function toCompletionKind(kind: SymbolKind): CompletionItemKind {
let completionKind = byCompletionKind.get(kind);
if (completionKind === undefined) {
console.info('No completion kind found for SymbolKind ' + kind);
completionKind = CompletionItemKind.File;
}
return completionKind;
}
}

export interface DocumentSymbol {
Expand Down
13 changes: 11 additions & 2 deletions src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,20 @@ export class EditsAttachmentModel extends ChatAttachmentModel {

override addContext(...attachments: IChatRequestVariableEntry[]) {
const currentAttachmentIds = this.getAttachmentIDs();

const fileAttachments = attachments.filter(attachment => attachment.isFile);
const newFileAttachments = fileAttachments.filter(attachment => !currentAttachmentIds.has(attachment.id));
const otherAttachments = attachments.filter(attachment => !attachment.isFile);

// deduplicate file attachments
const newFileAttachments = [];
const newFileAttachmentIds = new Set<string>();
for (const attachment of fileAttachments) {
if (newFileAttachmentIds.has(attachment.id) || currentAttachmentIds.has(attachment.id)) {
continue;
}
newFileAttachmentIds.add(attachment.id);
newFileAttachments.push(attachment);
}

const availableFileCount = Math.max(0, this._chatEditingService.editingSessionFileLimit - this.fileAttachments.length);
const fileAttachmentsToBeAdded = newFileAttachments.slice(0, availableFileCount);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { basename } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { Position } from '../../../../../editor/common/core/position.js';
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
import { isLocation, Location } from '../../../../../editor/common/languages.js';
import { ITextModel } from '../../../../../editor/common/model.js';
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
import { localize, localize2 } from '../../../../../nls.js';
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
Expand Down Expand Up @@ -564,3 +571,85 @@ registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
}
}
});

registerAction2(class ResolveSymbolsContextAction extends Action2 {
constructor() {
super({
id: 'workbench.action.edits.addFilesFromReferences',
title: localize2('addFilesFromReferences', "Add Files From References"),
f1: false,
category: CHAT_CATEGORY,
menu: {
id: MenuId.ChatInputSymbolAttachmentContext,
group: 'navigation',
order: 1,
when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), EditorContextKeys.hasReferenceProvider)
}
});
}

override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
const widgetService = accessor.get(IChatWidgetService);
const textModelService = accessor.get(ITextModelService);
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
const [widget] = widgetService.getWidgetsByLocations(ChatAgentLocation.EditingSession);
if (!widget || args.length === 0 || !isLocation(args[0])) {
return;
}

const symbol = args[0] as Location;

const modelReference = await textModelService.createModelReference(symbol.uri);
const textModel = modelReference.object.textEditorModel;
if (!textModel) {
return;
}

const position = new Position(symbol.range.startLineNumber, symbol.range.startColumn);

const [references, definitions, implementations] = await Promise.all([
this.getReferences(position, textModel, languageFeaturesService),
this.getDefinitions(position, textModel, languageFeaturesService),
this.getImplementations(position, textModel, languageFeaturesService)
]);

// Sort the references, definitions and implementations by
// how important it is that they make it into the working set as it has limited size
const attachments = [];
for (const reference of [...definitions, ...implementations, ...references]) {
attachments.push(widget.attachmentModel.asVariableEntry(reference.uri));
}

widget.attachmentModel.addContext(...attachments);
}

private async getReferences(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
const referenceProviders = languageFeaturesService.referenceProvider.all(textModel);

const references = await Promise.all(referenceProviders.map(async (referenceProvider) => {
return await referenceProvider.provideReferences(textModel, position, { includeDeclaration: true }, CancellationToken.None) ?? [];
}));

return references.flat();
}

private async getDefinitions(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
const definitionProviders = languageFeaturesService.definitionProvider.all(textModel);

const definitions = await Promise.all(definitionProviders.map(async (definitionProvider) => {
return await definitionProvider.provideDefinition(textModel, position, CancellationToken.None) ?? [];
}));

return definitions.flat();
}

private async getImplementations(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
const implementationProviders = languageFeaturesService.implementationProvider.all(textModel);

const implementations = await Promise.all(implementationProviders.map(async (implementationProvider) => {
return await implementationProvider.provideImplementation(textModel, position, CancellationToken.None) ?? [];
}));

return implementations.flat();
}
});
3 changes: 2 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatInputPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { EditorOptions } from '../../../../editor/common/config/editorOptions.js
import { IDimension } from '../../../../editor/common/core/dimension.js';
import { IPosition } from '../../../../editor/common/core/position.js';
import { IRange, Range } from '../../../../editor/common/core/range.js';
import { isLocation } from '../../../../editor/common/languages.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
Expand Down Expand Up @@ -1054,7 +1055,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}
// Factor file variables that are part of the user query into the working set
for (const part of chatWidget?.parsedInput.parts ?? []) {
if (part instanceof ChatRequestDynamicVariablePart && part.isFile && URI.isUri(part.data) && !seenEntries.has(part.data)) {
if (part instanceof ChatRequestDynamicVariablePart && part.isFile && (URI.isUri(part.data) && !seenEntries.has(part.data) || isLocation(part.data) && !seenEntries.has(part.data.uri))) {
entries.unshift({
reference: part.data,
state: WorkingSetEntryState.Attached,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { basename } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { IRange, Range } from '../../../../../editor/common/core/range.js';
import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js';
import { Command } from '../../../../../editor/common/languages.js';
import { Command, isLocation } from '../../../../../editor/common/languages.js';
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
import { localize } from '../../../../../nls.js';
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
Expand All @@ -23,6 +23,7 @@ import { IQuickInputService } from '../../../../../platform/quickinput/common/qu
import { IChatWidget } from '../chat.js';
import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js';
import { IChatRequestVariableValue, IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js';
import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js';

export const dynamicVariableDecorationType = 'chat-dynamic-variable';

Expand Down Expand Up @@ -111,6 +112,10 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC
const value = ref.data;
if (URI.isUri(value)) {
return new MarkdownString(this.labelService.getUriLabel(value, { relative: true }));
} else if (isLocation(value)) {
const prefix = ref.fullName ? ` ${ref.fullName}` : '';
const rangeString = `#${value.range.startLineNumber}-${value.range.endLineNumber}`;
return new MarkdownString(prefix + this.labelService.getUriLabel(value.uri, { relative: true }) + rangeString);
} else {
return undefined;
}
Expand All @@ -119,12 +124,12 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC

ChatWidget.CONTRIBS.push(ChatDynamicVariableModel);

interface SelectAndInsertFileActionContext {
interface SelectAndInsertActionContext {
widget: IChatWidget;
range: IRange;
}

function isSelectAndInsertFileActionContext(context: any): context is SelectAndInsertFileActionContext {
function isSelectAndInsertActionContext(context: any): context is SelectAndInsertActionContext {
return 'widget' in context && 'range' in context;
}

Expand All @@ -150,7 +155,7 @@ export class SelectAndInsertFileAction extends Action2 {
const chatVariablesService = accessor.get(IChatVariablesService);

const context = args[0];
if (!isSelectAndInsertFileActionContext(context)) {
if (!isSelectAndInsertActionContext(context)) {
return;
}

Expand Down Expand Up @@ -219,6 +224,69 @@ export class SelectAndInsertFileAction extends Action2 {
}
registerAction2(SelectAndInsertFileAction);

export class SelectAndInsertSymAction extends Action2 {
static readonly Name = 'symbols';
static readonly ID = 'workbench.action.chat.selectAndInsertSym';

constructor() {
super({
id: SelectAndInsertSymAction.ID,
title: '' // not displayed
});
}

async run(accessor: ServicesAccessor, ...args: any[]) {
const textModelService = accessor.get(ITextModelService);
const logService = accessor.get(ILogService);
const quickInputService = accessor.get(IQuickInputService);

const context = args[0];
if (!isSelectAndInsertActionContext(context)) {
return;
}

const doCleanup = () => {
// Failed, remove the dangling `sym`
context.widget.inputEditor.executeEdits('chatInsertSym', [{ range: context.range, text: `` }]);
};

// TODO: have dedicated UX for this instead of using the quick access picker
const picks = await quickInputService.quickAccess.pick('#', { enabledProviderPrefixes: ['#'] });
if (!picks?.length) {
logService.trace('SelectAndInsertSymAction: no symbol selected');
doCleanup();
return;
}

const editor = context.widget.inputEditor;
const range = context.range;

// Handle the case of selecting a specific file
const symbol = (picks[0] as ISymbolQuickPickItem).symbol;
if (!symbol || !textModelService.canHandleResource(symbol.location.uri)) {
logService.trace('SelectAndInsertSymAction: non-text resource selected');
doCleanup();
return;
}

const text = `#sym:${symbol.name}`;
const success = editor.executeEdits('chatInsertSym', [{ range, text: text + ' ' }]);
if (!success) {
logService.trace(`SelectAndInsertSymAction: failed to insert "${text}"`);
doCleanup();
return;
}

context.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID)?.addReference({
id: 'vscode.symbol',
prefix: 'symbol',
range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length },
data: symbol.location
});
}
}
registerAction2(SelectAndInsertSymAction);

export interface IAddDynamicVariableContext {
id: string;
widget: IChatWidget;
Expand Down
Loading
Loading