mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-19 18:14:50 +01:00
Compute instructions references when making request, show why attachment was added (#251497)
* Compute intsructions references when making request, show why attachment was added * remove unused getMetadata
This commit is contained in:
@@ -8,9 +8,7 @@ import { Emitter } from '../../../../base/common/event.js';
|
||||
import { basename } from '../../../../base/common/resources.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IChatRequestFileEntry, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry } from '../common/chatVariableEntries.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ChatPromptAttachmentsCollection } from './chatAttachmentModel/chatPromptAttachments.js';
|
||||
import { IChatRequestFileEntry, IChatRequestVariableEntry, isPromptFileVariableEntry } from '../common/chatVariableEntries.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';
|
||||
@@ -28,22 +26,17 @@ export interface IChatAttachmentChangeEvent {
|
||||
|
||||
export class ChatAttachmentModel extends Disposable {
|
||||
|
||||
private readonly _promptInstructions: ChatPromptAttachmentsCollection;
|
||||
private readonly _attachments = new Map<string, IChatRequestVariableEntry>();
|
||||
|
||||
private _onDidChange = this._register(new Emitter<IChatAttachmentChangeEvent>());
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._promptInstructions = this._register(instaService.createInstance(ChatPromptAttachmentsCollection));
|
||||
this._register(this._promptInstructions.onUpdate(variable => this._onDidChange.fire({ updated: [variable], deleted: [], added: [] })));
|
||||
}
|
||||
|
||||
get attachments(): ReadonlyArray<IChatRequestVariableEntry> {
|
||||
@@ -75,23 +68,6 @@ export class ChatAttachmentModel extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
addPromptFiles(promptFiles: readonly URI[]): void {
|
||||
const variables = promptFiles.map(uri => toPromptFileVariableEntry(uri, true));
|
||||
this.addContext(...variables);
|
||||
}
|
||||
|
||||
hasPromptFiles(languageId: string): boolean {
|
||||
return this._promptInstructions.hasPromptFiles(languageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of all prompt instruction attachment variables, including all
|
||||
* nested child references of each attachment explicitly attached by user.
|
||||
*/
|
||||
getPromptFileVariables(): Promise<readonly IChatRequestVariableEntry[]> {
|
||||
return this._promptInstructions.getAttachments();
|
||||
}
|
||||
|
||||
addFolder(uri: URI) {
|
||||
this.addContext({
|
||||
kind: 'directory',
|
||||
@@ -105,7 +81,6 @@ export class ChatAttachmentModel extends Disposable {
|
||||
if (clearStickyAttachments) {
|
||||
const deleted = Array.from(this._attachments.keys());
|
||||
this._attachments.clear();
|
||||
this._promptInstructions.clear();
|
||||
this._onDidChange.fire({ deleted, added: [], updated: [] });
|
||||
} else {
|
||||
const deleted: string[] = [];
|
||||
@@ -114,6 +89,7 @@ export class ChatAttachmentModel extends Disposable {
|
||||
const entry = this._attachments.get(id);
|
||||
if (entry && !isPromptFileVariableEntry(entry)) {
|
||||
this._attachments.delete(id);
|
||||
deleted.push(id);
|
||||
}
|
||||
}
|
||||
this._onDidChange.fire({ deleted, added: [], updated: [] });
|
||||
@@ -143,9 +119,6 @@ export class ChatAttachmentModel extends Disposable {
|
||||
if (item) {
|
||||
this._attachments.delete(id);
|
||||
deleted.push(id);
|
||||
if (isPromptFileVariableEntry(item)) {
|
||||
this._promptInstructions.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +127,6 @@ export class ChatAttachmentModel extends Disposable {
|
||||
if (!oldItem) {
|
||||
this._attachments.set(item.id, item);
|
||||
added.push(item);
|
||||
if (isPromptFileVariableEntry(item)) {
|
||||
this._promptInstructions.add([item]);
|
||||
}
|
||||
} else if (!equals(oldItem, item)) {
|
||||
this._attachments.set(item.id, item);
|
||||
updated.push(item);
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, createCancelablePromise } from '../../../../../base/common/async.js';
|
||||
import { Emitter } from '../../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { ResourceMap } from '../../../../../base/common/map.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { IChatRequestVariableEntry, IPromptFileVariableEntry, toPromptFileVariableEntry } from '../../common/chatVariableEntries.js';
|
||||
import { IPromptParserResult, IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
|
||||
|
||||
/**
|
||||
* Model for a collection of prompt instruction attachments.
|
||||
* Starts
|
||||
*/
|
||||
export class ChatPromptAttachmentsCollection extends Disposable {
|
||||
/**
|
||||
* Event that fires then this model is updated.
|
||||
*
|
||||
* See {@linkcode onUpdate}.
|
||||
*/
|
||||
protected _onUpdate = this._register(new Emitter<IPromptFileVariableEntry>());
|
||||
|
||||
/**
|
||||
* Subscribe to the `onUpdate` event.
|
||||
*/
|
||||
public onUpdate = this._onUpdate.event;
|
||||
|
||||
/**
|
||||
* List of all prompt instruction attachments.
|
||||
*/
|
||||
private _attachments = new ResourceMap<CancelablePromise<IPromptParserResult>>();
|
||||
|
||||
|
||||
constructor(
|
||||
@ILanguageService private readonly languageService: ILanguageService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IPromptsService private readonly promptsService: IPromptsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any of the attachments is a prompt file.
|
||||
*/
|
||||
public hasPromptFiles(promptFileLanguageId: string): boolean {
|
||||
const hasLanguage = (uri: URI) => {
|
||||
const model = this.modelService.getModel(uri);
|
||||
const languageId = model ? model.getLanguageId() : this.languageService.guessLanguageIdByFilepathOrFirstLine(uri);
|
||||
return languageId === promptFileLanguageId;
|
||||
};
|
||||
|
||||
for (const uri of this._attachments.keys()) {
|
||||
if (hasLanguage(uri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of all prompt instruction attachment variables, including all
|
||||
* nested child references of each attachment explicitly attached by user.
|
||||
*/
|
||||
public async getAttachments(): Promise<readonly IChatRequestVariableEntry[]> {
|
||||
const result = [];
|
||||
const attachments = [...this._attachments.values()];
|
||||
|
||||
for (const parseResultPromise of attachments) {
|
||||
const parseResult = await parseResultPromise;
|
||||
|
||||
// the usual URIs list of prompt instructions is `bottom-up`, therefore
|
||||
// we do the same here - first add all child references of the model
|
||||
for (const uri of parseResult.allValidReferences) {
|
||||
result.push(toPromptFileVariableEntry(uri, false));
|
||||
}
|
||||
|
||||
// then add the root reference of the model itself
|
||||
result.push(toPromptFileVariableEntry(parseResult.uri, true));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add prompt instruction attachment instances
|
||||
*/
|
||||
public add(entries: IPromptFileVariableEntry[]) {
|
||||
for (const entry of entries) {
|
||||
const uri = entry.value;
|
||||
|
||||
// if already exists, nothing to do
|
||||
if (this._attachments.has(uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parseResult = createCancelablePromise(token => this.promptsService.parse(uri, token));
|
||||
parseResult.then(() => {
|
||||
this._onUpdate.fire(entry);
|
||||
}).catch((error) => {
|
||||
// if parsing fails, we still create an attachment model
|
||||
// to allow the user to see the error and fix it
|
||||
console.error(`Failed to parse prompt file ${uri.toString()}:`, error);
|
||||
});
|
||||
this._attachments.set(uri, parseResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a prompt instruction attachment instancs
|
||||
*/
|
||||
public remove(entry: IPromptFileVariableEntry): this {
|
||||
const uri = entry.value;
|
||||
|
||||
const attachment = this._attachments.get(uri);
|
||||
if (attachment) {
|
||||
this._attachments.delete(uri);
|
||||
attachment.cancel();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all prompt instruction attachments.
|
||||
*/
|
||||
public clear(): this {
|
||||
for (const attachment of this._attachments.values()) {
|
||||
attachment.cancel();
|
||||
}
|
||||
this._attachments.clear();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
this.clear(); // disposes of all attachments
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener
|
||||
import { IThemeService, FolderThemeIcon } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IResourceLabel, ResourceLabels, IFileLabelOptions } from '../../../browser/labels.js';
|
||||
import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js';
|
||||
import { IChatRequestPasteVariableEntry, IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, ISCMHistoryItemVariableEntry, OmittedState } from '../common/chatVariableEntries.js';
|
||||
import { IChatRequestPasteVariableEntry, IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, ISCMHistoryItemVariableEntry, OmittedState } from '../common/chatVariableEntries.js';
|
||||
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';
|
||||
import { KeyCode } from '../../../../base/common/keyCodes.js';
|
||||
import { basename, dirname } from '../../../../base/common/path.js';
|
||||
@@ -54,6 +54,8 @@ import { getHistoryItemEditorTitle, getHistoryItemHoverContent } from '../../scm
|
||||
import { ILanguageModelToolsService, ToolSet } from '../common/languageModelToolsService.js';
|
||||
import { Iterable } from '../../../../base/common/iterator.js';
|
||||
import { getCleanPromptName } from '../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { IPromptsService } from '../common/promptSyntax/service/promptsService.js';
|
||||
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
|
||||
|
||||
abstract class AbstractChatAttachmentWidget extends Disposable {
|
||||
public readonly element: HTMLElement;
|
||||
@@ -481,7 +483,7 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
|
||||
|
||||
constructor(
|
||||
resource: URI,
|
||||
attachment: IChatRequestVariableEntry,
|
||||
attachment: IPromptFileVariableEntry,
|
||||
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
|
||||
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
|
||||
container: HTMLElement,
|
||||
@@ -490,7 +492,7 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@ILanguageService private readonly languageService: ILanguageService,
|
||||
@IPromptsService private readonly promptService: IPromptsService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);
|
||||
@@ -498,7 +500,7 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
|
||||
|
||||
this.hintElement = dom.append(this.element, dom.$('span.prompt-type'));
|
||||
|
||||
this.updateLabel(resource);
|
||||
this.updateLabel(attachment);
|
||||
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));
|
||||
@@ -508,11 +510,12 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
|
||||
this.attachClearButton();
|
||||
}
|
||||
|
||||
private updateLabel(resource: URI) {
|
||||
private updateLabel(attachment: IPromptFileVariableEntry) {
|
||||
const resource = attachment.value;
|
||||
const fileBasename = basename(resource.path);
|
||||
const fileDirname = dirname(resource.path);
|
||||
const friendlyName = `${fileBasename} ${fileDirname}`;
|
||||
const isPrompt = this.languageService.guessLanguageIdByFilepathOrFirstLine(resource) === 'prompt';
|
||||
const isPrompt = this.promptService.getPromptFileType(resource) === PromptsType.prompt;
|
||||
const ariaLabel = isPrompt
|
||||
? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName)
|
||||
: localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName);
|
||||
@@ -520,8 +523,7 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
|
||||
? localize('prompt', "Prompt")
|
||||
: localize('instructions', "Instructions");
|
||||
|
||||
const uriLabel = this.labelService.getUriLabel(resource, { relative: true });
|
||||
const title = `${typeLabel} ${uriLabel}`;
|
||||
const title = this.labelService.getUriLabel(resource) + (attachment.originLabel ? `\n${attachment.originLabel}` : '');
|
||||
|
||||
//const { topError } = this.promptFile;
|
||||
this.element.classList.remove('warning', 'error');
|
||||
|
||||
@@ -101,7 +101,8 @@ import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js';
|
||||
import { resizeImage } from './imageUtils.js';
|
||||
import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js';
|
||||
import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js';
|
||||
import { PROMPT_LANGUAGE_ID } from '../common/promptSyntax/promptTypes.js';
|
||||
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
|
||||
import { IPromptsService } from '../common/promptSyntax/service/promptsService.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -163,18 +164,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
|
||||
readonly selectedToolsModel: ChatSelectedTools;
|
||||
|
||||
public async getAttachedAndImplicitContext(sessionId: string): Promise<ChatRequestVariableSet> {
|
||||
public getAttachedAndImplicitContext(sessionId: string): ChatRequestVariableSet {
|
||||
|
||||
const contextArr = new ChatRequestVariableSet();
|
||||
|
||||
// get prompt file variables (instructions) and all rereferenced contents first
|
||||
contextArr.add(... await this.attachmentModel.getPromptFileVariables());
|
||||
|
||||
// then add all other attachments (includes prompt file variables, but they will be ignored if already added)
|
||||
contextArr.add(...this.attachmentModel.attachments);
|
||||
|
||||
if (this.implicitContext?.enabled && this.implicitContext.value) {
|
||||
const implicitChatVariables = await this.implicitContext.toBaseEntries();
|
||||
const implicitChatVariables = this.implicitContext.toBaseEntries();
|
||||
contextArr.add(...implicitChatVariables);
|
||||
}
|
||||
return contextArr;
|
||||
@@ -184,8 +181,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
* Check if the chat input part has any prompt file attachments.
|
||||
*/
|
||||
get hasPromptFileAttachments(): boolean {
|
||||
// if prompt attached explicitly as a "prompt" attachment
|
||||
return this._attachmentModel.hasPromptFiles(PROMPT_LANGUAGE_ID);
|
||||
return this._attachmentModel.attachments.some(entry => {
|
||||
return isPromptFileVariableEntry(entry) && entry.isRoot && this.promptsService.getPromptFileType(entry.value) === PromptsType.prompt;
|
||||
});
|
||||
}
|
||||
|
||||
private _indexOfLastAttachedContextDeletedWithKeyboard: number;
|
||||
@@ -361,6 +359,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
@IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService,
|
||||
@IChatEntitlementService private readonly entitlementService: IChatEntitlementService,
|
||||
@IChatModeService private readonly chatModeService: IChatModeService,
|
||||
@IPromptsService private readonly promptsService: IPromptsService,
|
||||
) {
|
||||
super();
|
||||
this._onDidLoadInputState = this._register(new Emitter<any>());
|
||||
|
||||
@@ -54,7 +54,7 @@ import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'
|
||||
import { ChatAgentLocation, ChatMode } from '../common/constants.js';
|
||||
import { ILanguageModelToolsService, IToolData, ToolSet } from '../common/languageModelToolsService.js';
|
||||
import { type TPromptMetadata } from '../common/promptSyntax/parsers/promptHeader/promptHeader.js';
|
||||
import { IMetadata, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
|
||||
import { IPromptParserResult, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
|
||||
import { handleModeSwitch } from './actions/chatActions.js';
|
||||
import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js';
|
||||
import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js';
|
||||
@@ -69,6 +69,7 @@ import { ChatViewWelcomePart } from './viewsWelcome/chatViewWelcomeController.js
|
||||
import { MicrotaskDelay } from '../../../../base/common/symbols.js';
|
||||
import { IChatRequestVariableEntry, ChatRequestVariableSet as ChatRequestVariableSet, isPromptFileVariableEntry, toPromptFileVariableEntry } from '../common/chatVariableEntries.js';
|
||||
import { PromptsConfig } from '../common/promptSyntax/config/config.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -1207,24 +1208,24 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
|
||||
private _findPromptFileInContext(attachedContext: ChatRequestVariableSet): URI | undefined {
|
||||
for (const item of attachedContext.asArray()) {
|
||||
if (isPromptFileVariableEntry(item) && item.isRoot) {
|
||||
if (isPromptFileVariableEntry(item) && item.isRoot && this.promptsService.getPromptFileType(item.value) === PromptsType.prompt) {
|
||||
return IChatRequestVariableEntry.toUri(item);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _applyPromptFileIfSet(requestInput: { input: string; attachedContext: ChatRequestVariableSet }): Promise<IMetadata | undefined> {
|
||||
private async _applyPromptFileIfSet(requestInput: { input: string; attachedContext: ChatRequestVariableSet }): Promise<IPromptParserResult | undefined> {
|
||||
|
||||
let metadata: IMetadata | undefined;
|
||||
let parseResult: IPromptParserResult | undefined;
|
||||
|
||||
// first check if the input has a prompt slash command
|
||||
const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart);
|
||||
if (agentSlashPromptPart) {
|
||||
metadata = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand);
|
||||
if (metadata) {
|
||||
parseResult = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand, CancellationToken.None);
|
||||
if (parseResult) {
|
||||
// add the prompt file to the context, but not sticky
|
||||
requestInput.attachedContext.add(toPromptFileVariableEntry(metadata.uri, true));
|
||||
requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, true));
|
||||
|
||||
// remove the slash command from the input
|
||||
requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim();
|
||||
@@ -1233,25 +1234,25 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
// if not, check if the context contains a prompt file: This is the old workflow that we still support for legacy reasons
|
||||
const uri = this._findPromptFileInContext(requestInput.attachedContext);
|
||||
if (uri) {
|
||||
metadata = await this.promptsService.getMetadata(uri);
|
||||
parseResult = await this.promptsService.parse(uri, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
if (!parseResult) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!requestInput.input.trim()) {
|
||||
// NOTE this is a prompt and therefore not localized
|
||||
requestInput.input = `Follow instructions from ${basename(metadata.uri)}`;
|
||||
requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()} )`;
|
||||
}
|
||||
|
||||
const meta = metadata.metadata;
|
||||
const meta = parseResult.metadata;
|
||||
if (meta?.promptType === PromptsType.prompt) {
|
||||
await this._applyPromptMetadata(meta);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
return parseResult;
|
||||
}
|
||||
|
||||
private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {
|
||||
@@ -1277,15 +1278,17 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
const requestId = this.chatAccessibilityService.acceptRequest();
|
||||
const requestInputs = {
|
||||
input: !query ? editorValue : query.query,
|
||||
attachedContext: await this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId),
|
||||
attachedContext: this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId),
|
||||
};
|
||||
|
||||
const isUserQuery = !query;
|
||||
|
||||
const instructionsEnabled = PromptsConfig.enabled(this.configurationService);
|
||||
if (instructionsEnabled) {
|
||||
// process the prompt command
|
||||
await this._applyPromptFileIfSet(requestInputs);
|
||||
await this.autoAttachInstructions(requestInputs.attachedContext);
|
||||
await this._autoAttachInstructions(requestInputs.attachedContext);
|
||||
await this._collectReferencedInstructions(requestInputs.attachedContext);
|
||||
}
|
||||
|
||||
if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) {
|
||||
@@ -1614,22 +1617,46 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
this.inputPart.selectedToolsModel.enable(enabledToolSets, enabledTools, true);
|
||||
}
|
||||
|
||||
private async _collectReferencedInstructions(attachedContext: ChatRequestVariableSet): Promise<void> {
|
||||
for (const variable of attachedContext.asArray()) {
|
||||
if (isPromptFileVariableEntry(variable)) {
|
||||
const result = await this.promptsService.parse(variable.value, CancellationToken.None);
|
||||
for (const ref of result.allValidReferences) {
|
||||
const reason = localize('instruction.file.reason.referenced', 'Referenced by {0}', basename(variable.value));
|
||||
attachedContext.add(toPromptFileVariableEntry(ref, true, reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves instructions that have `include` metadata that can
|
||||
* Resolves instructions that have `applyTo` metadata that can
|
||||
* match file references in the attached context and then attaches
|
||||
* such instructions to the context.
|
||||
*/
|
||||
private async autoAttachInstructions(attachedContext: ChatRequestVariableSet): Promise<void> {
|
||||
private async _autoAttachInstructions(attachedContext: ChatRequestVariableSet): Promise<void> {
|
||||
const existingInstructions = new ResourceSet();
|
||||
const fileInContext = [];
|
||||
|
||||
const variableUris = attachedContext.asArray().map(IChatRequestVariableEntry.toUri).filter(isDefined);
|
||||
for (const variable of attachedContext.asArray()) {
|
||||
if (isPromptFileVariableEntry(variable)) {
|
||||
existingInstructions.add(variable.value);
|
||||
} else {
|
||||
const uri = IChatRequestVariableEntry.toUri(variable);
|
||||
if (uri) {
|
||||
fileInContext.push(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const automaticInstructions = await this.promptsService.findInstructionFilesFor(variableUris);
|
||||
const automaticInstructions = await this.promptsService.findInstructionFilesFor(fileInContext, existingInstructions);
|
||||
const promptVariableEntries = automaticInstructions.map(instruction => toPromptFileVariableEntry(instruction.uri, true, instruction.reason));
|
||||
|
||||
// add instructions to the final context list
|
||||
attachedContext.add(...automaticInstructions.map(instruction => toPromptFileVariableEntry(instruction, true)));
|
||||
attachedContext.add(...promptVariableEntries);
|
||||
|
||||
// add to attached list to make the instructions sticky
|
||||
this.inputPart.attachmentModel.addPromptFiles(automaticInstructions);
|
||||
this.inputPart.attachmentModel.addContext(...promptVariableEntries);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli
|
||||
this._onDidChangeValue.fire();
|
||||
}
|
||||
|
||||
public async toBaseEntries(): Promise<readonly IChatRequestVariableEntry[]> {
|
||||
public toBaseEntries(): IChatRequestVariableEntry[] {
|
||||
return [{
|
||||
kind: 'file',
|
||||
id: this.id,
|
||||
|
||||
@@ -118,7 +118,7 @@ class AttachInstructionsAction extends Action2 {
|
||||
}
|
||||
|
||||
if (skipSelectionDialog && resource) {
|
||||
widget.attachmentModel.addPromptFiles([resource]);
|
||||
widget.attachmentModel.addContext(toPromptFileVariableEntry(resource, true));
|
||||
widget.focusInput();
|
||||
return;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ class AttachInstructionsAction extends Action2 {
|
||||
const result = await pickers.selectPromptFile({ resource, placeholder, type: PromptsType.instructions });
|
||||
|
||||
if (result !== undefined) {
|
||||
widget.attachmentModel.addPromptFiles([result.promptFile]);
|
||||
widget.attachmentModel.addContext(toPromptFileVariableEntry(result.promptFile, true));
|
||||
widget.focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry
|
||||
readonly kind: 'promptFile';
|
||||
readonly value: URI;
|
||||
readonly isRoot: boolean;
|
||||
readonly originLabel?: string;
|
||||
readonly modelDescription: string;
|
||||
}
|
||||
|
||||
@@ -272,7 +273,7 @@ export function isSCMHistoryItemVariableEntry(obj: IChatRequestVariableEntry): o
|
||||
* @param isRoot If the reference is the root reference in the references tree.
|
||||
* This object most likely was explicitly attached by the user.
|
||||
*/
|
||||
export function toPromptFileVariableEntry(uri: URI, isRoot: boolean): IPromptFileVariableEntry {
|
||||
export function toPromptFileVariableEntry(uri: URI, isRoot: boolean, originLabel?: string): IPromptFileVariableEntry {
|
||||
return {
|
||||
// `id` for all `prompt files` starts with the well-defined part that the copilot extension(or other chatbot) can rely on
|
||||
id: `vscode.prompt.instructions${isRoot ? '.root' : ''}}__${uri.toString()}`,
|
||||
@@ -281,29 +282,41 @@ export function toPromptFileVariableEntry(uri: URI, isRoot: boolean): IPromptFil
|
||||
kind: 'promptFile',
|
||||
modelDescription: 'Prompt instructions file',
|
||||
isRoot,
|
||||
originLabel,
|
||||
};
|
||||
}
|
||||
|
||||
export class ChatRequestVariableSet {
|
||||
private _entries = new Map<string, IChatRequestVariableEntry>();
|
||||
private _ids = new Set<string>();
|
||||
private _entries: IChatRequestVariableEntry[] = [];
|
||||
|
||||
|
||||
public add(...entry: IChatRequestVariableEntry[]): void {
|
||||
for (const e of entry) {
|
||||
if (!this._entries.has(e.id)) {
|
||||
this._entries.set(e.id, e);
|
||||
if (!this._ids.has(e.id)) {
|
||||
this._ids.add(e.id);
|
||||
this._entries.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public insertFirst(entry: IChatRequestVariableEntry): void {
|
||||
if (!this._ids.has(entry.id)) {
|
||||
this._ids.add(entry.id);
|
||||
this._entries.unshift(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public remove(entry: IChatRequestVariableEntry): void {
|
||||
this._entries.delete(entry.id);
|
||||
this._ids.delete(entry.id);
|
||||
this._entries = this._entries.filter(e => e.id !== entry.id);
|
||||
}
|
||||
|
||||
public has(entry: IChatRequestVariableEntry): boolean {
|
||||
return this._entries.has(entry.id);
|
||||
return this._ids.has(entry.id);
|
||||
}
|
||||
|
||||
public asArray(): IChatRequestVariableEntry[] {
|
||||
return Array.from(this._entries.values());
|
||||
return this._entries.slice(0); // return a copy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TTree } from '../utils/treeUtils.js';
|
||||
import { ChatMode } from '../../constants.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { Event } from '../../../../../../base/common/event.js';
|
||||
@@ -15,6 +14,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js
|
||||
import { PromptsType } from '../promptTypes.js';
|
||||
import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITopError } from '../parsers/types.js';
|
||||
import { ResourceSet } from '../../../../../../base/common/map.js';
|
||||
|
||||
/**
|
||||
* Provides prompt services.
|
||||
@@ -71,7 +71,7 @@ export interface IMetadata {
|
||||
/**
|
||||
* List of metadata for each valid child prompt reference.
|
||||
*/
|
||||
readonly children?: readonly TTree<IMetadata | null>[];
|
||||
readonly children?: readonly IMetadata[];
|
||||
}
|
||||
|
||||
export interface ICustomChatMode {
|
||||
@@ -173,7 +173,7 @@ export interface IPromptsService extends IDisposable {
|
||||
/**
|
||||
* Gets the prompt file for a slash command.
|
||||
*/
|
||||
resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise<IMetadata | undefined>;
|
||||
resolvePromptSlashCommand(data: IChatPromptSlashCommand, _token: CancellationToken): Promise<IPromptParserResult | undefined>;
|
||||
|
||||
/**
|
||||
* Returns a prompt command if the command name is valid.
|
||||
@@ -184,7 +184,7 @@ export interface IPromptsService extends IDisposable {
|
||||
* Find all instruction files which have a glob pattern in their
|
||||
* 'applyTo' metadata record that match the provided list of files.
|
||||
*/
|
||||
findInstructionFilesFor(fileUris: readonly URI[]): Promise<readonly URI[]>;
|
||||
findInstructionFilesFor(fileUris: readonly URI[], ignoreInstructions?: ResourceSet): Promise<readonly { uri: URI; reason: string }[]>;
|
||||
|
||||
/**
|
||||
* Event that is triggered when the list of custom chat modes changes.
|
||||
@@ -196,11 +196,6 @@ export interface IPromptsService extends IDisposable {
|
||||
*/
|
||||
getCustomChatModes(token: CancellationToken): Promise<readonly ICustomChatMode[]>;
|
||||
|
||||
/**
|
||||
* Gets the metadata for the given prompt file uri.
|
||||
*/
|
||||
getMetadata(promptFileUri: URI): Promise<IMetadata>;
|
||||
|
||||
/**
|
||||
* Get all metadata for entire prompt references tree
|
||||
* that spans out of each of the provided files.
|
||||
@@ -216,6 +211,12 @@ export interface IPromptsService extends IDisposable {
|
||||
* @param uris
|
||||
*/
|
||||
parse(uri: URI, token: CancellationToken): Promise<IPromptParserResult>;
|
||||
|
||||
/**
|
||||
* Returns the prompt file type for the given URI.
|
||||
* @param resource the URI of the resource
|
||||
*/
|
||||
getPromptFileType(resource: URI): PromptsType | undefined;
|
||||
}
|
||||
|
||||
export interface IChatPromptSlashCommand {
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { flatten } from '../utils/treeUtils.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { isValidPromptType, PROMPT_LANGUAGE_ID, PromptsType } from '../promptTypes.js';
|
||||
import { getPromptsTypeForLanguageId, isValidPromptType, PROMPT_LANGUAGE_ID, PromptsType } from '../promptTypes.js';
|
||||
import { PromptParser } from '../parsers/promptParser.js';
|
||||
import { match, splitGlobAware } from '../../../../../../base/common/glob.js';
|
||||
import { type URI } from '../../../../../../base/common/uri.js';
|
||||
@@ -27,6 +26,7 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/
|
||||
import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js';
|
||||
import type { IChatPromptSlashCommand, ICustomChatMode, IMetadata, IPromptParserResult, IPromptPath, IPromptsService, TPromptsStorage } from './promptsService.js';
|
||||
import { getCleanPromptName, PROMPT_FILE_EXTENSION } from '../config/promptFileLocations.js';
|
||||
import { ILanguageService } from '../../../../../../editor/common/languages/language.js';
|
||||
|
||||
/**
|
||||
* Provides prompt services.
|
||||
@@ -56,6 +56,7 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IUserDataProfileService private readonly userDataService: IUserDataProfileService,
|
||||
@ILanguageService private readonly languageService: ILanguageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -102,6 +103,12 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
return this.onDidChangeCustomChatModesEvent;
|
||||
}
|
||||
|
||||
public getPromptFileType(uri: URI): PromptsType | undefined {
|
||||
const model = this.modelService.getModel(uri);
|
||||
const languageId = model ? model.getLanguageId() : this.languageService.guessLanguageIdByFilepathOrFirstLine(uri);
|
||||
return languageId ? getPromptsTypeForLanguageId(languageId) : undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws {Error} if:
|
||||
@@ -152,12 +159,12 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise<IMetadata | undefined> {
|
||||
public async resolvePromptSlashCommand(data: IChatPromptSlashCommand, token: CancellationToken): Promise<IPromptParserResult | undefined> {
|
||||
const promptUri = await this.getPromptPath(data);
|
||||
if (!promptUri) {
|
||||
return undefined;
|
||||
}
|
||||
return await this.getMetadata(promptUri);
|
||||
return await this.parse(promptUri, token);
|
||||
}
|
||||
|
||||
private async getPromptPath(data: IChatPromptSlashCommand): Promise<URI | undefined> {
|
||||
@@ -248,31 +255,34 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
}
|
||||
|
||||
|
||||
public async findInstructionFilesFor(files: readonly URI[]): Promise<readonly URI[]> {
|
||||
public async findInstructionFilesFor(files: readonly URI[], ignoreInstructions?: ResourceSet): Promise<readonly { uri: URI; reason: string }[]> {
|
||||
const instructionFiles = await this.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
if (instructionFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const instructions = await this.getAllMetadata(
|
||||
instructionFiles.map(file => file.uri),
|
||||
);
|
||||
|
||||
const result: { uri: URI; reason: string }[] = [];
|
||||
const foundFiles = new ResourceSet();
|
||||
for (const instruction of instructions.flatMap(flatten)) {
|
||||
const { metadata, uri } = instruction;
|
||||
for (const instructionFile of instructionFiles) {
|
||||
const { metadata, uri } = await this.parse(instructionFile.uri, CancellationToken.None);
|
||||
|
||||
if (metadata?.promptType !== PromptsType.instructions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoreInstructions?.has(uri) || foundFiles.has(uri)) {
|
||||
// the instruction file is already part of the input or has already been processed
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const { applyTo } = metadata;
|
||||
if (applyTo === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const patterns = splitGlobAware(applyTo, ',');
|
||||
const patterMatches = (pattern: string) => {
|
||||
const patterMatches = (pattern: string): URI | true | false => {
|
||||
pattern = pattern.trim();
|
||||
if (pattern.length === 0) {
|
||||
// if glob pattern is empty, skip it
|
||||
@@ -293,22 +303,29 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
for (const file of files) {
|
||||
// if the file is not a valid URI, skip it
|
||||
if (match(pattern, file.path)) {
|
||||
return true;
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (patterns.some(patterMatches)) {
|
||||
foundFiles.add(uri);
|
||||
}
|
||||
}
|
||||
return [...foundFiles];
|
||||
}
|
||||
|
||||
public async getMetadata(promptFileUri: URI): Promise<IMetadata> {
|
||||
const metaDatas = await this.getAllMetadata([promptFileUri]);
|
||||
return metaDatas[0];
|
||||
for (const pattern of patterns) {
|
||||
const matchResult = patterMatches(pattern);
|
||||
if (matchResult !== false) {
|
||||
const reason = matchResult === true ?
|
||||
localize('instruction.file.reason.allFiles', 'Automatically attached as pattern is **') :
|
||||
localize('instruction.file.reason.specificFile', 'Automatically attached as pattern {0} matches {1}', applyTo, this.labelService.getUriLabel(matchResult, { relative: true }));
|
||||
|
||||
result.push({ uri, reason });
|
||||
foundFiles.add(uri);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getAllMetadata(promptUris: readonly URI[]): Promise<IMetadata[]> {
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { assert } from '../../../../../../base/common/assert.js';
|
||||
|
||||
/**
|
||||
* Type for a generic tree node.
|
||||
*/
|
||||
export type TTree<TTreenNode> = { children?: readonly TTree<TTreenNode>[] } & TTreenNode;
|
||||
|
||||
/**
|
||||
* Flatter a tree structure into a single flat array.
|
||||
*/
|
||||
export function flatten<TTreeNode>(treeRoot: TTree<TTreeNode>): TTreeNode[] {
|
||||
const result: TTreeNode[] = [];
|
||||
|
||||
result.push(treeRoot);
|
||||
|
||||
for (const child of treeRoot.children ?? []) {
|
||||
result.push(...flatten(child));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse a tree structure and execute a callback for each node.
|
||||
*/
|
||||
export function forEach<TTreeNode>(callback: (node: TTreeNode) => boolean, treeRoot: TTree<TTreeNode>): ReturnType<typeof callback> {
|
||||
const shouldStop = callback(treeRoot);
|
||||
|
||||
if (shouldStop === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const child of treeRoot.children ?? []) {
|
||||
const childShouldStop = forEach(callback, child);
|
||||
|
||||
if (childShouldStop === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps nodes of a tree to a new type preserving the original tree structure by invoking
|
||||
* the provided callback function for each node.
|
||||
*
|
||||
* @param callback Function to map each of the nodes in the tree. The callback receives the original
|
||||
* readonly tree node and a list of its already-mapped readonly children and expected
|
||||
* to return a new tree node object. If the new object does not have an explicit
|
||||
* `children` property set (e.g., set to `undefined` or an array), the utility will
|
||||
* automatically set the `children` property to the `new mapped children` for you,
|
||||
* otherwise the set `children` property is preserved. Likewise, if the callback
|
||||
* modifies the `newChildren` array directly, but doesn't explicitly set the `children`
|
||||
* property on the returned object, the modification to the `newChildren` array are
|
||||
* preserved in the resulting object.
|
||||
*
|
||||
* @param treeRoot The root node of the tree to be mapped.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```typescript
|
||||
* const tree = {
|
||||
* id: '1',
|
||||
* children: [
|
||||
* { id: '1.1' },
|
||||
* { id: '1.2' },
|
||||
* };
|
||||
*
|
||||
* const newTree = map((node, _newChildren) => {
|
||||
* return {
|
||||
* name: `name-of-${node.id}`,
|
||||
* };
|
||||
* }, tree);
|
||||
*
|
||||
* assert.deepStrictEqual(newTree, {
|
||||
* name: 'name-of-1',
|
||||
* children: [
|
||||
* { name: 'name-of-1.1' },
|
||||
* { name: 'name-of-1.2' },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function map<
|
||||
TTreeNode extends object,
|
||||
TNewTreeNode extends object
|
||||
>(
|
||||
callback: (
|
||||
originalNode: Readonly<TTree<TTreeNode>>,
|
||||
newChildren: Readonly<TNewTreeNode>[] | undefined,
|
||||
) => TTree<TNewTreeNode>,
|
||||
treeRoot: TTree<TTreeNode>,
|
||||
): TTree<TNewTreeNode> {
|
||||
// if the node does not have children, just call the callback
|
||||
if (treeRoot.children === undefined) {
|
||||
return callback(treeRoot, undefined);
|
||||
}
|
||||
|
||||
// otherwise process all the children recursively first
|
||||
const newChildren = treeRoot.children
|
||||
.map(curry(map, callback));
|
||||
|
||||
// then run the callback with the new children
|
||||
const newNode = callback(treeRoot, newChildren);
|
||||
|
||||
// if user explicitly set the children, preserve the value
|
||||
if ('children' in newNode) {
|
||||
return newNode;
|
||||
}
|
||||
|
||||
// otherwise if no children is explicitly set,
|
||||
// use the new children array instead
|
||||
newNode.children = newChildren;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for a generic comparable object - the one that implements
|
||||
* the `equals` method that allows to compare it with similar objects.
|
||||
*/
|
||||
type TComparable<T> = T & { equals: (other: T) => boolean };
|
||||
|
||||
/**
|
||||
* Type for a diff object that represents a difference between
|
||||
* a pair of objects. See {@link difference} utility and related
|
||||
* {@link TDifference} for more info.
|
||||
*/
|
||||
type TDiff<TObject1, TObject2> = {
|
||||
/**
|
||||
* Reference to the first object that was used during
|
||||
* comparison. Equal to an object of the first tree
|
||||
* parameter passed to {@link difference}. When set to
|
||||
* `null`, then the object was missing in the first tree,
|
||||
* was but present in the second tree.
|
||||
*/
|
||||
readonly object1: TObject1;
|
||||
|
||||
/**
|
||||
* Reference to the second object that was used during
|
||||
* comparison. Equal to an object of the second tree
|
||||
* parameter passed to {@link difference}. When set to
|
||||
* `null`, then the object was missing in the second tree,
|
||||
* was but present in the first tree.
|
||||
*/
|
||||
readonly object2: TObject2;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for a diff object that represents a difference between
|
||||
* a pair of objects of the same type.
|
||||
* See {@link difference} utility for more info.
|
||||
*
|
||||
* The type is on-purpose constrained as only one of the object
|
||||
* references can have the `null` reference but never both of
|
||||
* them at the same time. This is due to the fact that two `null`
|
||||
* values would indicate that both objects were missing during
|
||||
* comparison, which does not make sense in this context.
|
||||
*/
|
||||
type TDifference<T> = TDiff<T, T | null> | TDiff<T | null, T>;
|
||||
|
||||
/**
|
||||
* Type for a tree of differences between two trees.
|
||||
* See {@link difference} utility for more info.
|
||||
*/
|
||||
type TDiffTree<T> = TTree<TDifference<T> & {
|
||||
/**
|
||||
* Index inside the parent's tree node 'children' array
|
||||
* reflecting the position of the object pair that was
|
||||
* compared. Always equal to `0` for a difference at
|
||||
* the root node level of a tree.
|
||||
*/
|
||||
readonly index: number;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Utility to find a difference between two provided trees
|
||||
* of the same type. The result is another tree of difference
|
||||
* nodes that represent difference between tree node pairs.
|
||||
*/
|
||||
export function difference<T extends NonNullable<unknown>>(tree1: TTree<TComparable<T>>, tree2: TTree<TComparable<T>>): TDiffTree<T> | null {
|
||||
const tree1Children = tree1.children ?? [];
|
||||
const tree2Children = tree2.children ?? [];
|
||||
|
||||
// if there are no children in the both trees left anymore,
|
||||
// compare the nodes directly themselves and return the result
|
||||
if (tree1Children.length === 0 && tree2Children.length === 0) {
|
||||
if (tree1.equals(tree2)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
index: 0,
|
||||
object1: tree1,
|
||||
object2: tree2,
|
||||
};
|
||||
}
|
||||
|
||||
// with children present, iterate over them to find difference for each pair
|
||||
const maxChildren = Math.max(tree1Children.length, tree2Children.length);
|
||||
const children: TDiffTree<T>[] = [];
|
||||
for (let i = 0; i < maxChildren; i++) {
|
||||
const child1 = tree1Children[i];
|
||||
const child2 = tree2Children[i];
|
||||
|
||||
// sanity check to ensure that at least one of the children is defined
|
||||
// as otherwise this case most likely indicates a logic error or a bug
|
||||
assert(
|
||||
(child1 !== undefined) || (child2 !== undefined),
|
||||
'At least one of the children must be defined.',
|
||||
);
|
||||
|
||||
// if one of the children is missing, report it as a difference
|
||||
if ((child1 === undefined) || (child2 === undefined)) {
|
||||
children.push({
|
||||
index: i,
|
||||
object1: child1 ?? null,
|
||||
object2: child2 ?? null,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const diff = difference(child1, child2);
|
||||
if (diff === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
children.push({
|
||||
...diff,
|
||||
index: i,
|
||||
});
|
||||
}
|
||||
|
||||
// if there some children that are different, report them
|
||||
if (children.length !== 0) {
|
||||
return {
|
||||
index: 0,
|
||||
object1: tree1,
|
||||
object2: tree2,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
// there is no children difference, nor differences in the nodes
|
||||
// themselves, hence return explicit `null` value to indicate that
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for a rest parameters of function, excluding
|
||||
* the first argument.
|
||||
*/
|
||||
type TRestParameters<T extends (...args: any[]) => unknown> =
|
||||
T extends (first: Parameters<T>[0], ...rest: infer R) => unknown ? R : never;
|
||||
|
||||
/**
|
||||
* Type for a curried function.
|
||||
* See {@link curry} for more info.
|
||||
*/
|
||||
type TCurriedFunction<T extends (...args: any[]) => unknown> = ((...args: TRestParameters<T>) => ReturnType<T>);
|
||||
|
||||
/**
|
||||
* Curry a provided function with the first argument.
|
||||
*/
|
||||
export function curry<T, K>(
|
||||
callback: (arg1: T, ...args: any[]) => K,
|
||||
arg1: T,
|
||||
): TCurriedFunction<typeof callback> {
|
||||
return (...args) => {
|
||||
return callback(arg1, ...args);
|
||||
};
|
||||
}
|
||||
@@ -18,9 +18,6 @@ export class MockPromptsService implements IPromptsService {
|
||||
getAllMetadata(_files: readonly URI[]): Promise<readonly IMetadata[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getMetadata(_file: URI): Promise<IMetadata> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSyntaxParserFor(_model: ITextModel): TextModelPromptParser & { isDisposed: false } {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -33,13 +30,13 @@ export class MockPromptsService implements IPromptsService {
|
||||
asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined {
|
||||
return undefined;
|
||||
}
|
||||
resolvePromptSlashCommand(_data: IChatPromptSlashCommand): Promise<IMetadata | undefined> {
|
||||
resolvePromptSlashCommand(_data: IChatPromptSlashCommand, _token: CancellationToken): Promise<IPromptParserResult | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
findPromptSlashCommands(): Promise<IChatPromptSlashCommand[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
findInstructionFilesFor(_files: readonly URI[]): Promise<readonly URI[]> {
|
||||
findInstructionFilesFor(_files: readonly URI[]): Promise<readonly { uri: URI; reason: string }[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
onDidChangeCustomChatModes: Event<void> = Event.None;
|
||||
@@ -49,5 +46,8 @@ export class MockPromptsService implements IPromptsService {
|
||||
parse(uri: URI, token: CancellationToken): Promise<IPromptParserResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getPromptFileType(resource: URI): PromptsType | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispose(): void { }
|
||||
}
|
||||
|
||||
+4
-2
@@ -30,6 +30,7 @@ import { IPromptFileReference } from '../../../../common/promptSyntax/parsers/ty
|
||||
import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js';
|
||||
import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js';
|
||||
import { MockFilesystem } from '../testUtils/mockFilesystem.js';
|
||||
import { ILabelService } from '../../../../../../../platform/label/common/label.js';
|
||||
|
||||
/**
|
||||
* Helper class to assert the properties of a link.
|
||||
@@ -126,6 +127,7 @@ suite('PromptsService', () => {
|
||||
return 'plaintext';
|
||||
}
|
||||
});
|
||||
instaService.stub(ILabelService, { getUriLabel: (uri: URI) => uri.path });
|
||||
|
||||
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
|
||||
disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider));
|
||||
@@ -874,7 +876,7 @@ suite('PromptsService', () => {
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
instructions.map(i => i.path),
|
||||
instructions.map(i => i.uri.path),
|
||||
[
|
||||
// local instructions
|
||||
URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md').path,
|
||||
@@ -1055,7 +1057,7 @@ suite('PromptsService', () => {
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
instructions.map(i => i.path),
|
||||
instructions.map(i => i.uri.path),
|
||||
[
|
||||
// local instructions
|
||||
URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md').path,
|
||||
|
||||
@@ -1,681 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { randomInt } from '../../../../../../../base/common/numbers.js';
|
||||
import { Range } from '../../../../../../../editor/common/core/range.js';
|
||||
import { BaseToken } from '../../../../common/promptSyntax/codecs/base/baseToken.js';
|
||||
import { CompositeToken } from '../../../../common/promptSyntax/codecs/base/compositeToken.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
|
||||
import { curry, difference, flatten, forEach, map, TTree } from '../../../../common/promptSyntax/utils/treeUtils.js';
|
||||
import { ExclamationMark, Space, Tab, VerticalTab, Word } from '../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.js';
|
||||
|
||||
suite('tree utilities', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('flatten', () => {
|
||||
const tree = {
|
||||
id: '1',
|
||||
children: [
|
||||
{
|
||||
id: '1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.2',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.3',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1.2.2',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(flatten(tree), [
|
||||
tree,
|
||||
tree.children[0],
|
||||
tree.children[1],
|
||||
tree.children[1].children![0],
|
||||
tree.children[1].children![0].children![0],
|
||||
tree.children[1].children![0].children![1],
|
||||
tree.children[1].children![0].children![2],
|
||||
tree.children[1].children![1],
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(flatten({}), [{}]);
|
||||
});
|
||||
|
||||
suite('forEach', () => {
|
||||
test('iterates though all nodes', () => {
|
||||
const tree = {
|
||||
id: '1',
|
||||
children: [
|
||||
{
|
||||
id: '1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.2',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.3',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1.2.2',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const treeCopy = JSON.parse(JSON.stringify(tree));
|
||||
|
||||
const seenIds: string[] = [];
|
||||
forEach((node) => {
|
||||
seenIds.push(node.id);
|
||||
return false;
|
||||
}, tree);
|
||||
|
||||
assert.deepStrictEqual(seenIds, [
|
||||
'1',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.2.1',
|
||||
'1.2.1.1',
|
||||
'1.2.1.2',
|
||||
'1.2.1.3',
|
||||
'1.2.2',
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
treeCopy,
|
||||
tree,
|
||||
'forEach should not modify the tree',
|
||||
);
|
||||
});
|
||||
|
||||
test('can be stopped prematurely', () => {
|
||||
const tree = {
|
||||
id: '1',
|
||||
children: [
|
||||
{
|
||||
id: '1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.2',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.3',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1.3.1',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1.2.2',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const treeCopy = JSON.parse(JSON.stringify(tree));
|
||||
|
||||
const seenIds: string[] = [];
|
||||
forEach((node) => {
|
||||
seenIds.push(node.id);
|
||||
|
||||
if (node.id === '1.2.1') {
|
||||
return true; // stop traversing
|
||||
}
|
||||
|
||||
return false;
|
||||
}, tree);
|
||||
|
||||
assert.deepStrictEqual(seenIds, [
|
||||
'1',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.2.1',
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
treeCopy,
|
||||
tree,
|
||||
'forEach should not modify the tree',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('map', () => {
|
||||
test('maps a tree', () => {
|
||||
interface ITree {
|
||||
id: string;
|
||||
children?: ITree[];
|
||||
}
|
||||
|
||||
const tree: ITree = {
|
||||
id: '1',
|
||||
children: [
|
||||
{
|
||||
id: '1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1.1',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.2',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.3',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1.2.2',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const treeCopy = JSON.parse(JSON.stringify(tree));
|
||||
|
||||
const newRootNode = {
|
||||
newId: '__1__',
|
||||
};
|
||||
|
||||
const newChildNode = {
|
||||
newId: '__1.2.1.3__',
|
||||
};
|
||||
|
||||
const newTree = map((node) => {
|
||||
if (node.id === '1') {
|
||||
return newRootNode;
|
||||
}
|
||||
|
||||
if (node.id === '1.2.1.3') {
|
||||
return newChildNode;
|
||||
}
|
||||
|
||||
return {
|
||||
newId: `__${node.id}__`,
|
||||
};
|
||||
}, tree);
|
||||
|
||||
assert.deepStrictEqual(newTree, {
|
||||
newId: '__1__',
|
||||
children: [
|
||||
{
|
||||
newId: '__1.1__',
|
||||
},
|
||||
{
|
||||
newId: '__1.2__',
|
||||
children: [
|
||||
{
|
||||
newId: '__1.2.1__',
|
||||
children: [
|
||||
{
|
||||
newId: '__1.2.1.1__',
|
||||
},
|
||||
{
|
||||
newId: '__1.2.1.2__',
|
||||
},
|
||||
{
|
||||
newId: '__1.2.1.3__',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
newId: '__1.2.2__',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert(
|
||||
newRootNode === newTree,
|
||||
'Map should not replace return node reference (root node).',
|
||||
);
|
||||
|
||||
assert(
|
||||
newChildNode === newTree.children![1].children![0].children![2],
|
||||
'Map should not replace return node reference (child node).',
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
treeCopy,
|
||||
tree,
|
||||
'forEach should not modify the tree',
|
||||
);
|
||||
});
|
||||
|
||||
test('callback can control resulting children', () => {
|
||||
interface ITree {
|
||||
id: string;
|
||||
children?: ITree[];
|
||||
}
|
||||
|
||||
const tree: ITree = {
|
||||
id: '1',
|
||||
children: [
|
||||
{ id: '1.1' },
|
||||
{
|
||||
id: '1.2',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1',
|
||||
children: [
|
||||
{ id: '1.2.1.1' },
|
||||
{ id: '1.2.1.2' },
|
||||
{
|
||||
id: '1.2.1.3',
|
||||
children: [
|
||||
{
|
||||
id: '1.2.1.3.1',
|
||||
},
|
||||
{
|
||||
id: '1.2.1.3.2',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1.2.2',
|
||||
children: [
|
||||
{ id: '1.2.2.1' },
|
||||
{ id: '1.2.2.2' },
|
||||
{ id: '1.2.2.3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1.2.3',
|
||||
children: [
|
||||
{ id: '1.2.3.1' },
|
||||
{ id: '1.2.3.2' },
|
||||
{ id: '1.2.3.3' },
|
||||
{ id: '1.2.3.4' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const treeCopy = JSON.parse(JSON.stringify(tree));
|
||||
|
||||
const newNodeWithoutChildren = {
|
||||
newId: '__1.2.1.3__',
|
||||
children: undefined,
|
||||
};
|
||||
|
||||
const newTree = map((node, newChildren) => {
|
||||
// validates that explicitly setting `children` to
|
||||
// `undefined` will be preserved on the resulting new node
|
||||
if (node.id === '1.2.1.3') {
|
||||
return newNodeWithoutChildren;
|
||||
}
|
||||
|
||||
// validates that setting `children` to a new array
|
||||
// will be preserved on the resulting new node
|
||||
if (node.id === '1.2.2') {
|
||||
assert.deepStrictEqual(
|
||||
newChildren,
|
||||
[
|
||||
{ newId: '__1.2.2.1__' },
|
||||
{ newId: '__1.2.2.2__' },
|
||||
{ newId: '__1.2.2.3__' },
|
||||
],
|
||||
`Node '${node.id}' must have correct new children.`,
|
||||
);
|
||||
|
||||
return {
|
||||
newId: `__${node.id}__`,
|
||||
children: [newChildren[2]],
|
||||
};
|
||||
}
|
||||
|
||||
// validates that modifying `newChildren` directly
|
||||
// will be preserved on the resulting new node
|
||||
if (node.id === '1.2.3') {
|
||||
assert.deepStrictEqual(
|
||||
newChildren,
|
||||
[
|
||||
{ newId: '__1.2.3.1__' },
|
||||
{ newId: '__1.2.3.2__' },
|
||||
{ newId: '__1.2.3.3__' },
|
||||
{ newId: '__1.2.3.4__' },
|
||||
],
|
||||
`Node '${node.id}' must have correct new children.`,
|
||||
);
|
||||
|
||||
newChildren.length = 2;
|
||||
|
||||
return {
|
||||
newId: `__${node.id}__`,
|
||||
};
|
||||
}
|
||||
|
||||
// convert to a new node in all other cases
|
||||
return {
|
||||
newId: `__${node.id}__`,
|
||||
};
|
||||
}, tree);
|
||||
|
||||
assert.deepStrictEqual(newTree, {
|
||||
newId: '__1__',
|
||||
children: [
|
||||
{ newId: '__1.1__' },
|
||||
{
|
||||
newId: '__1.2__',
|
||||
children: [
|
||||
{
|
||||
newId: '__1.2.1__',
|
||||
children: [
|
||||
{ newId: '__1.2.1.1__' },
|
||||
{ newId: '__1.2.1.2__' },
|
||||
{
|
||||
newId: '__1.2.1.3__',
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
newId: '__1.2.2__',
|
||||
children: [
|
||||
{ newId: '__1.2.2.3__' },
|
||||
],
|
||||
},
|
||||
{
|
||||
newId: '__1.2.3__',
|
||||
children: [
|
||||
{ newId: '__1.2.3.1__' },
|
||||
{ newId: '__1.2.3.2__' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert(
|
||||
newNodeWithoutChildren === newTree.children![1].children![0].children![2],
|
||||
'Map should not replace return node reference (node without children).',
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
treeCopy,
|
||||
tree,
|
||||
'forEach should not modify the tree',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('curry', () => {
|
||||
const originalFunction = (a: number, b: number, c: number) => {
|
||||
return a + b + c;
|
||||
};
|
||||
|
||||
const firstArgument = randomInt(100, -100);
|
||||
const curriedFunction = curry(originalFunction, firstArgument);
|
||||
|
||||
let iterations = 10;
|
||||
while (iterations-- > 0) {
|
||||
const secondArgument = randomInt(100, -100);
|
||||
const thirdArgument = randomInt(100, -100);
|
||||
|
||||
assert.strictEqual(
|
||||
curriedFunction(secondArgument, thirdArgument),
|
||||
originalFunction(firstArgument, secondArgument, thirdArgument),
|
||||
'Curried and original functions must yield the same result.',
|
||||
);
|
||||
|
||||
// a sanity check to ensure we don't compare ambiguous infinities
|
||||
assert(
|
||||
isFinite(originalFunction(firstArgument, secondArgument, thirdArgument)),
|
||||
'Function results must be finite.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
suite('difference', () => {
|
||||
class TestCompositeToken extends CompositeToken<TTree<BaseToken[]>> {
|
||||
public override toString(): string {
|
||||
return `CompositeToken:\n${BaseToken.render(this.children, '\n')})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
test('tree roots differ (no children)', () => {
|
||||
const tree1 = new Word(new Range(1, 1, 1, 1 + 5), 'hello');
|
||||
const tree2 = new Word(new Range(1, 1, 1, 1 + 5), 'halou');
|
||||
|
||||
assert.deepStrictEqual(
|
||||
difference(tree1, tree2),
|
||||
{
|
||||
index: 0,
|
||||
object1: tree1,
|
||||
object2: tree2,
|
||||
},
|
||||
'Unexpected difference between token trees.',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns tree difference (single children level)', () => {
|
||||
const tree1 = asTreeNode<TTree<BaseToken>>(
|
||||
new Word(new Range(1, 1, 1, 1 + 5), 'hello'),
|
||||
[
|
||||
new Space(new Range(1, 6, 1, 7)),
|
||||
new Word(new Range(1, 7, 1, 7 + 5), 'world'),
|
||||
],
|
||||
);
|
||||
|
||||
const tree2 = asTreeNode<TTree<BaseToken>>(
|
||||
new Word(new Range(1, 1, 1, 1 + 5), 'hello'),
|
||||
[
|
||||
new Space(new Range(1, 6, 1, 7)),
|
||||
new Word(new Range(1, 7, 1, 7 + 6), 'world!'),
|
||||
],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
difference(tree1, tree2),
|
||||
{
|
||||
index: 0,
|
||||
object1: tree1,
|
||||
object2: tree2,
|
||||
children: [
|
||||
{
|
||||
index: 1,
|
||||
object1: new Word(
|
||||
new Range(1, 7, 1, 7 + 5),
|
||||
'world',
|
||||
),
|
||||
object2: new Word(
|
||||
new Range(1, 7, 1, 7 + 6),
|
||||
'world!',
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
'Unexpected difference between token trees.',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns tree difference (multiple children levels)', () => {
|
||||
const compositeToken1 = new TestCompositeToken([
|
||||
new VerticalTab(new Range(1, 13, 1, 14)),
|
||||
new Space(new Range(1, 14, 1, 15)),
|
||||
new Word(new Range(1, 15, 1, 15 + 5), 'again'),
|
||||
new ExclamationMark(new Range(1, 20, 1, 21)),
|
||||
]);
|
||||
const tree1: TTree<BaseToken> = asTreeNode<TTree<BaseToken>>(
|
||||
new Word(new Range(1, 1, 1, 1 + 5), 'hello'),
|
||||
[
|
||||
new Space(new Range(1, 6, 1, 7)),
|
||||
new Word(new Range(1, 7, 1, 7 + 5), 'world'),
|
||||
compositeToken1,
|
||||
],
|
||||
);
|
||||
|
||||
const compositeToken2 = new TestCompositeToken([
|
||||
new VerticalTab(new Range(1, 13, 1, 14)),
|
||||
new Space(new Range(1, 14, 1, 15)),
|
||||
new Word(new Range(1, 15, 1, 15 + 5), 'again'),
|
||||
new Tab(new Range(1, 20, 1, 21)),
|
||||
new ExclamationMark(new Range(1, 21, 1, 22)),
|
||||
]);
|
||||
const tree2: TTree<BaseToken> = asTreeNode<TTree<BaseToken>>(
|
||||
new Word(new Range(1, 1, 1, 1 + 5), 'hello'),
|
||||
[
|
||||
new Space(new Range(1, 6, 1, 7)),
|
||||
new Word(new Range(1, 7, 1, 7 + 5), 'world'),
|
||||
compositeToken2,
|
||||
],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
difference(tree1, tree2),
|
||||
{
|
||||
index: 0,
|
||||
object1: tree1,
|
||||
object2: tree2,
|
||||
children: [
|
||||
{
|
||||
index: 2,
|
||||
object1: compositeToken1,
|
||||
object2: compositeToken2,
|
||||
children: [
|
||||
{
|
||||
index: 3,
|
||||
object1: compositeToken1.children[3],
|
||||
object2: compositeToken2.children[3],
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
object1: null,
|
||||
object2: compositeToken2.children[4],
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
'Unexpected difference between token trees.',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null for equal trees', () => {
|
||||
const tree1 = new TestCompositeToken([
|
||||
asTreeNode(new Word(
|
||||
new Range(1, 1, 1, 1 + 5),
|
||||
'hello',
|
||||
), []),
|
||||
asTreeNode(new Space(new Range(1, 6, 1, 7)), []),
|
||||
asTreeNode(new Word(
|
||||
new Range(1, 7, 1, 7 + 6),
|
||||
'world!',
|
||||
), []),
|
||||
]);
|
||||
|
||||
const tree2 = new TestCompositeToken([
|
||||
asTreeNode(new Word(
|
||||
new Range(1, 1, 1, 1 + 5),
|
||||
'hello',
|
||||
), []),
|
||||
asTreeNode(new Space(new Range(1, 6, 1, 7)), []),
|
||||
asTreeNode(new Word(
|
||||
new Range(1, 7, 1, 7 + 6),
|
||||
'world!',
|
||||
), []),
|
||||
]);
|
||||
|
||||
assert.strictEqual(
|
||||
difference(tree1, tree2),
|
||||
null,
|
||||
'Unexpected difference between token trees.',
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
difference(tree1, tree1),
|
||||
null,
|
||||
'Must be a null difference when compared with itself.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Add provided 'children' list to a given object hence
|
||||
* allowing the object to be used as a general tree node.
|
||||
*/
|
||||
function asTreeNode<T extends object>(
|
||||
item: T,
|
||||
children: readonly TTree<T>[],
|
||||
): TTree<T> {
|
||||
return new Proxy(item, {
|
||||
get(target, prop, _receiver) {
|
||||
if (prop === 'children') {
|
||||
return children;
|
||||
}
|
||||
|
||||
// tokens equality uses the 'constructor' property for
|
||||
// comparison, hence we need to return the original one
|
||||
if (prop === 'constructor') {
|
||||
return target.constructor;
|
||||
}
|
||||
|
||||
const result = Reflect.get(target, prop);
|
||||
if (typeof result === 'function') {
|
||||
return result.bind(target);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -202,7 +202,7 @@ suite('InlineChatController', function () {
|
||||
[ITextModelService, new SyncDescriptor(TextModelResolverService)],
|
||||
[ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)],
|
||||
[IPromptsService, new class extends mock<IPromptsService>() {
|
||||
override async findInstructionFilesFor(_file: readonly URI[]): Promise<readonly URI[]> {
|
||||
override async findInstructionFilesFor(_file: readonly URI[]): Promise<readonly { uri: URI; reason: string }[]> {
|
||||
return [];
|
||||
}
|
||||
}],
|
||||
|
||||
Reference in New Issue
Block a user