Add "variable references" (#208478)

Towards microsoft/vscode-copilot-release#926
This commit is contained in:
Rob Lourens 2024-03-22 19:45:56 -03:00 committed by GitHub
parent 32c524af96
commit 871cab2b67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 104 additions and 65 deletions

View file

@ -2439,15 +2439,38 @@ export namespace ChatResponseCommandButtonPart {
export namespace ChatResponseReferencePart {
export function to(part: vscode.ChatResponseReferencePart): Dto<IChatContentReference> {
if ('variableName' in part.value) {
return {
kind: 'reference',
reference: {
variableName: part.value.variableName,
value: URI.isUri(part.value.value) ?
part.value.value :
Location.from(<vscode.Location>part.value.value)
}
};
}
return {
kind: 'reference',
reference: !URI.isUri(part.value) ? Location.from(<vscode.Location>part.value) : part.value
reference: URI.isUri(part.value) ?
part.value :
Location.from(<vscode.Location>part.value)
};
}
export function from(part: Dto<IChatContentReference>): vscode.ChatResponseReferencePart {
const value = revive<IChatContentReference>(part);
const mapValue = (value: URI | languages.Location): vscode.Uri | vscode.Location => URI.isUri(value) ?
value :
Location.to(value);
return new types.ChatResponseReferencePart(
URI.isUri(value.reference) ? value.reference : Location.to(value.reference)
'variableName' in value.reference ? {
variableName: value.reference.variableName,
value: value.reference.value && mapValue(value.reference.value)
} :
mapValue(value.reference)
);
}
}
@ -2556,36 +2579,6 @@ export namespace ChatResponseProgress {
}
}
export function to(progress: extHostProtocol.IChatProgressDto): vscode.ChatProgress | undefined {
switch (progress.kind) {
case 'markdownContent':
case 'inlineReference':
case 'treeData':
return ChatResponseProgress.to(progress);
case 'content':
return { content: progress.content };
case 'usedContext':
return { documents: progress.documents.map(d => ({ uri: URI.revive(d.uri), version: d.version, ranges: d.ranges.map(r => Range.to(r)) })) };
case 'reference':
return {
reference:
isUriComponents(progress.reference) ?
URI.revive(progress.reference) :
Location.to(progress.reference)
};
case 'agentDetection':
// For simplicity, don't sent back the 'extended' types
return undefined;
case 'progressMessage':
return { message: progress.content.value };
case 'vulnerability':
return { content: progress.content, vulnerabilities: progress.vulnerabilities };
default:
// Unknown type, eg something in history that was removed? Ignore
return undefined;
}
}
export function toProgressContent(progress: extHostProtocol.IChatContentProgressDto, commandsConverter: Command.ICommandsConverter): vscode.ChatContentProgress | undefined {
switch (progress.kind) {
case 'markdownContent':

View file

@ -4298,8 +4298,8 @@ export class ChatResponseCommandButtonPart {
}
export class ChatResponseReferencePart {
value: vscode.Uri | vscode.Location;
constructor(value: vscode.Uri | vscode.Location) {
value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location };
constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }) {
this.value = value;
}
}

View file

@ -25,6 +25,7 @@ import { ResourceMap } from 'vs/base/common/map';
import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network';
import { clamp } from 'vs/base/common/numbers';
import { basename } from 'vs/base/common/path';
import { basenameOrAuthority, dirname } from 'vs/base/common/resources';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
@ -41,6 +42,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { FileKind, FileType } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILabelService } from 'vs/platform/label/common/label';
import { WorkbenchCompressibleAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService';
import { ILogService } from 'vs/platform/log/common/log';
import { IOpenerService } from 'vs/platform/opener/common/opener';
@ -58,6 +60,7 @@ import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT
import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView';
@ -800,17 +803,22 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
listDisposables.add(list.onDidOpen((e) => {
if (e.element) {
this.openerService.open(
'uri' in e.element.reference ? e.element.reference.uri : e.element.reference,
{
fromUserGesture: true,
editorOptions: {
...e.editorOptions,
...{
selection: 'range' in e.element.reference ? e.element.reference.range : undefined
const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference;
const uri = URI.isUri(uriOrLocation) ? uriOrLocation :
uriOrLocation?.uri;
if (uri) {
this.openerService.open(
uri,
{
fromUserGesture: true,
editorOptions: {
...e.editorOptions,
...{
selection: 'range' in e.element.reference ? e.element.reference.range : undefined
}
}
}
});
});
}
}
}));
listDisposables.add(list.onContextMenu((e) => {
@ -1163,10 +1171,13 @@ class ContentReferencesListPool extends Disposable {
alwaysConsumeMouseWheel: false,
accessibilityProvider: {
getAriaLabel: (element: IChatContentReference) => {
if (URI.isUri(element.reference)) {
return basename(element.reference.path);
const reference = element.reference;
if ('variableName' in reference) {
return reference.variableName;
} else if (URI.isUri(reference)) {
return basename(reference.path);
} else {
return basename(element.reference.uri.path);
return basename(reference.uri.path);
}
},
@ -1212,6 +1223,8 @@ class ContentReferencesListRenderer implements IListRenderer<IChatContentReferen
constructor(
private labels: ResourceLabels,
@ILabelService private readonly labelService: ILabelService,
@IChatVariablesService private readonly chatVariablesService: IChatVariablesService,
) { }
renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate {
@ -1220,18 +1233,30 @@ class ContentReferencesListRenderer implements IListRenderer<IChatContentReferen
return { templateDisposables, label };
}
renderElement(element: IChatContentReference, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void {
renderElement(data: IChatContentReference, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void {
const reference = data.reference;
templateData.label.element.style.display = 'flex';
const uri = 'uri' in element.reference ? element.reference.uri : element.reference;
if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) {
templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: Codicon.globe });
if ('variableName' in reference) {
if (reference.value) {
const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri;
const title = this.labelService.getUriLabel(dirname(uri), { relative: true });
templateData.label.setResource({ resource: uri, name: basenameOrAuthority(uri), description: `#${reference.variableName}` }, { title });
} else {
const variable = this.chatVariablesService.getVariable(reference.variableName);
templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description });
}
} else {
templateData.label.setFile(uri, {
fileKind: FileKind.FILE,
// Should not have this live-updating data on a historical reference
fileDecorations: { badges: false, colors: false },
range: 'range' in element.reference ? element.reference.range : undefined
});
const uri = 'uri' in reference ? reference.uri : reference;
if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) {
templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: Codicon.globe });
} else {
templateData.label.setFile(uri, {
fileKind: FileKind.FILE,
// Should not have this live-updating data on a historical reference
fileDecorations: { badges: false, colors: false },
range: 'range' in reference ? reference.range : undefined
});
}
}
}

View file

@ -73,6 +73,10 @@ export class ChatVariablesService implements IChatVariablesService {
return this._resolver.has(name.toLowerCase());
}
getVariable(name: string): IChatVariableData | undefined {
return this._resolver.get(name.toLowerCase())?.data;
}
getVariables(): Iterable<Readonly<IChatVariableData>> {
const all = Iterable.map(this._resolver.values(), data => data.data);
return Iterable.filter(all, data => !data.hidden);

View file

@ -74,8 +74,13 @@ export function isIUsedContext(obj: unknown): obj is IChatUsedContext {
);
}
export interface IChatContentVariableReference {
variableName: string;
value?: URI | Location;
}
export interface IChatContentReference {
reference: URI | Location;
reference: URI | Location | IChatContentVariableReference;
kind: 'reference';
}

View file

@ -41,6 +41,7 @@ export interface IChatVariablesService {
_serviceBrand: undefined;
registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable;
hasVariable(name: string): boolean;
getVariable(name: string): IChatVariableData | undefined;
getVariables(): Iterable<Readonly<IChatVariableData>>;
getDynamicVariables(sessionId: string): ReadonlyArray<IDynamicVariable>; // should be its own service?

View file

@ -108,15 +108,22 @@ export class CodeBlockModelCollection extends Disposable {
return {
references: chat.contentReferences.map(ref => {
if (URI.isUri(ref.reference)) {
const uriOrLocation = 'variableName' in ref.reference ?
ref.reference.value :
ref.reference;
if (!uriOrLocation) {
return;
}
if (URI.isUri(uriOrLocation)) {
return {
uri: ref.reference.toJSON()
uri: uriOrLocation.toJSON()
};
}
return {
uri: ref.reference.uri.toJSON(),
range: ref.reference.range,
uri: uriOrLocation.uri.toJSON(),
range: uriOrLocation.range,
};
})
};

View file

@ -15,6 +15,10 @@ export class MockChatVariablesService implements IChatVariablesService {
throw new Error('Method not implemented.');
}
getVariable(name: string): IChatVariableData | undefined {
throw new Error('Method not implemented.');
}
hasVariable(name: string): boolean {
throw new Error('Method not implemented.');
}

View file

@ -392,7 +392,7 @@ declare module 'vscode' {
* @param value A uri or location
* @returns This stream.
*/
reference(value: Uri | Location): ChatResponseStream;
reference(value: Uri | Location | { variableName: string; value?: Uri | Location }): ChatResponseStream;
/**
* Pushes a part to this stream.
@ -430,8 +430,8 @@ declare module 'vscode' {
}
export class ChatResponseReferencePart {
value: Uri | Location;
constructor(value: Uri | Location);
value: Uri | Location | { variableName: string; value?: Uri | Location };
constructor(value: Uri | Location | { variableName: string; value?: Uri | Location });
}
export class ChatResponseCommandButtonPart {