From 714bd922131f3da85e54ba6a63dda2fa02f357fe Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 18 Feb 2025 15:45:54 -0800 Subject: [PATCH 01/36] Always show folder icons for inline chat anchors --- .../chatAttachmentsContentPart.ts | 2 +- .../chat/browser/chatInlineAnchorWidget.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 0e1a1567379..0b89c05c70b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -51,7 +51,7 @@ export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); private readonly _onDidChangeVisibility = this._register(new Emitter()); - private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + private readonly _contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); constructor( private readonly variables: IChatRequestVariableEntry[], diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index e74cab4ab0c..6849ca7b081 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -17,9 +17,7 @@ import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.j import { Location, SymbolKinds } from '../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; -import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { DefinitionAction } from '../../../../editor/contrib/gotoSymbol/browser/goToCommands.js'; import * as nls from '../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; @@ -36,6 +34,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../browser/dnd.js'; import { ResourceContextKey } from '../../../common/contextkeys.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; @@ -74,12 +73,11 @@ export class InlineAnchorWidget extends Disposable { @IHoverService hoverService: IHoverService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService labelService: ILabelService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @ILanguageService languageService: ILanguageService, @IMenuService menuService: IMenuService, @IModelService modelService: IModelService, - @ITextModelService textModelService: ITextModelService, @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, ) { super(); @@ -133,7 +131,15 @@ export class InlineAnchorWidget extends Disposable { label; const fileKind = location.uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE; - iconClasses = getIconClasses(modelService, languageService, location.uri, fileKind); + const recomputeIconClasses = () => getIconClasses(modelService, languageService, location.uri, fileKind, fileKind === FileKind.FOLDER && !themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined); + + iconClasses = recomputeIconClasses(); + + this._register(themeService.onDidFileIconThemeChange(() => { + iconEl.classList.remove(...iconClasses); + iconClasses = recomputeIconClasses(); + iconEl.classList.add(...iconClasses); + })); const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService); fileService.stat(location.uri) From d34dd938fe3e9ef0df8ea6b4cc8d29a9a168e0f8 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:18:58 -0800 Subject: [PATCH 02/36] debt: compute useNewKeyMatchingSearch earlier (#241247) --- .../preferences/browser/preferencesSearch.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 8338468c3bd..128ffb047ad 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -98,8 +98,16 @@ export class LocalSearchProvider implements ISearchProvider { } let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable + const useNewKeyMatchingSearch = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true; const settingMatcher = (setting: ISetting) => { - const { matches, matchType, keyMatchScore } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); + const { matches, matchType, keyMatchScore } = new SettingMatches( + this._filter, + setting, + true, + (filter, setting) => preferencesModel.findValueMatches(filter, setting), + useNewKeyMatchingSearch, + this.configurationService + ); const score = strings.equalsIgnoreCase(this._filter, setting.key) ? LocalSearchProvider.EXACT_MATCH_SCORE : orderedScore--; @@ -123,7 +131,8 @@ export class LocalSearchProvider implements ISearchProvider { }); } else { return Promise.resolve({ - filterMatches + filterMatches: filterMatches, + exactMatch: false }); } } @@ -138,8 +147,6 @@ export class LocalSearchProvider implements ISearchProvider { export class SettingMatches { readonly matches: IRange[]; - /** Whether to use the new key matching search algorithm that calculates more weights for each result */ - useNewKeyMatchingSearch: boolean = false; matchType: SettingMatchType = SettingMatchType.None; /** * A match score for key matches to allow comparing key matches against each other. @@ -150,12 +157,11 @@ export class SettingMatches { constructor( searchString: string, setting: ISetting, - requireFullQueryMatch: boolean, private searchDescription: boolean, valuesMatcher: (filter: string, setting: ISetting) => IRange[], - @IConfigurationService private readonly configurationService: IConfigurationService + private useNewKeyMatchingSearch: boolean, + private readonly configurationService: IConfigurationService ) { - this.useNewKeyMatchingSearch = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true; this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); } @@ -226,7 +232,7 @@ export class SettingMatches { // Check if the match was for a language tag group setting such as [markdown]. // In such a case, move that setting to be last. - if (setting.overrides?.length && (this.matchType & SettingMatchType.KeyMatch)) { + if (setting.overrides?.length && (this.matchType !== SettingMatchType.None)) { this.matchType = SettingMatchType.LanguageTagSettingMatch; const keyRanges = keyMatchingWords.size ? Array.from(keyMatchingWords.values()).flat() : []; @@ -234,7 +240,7 @@ export class SettingMatches { } // New algorithm only: exit early if the key already matched. - if (this.useNewKeyMatchingSearch && (this.matchType & SettingMatchType.KeyMatch)) { + if (this.useNewKeyMatchingSearch && (this.matchType !== SettingMatchType.None)) { const keyRanges = keyMatchingWords.size ? Array.from(keyMatchingWords.values()).flat() : []; return [...keyRanges]; @@ -430,7 +436,11 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { const settingsRecord = this._keysProvider.getSettingsRecord(); const filterMatches: ISettingMatch[] = []; - const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation(this._filter, [RelatedInformationType.SettingInformation], token ?? CancellationToken.None) as SettingInformationResult[]; + const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation( + this._filter, + [RelatedInformationType.SettingInformation], + token ?? CancellationToken.None + ) as SettingInformationResult[]; relatedInformation.sort((a, b) => b.weight - a.weight); for (const info of relatedInformation) { From c243707ffa6fdc7e95db214e98cc410e252ad857 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 19 Feb 2025 16:43:22 -0600 Subject: [PATCH 03/36] allow markdown string for terminal completion item doc (#241264) --- .../workbench/api/common/extHost.protocol.ts | 20 +++++++++++++++---- .../api/common/extHostTerminalService.ts | 8 ++------ .../api/common/extHostTypeConverters.ts | 19 +++++++++++++++--- src/vs/workbench/api/common/extHostTypes.ts | 4 +++- .../suggest/browser/simpleCompletionItem.ts | 4 ++-- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6bf4811257b..0e4072c38d9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -23,6 +23,7 @@ import { IChange } from '../../../editor/common/diff/legacyLinesDiffComputer.js' import * as editorCommon from '../../../editor/common/editorCommon.js'; import { StandardTokenType } from '../../../editor/common/encodedTokenAttributes.js'; import * as languages from '../../../editor/common/languages.js'; +import { CompletionItemLabel } from '../../../editor/common/languages.js'; import { CharacterPair, CommentRule, EnterAction } from '../../../editor/common/languages/languageConfiguration.js'; import { EndOfLineSequence } from '../../../editor/common/model.js'; import { IModelChangedEvent } from '../../../editor/common/model/mirrorTextModel.js'; @@ -84,7 +85,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic import * as search from '../../services/search/common/search.js'; import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; -import { TerminalCompletionItem, TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; +import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; export interface IWorkspaceData extends IStaticWorkspaceData { @@ -2428,11 +2429,22 @@ export interface ITerminalCompletionContextDto { allowFallbackCompletions: boolean; } +export interface ITerminalCompletionItemDto { + label: string | CompletionItemLabel; + detail?: string; + documentation?: string | IMarkdownString; + icon?: ThemeIcon | undefined; + isFile?: boolean | undefined; + isDirectory?: boolean | undefined; + isKeyword?: boolean | undefined; + replacementIndex: number; + replacementLength: number; +} export interface ITerminalCompletionProvider { id: string; shellTypes?: TerminalShellType[]; - provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise | undefined>; + provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise | undefined>; triggerCharacters?: string[]; isBuiltin?: boolean; } @@ -2440,7 +2452,7 @@ export interface ITerminalCompletionProvider { * Represents a collection of {@link CompletionItem completion items} to be presented * in the editor. */ -export class TerminalCompletionListDto { +export class TerminalCompletionListDto { /** * Resources should be shown in the completions list @@ -2498,7 +2510,7 @@ export interface ExtHostTerminalServiceShape { $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void; $createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise; $provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise | undefined>; - $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise; + $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise; } export interface ExtHostTerminalShellIntegrationShape { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 4cf0cc957bc..c88dbbcaa35 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -779,7 +779,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } - public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise { + public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise { const token = new CancellationTokenSource().token; if (token.isCancellationRequested || !this.activeTerminal) { return undefined; @@ -794,11 +794,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I if (completions === null || completions === undefined) { return undefined; } - if (Array.isArray(completions)) { - return completions; - } else { - return TerminalCompletionList.from(completions); - } + return TerminalCompletionList.from(completions); } public $acceptTerminalShellType(id: number, shellType: TerminalShellType | undefined): void { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 09b5dfcbdf0..1acf66bce5e 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2947,12 +2947,25 @@ export namespace TerminalQuickFix { return converter.toInternal(quickFix, disposables); } } +export namespace TerminalCompletionItemDto { + export function from(item: vscode.TerminalCompletionItem): extHostProtocol.ITerminalCompletionItemDto { + return { + ...item, + documentation: MarkdownString.fromStrict(item.documentation), + }; + } +} export namespace TerminalCompletionList { - export function from(completionList: vscode.TerminalCompletionList): extHostProtocol.TerminalCompletionListDto { + export function from(completions: vscode.TerminalCompletionList | vscode.TerminalCompletionItem[]): extHostProtocol.TerminalCompletionListDto { + if (Array.isArray(completions)) { + return { + items: completions.map(i => TerminalCompletionItemDto.from(i)), + }; + } return { - ...completionList, - resourceRequestConfig: completionList.resourceRequestConfig ? TerminalResourceRequestConfig.from(completionList.resourceRequestConfig) : undefined, + items: completions.items.map(i => TerminalCompletionItemDto.from(i)), + resourceRequestConfig: completions.resourceRequestConfig ? TerminalResourceRequestConfig.from(completions.resourceRequestConfig) : undefined, }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index f7bb7d4806f..f27aac8e007 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2151,16 +2151,18 @@ export class TerminalCompletionItem implements vscode.TerminalCompletionItem { label: string | CompletionItemLabel; icon?: ThemeIcon | undefined; detail?: string | undefined; + documentation?: string | vscode.MarkdownString | undefined; isFile?: boolean | undefined; isDirectory?: boolean | undefined; isKeyword?: boolean | undefined; replacementIndex: number; replacementLength: number; - constructor(label: string | CompletionItemLabel, icon?: ThemeIcon, detail?: string, isFile?: boolean, isDirectory?: boolean, isKeyword?: boolean, replacementIndex?: number, replacementLength?: number) { + constructor(label: string | CompletionItemLabel, icon?: ThemeIcon, detail?: string, documentation?: string | vscode.MarkdownString, isFile?: boolean, isDirectory?: boolean, isKeyword?: boolean, replacementIndex?: number, replacementLength?: number) { this.label = label; this.icon = icon; this.detail = detail; + this.documentation = documentation; this.isFile = isFile; this.isDirectory = isDirectory; this.isKeyword = isKeyword; diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 1b18d26ad80..e89c53ae4b8 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { FuzzyScore } from '../../../../base/common/filters.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; export interface CompletionItemLabel { @@ -37,7 +37,7 @@ export interface ISimpleCompletion { /** * A human-readable string that represents a doc-comment. */ - documentation?: string | MarkdownString; + documentation?: string | IMarkdownString; /** * The start of the replacement. From a3d87bbb04cb3d998647fe2c2affcffee3be3617 Mon Sep 17 00:00:00 2001 From: Joyce Er <30305945+joyceerhl@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:45:30 -0800 Subject: [PATCH 04/36] fix: consolidate working set and chat attachments (#241116) * Render attachments above chat input part * Remove Add Files... button * Remove edits input part progress * Use same attachment model for chat and edits * Remove gap for chat attached context element * Don't render changed files list if edits has not made changes * Roll active editor tracking into chat implicit context widget * Fix rendering chat related files * Render related files below chat attachments and above input editor * Use chat attachments as source of truth for building chat variables in edits requests * Render a `+` for suggested related files * Don't suggest same file twice * Read working set entries from editing session * Make working set list title more succinct * Render working set files in chat requests * Move Attach button to top of chat input part * Render keybinding hint in Add Files button hover * Don't compute/apply related file suggestions if there are no attachments --- .../browser/actions/chatContextActions.ts | 264 +++++++++--------- src/vs/workbench/contrib/chat/browser/chat.ts | 1 - .../chat/browser/chatAttachmentModel.ts | 54 +--- .../chatAttachmentsContentPart.ts | 5 - .../chatReferencesContentPart.ts | 28 +- .../browser/chatEditing/chatEditingActions.ts | 7 +- .../chatEditing/chatEditingServiceImpl.ts | 17 +- .../browser/chatEditing/chatEditingSession.ts | 76 +---- .../contrib/chat/browser/chatEditor.ts | 1 - .../contrib/chat/browser/chatInputPart.ts | 184 +++++------- .../contrib/chat/browser/chatListRenderer.ts | 6 +- .../contrib/chat/browser/chatQuick.ts | 2 +- .../contrib/chat/browser/chatViewPane.ts | 1 - .../contrib/chat/browser/chatWidget.ts | 15 +- .../browser/contrib/chatInputCompletions.ts | 2 +- .../contrib/chatInputRelatedFilesContrib.ts | 42 +-- .../contrib/chat/browser/media/chat.css | 65 ++++- .../contrib/chat/common/chatEditingService.ts | 6 +- .../contrib/chat/common/chatModel.ts | 11 +- .../contrib/chat/common/chatService.ts | 1 - .../contrib/chat/common/chatServiceImpl.ts | 8 +- .../contrib/chat/common/chatViewModel.ts | 5 - .../ChatService_can_deserialize.0.snap | 1 - ...rvice_can_deserialize_with_response.0.snap | 1 - .../ChatService_can_serialize.1.snap | 2 - .../ChatService_sendRequest_fails.0.snap | 1 - 26 files changed, 329 insertions(+), 477 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index f6657f85254..de0ad326f51 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -48,7 +48,7 @@ import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../searc import { SearchContext } from '../../../search/common/constants.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; @@ -445,13 +445,13 @@ export class AttachContextAction extends Action2 { category: CHAT_CATEGORY, precondition: ContextKeyExpr.or(AttachContextAction._cdt, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession))), keybinding: { - when: ChatContextKeys.inChatInput, + when: ContextKeyExpr.and(ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatInput), primary: KeyMod.CtrlCmd | KeyCode.Slash, weight: KeybindingWeight.EditorContrib }, menu: [ { - when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), AttachContextAction._cdt)), + when: AttachContextAction._cdt, id: MenuId.ChatInput, group: 'navigation', order: 2 @@ -515,26 +515,21 @@ export class AttachContextAction extends Action2 { }); } } else { - // file attachment - if (chatEditingService) { - getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(pick.resource); - } else { - let isOmitted = false; - try { - const createdModel = await textModelService.createModelReference(pick.resource); - createdModel.dispose(); - } catch { - isOmitted = true; - } - - toAttach.push({ - id: this._getFileContextId({ resource: pick.resource }), - value: pick.resource, - name: pick.label, - isFile: true, - isOmitted - }); + let isOmitted = false; + try { + const createdModel = await textModelService.createModelReference(pick.resource); + createdModel.dispose(); + } catch { + isOmitted = true; } + + toAttach.push({ + id: this._getFileContextId({ resource: pick.resource }), + value: pick.resource, + name: pick.label, + isFile: true, + isOmitted + }); } } else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) { toAttach.push({ @@ -548,31 +543,23 @@ export class AttachContextAction extends Action2 { for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; if (uri) { - if (chatEditingService) { - getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(uri); - } else { - toAttach.push({ - id: this._getFileContextId({ resource: uri }), - value: uri, - name: labelService.getUriBasenameLabel(uri), - isFile: true, - }); - } + toAttach.push({ + id: this._getFileContextId({ resource: uri }), + value: uri, + name: labelService.getUriBasenameLabel(uri), + isFile: true, + }); } } } else if (isISearchResultsQuickPickItem(pick)) { const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; for (const result of searchView.model.searchResult.matches()) { - if (chatEditingService) { - getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(result.resource); - } else { - toAttach.push({ - id: this._getFileContextId({ resource: result.resource }), - value: result.resource, - name: labelService.getUriBasenameLabel(result.resource), - isFile: true, - }); - } + toAttach.push({ + id: this._getFileContextId({ resource: result.resource }), + value: result.resource, + name: labelService.getUriBasenameLabel(result.resource), + isFile: true, + }); } } else if (isRelatedFileQuickPickItem(pick)) { // Get all provider results and show them in a second tier picker @@ -580,12 +567,12 @@ export class AttachContextAction extends Action2 { if (!chatSessionId || !chatEditingService) { continue; } - const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None); + const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); if (!relatedFiles) { continue; } const attachments = widget.attachmentModel.getAttachmentIDs(); - const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None) + const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { acc.push({ type: 'separator', label: cur.group }); for (const file of cur.files) { @@ -685,103 +672,103 @@ export class AttachContextAction extends Action2 { const chatEditingService = widget.location === ChatAgentLocation.EditingSession ? accessor.get(IChatEditingService) : undefined; const quickPickItems: IAttachmentQuickPickItem[] = []; - if (!context || !context.showFilesOnly) { - if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { - const imageData = await clipboardService.readImage(); - if (isImage(imageData)) { - quickPickItems.push({ - kind: 'image', - id: await imageToHash(imageData), - label: localize('imageFromClipboard', 'Image from Clipboard'), - iconClass: ThemeIcon.asClassName(Codicon.fileMedia), - }); - } - + if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { + const imageData = await clipboardService.readImage(); + if (isImage(imageData)) { quickPickItems.push({ - kind: 'screenshot', - id: ScreenshotVariableId, - icon: ThemeIcon.fromId(Codicon.deviceCamera.id), - iconClass: ThemeIcon.asClassName(Codicon.deviceCamera), - label: (isElectron - ? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window') - : localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')), + kind: 'image', + id: await imageToHash(imageData), + label: localize('imageFromClipboard', 'Image from Clipboard'), + iconClass: ThemeIcon.asClassName(Codicon.fileMedia), }); } - if (widget.viewModel?.sessionId) { - const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - if (agentPart) { - const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); - for (const variable of completions) { - if (variable.fullName && variable.command) { - quickPickItems.push({ - kind: 'command', - label: variable.fullName, - id: variable.id, - command: variable.command, - icon: variable.icon, - iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, - value: variable.value, - name: variable.name - }); - } else { - // Currently there's nothing that falls into this category - } + quickPickItems.push({ + kind: 'screenshot', + id: ScreenshotVariableId, + icon: ThemeIcon.fromId(Codicon.deviceCamera.id), + iconClass: ThemeIcon.asClassName(Codicon.deviceCamera), + label: (isElectron + ? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window') + : localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')), + }); + } + + if (widget.viewModel?.sessionId) { + const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); + if (agentPart) { + const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); + for (const variable of completions) { + if (variable.fullName && variable.command) { + quickPickItems.push({ + kind: 'command', + label: variable.fullName, + id: variable.id, + command: variable.command, + icon: variable.icon, + iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, + value: variable.value, + name: variable.name + }); + } else { + // Currently there's nothing that falls into this category } } } + } - for (const tool of languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - const item: IToolQuickPickItem = { - kind: 'tool', - label: tool.displayName ?? '', - id: tool.id, - icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path? - }; - if (ThemeIcon.isThemeIcon(tool.icon)) { - item.iconClass = ThemeIcon.asClassName(tool.icon); - } else if (tool.icon) { - item.iconPath = tool.icon; - } - - quickPickItems.push(item); + for (const tool of languageModelToolsService.getTools()) { + if (tool.canBeReferencedInPrompt) { + const item: IToolQuickPickItem = { + kind: 'tool', + label: tool.displayName ?? '', + id: tool.id, + icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path? + }; + if (ThemeIcon.isThemeIcon(tool.icon)) { + item.iconClass = ThemeIcon.asClassName(tool.icon); + } else if (tool.icon) { + item.iconPath = tool.icon; } + + quickPickItems.push(item); } + } + quickPickItems.push({ + kind: 'quickaccess', + label: localize('chatContext.symbol', 'Symbol...'), + iconClass: ThemeIcon.asClassName(Codicon.symbolField), + prefix: SymbolsQuickAccessProvider.PREFIX, + id: 'symbol' + }); + + quickPickItems.push({ + kind: 'folder', + label: localize('chatContext.folder', 'Folder...'), + iconClass: ThemeIcon.asClassName(Codicon.folder), + id: 'folder', + }); + + if (widget.location === ChatAgentLocation.Notebook) { quickPickItems.push({ - kind: 'quickaccess', - label: localize('chatContext.symbol', 'Symbol...'), - iconClass: ThemeIcon.asClassName(Codicon.symbolField), - prefix: SymbolsQuickAccessProvider.PREFIX, - id: 'symbol' + kind: 'command', + id: 'chatContext.notebook.kernelVariable', + icon: ThemeIcon.fromId(Codicon.serverEnvironment.id), + iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment), + value: 'kernelVariable', + label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'), + command: { + id: 'notebook.chat.selectAndInsertKernelVariable', + title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'), + arguments: [{ widget, range: undefined }] + } }); + } - quickPickItems.push({ - kind: 'folder', - label: localize('chatContext.folder', 'Folder...'), - iconClass: ThemeIcon.asClassName(Codicon.folder), - id: 'folder', - }); - - if (widget.location === ChatAgentLocation.Notebook) { - quickPickItems.push({ - kind: 'command', - id: 'chatContext.notebook.kernelVariable', - icon: ThemeIcon.fromId(Codicon.serverEnvironment.id), - iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment), - value: 'kernelVariable', - label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'), - command: { - id: 'notebook.chat.selectAndInsertKernelVariable', - title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'), - arguments: [{ widget, range: undefined }] - } - }); - } - } else if (context.showFilesOnly) { + if (context?.showFilesOnly) { if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || (getEditingSession(chatEditingService, widget)?.workingSet.size))) { - quickPickItems.push({ + quickPickItems.unshift({ kind: 'related-files', id: 'related-files', label: localize('chatContext.relatedFiles', 'Related Files'), @@ -789,7 +776,7 @@ export class AttachContextAction extends Action2 { }); } if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { - quickPickItems.push({ + quickPickItems.unshift({ kind: 'open-editors', id: 'open-editors', label: localize('chatContext.editors', 'Open Editors'), @@ -797,7 +784,7 @@ export class AttachContextAction extends Action2 { }); } if (SearchContext.HasSearchResults.getValue(contextKeyService)) { - quickPickItems.push({ + quickPickItems.unshift({ kind: 'search-results', id: 'search-results', label: localize('chatContext.searchResults', 'Search Results'), @@ -827,6 +814,9 @@ export class AttachContextAction extends Action2 { this._show(quickInputService, commandService, widget, quickChatService, quickPickItems.sort(function (a, b) { + if (a.kind === 'open-editors') { return -1; } + if (b.kind === 'open-editors') { return 1; } + const first = extractTextFromIconLabel(a.label).toUpperCase(); const second = extractTextFromIconLabel(b.label).toUpperCase(); @@ -862,13 +852,6 @@ export class AttachContextAction extends Action2 { filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); - if (chatEditingService) { - for (const [file, state] of getEditingSession(chatEditingService, widget)?.workingSet.entries() ?? []) { - if (state.state !== WorkingSetEntryState.Suggested) { - attachedContext.add(this._getFileContextId({ resource: file })); - } - } - } if (isIOpenEditorsQuickPickItem(item)) { for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) { @@ -935,16 +918,21 @@ registerAction2(class AttachFilesAction extends AttachContextAction { constructor() { super({ id: 'workbench.action.chat.editing.attachFiles', - title: localize2('workbench.action.chat.editing.attachFiles.label', "Add Files to Working Set"), + title: localize2('workbench.action.chat.editing.attachFiles.label', "Add Files to Copilot Edits"), f1: false, category: CHAT_CATEGORY, - precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession) + precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + keybinding: { + when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.EditorContrib + } }); } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const context = args[0]; - const attachFilesContext = { ...context, showFilesOnly: true, placeholder: localize('chatAttachFiles', 'Search for files to add to your working set') }; + const attachFilesContext = { ...context, showFilesOnly: true }; return super.run(accessor, attachFilesContext); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index b127232b593..982143743ce 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -190,7 +190,6 @@ export interface IChatWidgetViewOptions { }; defaultElementHeight?: number; editorOverflowWidgetsDomNode?: HTMLElement; - enableImplicitContext?: boolean; enableWorkingSet?: 'explicit' | 'implicit'; } diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index b5f9c250ac2..9278fe4aed0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -42,6 +42,15 @@ export class ChatAttachmentModel extends Disposable { return this._attachments.size; } + get fileAttachments(): URI[] { + return this.attachments.reduce((acc, file) => { + if (file.isFile && URI.isUri(file.value)) { + acc.push(file.value); + } + return acc; + }, []); + } + getAttachmentIDs() { return new Set(this._attachments.keys()); } @@ -92,48 +101,3 @@ export class ChatAttachmentModel extends Disposable { this.addContext(...attachments); } } - -export class EditsAttachmentModel extends ChatAttachmentModel { - - private _onFileLimitExceeded = this._register(new Emitter()); - readonly onFileLimitExceeded = this._onFileLimitExceeded.event; - - get fileAttachments() { - return this.attachments.filter(attachment => attachment.isFile); - } - - private readonly _excludedFileAttachments: IChatRequestVariableEntry[] = []; - get excludedFileAttachments(): IChatRequestVariableEntry[] { - return this._excludedFileAttachments; - } - - constructor( - @IInstantiationService _initService: IInstantiationService, - ) { - super(_initService); - } - - override addContext(...attachments: IChatRequestVariableEntry[]) { - const currentAttachmentIds = this.getAttachmentIDs(); - const fileAttachments = attachments.filter(attachment => attachment.isFile); - const otherAttachments = attachments.filter(attachment => !attachment.isFile); - - // deduplicate file attachments - const newFileAttachments = []; - const newFileAttachmentIds = new Set(); - for (const attachment of fileAttachments) { - if (newFileAttachmentIds.has(attachment.id) || currentAttachmentIds.has(attachment.id)) { - continue; - } - newFileAttachmentIds.add(attachment.id); - newFileAttachments.push(attachment); - } - - super.addContext(...otherAttachments, ...newFileAttachments); - } - - override clear(): void { - this._excludedFileAttachments.splice(0, this._excludedFileAttachments.length); - super.clear(); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 0b89c05c70b..b62621ea5bb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -56,7 +56,6 @@ export class ChatAttachmentsContentPart extends Disposable { constructor( private readonly variables: IChatRequestVariableEntry[], private readonly contentReferences: ReadonlyArray = [], - private readonly workingSet: ReadonlyArray = [], public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -84,10 +83,6 @@ export class ChatAttachmentsContentPart extends Disposable { this.variables.forEach(async (attachment) => { let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - if (resource && attachment.isFile && this.workingSet.find(entry => entry.toString() === resource?.toString())) { - // Don't render attachment if it's in the working set - return; - } const widget = dom.append(container, dom.$('.chat-attached-context-attachment.show-file-icons')); const label = this._contextResourceLabels.create(widget, { supportIcons: true, hoverDelegate, hoverTargetOverride: widget }); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index c73c3883381..427e7b2fca6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -435,25 +435,15 @@ class CollapsibleListRenderer implements IListRenderer (attachment.value as URI).toString()); + const fileAttachments = chatWidget.attachmentModel ? chatWidget.attachmentModel.fileAttachments : []; + const attachmentIdsToRemove = fileAttachments.map(attachment => attachment.toString()); chatWidget.attachmentModel.delete(...attachmentIdsToRemove); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 24af03c47e8..6f31c908335 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -369,24 +369,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic }); } - async getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> { - const currentSession = this.getEditingSession(chatSessionId); - if (!currentSession) { - return undefined; - } - const userAddedWorkingSetEntries: URI[] = []; - for (const [uri, metadata] of currentSession.workingSet) { - // Don't incorporate suggested files into the related files request - // but do consider transient entries like open editors - if (metadata.state !== WorkingSetEntryState.Suggested) { - userAddedWorkingSetEntries.push(uri); - } - } - + async getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> { const providers = Array.from(this._chatRelatedFilesProviders.values()); const result = await Promise.all(providers.map(async provider => { try { - const relatedFiles = await provider.provideRelatedFiles({ prompt, files: userAddedWorkingSetEntries }, token); + const relatedFiles = await provider.provideRelatedFiles({ prompt, files }, token); if (relatedFiles?.length) { return { group: provider.description, files: relatedFiles }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 45dbea5f3d4..a8f60cd6404 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -17,7 +17,6 @@ import { asyncTransaction, autorun, derived, derivedOpts, derivedWithStore, IObs import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; @@ -35,16 +34,15 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IEditorCloseEvent, SaveReason } from '../../../../common/editor.js'; +import { SaveReason } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; -import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ChatEditingSessionChangeType, ChatEditingSessionState, chatEditingSnapshotScheme, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; @@ -56,10 +54,6 @@ const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; const POST_EDIT_STOP_ID = 'd19944f6-f46c-4e17-911b-79a8e843c7c0'; // randomly generated -const untrackedSchemes: readonly string[] = [ - chatEditingSnapshotScheme, -]; - class ThrottledSequencer extends Sequencer { private _size = 0; @@ -234,12 +228,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); } - // Add the currently active editors to the working set - this._trackCurrentEditorsInWorkingSet(); this._triggerSaveParticipantsOnAccept(); - this._register(this._editorService.onDidVisibleEditorsChange(() => { - this._trackCurrentEditorsInWorkingSet(); - })); this._register(autorun(reader => { const entries = this.entries.read(reader); entries.forEach(entry => { @@ -306,61 +295,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio )); } - private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) { - const existingTransientEntries = new ResourceSet(); - for (const file of this._workingSet.keys()) { - if (this._workingSet.get(file)?.state === WorkingSetEntryState.Transient) { - existingTransientEntries.add(file); - } - } - - const activeEditors = new ResourceSet(); - this._editorGroupsService.groups.forEach((group) => { - if (!group.activeEditorPane) { - return; - } - let uri; - if (isNotebookEditorInput(group.activeEditorPane.input)) { - uri = group.activeEditorPane.input.resource; - } else { - let activeEditorControl = group.activeEditorPane.getControl(); - if (isDiffEditor(activeEditorControl)) { - activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); - } - if ((isCodeEditor(activeEditorControl)) && activeEditorControl.hasModel()) { - uri = activeEditorControl.getModel().uri; - } - } - if (!uri) { - return; - } - if (untrackedSchemes.includes(uri.scheme)) { - return; - } - if (existingTransientEntries.has(uri)) { - existingTransientEntries.delete(uri); - } else if ((!this._workingSet.has(uri) || this._workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) && !this._removedTransientEntries.has(uri)) { - // Don't add as a transient entry if it's already a confirmed part of the working set - // or if the user has intentionally removed it from the working set - activeEditors.add(uri); - } - }); - - let didChange = false; - for (const entry of existingTransientEntries) { - didChange = this._workingSet.delete(entry) || didChange; - } - - for (const entry of activeEditors) { - this._workingSet.set(entry, { state: WorkingSetEntryState.Transient, description: localize('chatEditing.transient', "Open Editor") }); - didChange = true; - } - - if (didChange) { - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } - } - private _findSnapshot(requestId: string): IChatEditingSessionSnapshot | undefined { return this._linearHistory.get().find(s => s.requestId === requestId); } @@ -610,7 +544,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const state = this._workingSet.get(uri); if (state !== undefined) { didRemoveUris = this._workingSet.delete(uri) || didRemoveUris; - if (reason === WorkingSetEntryRemovalReason.User && (state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested)) { + if (reason === WorkingSetEntryRemovalReason.User && state.state === WorkingSetEntryState.Suggested) { this._removedTransientEntries.add(uri); } } @@ -626,7 +560,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio markIsReadonly(resource: URI, isReadonly?: boolean): void { const entry = this._workingSet.get(resource); if (entry) { - if (entry.state === WorkingSetEntryState.Transient || entry.state === WorkingSetEntryState.Suggested) { + if (entry.state === WorkingSetEntryState.Suggested) { entry.state = WorkingSetEntryState.Attached; } entry.isMarkedReadonly = isReadonly ?? !entry.isMarkedReadonly; @@ -850,7 +784,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._workingSet.set(resource, { description, state: WorkingSetEntryState.Suggested }); this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } else if (state === undefined || state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested) { + } else if (state === undefined || state.state === WorkingSetEntryState.Suggested) { this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached }); this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 46c63ff9344..7b89c3d65a6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -68,7 +68,6 @@ export class ChatEditor extends EditorPane { undefined, { supportsFileReferences: true, - enableImplicitContext: true }, { listForeground: editorForeground, diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d30dcdf08b5..e8885e46d43 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -15,9 +15,7 @@ import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/ import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; import { IAction, Separator, toAction } from '../../../../base/common/actions.js'; -import { coalesce } from '../../../../base/common/arrays.js'; import { Promises } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; @@ -35,7 +33,6 @@ 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 { ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; @@ -81,9 +78,8 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { ChatEditingSessionState, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; -import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -93,7 +89,7 @@ import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSeco import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js'; import { IChatWidget } from './chat.js'; -import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel.js'; +import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { toChatVariable } from './chatAttachmentModel/chatInstructionAttachmentsModel.js'; import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; @@ -124,7 +120,6 @@ interface IChatInputPartOptions { telemetrySource?: string; }; editorOverflowWidgetsDomNode?: HTMLElement; - enableImplicitContext?: boolean; renderWorkingSet?: boolean; } @@ -243,6 +238,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private attachedContextContainer!: HTMLElement; private readonly attachedContextDisposables = this._register(new MutableDisposable()); + private relatedFilesContainer!: HTMLElement; + private chatEditingSessionWidgetContainer!: HTMLElement; private _inputPartHeight: number = 0; @@ -299,7 +296,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _chatEditsActionsDisposables = this._register(new DisposableStore()); private readonly _chatEditsDisposables = this._register(new DisposableStore()); - private _chatEditsProgress: ProgressBar | undefined; private _chatEditsListPool: CollapsibleListPool; private _chatEditList: IDisposableReference> | undefined; get selectedElements(): URI[] { @@ -322,10 +318,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get attemptedWorkingSetEntriesCount() { return this._attemptedWorkingSetEntriesCount; } - private _combinedChatEditWorkingSetEntries: IWorkingSetEntry[] = []; - public get chatEditWorkingSetFiles() { - return this._combinedChatEditWorkingSetEntries; - } private readonly getInputState: () => IChatInputState; @@ -365,11 +357,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ) { super(); + this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); if (this.location === ChatAgentLocation.EditingSession) { - this._attachmentModel = this._register(this.instantiationService.createInstance(EditsAttachmentModel)); this.dnd = this._register(this.instantiationService.createInstance(EditsDragAndDrop, this.attachmentModel, styles)); } else { - this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this.attachmentModel, styles)); } @@ -684,6 +675,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ]), ]), dom.h('.chat-attached-context@attachedContextContainer'), + dom.h('.chat-related-files@relatedFilesContainer'), dom.h('.interactive-input-followups@followupsContainer'), ]) ]); @@ -693,8 +685,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ - dom.h('.chat-editor-container@editorContainer'), dom.h('.chat-attached-context@attachedContextContainer'), + dom.h('.chat-related-files@relatedFilesContainer'), + dom.h('.chat-editor-container@editorContainer'), dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), @@ -708,13 +701,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars const editorContainer = elements.editorContainer; this.attachedContextContainer = elements.attachedContextContainer; + this.relatedFilesContainer = elements.relatedFilesContainer; const toolbarsContainer = elements.inputToolbars; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; - this.renderAttachedContext(); - if (this.options.enableImplicitContext) { - this._implicitContext = this._register(new ChatImplicitContext()); - this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); - } + this.renderAttachedContext(widget); + this._implicitContext = this._register(new ChatImplicitContext()); + this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); this._register(this._attachmentModel.onDidChangeContext(() => this._handleAttachedContextChange())); this.renderChatEditingSessionState(null, widget); @@ -830,7 +822,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { this.setCurrentLanguageModelByUser(model); - this.renderAttachedContext(); + this.renderAttachedContext(widget); }, getModels: () => this.getModels() }; @@ -907,11 +899,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge onDidChangeCursorPosition(); this._register(this.themeService.onDidFileIconThemeChange(() => { - this.renderAttachedContext(); + this.renderAttachedContext(widget); })); } - private async renderAttachedContext() { + private renderAddFilesHint(container: HTMLElement, hoverDelegate: IHoverDelegate, store: DisposableStore, chatWidget: IChatWidget) { + const button = store.add(new Button(container, { + supportIcons: true, + secondary: true, + hoverDelegate + })); + button.element.classList.add('chat-attached-context-attachment', 'chat-add-files'); + button.label = localize('chatAddFiles', '{0} Add Files...', '$(attach)'); + const commandId = 'workbench.action.chat.editing.attachFiles'; + const kb = this.keybindingService.lookupKeybinding(commandId)?.getLabel(); + const title = localize('attachFiles.label', 'Attach files to your request{0}', kb ? ` (${kb})` : ''); + button.setTitle(title); + store.add(button.onDidClick(() => { + this.commandService.executeCommand(commandId, { widget: chatWidget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') }); + })); + dom.append(container, button.element); + } + + private async renderAttachedContext(widget?: IChatWidget) { const container = this.attachedContextContainer; const oldHeight = container.offsetHeight; const store = new DisposableStore(); @@ -919,11 +929,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(container); const hoverDelegate = store.add(createInstantHoverDelegate()); - const attachments = this.location === ChatAgentLocation.EditingSession - // Render as attachments anything that isn't a file, but still render specific ranges in a file - ? [...this.attachmentModel.attachments.entries()].filter(([_, attachment]) => !attachment.isFile || attachment.isFile && typeof attachment.value === 'object' && !!attachment.value && 'range' in attachment.value) - : [...this.attachmentModel.attachments.entries()]; - dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); + if (widget && this.options.renderWorkingSet) { + this.renderAddFilesHint(container, hoverDelegate, store, widget); + } + const attachments = [...this.attachmentModel.attachments.entries()]; + dom.setVisibility(Boolean(widget && this.options.renderWorkingSet) || Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); if (!attachments.length) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; } @@ -1193,22 +1203,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) { dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - if (!chatEditingSession || !this.options.renderWorkingSet) { - dom.clearNode(this.chatEditingSessionWidgetContainer); - this._chatEditsDisposables.clear(); - this._chatEditList = undefined; - this._combinedChatEditWorkingSetEntries = []; - this._chatEditsProgress?.dispose(); - return; + await this.renderAttachedContext(chatWidget); + if (chatEditingSession) { + this.renderChatRelatedFiles(chatEditingSession, this.relatedFilesContainer); } - const currentChatEditingState = chatEditingSession.state.get(); - if (this._chatEditList && !chatWidget?.viewModel?.requestInProgress && (currentChatEditingState === ChatEditingSessionState.Idle || currentChatEditingState === ChatEditingSessionState.Initial)) { - this._chatEditsProgress?.stop(); - } - - // Summary of number of files changed - const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); const seenEntries = new ResourceSet(); const entries: IChatCollapsibleListItem[] = chatEditingSession?.entries.get().map((entry) => { seenEntries.add(entry.modifiedURI); @@ -1218,16 +1217,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge kind: 'reference', }; }) ?? []; - for (const attachment of (this.attachmentModel as EditsAttachmentModel).fileAttachments) { - if (URI.isUri(attachment.value) && !seenEntries.has(attachment.value)) { - entries.unshift({ - reference: attachment.value, - state: WorkingSetEntryState.Attached, - kind: 'reference', - }); - seenEntries.add(attachment.value); - } + + if (!chatEditingSession || !this.options.renderWorkingSet || !entries.length) { + dom.clearNode(this.chatEditingSessionWidgetContainer); + this._chatEditsDisposables.clear(); + this._chatEditList = undefined; + return; } + + // Summary of number of files changed + const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); for (const [file, metadata] of chatEditingSession.workingSet.entries()) { if (!seenEntries.has(file) && metadata.state !== WorkingSetEntryState.Suggested) { entries.unshift({ @@ -1240,16 +1239,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge seenEntries.add(file); } } - // 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) || isLocation(part.data) && !seenEntries.has(part.data.uri))) { - entries.unshift({ - reference: part.data, - state: WorkingSetEntryState.Attached, - kind: 'reference', - }); - } - } entries.sort((a, b) => { if (a.kind === 'reference' && b.kind === 'reference') { @@ -1260,18 +1249,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } return 0; }); + const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview')); const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title')); - const overviewWorkingSet = overviewTitle.querySelector('span') ?? dom.append(overviewTitle, $('span')); const overviewFileCount = overviewTitle.querySelector('span.working-set-count') ?? dom.append(overviewTitle, $('span.working-set-count')); - overviewWorkingSet.textContent = localize('chatEditingSession.workingSet', 'Working Set'); - - let suggestedFilesInWorkingSetCount = 0; overviewFileCount.textContent = ''; if (entries.length === 1) { - overviewFileCount.textContent = ' ' + localize('chatEditingSession.oneFile', '(1 file)'); + overviewFileCount.textContent = localize('chatEditingSession.oneFile.1', '1 file changed'); suggestedFilesInWorkingSetCount = entries[0].kind === 'reference' && entries[0].state === WorkingSetEntryState.Suggested ? 1 : 0; } else { suggestedFilesInWorkingSetCount = entries.filter(e => e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length; @@ -1279,10 +1265,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (entries.length > 1) { const fileCount = entries.length - suggestedFilesInWorkingSetCount; - overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount)); + overviewFileCount.textContent = (fileCount === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', fileCount)); } - overviewTitle.ariaLabel = overviewWorkingSet.textContent + overviewFileCount.textContent; + overviewTitle.ariaLabel = overviewFileCount.textContent; overviewTitle.tabIndex = 0; // Clear out the previous actions (if any) @@ -1308,11 +1294,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - if (currentChatEditingState === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress) { - // this._chatEditsProgress ??= new ProgressBar(innerContainer); - this._chatEditsProgress?.infinite().show(500); - } - // Working set const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list')); if (!this._chatEditList) { @@ -1354,53 +1335,42 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge list.layout(height); list.getHTMLElement().style.height = `${height}px`; list.splice(0, list.length, entries); - this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? ({ uri: e.reference, isMarkedReadonly: e.isMarkedReadonly }) : undefined)); - - const addFilesElement = innerContainer.querySelector('.chat-editing-session-toolbar-actions') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-toolbar-actions')); + this._onDidChangeHeight.fire(); + } + async renderChatRelatedFiles(chatEditingSession: IChatEditingSession, anchor: HTMLElement) { + dom.clearNode(anchor); const hoverDelegate = getDefaultHoverDelegate('element'); - - const button = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { - supportIcons: true, - secondary: true, - hoverDelegate - })); - button.label = localize('chatAddFiles', '{0} Add Files...', '$(add)'); - button.setTitle(button.enabled ? localize('addFiles.label', 'Add files to your working set') : localize('chatEditingSession.fileLimitReached', 'You have reached the maximum number of files that can be added to the working set.')); - this._chatEditsActionsDisposables.add(button.onDidClick(() => { - this.commandService.executeCommand('workbench.action.chat.editing.attachFiles', { widget: chatWidget }); - })); - dom.append(addFilesElement, button.element); - - // RELATED files (after Add Files...) for (const [uri, metadata] of chatEditingSession.workingSet) { if (metadata.state !== WorkingSetEntryState.Suggested) { continue; } - const addBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { + const uriLabel = this._chatEditsActionsDisposables.add(new Button(anchor, { supportIcons: true, secondary: true, hoverDelegate })); - addBtn.label = this.labelService.getUriBasenameLabel(uri); - addBtn.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE)); - addBtn.setTitle(localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? '')); + uriLabel.label = this.labelService.getUriBasenameLabel(uri); + uriLabel.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE)); + uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? ''); - this._chatEditsActionsDisposables.add(addBtn.onDidClick(() => { + this._chatEditsActionsDisposables.add(uriLabel.onDidClick(() => { group.remove(); // REMOVE asap - chatEditingSession.addFileToWorkingSet(uri); + this._attachmentModel.addFile(uri); + chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); })); - const rmBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { + const addButton = this._chatEditsActionsDisposables.add(new Button(anchor, { supportIcons: false, secondary: true, hoverDelegate, - ariaLabel: localize('chatEditingSession.removeSuggestion', 'Remove suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })), + ariaLabel: localize('chatEditingSession.addSuggestion', 'Add suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })), })); - rmBtn.icon = Codicon.close; - rmBtn.setTitle(localize('chatEditingSession.removeSuggested', 'Remove suggestion')); - this._chatEditsActionsDisposables.add(rmBtn.onDidClick(() => { + addButton.icon = Codicon.add; + addButton.setTitle(localize('chatEditingSession.addSuggested', 'Add suggestion')); + this._chatEditsActionsDisposables.add(addButton.onDidClick(() => { group.remove(); // REMOVE asap + this._attachmentModel.addFile(uri); chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); })); @@ -1408,17 +1378,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge sep.classList.add('separator'); const group = document.createElement('span'); - group.classList.add('monaco-button-dropdown', 'sidebyside-button'); - group.appendChild(addBtn.element); + group.classList.add('monaco-button-dropdown', 'sidebyside-button', 'show-file-icons'); + group.appendChild(uriLabel.element); group.appendChild(sep); - group.appendChild(rmBtn.element); - dom.append(addFilesElement, group); + group.appendChild(addButton.element); + dom.append(anchor, group); this._chatEditsActionsDisposables.add(toDisposable(() => { group.remove(); })); } - + this._onDidChangeHeight.fire(); } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -1484,7 +1454,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28, - attachmentsHeight: this.attachedContextContainer.offsetHeight, + attachmentsHeight: this.attachedContextContainer.offsetHeight + this.relatedFilesContainer.offsetHeight, editorBorder: 2, inputPartHorizontalPaddingInside: 12, toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0, diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index e79f6690f34..8f5d0743d04 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -578,7 +578,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, workingSet: ReadonlyArray | undefined, templateData: IChatListItemTemplate) { - return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, workingSet, undefined); + private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray | undefined, templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); } private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index fe13bc97df6..412295c39eb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -221,7 +221,7 @@ class QuickChat extends Disposable { ChatWidget, ChatAgentLocation.Panel, { isQuickChat: true }, - { autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide }, enableImplicitContext: true }, + { autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide } }, { listForeground: quickInputForeground, listBackground: quickInputBackground, diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index bb826d44c31..a7ad1507eb6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -185,7 +185,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { referencesExpandedWhenEmptyResponse: this.chatOptions.location !== ChatAgentLocation.EditingSession, progressMessageAtBottomOfResponse: this.chatOptions.location === ChatAgentLocation.EditingSession, }, - enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, editorOverflowWidgetsDomNode: editorOverflowNode, enableWorkingSet: this.chatOptions.location === ChatAgentLocation.EditingSession ? 'explicit' : undefined }, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 0d2cf3f81dc..f2474822d75 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -830,7 +830,6 @@ export class ChatWidget extends Disposable implements IChatWidget { renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, - enableImplicitContext: this.viewOptions.enableImplicitContext, renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit' }, this.styles, @@ -1100,22 +1099,20 @@ export class ChatWidget extends Disposable implements IChatWidget { } let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); - let workingSet: URI[] | undefined; if (this.viewOptions.enableWorkingSet !== undefined) { const currentEditingSession = this._editingSession; const unconfirmedSuggestions = new ResourceSet(); const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set - const editingSessionAttachedContext: IChatRequestVariableEntry[] = []; + const editingSessionAttachedContext: IChatRequestVariableEntry[] = attachedContext; // Pick up everything that the user sees is part of the working set. - // This should never exceed the maximum file entries limit above. - for (const { uri, isMarkedReadonly } of this.inputPart.chatEditWorkingSetFiles) { + for (const [uri, state] of currentEditingSession.get()?.workingSet || []) { // Skip over any suggested files that haven't been confirmed yet in the working set - if (currentEditingSession.get()?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) { + if (state.state === WorkingSetEntryState.Suggested) { unconfirmedSuggestions.add(uri); } else { uniqueWorkingSetEntries.add(uri); - editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(uri, undefined, isMarkedReadonly)); + editingSessionAttachedContext.unshift(this.attachmentModel.asVariableEntry(uri, undefined, state.isMarkedReadonly)); } } @@ -1153,7 +1150,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } } } - workingSet = [...uniqueWorkingSetEntries.values()]; attachedContext = editingSessionAttachedContext; type ChatEditingWorkingSetClassification = { @@ -1166,7 +1162,7 @@ export class ChatWidget extends Disposable implements IChatWidget { originalSize: number; actualSize: number; }; - this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size }); + this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size }); currentEditingSession.get()?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } @@ -1178,7 +1174,6 @@ export class ChatWidget extends Disposable implements IChatWidget { locationData: this._location.resolveData?.(), parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext, - workingSet, noCommandDetection: options?.noCommandDetection, hasInstructionAttachments: this.inputPart.hasInstructionAttachments, }); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 8e7bf6a8e53..c5c3fedf9a6 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -666,7 +666,7 @@ class BuiltinDynamicCompletions extends Disposable { // RELATED FILES if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.getEditingSession(widget.viewModel.sessionId)) { - const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), token), 200)) ?? []; + const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { if (seen.has(relatedFile.uri)) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 46270b02a6b..3860b5d5efa 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -6,12 +6,12 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; +import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; +import { autorun } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ChatAgentLocation } from '../../common/chatAgents.js'; -import { ChatEditingSessionChangeType, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution { @@ -26,16 +26,15 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben ) { super(); - this._register( - this.chatWidgetService.onDidAddWidget(widget => { - if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel?.sessionId) { - const editingSession = this.chatEditingService.getEditingSession(widget.viewModel.sessionId); - if (editingSession) { - this._handleNewEditingSession(editingSession, widget); - } + this._register(autorun((reader) => { + const sessions = this.chatEditingService.editingSessionsObs.read(reader); + sessions.forEach(session => { + const widget = this.chatWidgetService.getWidgetBySessionId(session.chatSessionId); + if (widget && !this.chatEditingSessionDisposables.has(session.chatSessionId)) { + this._handleNewEditingSession(session, widget); } - }), - ); + }); + })); } private _updateRelatedFileSuggestions(currentEditingSession: IChatEditingSession, widget: IChatWidget) { @@ -44,12 +43,12 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben } const workingSetEntries = currentEditingSession.entries.get(); - if (workingSetEntries.length > 0) { + if (workingSetEntries.length > 0 || widget.attachmentModel.fileAttachments.length === 0) { // Do this only for the initial working set state return; } - this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None) + this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => { if (!files?.length || !widget.viewModel?.sessionId) { return; @@ -60,6 +59,11 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben return; // Might have disposed while we were calculating } + const existingFiles = new ResourceSet(widget.attachmentModel.fileAttachments); + if (!existingFiles.size) { + return; + } + // Pick up to 2 related files const newSuggestions = new ResourceMap<{ description: string; group: string }>(); for (const group of files) { @@ -67,7 +71,11 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben if (newSuggestions.size >= 2) { break; } + if (existingFiles.has(file.uri)) { + continue; + } newSuggestions.set(file.uri, { group: group.group, description: file.description }); + existingFiles.add(file.uri); } } @@ -101,10 +109,8 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben disposableStore.add(onDebouncedType(() => { this._updateRelatedFileSuggestions(currentEditingSession, widget); })); - disposableStore.add(currentEditingSession.onDidChange((e) => { - if (e === ChatEditingSessionChangeType.WorkingSet) { - this._updateRelatedFileSuggestions(currentEditingSession, widget); - } + disposableStore.add(widget.attachmentModel.onDidChangeContext(() => { + this._updateRelatedFileSuggestions(currentEditingSession, widget); })); disposableStore.add(currentEditingSession.onDidDispose(() => { disposableStore.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 89cefce6b6c..e9dd2540fca 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -685,25 +685,68 @@ have to be updated for changes to the rules above, or to support more deeply nes justify-content: start; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:first-child { +.chat-related-files { + padding: 4px 0 0 0; + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.chat-related-files .monaco-button-dropdown .monaco-text-button { + font-size: 11px; + justify-content: left; + width: fit-content; + padding: 0px; + border: none; +} + +.chat-related-files .monaco-button-dropdown .monaco-text-button span { + font-style: italic; + height: 18px; + opacity: 0.7; +} + +.chat-related-files .monaco-button-dropdown { + border-radius: 4px; + height: 18px; + border: 1px solid var(--vscode-input-border); + align-items: center; + overflow: hidden; + padding: 0 0 0 2px; +} + +.chat-related-files .monaco-button.codicon.codicon-add { + display: flex; + color: var(--vscode-descriptionForeground); + padding-right: 4px; + padding-left: 4px; + padding-top: 3px; + height: 100%; +} + +.interactive-session .chat-related-files .monaco-icon-label::before { + padding: 2px 2px 0 0; +} + +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary:first-child { margin: 3px 0px 3px 3px; flex-shrink: 0; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary.monaco-icon-label::before { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary.monaco-icon-label::before { display: inline-flex; align-items: center; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:only-child { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary:only-child { width: 100%; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary.disabled { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary.disabled { cursor: initial; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary .codicon { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary .codicon { font-size: 12px; margin-left: 4px; } @@ -1078,6 +1121,13 @@ have to be updated for changes to the rules above, or to support more deeply nes border-radius: 4px; height: 18px; max-width: 100%; + width: fit-content; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment.chat-add-files { + height: 20px; + gap: 0px; + color: var(--vscode-descriptionForeground); } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button { @@ -1091,6 +1141,8 @@ have to be updated for changes to the rules above, or to support more deeply nes outline-offset: -4px; } +.chat-related-files .monaco-button.codicon.codicon-add:hover, +.interactive-session .chat-attached-context .chat-attached-context-attachment.chat-add-files:hover, .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { cursor: pointer; background: var(--vscode-toolbar-hoverBackground); @@ -1122,9 +1174,8 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context { - padding: 0 0 4px 0; + padding: 5px 0 0 0; display: flex; - gap: 4px; flex-wrap: wrap; cursor: default; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 0fd3869b4aa..0f4d8ff0dc9 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -41,7 +41,7 @@ export interface IChatEditingService { hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; - getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; //#endregion } @@ -88,7 +88,7 @@ export interface IChatEditingSession extends IDisposable { readonly state: IObservable; readonly entries: IObservable; readonly workingSet: ResourceMap; - addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; + addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Suggested): void; show(): Promise; remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; markIsReadonly(uri: URI, isReadonly?: boolean): void; @@ -155,7 +155,7 @@ export const enum WorkingSetEntryState { Modified, Accepted, Rejected, - Transient, + Transient, // TODO@joyceerhl remove this Attached, Sent, // TODO@joyceerhl remove this Suggested, diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 5a8a97a0d16..dec4623cd07 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -125,7 +125,6 @@ export interface IChatRequestModel { readonly confirmation?: string; readonly locationData?: IChatLocationData; readonly attachedContext?: IChatRequestVariableEntry[]; - readonly workingSet?: URI[]; readonly isCompleteAddedRequest: boolean; readonly response?: IChatResponseModel; shouldBeRemovedOnSend: IChatRequestDisablement | undefined; @@ -290,10 +289,6 @@ export class ChatRequestModel implements IChatRequestModel { return this._attachedContext; } - public get workingSet(): URI[] | undefined { - return this._workingSet; - } - constructor( private _session: ChatModel, public readonly message: IParsedChatRequest, @@ -303,7 +298,6 @@ export class ChatRequestModel implements IChatRequestModel { private _confirmation?: string, private _locationData?: IChatLocationData, private _attachedContext?: IChatRequestVariableEntry[], - private _workingSet?: URI[], public readonly isCompleteAddedRequest = false, restoredId?: string, ) { @@ -1254,7 +1248,7 @@ export class ChatModel extends Disposable implements IChatModel { // Old messages don't have variableData, or have it in the wrong (non-array) shape const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); - const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, raw.workingSet?.map((uri) => URI.revive(uri)), undefined, raw.requestId); + const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, undefined, raw.requestId); request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format @@ -1388,7 +1382,7 @@ export class ChatModel extends Disposable implements IChatModel { } addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], workingSet?: URI[], isCompleteAddedRequest?: boolean): ChatRequestModel { - const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, workingSet, isCompleteAddedRequest); + const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, isCompleteAddedRequest); request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id, undefined, undefined, undefined, undefined, undefined, undefined, isCompleteAddedRequest); this._requests.push(request); @@ -1548,7 +1542,6 @@ export class ChatModel extends Disposable implements IChatModel { vote: r.response?.vote, voteDownReason: r.response?.voteDownReason, agent: agentJson, - workingSet: r.workingSet, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences, diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 642855f9f83..b2246a10052 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -438,7 +438,6 @@ export interface IChatSendRequestOptions { acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; attachedContext?: IChatRequestVariableEntry[]; - workingSet?: URI[]; /** The target agent ID can be specified with this property instead of using @ in 'message' */ agentId?: string; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 9622f30adba..6e5e83b27a0 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -476,7 +476,6 @@ export class ChatService extends Disposable implements IChatService { ...options, locationData: request.locationData, attachedContext: request.attachedContext, - workingSet: request.workingSet, hasInstructionAttachments: options?.hasInstructionAttachments ?? false, }; await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; @@ -635,7 +634,7 @@ export class ChatService extends Disposable implements IChatService { if (agentPart || (defaultAgent && !commandPart)) { const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): Promise => { const initVariableData: IChatRequestVariableData = { variables: [] }; - request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, options?.workingSet); + request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext); let variableData: IChatRequestVariableData; let message: string; @@ -644,11 +643,6 @@ export class ChatService extends Disposable implements IChatService { message = getPromptText(request.message).message; } else { variableData = this.chatVariablesService.resolveVariables(parsedRequest, request.attachedContext); - for (const variable of variableData.variables) { - if (request.workingSet && variable.isFile && URI.isUri(variable.value)) { - request.workingSet.push(variable.value); - } - } model.updateRequest(request, variableData); const promptTextResult = getPromptText(request.message); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 0dadc461d0c..88d973ecc41 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -74,7 +74,6 @@ export interface IChatRequestViewModel { readonly variables: IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; readonly contentReferences?: ReadonlyArray; - readonly workingSet?: ReadonlyArray; readonly confirmation?: string; readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined; readonly isComplete: boolean; @@ -379,10 +378,6 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.response?.contentReferences; } - get workingSet() { - return this._model.workingSet; - } - get confirmation() { return this._model.confirmation; } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 127a458fd8a..2de45db43d1 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -78,7 +78,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: { documents: [ diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index dd966544c50..0a9e4d67229 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -78,7 +78,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index 637d3801e22..36fd2784d41 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -85,7 +85,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: { documents: [ @@ -154,7 +153,6 @@ disambiguation: [ ], isDefault: true }, - workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 9ebead9b671..8e5fd37cea2 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -78,7 +78,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], From 5ffa9a30cb19614bc25070cd6103e6c0cea1a135 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 19 Feb 2025 17:09:37 -0600 Subject: [PATCH 05/36] use builtin over spec if it has more command properties (#241238) fix #241220 Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../src/terminalSuggestMain.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 46158182f6e..08965a0e58d 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -239,7 +239,6 @@ export async function getCompletionItemsFromSpecs( for (const command of availableCommands) { const commandTextLabel = typeof command.label === 'string' ? command.label : command.label.label; if (!labels.has(commandTextLabel)) { - //TODO: maybe prioritize builtin label over spec's items.push(createCompletionItem( terminalContext.cursorPosition, prefix, @@ -248,6 +247,15 @@ export async function getCompletionItemsFromSpecs( command.documentation )); labels.add(commandTextLabel); + } else { + const existingItem = items.find(i => (typeof i.label === 'string' ? i.label : i.label.label) === commandTextLabel); + if (!existingItem) { + continue; + } + const preferredItem = compareItems(existingItem, command); + if (preferredItem) { + items.splice(items.indexOf(existingItem), 1, preferredItem); + } } } filesRequested = true; @@ -269,6 +277,20 @@ export async function getCompletionItemsFromSpecs( return { items, filesRequested, foldersRequested, cwd }; } +function compareItems(existingItem: vscode.TerminalCompletionItem, command: ICompletionResource): vscode.TerminalCompletionItem | undefined { + let score = typeof command.label === 'object' ? (command.label.detail !== undefined ? 1 : 0) : 0; + score += typeof command.label === 'object' ? (command.label.description !== undefined ? 2 : 0) : 0; + score += command.documentation ? typeof command.documentation === 'string' ? 2 : 3 : 0; + if (score > 0) { + score -= typeof existingItem.label === 'object' ? (existingItem.label.detail !== undefined ? 1 : 0) : 0; + score -= typeof existingItem.label === 'object' ? (existingItem.label.description !== undefined ? 2 : 0) : 0; + score -= existingItem.documentation ? typeof existingItem.documentation === 'string' ? 2 : 3 : 0; + if (score >= 0) { + return { ...command, replacementIndex: existingItem.replacementIndex, replacementLength: existingItem.replacementLength }; + } + } +} + function getShell(shellType: TerminalShellType): string | undefined { switch (shellType) { case TerminalShellType.Bash: From 3d46cb8d5c9de29618cb001b0eec35ed663acb7e Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:55:02 -0800 Subject: [PATCH 06/36] fix: add fallback comparison (#241266) Fixes #238918 --- .../contrib/preferences/browser/settingsTreeModels.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index f900df1c689..a267f7d219f 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -963,7 +963,8 @@ export class SearchResultModel extends SettingsTreeModel { } else if (a.matchType === SettingMatchType.KeyMatch) { // The match types are the same and are KeyMatch. // Sort by the number of words matched in the key. - return b.keyMatchScore - a.keyMatchScore; + // If those are the same, sort by the order in the table of contents. + return (b.keyMatchScore - a.keyMatchScore) || compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder); } else if (a.matchType === SettingMatchType.RemoteMatch) { // The match types are the same and are RemoteMatch. // Sort by score. From 84e814b9c52eb120bf027b458ccab4e1bb5bd395 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 18 Feb 2025 13:18:04 -0800 Subject: [PATCH 07/36] move parts of config/constants under /platform to share those with more code --- .../prompts/common}/config.ts | 90 ++--------- src/vs/platform/prompts/common/constants.ts | 43 ++++++ .../common/promptsSync/promptsSync.ts | 3 +- .../showPromptSelectionDialog.ts | 8 +- .../instructionsAttachment.ts | 4 +- .../contrib/chat/browser/chat.contribution.ts | 26 +++- .../chatInstructionAttachmentsModel.ts | 4 +- .../browser/contrib/chatDynamicVariables.ts | 4 +- .../dialogs/askForPromptName.ts | 2 +- .../utils/createPromptFile.ts | 7 +- .../contributions/usePromptCommand.ts | 4 +- .../chat/common/promptSyntax/constants.ts | 7 +- .../promptContentsProviderBase.ts | 4 +- .../promptSyntax/parsers/basePromptParser.ts | 11 +- .../promptSyntax/utils/promptFilesLocator.ts | 10 +- .../test/common/promptSyntax/config.test.ts | 140 +++++++++--------- .../utils/promptFilesLocator.test.ts | 6 +- .../userDataSync/browser/userDataSync.ts | 20 ++- 18 files changed, 190 insertions(+), 203 deletions(-) rename src/vs/{workbench/contrib/chat/common/promptSyntax => platform/prompts/common}/config.ts (78%) create mode 100644 src/vs/platform/prompts/common/constants.ts diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/src/vs/platform/prompts/common/config.ts similarity index 78% rename from src/vs/workbench/contrib/chat/common/promptSyntax/config.ts rename to src/vs/platform/prompts/common/config.ts index bbfe61e4929..6658b3858c9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts +++ b/src/vs/platform/prompts/common/config.ts @@ -3,9 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from '../../../../../nls.js'; -import { DOCUMENTATION_URL, PROMPT_FILE_EXTENSION } from './constants.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; + +/** + * TODO: @legomushroom + * - update doc comment for config.ts + * - move unit tests for config.ts + * - add unit tests for config.ts + */ /** * `!Note!` This doc comment is deprecated and is set to be updated during `debt` week. @@ -119,7 +124,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * - root of each top-level folder in the workspace (if there are multiple workspace folders) * - current root folder (if a single folder is open) */ -export namespace PromptFilesConfig { +export namespace PromptsConfig { /** * Configuration key for the `prompt files` feature (also * known as `prompt files`, `prompt instructions`, etc.). @@ -136,40 +141,13 @@ export namespace PromptFilesConfig { */ export const getValue = ( configService: IConfigurationService, - ): string | string[] | Record | boolean | undefined => { + ): Record | undefined => { const configValue = configService.getValue(CONFIG_KEY); - if (configValue === undefined || configValue === null) { + if (configValue === undefined || configValue === null || Array.isArray(configValue)) { return undefined; } - if (typeof configValue === 'string') { - const trimmedValue = configValue.trim(); - const lowercasedValue = trimmedValue.toLowerCase(); - - if (!lowercasedValue) { - return undefined; - } - - if (asBoolean(lowercasedValue) !== undefined) { - return asBoolean(lowercasedValue); - } - - return trimmedValue; - } - - if (typeof configValue === 'boolean') { - return configValue; - } - - if (Array.isArray(configValue)) { - const cleanArray = configValue.filter((item) => { - return typeof item === 'string' && !!item.trim(); - }); - - return cleanArray; - } - // note! this would be also true for `null` and `array`, // but those cases are already handled above if (typeof configValue === 'object') { @@ -200,7 +178,7 @@ export namespace PromptFilesConfig { ): boolean => { const value = getValue(configService); - return value !== undefined && value !== false; + return value !== undefined; }; /** @@ -212,32 +190,6 @@ export namespace PromptFilesConfig { ): string[] => { const value = getValue(configService); - if (value === true) { - return [DEFAULT_SOURCE_FOLDER]; - } - - if (typeof value === 'string') { - const result = [DEFAULT_SOURCE_FOLDER]; - const trimmedValue = value.trim(); - - if (trimmedValue !== DEFAULT_SOURCE_FOLDER) { - result.push(trimmedValue); - } - - return result; - } - - if (Array.isArray(value)) { - const result = [DEFAULT_SOURCE_FOLDER]; - - return [ - ...result, - ...value.filter((item) => { - return item !== DEFAULT_SOURCE_FOLDER; - }), - ]; - } - // note! the `value &&` part handles the `undefined`, `null`, and `false` cases if (value && (typeof value === 'object')) { const paths: string[] = []; @@ -260,24 +212,6 @@ export namespace PromptFilesConfig { // `undefined`, `null`, and `false` cases return []; }; - - /** - * Configuration setting description to use in the settings UI. - */ - export const CONFIG_DESCRIPTION = nls.localize( - 'chat.promptFiles.config.description', - "Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", - PROMPT_FILE_EXTENSION, - DOCUMENTATION_URL, - ); - - /** - * Configuration setting title to use in the settings UI. - */ - export const CONFIG_TITLE = nls.localize( - 'chat.promptFiles.config.title', - "Prompt Files", - ); } /** diff --git a/src/vs/platform/prompts/common/constants.ts b/src/vs/platform/prompts/common/constants.ts new file mode 100644 index 00000000000..a222dc61034 --- /dev/null +++ b/src/vs/platform/prompts/common/constants.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../base/common/uri.js'; +import { assert } from '../../../base/common/assert.js'; +import { basename } from '../../../base/common/resources.js'; + +/** + * File extension for the reusable prompt files. + */ +export const PROMPT_FILE_EXTENSION = '.prompt.md'; + +/** + * Check if provided path is a prompt file. + */ +export const isPromptFile = ( + fileUri: URI, +): boolean => { + return fileUri + .path + .endsWith(PROMPT_FILE_EXTENSION); +}; + +/** + * Gets clean prompt name without file extension. + * + * @throws If provided path is not a prompt file + * (does not end with {@link PROMPT_FILE_EXTENSION}). + */ +export const getCleanPromptName = ( + fileUri: URI, +): string => { + assert( + isPromptFile(fileUri), + `Provided path '${fileUri.fsPath}' is not a prompt file.`, + ); + + const fileBasename = basename(fileUri); + return fileBasename + .substring(0, fileBasename.length - PROMPT_FILE_EXTENSION.length); +}; diff --git a/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts b/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts index bdb2216f198..918e1cf6eb8 100644 --- a/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts +++ b/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts @@ -7,6 +7,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { deepClone } from '../../../../base/common/objects.js'; +import { isPromptFile } from '../../../prompts/common/constants.js'; import { IStorageService } from '../../../storage/common/storage.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; @@ -516,7 +517,7 @@ export class PromptsSynchronizer extends AbstractSynchroniser implements IUserDa for (const entry of stat.children || []) { const resource = entry.resource; - if (!resource.path.endsWith('.prompt.md')) { + if (!isPromptFile(resource)) { continue; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/showPromptSelectionDialog.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/showPromptSelectionDialog.ts index d010e00950c..2c71d30d3dd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/showPromptSelectionDialog.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/showPromptSelectionDialog.ts @@ -7,12 +7,13 @@ import { IChatWidget } from '../../chat.js'; import { localize } from '../../../../../../nls.js'; import { URI } from '../../../../../../base/common/uri.js'; import { WithUriValue } from '../../../../../../base/common/types.js'; +import { dirname, extUri } from '../../../../../../base/common/resources.js'; +import { DOCUMENTATION_URL } from '../../../common/promptSyntax/constants.js'; import { isLinux, isWindows } from '../../../../../../base/common/platform.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; -import { basename, dirname, extUri } from '../../../../../../base/common/resources.js'; import { IPromptPath, IPromptsService } from '../../../common/promptSyntax/service/types.js'; -import { DOCUMENTATION_URL, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/constants.js'; +import { getCleanPromptName } from '../../../../../../platform/prompts/common/constants.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../../../platform/quickinput/common/quickInput.js'; @@ -66,8 +67,7 @@ const createPickItem = ( { uri }: IPromptPath, labelService: ILabelService, ): WithUriValue => { - const fileBasename = basename(uri); - const fileWithoutExtension = fileBasename.replace(PROMPT_FILE_EXTENSION, ''); + const fileWithoutExtension = getCleanPromptName(uri); return { type: 'item', diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index 01c53e83a1e..dc2742e1934 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -17,11 +17,11 @@ import { ILabelService } from '../../../../../../platform/label/common/label.js' import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; import { IModelService } from '../../../../../../editor/common/services/model.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; -import { PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/constants.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { FileKind, IFileService } from '../../../../../../platform/files/common/files.js'; import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { getCleanPromptName } from '../../../../../../platform/prompts/common/constants.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js'; @@ -132,7 +132,7 @@ export class InstructionsAttachmentWidget extends Disposable { title += `\n-\n[${errorCaption}]: ${details}`; } - const fileWithoutExtension = fileBasename.replace(PROMPT_FILE_EXTENSION, ''); + const fileWithoutExtension = getCleanPromptName(file); label.setFile(URI.file(fileWithoutExtension), { fileKind: FileKind.FILE, hidePath: true, diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 223c03d3ba7..e1110b3e84f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -40,7 +40,6 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; -import { PromptFilesConfig } from '../common/promptSyntax/config.js'; import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; import '../common/promptSyntax/languageFeatures/promptPathAutocompletion.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; @@ -93,6 +92,9 @@ import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesCon import { LanguageModelToolsService } from './languageModelToolsService.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js'; +import { PromptsConfig } from '../../../../platform/prompts/common/config.js'; +import { PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; +import { DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -162,14 +164,22 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.detectParticipant.enabled', "Enables chat participant autodetection for panel chat."), default: true }, - [PromptFilesConfig.CONFIG_KEY]: { + [PromptsConfig.CONFIG_KEY]: { type: 'object', - title: PromptFilesConfig.CONFIG_TITLE, - markdownDescription: PromptFilesConfig.CONFIG_DESCRIPTION, + title: nls.localize( + 'chat.promptFiles.config.title', + "Prompt Files", + ), + markdownDescription: nls.localize( + 'chat.promptFiles.config.description', + "Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", + PROMPT_FILE_EXTENSION, + DOCUMENTATION_URL, + ), default: { - [PromptFilesConfig.DEFAULT_SOURCE_FOLDER]: false, + [PromptsConfig.DEFAULT_SOURCE_FOLDER]: false, }, - required: [PromptFilesConfig.DEFAULT_SOURCE_FOLDER], + required: [PromptsConfig.DEFAULT_SOURCE_FOLDER], additionalProperties: { type: 'boolean' }, unevaluatedProperties: { type: 'boolean' }, restricted: true, @@ -177,10 +187,10 @@ configurationRegistry.registerConfiguration({ tags: ['experimental'], examples: [ { - [PromptFilesConfig.DEFAULT_SOURCE_FOLDER]: true, + [PromptsConfig.DEFAULT_SOURCE_FOLDER]: true, }, { - [PromptFilesConfig.DEFAULT_SOURCE_FOLDER]: true, + [PromptsConfig.DEFAULT_SOURCE_FOLDER]: true, '/Users/vscode/repos/prompts': true, }, ], diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index c03f610abc1..144e5bb96a1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -6,7 +6,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; -import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; +import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js'; import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; @@ -214,6 +214,6 @@ export class ChatInstructionAttachmentsModel extends Disposable { * Checks if the prompt instructions feature is enabled in the user settings. */ public get featureEnabled(): boolean { - return PromptFilesConfig.enabled(this.configService); + return PromptsConfig.enabled(this.configService); } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 62193a4a3a1..9c805598653 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -28,7 +28,6 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm import { getExcludes, ISearchConfiguration, IFileQuery, QueryType, ISearchComplete, ISearchService } from '../../../../services/search/common/search.js'; import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; -import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import * as glob from '../../../../../base/common/glob.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; @@ -36,6 +35,7 @@ import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; +import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; @@ -139,7 +139,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { // use `ChatFileReference` for file references and `IDynamicVariable` for other variables - const promptSnippetsEnabled = PromptFilesConfig.enabled(this.configService); + const promptSnippetsEnabled = PromptsConfig.enabled(this.configService); const variable = (ref.id === 'vscode.file' && promptSnippetsEnabled) ? this.instantiationService.createInstance(ChatFileReference, ref) : ref; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts index 6b7b69d581b..0c7ff2db72f 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../../../../nls.js'; -import { PROMPT_FILE_EXTENSION } from '../../../../../common/promptSyntax/constants.js'; +import { PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; import { IQuickInputService } from '../../../../../../../../platform/quickinput/common/quickInput.js'; /** diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts index 707e6c18142..296937b25ef 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts @@ -9,8 +9,7 @@ import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; import { dirname } from '../../../../../../../../base/common/resources.js'; import { FolderExists, InvalidPromptName, PromptExists } from '../errors.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; -import { PROMPT_FILE_EXTENSION } from '../../../../../common/promptSyntax/constants.js'; -import { BasePromptParser } from '../../../../../common/promptSyntax/parsers/basePromptParser.js'; +import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; /** * Options for the {@link createPromptFile} utility. @@ -40,7 +39,7 @@ interface ICreatePromptFileOptions { * the provided file content. * * @throws in the following cases: - * - if the prompt filename does not end with {@link PROMPT_FILE_EXTENSION} + * - if the `fileName` does not end with {@link PROMPT_FILE_EXTENSION} * - if a folder or file with the same already name exists in the destination folder */ export const createPromptFile = async ( @@ -51,7 +50,7 @@ export const createPromptFile = async ( const promptUri = URI.joinPath(folder, fileName); assert( - BasePromptParser.isPromptSnippet(promptUri), + isPromptFile(promptUri), new InvalidPromptName(fileName), ); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts index 7bcd4b19a7c..0b89425d47b 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts @@ -8,9 +8,9 @@ import { URI } from '../../../../../../base/common/uri.js'; import { CHAT_CATEGORY } from '../../actions/chatActions.js'; import { IChatWidget, IChatWidgetService } from '../../chat.js'; import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { BasePromptParser } from '../../../common/promptSyntax/parsers/basePromptParser.js'; import { appendToCommandPalette } from '../../../../files/browser/fileActions.contribution.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../../editor/browser/editorBrowser.js'; @@ -133,7 +133,7 @@ const getActivePromptUri = ( } const { uri } = activeEditor.getModel(); - if (BasePromptParser.isPromptSnippet(uri)) { + if (isPromptFile(uri)) { return uri; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts index 11d67b1c6df..3768c6c5c7a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts @@ -3,16 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js'; + /** * Documentation link for the reusable prompts feature. */ export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; -/** - * File extension for the reusable prompt files. - */ -export const PROMPT_FILE_EXTENSION = '.prompt.md'; - /** * Prompt files language selector. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts index be608ef00d2..11cb5f84f19 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { IPromptContentsProvider } from './types.js'; -import { PROMPT_FILE_EXTENSION } from '../constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { assert } from '../../../../../../base/common/assert.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { FailedToResolveContentsStream, ParseError } from '../../promptFileReferenceErrors.js'; import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js'; @@ -145,6 +145,6 @@ export abstract class PromptContentsProviderBase< * Check if the current URI points to a prompt snippet. */ public isPromptSnippet(): boolean { - return this.uri.path.endsWith(PROMPT_FILE_EXTENSION); + return isPromptFile(this.uri); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index c6280db36f6..4e6ce0657b9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../../nls.js'; -import { PROMPT_FILE_EXTENSION } from '../constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; import { Emitter } from '../../../../../../base/common/event.js'; @@ -19,6 +18,7 @@ import { DeferredPromise } from '../../../../../../base/common/async.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { basename, extUri } from '../../../../../../base/common/resources.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; +import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -503,18 +503,11 @@ export abstract class BasePromptParser extend return this.uri.toString() === otherUri.toString(); } - /** - * Check if the provided URI points to a prompt snippet. - */ - public static isPromptSnippet(uri: URI): boolean { - return uri.path.endsWith(PROMPT_FILE_EXTENSION); - } - /** * Check if the current reference points to a prompt snippet file. */ public get isPromptSnippet(): boolean { - return BasePromptParser.isPromptSnippet(this.uri); + return isPromptFile(this.uri); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 222375d8812..2d787402fbe 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PromptFilesConfig } from '../config.js'; -import { PROMPT_FILE_EXTENSION } from '../constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; import { dirname, extUri } from '../../../../../../base/common/resources.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -71,7 +71,7 @@ export class PromptFilesLocator { */ public getConfigBasedSourceFolders(): readonly URI[] { const paths = new ResourceSet(); - const sourceFolders = PromptFilesConfig.promptSourceFolders(this.configService); + const sourceFolders = PromptsConfig.promptSourceFolders(this.configService); // otherwise for each folder provided in the configuration, create // a URI per each folder in the current workspace @@ -154,13 +154,13 @@ export class PromptFilesLocator { } for (const child of stat.children) { - const { name, resource, isDirectory } = child; + const { resource, isDirectory } = child; if (isDirectory) { continue; } - if (!name.endsWith(PROMPT_FILE_EXTENSION)) { + if (!isPromptFile(resource)) { continue; } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts index 77473b9cb0b..081eb173258 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts @@ -6,8 +6,8 @@ import assert from 'assert'; import { mockService } from './testUtils/mock.js'; import { randomInt } from '../../../../../../base/common/numbers.js'; -import { PromptFilesConfig } from '../../../common/promptSyntax/config.js'; import { randomBoolean } from '../../../../../../base/test/common/testUtils.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationOverrides, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -19,8 +19,8 @@ const createMock = (value: T): IConfigurationService => { getValue(key?: string | IConfigurationOverrides) { assert.strictEqual( key, - PromptFilesConfig.CONFIG_KEY, - `Mocked service supports only one configuration key: '${PromptFilesConfig.CONFIG_KEY}'.`, + PromptsConfig.CONFIG_KEY, + `Mocked service supports only one configuration key: '${PromptsConfig.CONFIG_KEY}'.`, ); return value; @@ -36,7 +36,7 @@ suite('PromptFilesConfig', () => { const configService = createMock(undefined); assert.strictEqual( - PromptFilesConfig.getValue(configService), + PromptsConfig.getValue(configService), undefined, 'Must read correct value.', ); @@ -46,7 +46,7 @@ suite('PromptFilesConfig', () => { const configService = createMock(null); assert.strictEqual( - PromptFilesConfig.getValue(configService), + PromptsConfig.getValue(configService), undefined, 'Must read correct value.', ); @@ -55,43 +55,43 @@ suite('PromptFilesConfig', () => { suite('• string', () => { test('• empty', () => { assert.strictEqual( - PromptFilesConfig.getValue(createMock('')), + PromptsConfig.getValue(createMock('')), undefined, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock(' ')), + PromptsConfig.getValue(createMock(' ')), undefined, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('\t')), + PromptsConfig.getValue(createMock('\t')), undefined, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('\v')), + PromptsConfig.getValue(createMock('\v')), undefined, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('\f')), + PromptsConfig.getValue(createMock('\f')), undefined, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('\n')), + PromptsConfig.getValue(createMock('\n')), undefined, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('\r\n')), + PromptsConfig.getValue(createMock('\r\n')), undefined, 'Must read correct value.', ); @@ -99,19 +99,19 @@ suite('PromptFilesConfig', () => { test('• true', () => { assert.strictEqual( - PromptFilesConfig.getValue(createMock('true')), + PromptsConfig.getValue(createMock('true')), true, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('TRUE')), + PromptsConfig.getValue(createMock('TRUE')), true, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('TrUe')), + PromptsConfig.getValue(createMock('TrUe')), true, 'Must read correct value.', ); @@ -119,19 +119,19 @@ suite('PromptFilesConfig', () => { test('• false', () => { assert.strictEqual( - PromptFilesConfig.getValue(createMock('false')), + PromptsConfig.getValue(createMock('false')), false, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('FALSE')), + PromptsConfig.getValue(createMock('FALSE')), false, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('fAlSe')), + PromptsConfig.getValue(createMock('fAlSe')), false, 'Must read correct value.', ); @@ -139,49 +139,49 @@ suite('PromptFilesConfig', () => { test('• non-empty', () => { assert.strictEqual( - PromptFilesConfig.getValue(createMock('/absolute/path/to/folder')), + PromptsConfig.getValue(createMock('/absolute/path/to/folder')), '/absolute/path/to/folder', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('/Absolute/path/to/folder')), + PromptsConfig.getValue(createMock('/Absolute/path/to/folder')), '/Absolute/path/to/folder', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('./relative-path/to/folder')), + PromptsConfig.getValue(createMock('./relative-path/to/folder')), './relative-path/to/folder', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('./relative-path/To/folder')), + PromptsConfig.getValue(createMock('./relative-path/To/folder')), './relative-path/To/folder', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('.github/prompts')), + PromptsConfig.getValue(createMock('.github/prompts')), '.github/prompts', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('.github/Prompts')), + PromptsConfig.getValue(createMock('.github/Prompts')), '.github/Prompts', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('/abs/path/to/file.prompt.md')), + PromptsConfig.getValue(createMock('/abs/path/to/file.prompt.md')), '/abs/path/to/file.prompt.md', 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock('/Abs/Path/To/File.prompt.md')), + PromptsConfig.getValue(createMock('/Abs/Path/To/File.prompt.md')), '/Abs/Path/To/File.prompt.md', 'Must read correct value.', ); @@ -190,13 +190,13 @@ suite('PromptFilesConfig', () => { test('• boolean', () => { assert.strictEqual( - PromptFilesConfig.getValue(createMock(true)), + PromptsConfig.getValue(createMock(true)), true, 'Must read correct value.', ); assert.strictEqual( - PromptFilesConfig.getValue(createMock(false)), + PromptsConfig.getValue(createMock(false)), false, 'Must read correct value.', ); @@ -205,7 +205,7 @@ suite('PromptFilesConfig', () => { suite('• array', () => { test('• empty', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock([])), + PromptsConfig.getValue(createMock([])), [], 'Must read correct value.', ); @@ -213,7 +213,7 @@ suite('PromptFilesConfig', () => { test('• valid strings', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock([ + PromptsConfig.getValue(createMock([ '/absolute/path/to/folder', './relative-path/to/folder', './another-Relative/Path/to/folder', @@ -235,7 +235,7 @@ suite('PromptFilesConfig', () => { test('• filters out not valid string values', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock([ + PromptsConfig.getValue(createMock([ '/usr/local/bin/.hidden-tool', '../config/.env.example', [ @@ -279,7 +279,7 @@ suite('PromptFilesConfig', () => { test('• only invalid values', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock([ + PromptsConfig.getValue(createMock([ null, undefined, '', @@ -306,7 +306,7 @@ suite('PromptFilesConfig', () => { suite('• object', () => { test('• empty', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock({})), + PromptsConfig.getValue(createMock({})), {}, 'Must read correct value.', ); @@ -314,7 +314,7 @@ suite('PromptFilesConfig', () => { test('• only valid strings', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock({ + PromptsConfig.getValue(createMock({ '/root/.bashrc': true, '../../folder/.hidden-folder/config.xml': true, '/srv/www/Public_html/.htaccess': true, @@ -348,7 +348,7 @@ suite('PromptFilesConfig', () => { test('• filters out non valid entries', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock({ + PromptsConfig.getValue(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '../assets/img/logo.v2.png': true, @@ -387,7 +387,7 @@ suite('PromptFilesConfig', () => { test('• only invalid or false values', () => { assert.deepStrictEqual( - PromptFilesConfig.getValue(createMock({ + PromptsConfig.getValue(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '../assets/IMG/logo.v2.png': '', @@ -413,7 +413,7 @@ suite('PromptFilesConfig', () => { const configService = createMock(undefined); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(configService), + PromptsConfig.promptSourceFolders(configService), [], 'Must read correct value.', ); @@ -423,7 +423,7 @@ suite('PromptFilesConfig', () => { const configService = createMock(null); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(configService), + PromptsConfig.promptSourceFolders(configService), [], 'Must read correct value.', ); @@ -432,43 +432,43 @@ suite('PromptFilesConfig', () => { suite('• string', () => { test('• empty', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('')), + PromptsConfig.promptSourceFolders(createMock('')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock(' ')), + PromptsConfig.promptSourceFolders(createMock(' ')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('\t')), + PromptsConfig.promptSourceFolders(createMock('\t')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('\v')), + PromptsConfig.promptSourceFolders(createMock('\v')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('\f')), + PromptsConfig.promptSourceFolders(createMock('\f')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('\n')), + PromptsConfig.promptSourceFolders(createMock('\n')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('\r\n')), + PromptsConfig.promptSourceFolders(createMock('\r\n')), [], 'Must read correct value.', ); @@ -476,19 +476,19 @@ suite('PromptFilesConfig', () => { test('• true', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('true')), + PromptsConfig.promptSourceFolders(createMock('true')), ['.github/prompts'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('TRUE')), + PromptsConfig.promptSourceFolders(createMock('TRUE')), ['.github/prompts'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('TrUe')), + PromptsConfig.promptSourceFolders(createMock('TrUe')), ['.github/prompts'], 'Must read correct value.', ); @@ -496,19 +496,19 @@ suite('PromptFilesConfig', () => { test('• false', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('false')), + PromptsConfig.promptSourceFolders(createMock('false')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('FALSE')), + PromptsConfig.promptSourceFolders(createMock('FALSE')), [], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('fAlSe')), + PromptsConfig.promptSourceFolders(createMock('fAlSe')), [], 'Must read correct value.', ); @@ -516,49 +516,49 @@ suite('PromptFilesConfig', () => { test('• non-empty', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('/absolute/path/to/folder')), + PromptsConfig.promptSourceFolders(createMock('/absolute/path/to/folder')), ['.github/prompts', '/absolute/path/to/folder'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('/Absolute/path/to/folder')), + PromptsConfig.promptSourceFolders(createMock('/Absolute/path/to/folder')), ['.github/prompts', '/Absolute/path/to/folder'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('./relative-path/to/folder')), + PromptsConfig.promptSourceFolders(createMock('./relative-path/to/folder')), ['.github/prompts', './relative-path/to/folder'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('./relative-path/To/folder')), + PromptsConfig.promptSourceFolders(createMock('./relative-path/To/folder')), ['.github/prompts', './relative-path/To/folder'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('.github/prompts')), + PromptsConfig.promptSourceFolders(createMock('.github/prompts')), ['.github/prompts'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('.github/Prompts')), + PromptsConfig.promptSourceFolders(createMock('.github/Prompts')), ['.github/prompts', '.github/Prompts'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('/abs/path/to/file.prompt.md')), + PromptsConfig.promptSourceFolders(createMock('/abs/path/to/file.prompt.md')), ['.github/prompts', '/abs/path/to/file.prompt.md'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock('/Abs/Path/To/File.prompt.md')), + PromptsConfig.promptSourceFolders(createMock('/Abs/Path/To/File.prompt.md')), ['.github/prompts', '/Abs/Path/To/File.prompt.md'], 'Must read correct value.', ); @@ -567,13 +567,13 @@ suite('PromptFilesConfig', () => { test('• boolean', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock(true)), + PromptsConfig.promptSourceFolders(createMock(true)), ['.github/prompts'], 'Must read correct value.', ); assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock(false)), + PromptsConfig.promptSourceFolders(createMock(false)), [], 'Must read correct value.', ); @@ -582,7 +582,7 @@ suite('PromptFilesConfig', () => { suite('• array', () => { test('• empty', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock([])), + PromptsConfig.promptSourceFolders(createMock([])), ['.github/prompts'], 'Must read correct value.', ); @@ -590,7 +590,7 @@ suite('PromptFilesConfig', () => { test('• valid strings', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock([ + PromptsConfig.promptSourceFolders(createMock([ '/absolute/path/to/folder', './relative-path/to/folder', './another-Relative/Path/to/folder', @@ -614,7 +614,7 @@ suite('PromptFilesConfig', () => { test('• filters out not valid string values', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock([ + PromptsConfig.promptSourceFolders(createMock([ '/usr/local/bin/.hidden-tool', '../config/.env.example', [ @@ -659,7 +659,7 @@ suite('PromptFilesConfig', () => { test('• only invalid values', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock([ + PromptsConfig.promptSourceFolders(createMock([ null, undefined, '', @@ -686,7 +686,7 @@ suite('PromptFilesConfig', () => { suite('• object', () => { test('• empty', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock({})), + PromptsConfig.promptSourceFolders(createMock({})), ['.github/prompts'], 'Must read correct value.', ); @@ -694,7 +694,7 @@ suite('PromptFilesConfig', () => { test('• only valid strings', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock({ + PromptsConfig.promptSourceFolders(createMock({ '/root/.bashrc': true, '../../folder/.hidden-folder/config.xml': true, '/srv/www/Public_html/.htaccess': true, @@ -731,7 +731,7 @@ suite('PromptFilesConfig', () => { test('• filters out non valid entries', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock({ + PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '../assets/img/logo.v2.png': true, @@ -773,7 +773,7 @@ suite('PromptFilesConfig', () => { test('• only invalid or false values', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock({ + PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '../assets/IMG/logo.v2.png': '', @@ -794,7 +794,7 @@ suite('PromptFilesConfig', () => { test('• filters out disabled default location', () => { assert.deepStrictEqual( - PromptFilesConfig.promptSourceFolders(createMock({ + PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '.github/prompts': false, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 8c21d0bb012..28fc8417d9b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -10,8 +10,8 @@ import { Schemas } from '../../../../../../../base/common/network.js'; import { basename } from '../../../../../../../base/common/resources.js'; import { isWindows } from '../../../../../../../base/common/platform.js'; import { IMockFolder, MockFilesystem } from '../testUtils/mockFilesystem.js'; -import { PromptFilesConfig } from '../../../../common/promptSyntax/config.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { PromptsConfig } from '../../../../../../../platform/prompts/common/config.js'; import { FileService } from '../../../../../../../platform/files/common/fileService.js'; import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; import { PromptFilesLocator } from '../../../../common/promptSyntax/utils/promptFilesLocator.js'; @@ -29,8 +29,8 @@ const mockConfigService = (value: T): IConfigurationService => { getValue(key?: string | IConfigurationOverrides) { assert.strictEqual( key, - PromptFilesConfig.CONFIG_KEY, - `Mocked service supports only one configuration key: '${PromptFilesConfig.CONFIG_KEY}'.`, + PromptsConfig.CONFIG_KEY, + `Mocked service supports only one configuration key: '${PromptsConfig.CONFIG_KEY}'.`, ); return value; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 2d274da6baa..8f2a4d379fa 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -53,6 +53,8 @@ import { IWorkbenchIssueService } from '../../issue/common/issue.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { isWeb } from '../../../../base/common/platform.js'; +import { PromptsConfig } from '../../../../platform/prompts/common/config.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; type ConfigureSyncQuickPickItem = { id: SyncResource; label: string; description?: string }; @@ -114,7 +116,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IHostService private readonly hostService: IHostService, @ICommandService private readonly commandService: ICommandService, - @IWorkbenchIssueService private readonly workbenchIssueService: IWorkbenchIssueService + @IWorkbenchIssueService private readonly workbenchIssueService: IWorkbenchIssueService, + @IConfigurationService private readonly configService: IConfigurationService, ) { super(); @@ -549,7 +552,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] { - return [{ + const result = [{ id: SyncResource.Settings, label: getSyncAreaLabel(SyncResource.Settings) }, { @@ -558,9 +561,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, { id: SyncResource.Snippets, label: getSyncAreaLabel(SyncResource.Snippets) - }, { - id: SyncResource.Prompts, - label: getSyncAreaLabel(SyncResource.Prompts) }, { id: SyncResource.Tasks, label: getSyncAreaLabel(SyncResource.Tasks) @@ -574,6 +574,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: SyncResource.Profiles, label: getSyncAreaLabel(SyncResource.Profiles), }]; + + // if the `reusable prompt` feature is enabled, add appropriate item to the list + if (PromptsConfig.enabled(this.configService)) { + result.push({ + id: SyncResource.Prompts, + label: getSyncAreaLabel(SyncResource.Prompts) + }); + } + + return result; } private updateConfiguration(items: ConfigureSyncQuickPickItem[], selectedItems: ReadonlyArray): void { From 7a770bd8d2766fff78749b53e91d9a84b563b95b Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 09:26:38 -0800 Subject: [PATCH 08/36] add move unit tests for config and add mock utility --- .../prompts/test/common}/config.test.ts | 28 +++++++++---------- .../prompts/test/common/utils}/mock.test.ts | 8 +++--- .../prompts/test/common/utils}/mock.ts | 2 +- .../utils/promptFilesLocator.test.ts | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) rename src/vs/{workbench/contrib/chat/test/common/promptSyntax => platform/prompts/test/common}/config.test.ts (96%) rename src/vs/{workbench/contrib/chat/test/common/promptSyntax/testUtils => platform/prompts/test/common/utils}/mock.test.ts (93%) rename src/vs/{workbench/contrib/chat/test/common/promptSyntax/testUtils => platform/prompts/test/common/utils}/mock.ts (96%) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts b/src/vs/platform/prompts/test/common/config.test.ts similarity index 96% rename from src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts rename to src/vs/platform/prompts/test/common/config.test.ts index 081eb173258..fe448de642e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config.test.ts +++ b/src/vs/platform/prompts/test/common/config.test.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { mockService } from './testUtils/mock.js'; -import { randomInt } from '../../../../../../base/common/numbers.js'; -import { randomBoolean } from '../../../../../../base/test/common/testUtils.js'; -import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { IConfigurationOverrides, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { mockService } from './utils/mock.js'; +import { PromptsConfig } from '../../common/config.js'; +import { randomInt } from '../../../../base/common/numbers.js'; +import { randomBoolean } from '../../../../base/test/common/testUtils.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { IConfigurationOverrides, IConfigurationService } from '../../../configuration/common/configuration.js'; /** * Mocked instance of {@link IConfigurationService}. @@ -28,7 +28,7 @@ const createMock = (value: T): IConfigurationService => { }); }; -suite('PromptFilesConfig', () => { +suite('PromptsConfig', () => { ensureNoDisposablesAreLeakedInTestSuite(); suite('• getValue', () => { @@ -657,7 +657,7 @@ suite('PromptFilesConfig', () => { ); }); - test('• only invalid values', () => { + test('only invalid values', () => { assert.deepStrictEqual( PromptsConfig.promptSourceFolders(createMock([ null, @@ -683,8 +683,8 @@ suite('PromptFilesConfig', () => { }); }); - suite('• object', () => { - test('• empty', () => { + suite('object', () => { + test('empty', () => { assert.deepStrictEqual( PromptsConfig.promptSourceFolders(createMock({})), ['.github/prompts'], @@ -692,7 +692,7 @@ suite('PromptFilesConfig', () => { ); }); - test('• only valid strings', () => { + test('only valid strings', () => { assert.deepStrictEqual( PromptsConfig.promptSourceFolders(createMock({ '/root/.bashrc': true, @@ -729,7 +729,7 @@ suite('PromptFilesConfig', () => { ); }); - test('• filters out non valid entries', () => { + test('filters out non valid entries', () => { assert.deepStrictEqual( PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', @@ -771,7 +771,7 @@ suite('PromptFilesConfig', () => { ); }); - test('• only invalid or false values', () => { + test('only invalid or false values', () => { assert.deepStrictEqual( PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', @@ -792,7 +792,7 @@ suite('PromptFilesConfig', () => { ); }); - test('• filters out disabled default location', () => { + test('filters out disabled default location', () => { assert.deepStrictEqual( PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mock.test.ts b/src/vs/platform/prompts/test/common/utils/mock.test.ts similarity index 93% rename from src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mock.test.ts rename to src/vs/platform/prompts/test/common/utils/mock.test.ts index 8e8413716ba..454db2ff08b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mock.test.ts +++ b/src/vs/platform/prompts/test/common/utils/mock.test.ts @@ -5,10 +5,10 @@ import assert from 'assert'; import { mockObject } from './mock.js'; -import { typeCheck } from '../../../../../../../base/common/types.js'; -import { randomInt } from '../../../../../../../base/common/numbers.js'; -import { randomBoolean } from '../../../../../../../base/test/common/testUtils.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { typeCheck } from '../../../../../base/common/types.js'; +import { randomInt } from '../../../../../base/common/numbers.js'; +import { randomBoolean } from '../../../../../base/test/common/testUtils.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; suite('mock', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mock.ts b/src/vs/platform/prompts/test/common/utils/mock.ts similarity index 96% rename from src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mock.ts rename to src/vs/platform/prompts/test/common/utils/mock.ts index f4754542e1c..a03543c6118 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mock.ts +++ b/src/vs/platform/prompts/test/common/utils/mock.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertOneOf } from '../../../../../../../base/common/types.js'; +import { assertOneOf } from '../../../../../base/common/types.js'; /** * Mocks an `TObject` with the provided `overrides`. diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 28fc8417d9b..5caeec810ce 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -5,7 +5,6 @@ import assert from 'assert'; import { createURI } from '../testUtils/createUri.js'; -import { mockObject, mockService } from '../testUtils/mock.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { basename } from '../../../../../../../base/common/resources.js'; import { isWindows } from '../../../../../../../base/common/platform.js'; @@ -16,6 +15,7 @@ import { FileService } from '../../../../../../../platform/files/common/fileServ import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; import { PromptFilesLocator } from '../../../../common/promptSyntax/utils/promptFilesLocator.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { mockObject, mockService } from '../../../../../../../platform/prompts/test/common/utils/mock.js'; import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IConfigurationOverrides, IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; From dca51c92e6960907c856cff074f520370004efb0 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 09:27:46 -0800 Subject: [PATCH 09/36] fix unit tests from the config module --- .../prompts/test/common/config.test.ts | 506 ------------------ 1 file changed, 506 deletions(-) diff --git a/src/vs/platform/prompts/test/common/config.test.ts b/src/vs/platform/prompts/test/common/config.test.ts index fe448de642e..672ae2c74c0 100644 --- a/src/vs/platform/prompts/test/common/config.test.ts +++ b/src/vs/platform/prompts/test/common/config.test.ts @@ -7,7 +7,6 @@ import assert from 'assert'; import { mockService } from './utils/mock.js'; import { PromptsConfig } from '../../common/config.js'; import { randomInt } from '../../../../base/common/numbers.js'; -import { randomBoolean } from '../../../../base/test/common/testUtils.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { IConfigurationOverrides, IConfigurationService } from '../../../configuration/common/configuration.js'; @@ -52,257 +51,6 @@ suite('PromptsConfig', () => { ); }); - suite('• string', () => { - test('• empty', () => { - assert.strictEqual( - PromptsConfig.getValue(createMock('')), - undefined, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock(' ')), - undefined, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('\t')), - undefined, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('\v')), - undefined, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('\f')), - undefined, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('\n')), - undefined, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('\r\n')), - undefined, - 'Must read correct value.', - ); - }); - - test('• true', () => { - assert.strictEqual( - PromptsConfig.getValue(createMock('true')), - true, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('TRUE')), - true, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('TrUe')), - true, - 'Must read correct value.', - ); - }); - - test('• false', () => { - assert.strictEqual( - PromptsConfig.getValue(createMock('false')), - false, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('FALSE')), - false, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('fAlSe')), - false, - 'Must read correct value.', - ); - }); - - test('• non-empty', () => { - assert.strictEqual( - PromptsConfig.getValue(createMock('/absolute/path/to/folder')), - '/absolute/path/to/folder', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('/Absolute/path/to/folder')), - '/Absolute/path/to/folder', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('./relative-path/to/folder')), - './relative-path/to/folder', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('./relative-path/To/folder')), - './relative-path/To/folder', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('.github/prompts')), - '.github/prompts', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('.github/Prompts')), - '.github/Prompts', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('/abs/path/to/file.prompt.md')), - '/abs/path/to/file.prompt.md', - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock('/Abs/Path/To/File.prompt.md')), - '/Abs/Path/To/File.prompt.md', - 'Must read correct value.', - ); - }); - }); - - test('• boolean', () => { - assert.strictEqual( - PromptsConfig.getValue(createMock(true)), - true, - 'Must read correct value.', - ); - - assert.strictEqual( - PromptsConfig.getValue(createMock(false)), - false, - 'Must read correct value.', - ); - }); - - suite('• array', () => { - test('• empty', () => { - assert.deepStrictEqual( - PromptsConfig.getValue(createMock([])), - [], - 'Must read correct value.', - ); - }); - - test('• valid strings', () => { - assert.deepStrictEqual( - PromptsConfig.getValue(createMock([ - '/absolute/path/to/folder', - './relative-path/to/folder', - './another-Relative/Path/to/folder', - '.github/prompts', - '/abs/path/to/file.prompt.md', - '/ABS/path/to/prompts/', - ])), - [ - '/absolute/path/to/folder', - './relative-path/to/folder', - './another-Relative/Path/to/folder', - '.github/prompts', - '/abs/path/to/file.prompt.md', - '/ABS/path/to/prompts/', - ], - 'Must read correct value.', - ); - }); - - test('• filters out not valid string values', () => { - assert.deepStrictEqual( - PromptsConfig.getValue(createMock([ - '/usr/local/bin/.hidden-tool', - '../config/.env.example', - [ - 'test', - randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - ], - '', - './scripts/BUILD.sh', - randomBoolean(), - '/home/user/Documents/report.v1.pdf', - 'tmp/.cache/.session.lock', - '/var/log/backup.2025-02-05.log', - { - 'key1': randomBoolean(), - 'key2': 'value2', - }, - '../.git/hooks/pre-commit.sample', - randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - '\t\t', - '/opt/app/data/config.yaml', - './.config/Subfolder.file', - undefined, - '/usr/share/man/man1/bash.1', - null, - ])), - [ - '/usr/local/bin/.hidden-tool', - '../config/.env.example', - './scripts/BUILD.sh', - '/home/user/Documents/report.v1.pdf', - 'tmp/.cache/.session.lock', - '/var/log/backup.2025-02-05.log', - '../.git/hooks/pre-commit.sample', - '/opt/app/data/config.yaml', - './.config/Subfolder.file', - '/usr/share/man/man1/bash.1', - ], - 'Must read correct value.', - ); - }); - - test('• only invalid values', () => { - assert.deepStrictEqual( - PromptsConfig.getValue(createMock([ - null, - undefined, - '', - ' ', - '\t', - '\v', - '\f', - '\n', - '\r\n', - { - 'some-key': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - 'another-key': randomBoolean(), - 'one_more_key': '../relative/path.to.file', - }, - [randomBoolean(), randomBoolean()], - randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - ])), - [], - 'Must read correct value.', - ); - }); - }); - suite('• object', () => { test('• empty', () => { assert.deepStrictEqual( @@ -429,260 +177,6 @@ suite('PromptsConfig', () => { ); }); - suite('• string', () => { - test('• empty', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock(' ')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('\t')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('\v')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('\f')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('\n')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('\r\n')), - [], - 'Must read correct value.', - ); - }); - - test('• true', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('true')), - ['.github/prompts'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('TRUE')), - ['.github/prompts'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('TrUe')), - ['.github/prompts'], - 'Must read correct value.', - ); - }); - - test('• false', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('false')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('FALSE')), - [], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('fAlSe')), - [], - 'Must read correct value.', - ); - }); - - test('• non-empty', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('/absolute/path/to/folder')), - ['.github/prompts', '/absolute/path/to/folder'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('/Absolute/path/to/folder')), - ['.github/prompts', '/Absolute/path/to/folder'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('./relative-path/to/folder')), - ['.github/prompts', './relative-path/to/folder'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('./relative-path/To/folder')), - ['.github/prompts', './relative-path/To/folder'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('.github/prompts')), - ['.github/prompts'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('.github/Prompts')), - ['.github/prompts', '.github/Prompts'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('/abs/path/to/file.prompt.md')), - ['.github/prompts', '/abs/path/to/file.prompt.md'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock('/Abs/Path/To/File.prompt.md')), - ['.github/prompts', '/Abs/Path/To/File.prompt.md'], - 'Must read correct value.', - ); - }); - }); - - test('• boolean', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock(true)), - ['.github/prompts'], - 'Must read correct value.', - ); - - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock(false)), - [], - 'Must read correct value.', - ); - }); - - suite('• array', () => { - test('• empty', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock([])), - ['.github/prompts'], - 'Must read correct value.', - ); - }); - - test('• valid strings', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock([ - '/absolute/path/to/folder', - './relative-path/to/folder', - './another-Relative/Path/to/folder', - '.github/prompts', - '/abs/path/to/file.prompt.md', - '.githuB/prompts', - '/ABS/path/to/prompts/', - ])), - [ - '.github/prompts', - '/absolute/path/to/folder', - './relative-path/to/folder', - './another-Relative/Path/to/folder', - '/abs/path/to/file.prompt.md', - '.githuB/prompts', - '/ABS/path/to/prompts/', - ], - 'Must read correct value.', - ); - }); - - test('• filters out not valid string values', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock([ - '/usr/local/bin/.hidden-tool', - '../config/.env.example', - [ - 'test', - randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - ], - '', - './scripts/BUILD.sh', - randomBoolean(), - '/home/user/Documents/report.v1.pdf', - 'tmp/.cache/.session.lock', - '/var/log/backup.2025-02-05.log', - { - 'key1': randomBoolean(), - 'key2': 'value2', - }, - '../.git/hooks/pre-commit.sample', - randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - '\t\t', - '/opt/app/data/config.yaml', - './.config/Subfolder.file', - undefined, - '/usr/share/man/man1/bash.1', - null, - ])), - [ - '.github/prompts', - '/usr/local/bin/.hidden-tool', - '../config/.env.example', - './scripts/BUILD.sh', - '/home/user/Documents/report.v1.pdf', - 'tmp/.cache/.session.lock', - '/var/log/backup.2025-02-05.log', - '../.git/hooks/pre-commit.sample', - '/opt/app/data/config.yaml', - './.config/Subfolder.file', - '/usr/share/man/man1/bash.1', - ], - 'Must read correct value.', - ); - }); - - test('only invalid values', () => { - assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock([ - null, - undefined, - '', - ' ', - '\t', - '\v', - '\f', - '\n', - '\r\n', - { - 'some-key': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - 'another-key': randomBoolean(), - 'one_more_key': '../relative/path.to.file', - }, - [randomBoolean(), randomBoolean()], - randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - ])), - ['.github/prompts'], - 'Must read correct value.', - ); - }); - }); - suite('object', () => { test('empty', () => { assert.deepStrictEqual( From be7a39edbdc3b29cac4803ffb67c6fe85c4dbed1 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 09:29:56 -0800 Subject: [PATCH 10/36] fix unit tests for the `PromptFilesLocator` class --- .../utils/promptFilesLocator.test.ts | 1242 ----------------- 1 file changed, 1242 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 5caeec810ce..14347ec2af3 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -159,90 +159,6 @@ suite('PromptFilesLocator', () => { }); suite('• non-empty filesystem', () => { - suite('• boolean config value', () => { - test('• true', async () => { - const locator = await createPromptsLocator( - true, - EMPTY_WORKSPACE, - [ - { - name: '/vsl/files/pmts', - children: [ - { - name: 'file.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/abs/prompts/files/misc', - children: [ - { - name: 'another.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/.github/prompts', - children: [ - { - name: 'my-prompt.prompt.md', - contents: 'oh hi', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [], - 'Must find correct prompts.', - ); - }); - - test('• false', async () => { - const locator = await createPromptsLocator( - false, - EMPTY_WORKSPACE, - [ - { - name: '/vsl/pmts/files', - children: [ - { - name: 'omt.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/var/lib/prompts.shared', - children: [ - { - name: 'smt.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/.github/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [], - 'Must find correct prompts.', - ); - }); - }); - test('• object config value', async () => { const locator = await createPromptsLocator( { @@ -296,59 +212,6 @@ suite('PromptFilesLocator', () => { 'Must find correct prompts.', ); }); - - test('• array config value', async () => { - const locator = await createPromptsLocator( - [ - '/var/prompts', - '/usr/local/prompts/', - '.github/prompts', - ], - EMPTY_WORKSPACE, - [ - { - name: '/var/prompts', - children: [ - { - name: 'alpha.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'beta.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/usr/local/prompts', - children: [ - { - name: 'gamma.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/data/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/var/prompts/alpha.prompt.md'), - createURI('/var/prompts/beta.prompt.md'), - createURI('/usr/local/prompts/gamma.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); }); }); @@ -521,221 +384,6 @@ suite('PromptFilesLocator', () => { ); }); }); - - test('• array config value', async () => { - const locator = await createPromptsLocator( - [ - '/Users/legomushroom/repos/prompts', - '/tmp/prompts/', - '.copilot/prompts', - ], - [ - '/Users/legomushroom/repos/vscode', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-rust.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: '.copilot/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/test.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/refactor-tests.prompt.md'), - createURI('/tmp/prompts/translate.to-rust.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/.copilot/prompts/default.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - - suite('• string config value', () => { - test('• relative path', async () => { - const locator = await createPromptsLocator( - '.github/prompts', - [ - '/Users/legomushroom/test/vscode', - ], - [ - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-rust.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/test/vscode', - children: [ - { - name: '.copilot/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'default-github.prompt.md', - contents: 'oh hi, raw-bot!', - }, - ], - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/test/vscode/.github/prompts/default-github.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - - test('• absolute path', async () => { - const locator = await createPromptsLocator( - '/Users/legomushroom/test/prompts', - [ - '/Users/legomushroom/test/vscode', - ], - [ - { - name: '/Users/legomushroom/test/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-rust.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/test/vscode', - children: [ - { - name: '.copilot/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'file.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/test/vscode/.github/prompts/file.prompt.md'), - createURI('/Users/legomushroom/test/prompts/test.prompt.md'), - createURI('/Users/legomushroom/test/prompts/refactor-tests.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - }); }); }); @@ -1102,896 +750,6 @@ suite('PromptFilesLocator', () => { ); }); }); - - suite('• array config value', () => { - test('• without top-level `copilot` folder', async () => { - const locator = await createPromptsLocator( - { - '/Users/legomushroom/repos/prompts': true, - '/tmp/prompts/': true, - 'copilot/PROMPTS/': true, - }, - [ - '/Users/legomushroom/repos/vscode', - '/Users/legomushroom/repos/node', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-rust.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'prompt1.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'prompt2.prompt.md', - contents: 'oh hi, raw boot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'prompt.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/node', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'Build-Constructed-Structures.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'refactor-static-classes.prompt.md', - contents: 'file contents', - }, - ], - }, - ], - }, - // note! this folder is not part of the workspace, so prompt files are `ignored` - { - name: '/Users/legomushroom/repos/copilot/PROMPTS', - children: [ - { - name: 'prompt-name.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - // note! this folder is not part of the workspace, so prompt files are `ignored` - { - name: '/Users/legomushroom/repos/.github/prompts', - children: [ - { - name: 'prompt-name-22.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt-56.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/prompt.prompt.md'), - createURI('/Users/legomushroom/repos/node/.github/prompts/refactor-static-classes.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/test.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/refactor-tests.prompt.md'), - createURI('/tmp/prompts/translate.to-rust.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/copilot/PROMPTS/prompt1.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/copilot/PROMPTS/prompt2.prompt.md'), - createURI('/Users/legomushroom/repos/node/copilot/PROMPTS/Build-Constructed-Structures.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - - test('• with top-level `copilot` folder', async () => { - const locator = await createPromptsLocator( - [ - '/Users/legomushroom/repos/prompts', - '/tmp/prompts/', - 'copilot/PROMPTS/', - ], - [ - '/Users/legomushroom/repos/vscode', - '/Users/legomushroom/repos/node', - '/Users/legomushroom/repos/copilot/', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-rust.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'prompt1.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'prompt2.prompt.md', - contents: 'oh hi, raw boot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/node', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'Build-Constructed-Structures.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'refactor-static-classes.prompt.md', - contents: 'file contents', - }, - ], - }, - ], - }, - // note! this folder is not part of the workspace, so prompt files are `included` - { - name: '/Users/legomushroom/repos/copilot/PROMPTS', - children: [ - { - name: 'prompt-name.tests.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt.tests.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - // note! this folder is not part of the workspace, so prompt files are `ignored` - { - name: '/Users/legomushroom/repos/.github/prompts', - children: [ - { - name: 'prompt-name-22.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt-56.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md'), - createURI('/Users/legomushroom/repos/node/.github/prompts/refactor-static-classes.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/test.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/refactor-tests.prompt.md'), - createURI('/tmp/prompts/translate.to-rust.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/copilot/PROMPTS/prompt1.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/copilot/PROMPTS/prompt2.prompt.md'), - createURI('/Users/legomushroom/repos/node/copilot/PROMPTS/Build-Constructed-Structures.prompt.md'), - createURI('/Users/legomushroom/repos/copilot/PROMPTS/prompt-name.tests.prompt.md'), - createURI('/Users/legomushroom/repos/copilot/PROMPTS/name-of-the-prompt.tests.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - }); - - suite('• string config value', () => { - suite('• absolute path', () => { - test('• without top-level `copilot` folder', async () => { - const locator = await createPromptsLocator( - '/tmp/prompts/', - [ - '/Users/legomushroom/repos/vscode', - '/Users/legomushroom/repos/node', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-go.prompt.md', - contents: 'some more random file contents', - }, - { - name: 'find-mean-error-rate.prompt.md', - contents: 'random file contents', - }, - { - name: '.github', - children: [ - { - name: 'prompts', - children: [ - { - name: 'github-prompt.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'prompt1.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'prompt2.prompt.md', - contents: 'oh hi, raw boot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'bot.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/node', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'Build-Constructed-Structures.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'classes-refactor-static.prompt.md', - contents: 'file contents', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/copilot/PROMPTS', - children: [ - { - name: 'prompt-name.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - { - name: '/Users/legomushroom/repos/.github/prompts', - children: [ - { - name: 'prompt-name-22.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt-56.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/bot.prompt.md'), - createURI('/Users/legomushroom/repos/node/.github/prompts/classes-refactor-static.prompt.md'), - createURI('/tmp/prompts/translate.to-go.prompt.md'), - createURI('/tmp/prompts/find-mean-error-rate.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - - test('• with top-level `.github` folder', async () => { - const locator = await createPromptsLocator( - '/Users/legomushroom/repos/prompts', - [ - '/Users/legomushroom/repos/vscode', - '/Users/legomushroom/repos/node', - '/Users/legomushroom/repos/.github/', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.file.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.file.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-rust.prompt.md', - contents: 'some more random file contents', - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: 'copilot/PROMPTS', - children: [ - { - name: 'prompt1.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'prompt2.prompt.md', - contents: 'oh hi, raw boot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'hi.prompt.md', - contents: 'oh hi, raw bot!', - }, - { - name: 'bye.prompt.md', - contents: 'oh bye, raw bot!', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/node', - children: [ - { - name: '.copilot/PROMPTS', - children: [ - { - name: 'Build-Constructed-Structures.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'static-refactor-classes.prompt.md', - contents: 'file contents', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/copilot/PROMPTS', - children: [ - { - name: 'prompt-name.tests.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt.tests.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - { - name: '/Users/legomushroom/repos/.github/prompts', - children: [ - { - name: 'prompt-name-22.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt-56.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/hi.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/.github/prompts/bye.prompt.md'), - createURI('/Users/legomushroom/repos/node/.github/prompts/static-refactor-classes.prompt.md'), - createURI('/Users/legomushroom/repos/.github/prompts/prompt-name-22.prompt.md'), - createURI('/Users/legomushroom/repos/.github/prompts/name-of-the-prompt-56.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/test.file.prompt.md'), - createURI('/Users/legomushroom/repos/prompts/refactor-tests.file.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - }); - - suite('• relative path', () => { - test('• without top-level `my-prompts` folder', async () => { - const locator = await createPromptsLocator( - 'my-prompts', - [ - '/Users/legomushroom/repos/vscode', - '/Users/legomushroom/repos/node', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-go.prompt.md', - contents: 'some more random file contents', - }, - { - name: 'find-mean-error-rate.prompt.md', - contents: 'random file contents', - }, - { - name: '.github', - children: [ - { - name: 'prompts', - children: [ - { - name: 'github-prompt.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: 'my-prompts', - children: [ - { - name: 'github-prompt.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: 'my-prompts', - children: [ - { - name: 'prompt1.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'prompt2.prompt.md', - contents: 'oh hi, raw boot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: '1.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/node', - children: [ - { - name: 'my-prompts', - children: [ - { - name: 'Build-Constructed-Structures.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: '55.prompt.md', - contents: 'file contents', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/my-prompts', - children: [ - { - name: 'prompt-name.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - { - name: '/Users/legomushroom/repos/.github/prompts', - children: [ - { - name: 'prompt-name-22.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt-56.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/1.prompt.md'), - createURI('/Users/legomushroom/repos/node/.github/prompts/55.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/my-prompts/prompt1.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/my-prompts/prompt2.prompt.md'), - createURI('/Users/legomushroom/repos/node/my-prompts/Build-Constructed-Structures.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - - test('• with top-level `my-prompts` folder', async () => { - const locator = await createPromptsLocator( - 'my-prompts', - [ - '/Users/legomushroom/repos/vscode', - '/Users/legomushroom/repos/node', - '/Users/legomushroom/repos/my-prompts', - ], - [ - { - name: '/Users/legomushroom/repos/prompts', - children: [ - { - name: 'test.prompt.md', - contents: 'Hello, World!', - }, - { - name: 'refactor-tests.prompt.md', - contents: 'some file content goes here', - }, - ], - }, - { - name: '/tmp/prompts', - children: [ - { - name: 'translate.to-go.prompt.md', - contents: 'some more random file contents', - }, - { - name: 'find-mean-error-rate.prompt.md', - contents: 'random file contents', - }, - { - name: '.github', - children: [ - { - name: 'prompts', - children: [ - { - name: 'github-prompt.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: 'my-prompts', - children: [ - { - name: 'github-prompt.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/absolute/path/prompts', - children: [ - { - name: 'some-prompt-file.prompt.md', - contents: 'hey hey hey', - }, - ], - }, - { - name: '/Users/legomushroom/repos/vscode', - children: [ - { - name: 'my-prompts', - children: [ - { - name: 'prompt1.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'prompt2.prompt.md', - contents: 'oh hi, raw boot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'default.prompt.md', - contents: 'oh hi, bot!', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/node', - children: [ - { - name: 'my-prompts', - children: [ - { - name: 'Build-Constructed-Structures.prompt.md', - contents: 'oh hi, robot!', - }, - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'refactor-static-classes.prompt.md', - contents: 'file contents', - }, - ], - }, - ], - }, - { - name: '/Users/legomushroom/repos/my-prompts', - children: [ - { - name: 'prompt.name.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name.of.the.prompt.md', - contents: 'oh hi, raw bot! why sad?', - }, - ], - }, - { - name: '/Users/legomushroom/repos/.github/prompts', - children: [ - { - name: 'prompt-name-22.prompt.md', - contents: 'oh hi, robot!', - }, - { - name: 'name-of-the-prompt-56.prompt.md', - contents: 'oh hi, raw bot!', - }, - ], - }, - ]); - - assert.deepStrictEqual( - await locator.listFiles([]), - [ - createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md'), - createURI('/Users/legomushroom/repos/node/.github/prompts/refactor-static-classes.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/my-prompts/prompt1.prompt.md'), - createURI('/Users/legomushroom/repos/vscode/my-prompts/prompt2.prompt.md'), - createURI('/Users/legomushroom/repos/node/my-prompts/Build-Constructed-Structures.prompt.md'), - createURI('/Users/legomushroom/repos/my-prompts/prompt.name.prompt.md'), - createURI('/Users/legomushroom/repos/my-prompts/name.of.the.prompt.md'), - ], - 'Must find correct prompts.', - ); - }); - }); - }); }); }); }); From 410413e9638a9a577aaa7386a5b08d259bdc69d1 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 09:42:15 -0800 Subject: [PATCH 11/36] update docs comment for the config module --- src/vs/platform/prompts/common/config.ts | 83 ++++-------------------- 1 file changed, 12 insertions(+), 71 deletions(-) diff --git a/src/vs/platform/prompts/common/config.ts b/src/vs/platform/prompts/common/config.ts index 6658b3858c9..06032e80341 100644 --- a/src/vs/platform/prompts/common/config.ts +++ b/src/vs/platform/prompts/common/config.ts @@ -6,19 +6,8 @@ import { IConfigurationService } from '../../configuration/common/configuration.js'; /** - * TODO: @legomushroom - * - update doc comment for config.ts - * - move unit tests for config.ts - * - add unit tests for config.ts - */ - -/** - * `!Note!` This doc comment is deprecated and is set to be updated during `debt` week. - * The configuration value can now be one of `{ '/path/to/folder': boolean }` or 'null' types. - * This comment is tracked by [#13119](https://github.com/microsoft/vscode-copilot/issues/13119). - * - * Configuration helper for the `prompt files` feature. - * @see {@link CONFIG_KEY} and {@link DEFAULT_SOURCE_FOLDER} + * Configuration helper for the `reusable prompts` feature. + * @see {@link CONFIG_KEY}. * * ### Functions * @@ -28,23 +17,14 @@ import { IConfigurationService } from '../../configuration/common/configuration. * * ### Configuration Examples * - * Enable the feature, using defaults for prompt files source folders - * (see {@link DEFAULT_SOURCE_FOLDER}): + * Enable the feature using the default `'.github/prompts'` folder as a source of prompt files: * ```json * { - * "chat.promptFiles": true, + * "chat.promptFiles": {}, * } * ``` * - * Enable the feature, specifying a single prompt files source folders, - * in addition to the default `'.github/prompts'` one: - * ```json - * { - * "chat.promptFiles": '.copilot/prompts', - * } - * ``` - * - * Enable the feature, specifying multiple prompt files source folders, + * Enable the feature, providing multiple source folder paths for prompt files, * in addition to the default `'.github/prompts'` one: * ```json * { @@ -55,50 +35,15 @@ import { IConfigurationService } from '../../configuration/common/configuration. * } * ``` * - * Enable the feature, specifying multiple prompt files source folders, - * in addition to the default `'.github/prompts'` one: - * ```json - * { - * "chat.promptFiles": [ - * ".copilot/prompts", - * "/Users/legomushroom/repos/prompts", - * ], - * } - * ``` - * - * The "array" case is similar to the "object" one, but there is one difference. - * At the time of writing, configuration settings with the `array` value cannot - * be merged into a single entry when the setting is specified in both the `user` - * and the `workspace` settings. On the other hand, the "object" case provides - * more flexibility - the settings are combined into a single object. - * - * Enable the feature, using defaults for prompt files source folders - * (see {@link DEFAULT_SOURCE_FOLDER}): - * ```jsonc - * { - * "chat.promptFiles": {}, // same as setting to `true` - * } - * ``` - * * See the next section for details on how we treat the config value. * * ### Possible Values * - * - `undefined`/`null`: feature is disabled - * - `boolean`: - * - `true`: feature is enabled, prompt files source folders - * fallback to {@link DEFAULT_SOURCE_FOLDER} - * - `false`: feature is disabled - * - `string`: - * - values that can be mapped to `boolean`(`"true"`, `"FALSE", "TrUe"`, etc.) - * are treated the same as the `boolean` case above - * - any other `non-empty` string value is treated as a single prompt files source folder path, - * which is used in addition to the default {@link DEFAULT_SOURCE_FOLDER} - * - `empty` string value is treated the same as the `undefined`/`null` case above (disabled) + * - `undefined`/`null`: feature is disabledx * - `object`: * - expects the { "string": `boolean` } pairs, where the `string` is a path and the `boolean` * is a flag that defines if this additional source folder is enabled or disabled; - * enabled source folders are used in addition to the default {@link DEFAULT_SOURCE_FOLDER} path + * enabled source folders are used in addition to the default {@link DEFAULT_SOURCE_FOLDER} path; * you can explicitly disable the default source folder by setting it to `false` in the object * - value of a record in the object can also be a `string`: * - if the string can be clearly mapped to a `boolean` (e.g., `"true"`, `"FALSE", "TrUe"`, etc.), @@ -106,18 +51,14 @@ import { IConfigurationService } from '../../configuration/common/configuration. * - any other string value is treated as `false` and is effectively ignored * - if the record `key` is an `empty` string, it is ignored * - if the resulting object is empty, the feature is considered `enabled`, prompt files source - * folders fallback to {@link DEFAULT_SOURCE_FOLDER} - * - `array`: - * - `string` items(non-empty) in the array are treated as prompt files source folder paths, - * in addition to the default {@link DEFAULT_SOURCE_FOLDER} folder - * - all `non-string` items in the array are `ignored` - * - if the resulting array is empty, the feature is considered `enabled`, prompt files - * source folders fallback to {@link DEFAULT_SOURCE_FOLDER} + * folders fallback to the default {@link DEFAULT_SOURCE_FOLDER} path + * - if the resulting object is not empty, and the default {@link DEFAULT_SOURCE_FOLDER} path + * is not explicitly disabled, it is added to the list of prompt files source folders * * ### File Paths Resolution * - * We resolve only `*.prompt.md` files inside the resulting source folders and - * all `relative` folder paths are resolved relative to: + * We resolve only `*.prompt.md` files inside the resulting source folders. Relative paths are resolved + * relative to: * * - the current workspace `root`, if applicable, in other words one of the workspace folders * can be used as a prompt files source folder From 244137ccc826a3ae0d77298185eb6795ea15c107 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 15:43:50 -0800 Subject: [PATCH 12/36] address PR feedback, add unit tests for the constant utils --- src/vs/platform/prompts/common/constants.ts | 6 +- .../prompts/test/common/constants.test.ts | 84 +++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/vs/platform/prompts/test/common/constants.test.ts diff --git a/src/vs/platform/prompts/common/constants.ts b/src/vs/platform/prompts/common/constants.ts index a222dc61034..a7ea766894b 100644 --- a/src/vs/platform/prompts/common/constants.ts +++ b/src/vs/platform/prompts/common/constants.ts @@ -5,7 +5,7 @@ import { URI } from '../../../base/common/uri.js'; import { assert } from '../../../base/common/assert.js'; -import { basename } from '../../../base/common/resources.js'; +import { basename } from '../../../base/common/path.js'; /** * File extension for the reusable prompt files. @@ -37,7 +37,5 @@ export const getCleanPromptName = ( `Provided path '${fileUri.fsPath}' is not a prompt file.`, ); - const fileBasename = basename(fileUri); - return fileBasename - .substring(0, fileBasename.length - PROMPT_FILE_EXTENSION.length); + return basename(fileUri.path, PROMPT_FILE_EXTENSION); }; diff --git a/src/vs/platform/prompts/test/common/constants.test.ts b/src/vs/platform/prompts/test/common/constants.test.ts new file mode 100644 index 00000000000..376f8318c40 --- /dev/null +++ b/src/vs/platform/prompts/test/common/constants.test.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../base/common/uri.js'; +import { getCleanPromptName, isPromptFile } from '../../common/constants.js'; +import { randomInt } from '../../../../base/common/numbers.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; + + +suite('Prompt Constants', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('• getCleanPromptName', () => { + test('• returns a clean prompt name', () => { + assert.strictEqual( + getCleanPromptName(URI.file('/path/to/my-prompt.prompt.md')), + 'my-prompt', + ); + + assert.strictEqual( + getCleanPromptName(URI.file('../common.prompt.md')), + 'common', + ); + + const expectedPromptName = `some-${randomInt(1000)}`; + assert.strictEqual( + getCleanPromptName(URI.file(`./${expectedPromptName}.prompt.md`)), + expectedPromptName, + ); + }); + + test('• throws if not a prompt file URI provided', () => { + assert.throws(() => { + getCleanPromptName(URI.file('/path/to/default.prompt.md1')); + }); + + assert.throws(() => { + getCleanPromptName(URI.file('./some.md')); + }); + + + assert.throws(() => { + getCleanPromptName(URI.file('../some-folder/frequent.txt')); + }); + + assert.throws(() => { + getCleanPromptName(URI.file('/etc/prompts/my-prompt')); + }); + }); + }); + + suite('• isPromptFile', () => { + test('• returns `true` for prompt files', () => { + assert( + isPromptFile(URI.file('/path/to/my-prompt.prompt.md')), + ); + + assert( + isPromptFile(URI.file('../common.prompt.md')), + ); + + assert( + isPromptFile(URI.file(`./some-${randomInt(1000)}.prompt.md`)), + ); + }); + + test('• returns `false` for non-prompt files', () => { + assert( + !isPromptFile(URI.file('/path/to/my-prompt.prompt.md1')), + ); + + assert( + !isPromptFile(URI.file('../common.md')), + ); + + assert( + !isPromptFile(URI.file(`./some-${randomInt(1000)}.txt`)), + ); + }); + }); +}); From 5312579ab04a3c12eca4270b8f37f38fb8bef642 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Feb 2025 12:21:59 +1100 Subject: [PATCH 13/36] Ensure acceptAgentEdits is async (#241278) * Ensure acceptAgentEdits is async * Updates --- .../browser/chatEditing/chatEditingModifiedDocumentEntry.ts | 2 +- .../chat/browser/chatEditing/chatEditingModifiedFileEntry.ts | 2 +- .../browser/chatEditing/chatEditingModifiedNotebookEntry.ts | 2 +- .../contrib/chat/browser/chatEditing/chatEditingSession.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 6d8e6f6b27b..1f90c36696c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -272,7 +272,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } } - acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): void { + async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { assertType(textEdits.every(TextEdit.isTextEdit), 'INVALID args, can only handle text edits'); assert(isEqual(resource, this.modifiedURI), ' INVALID args, can only edit THIS document'); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index fb1b7d36274..4cd72fcc07c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -225,7 +225,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im this._autoAcceptCtrl.get()?.cancel(); } - abstract acceptAgentEdits(uri: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): void; + abstract acceptAgentEdits(uri: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise; async acceptStreamingEditsEnd(tx: ITransaction) { this._resetEditsState(tx); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index be5bfde342a..5f6deab0d15 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -36,7 +36,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie throw new Error('Method not implemented.'); } - override acceptAgentEdits(resource: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): void { + override async acceptAgentEdits(resource: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { const isCellUri = resource.scheme === Schemas.vscodeNotebookCell; assert(isCellUri || isEqual(resource, this.modifiedURI)); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index a8f60cd6404..281bf906fc3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -930,7 +930,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private async _acceptEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { const entry = await this._getOrCreateModifiedFileEntry(resource, this._getTelemetryInfoForModel(responseModel)); - entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel); + await entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel); } private _getTelemetryInfoForModel(responseModel: IChatResponseModel): IModifiedEntryTelemetryInfo { From f9dd5e1a8dc6b01d2f9eaef1a87afa869a4e04bb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 19 Feb 2025 17:38:32 -0800 Subject: [PATCH 14/36] chat: better problems integration (#241276) chat: allow referencing and dragging in diagnostics - There is a new proposal which adds `ChatReferenceDiagnostic` as a prompt reference type - You can now pick "Problems..." as part of the chat attachment context - You can drag and drop files and individual diagnostics from the Problems view into chat. Previously trying to do this would just attach the file. --- package.json | 2 +- src/vs/platform/dnd/browser/dnd.ts | 32 +++- .../common/extensionsApiProposals.ts | 3 + src/vs/platform/markers/common/markers.ts | 9 ++ .../workbench/api/common/extHost.api.impl.ts | 3 +- .../api/common/extHostChatAgents2.ts | 17 ++- .../api/common/extHostTypeConverters.ts | 43 +++++- src/vs/workbench/api/common/extHostTypes.ts | 4 + src/vs/workbench/browser/dnd.ts | 8 +- .../browser/actions/chatContextActions.ts | 71 +++++++-- .../contrib/chat/browser/chatDragAndDrop.ts | 44 +++++- .../browser/contrib/chatDynamicVariables.ts | 71 ++++++++- .../contrib/chat/common/chatModel.ts | 39 ++++- .../contrib/markers/browser/markersView.ts | 144 +++++++++++------- ...code.proposed.chatReferenceDiagnostic.d.ts | 23 +++ 15 files changed, 414 insertions(+), 99 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts diff --git a/package.json b/package.json index 3f736d096b4..090d88e772c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.98.0", - "distro": "b37092a45bb95e5098cefcf6809fa6973cfd2538", + "distro": "18d6e62cad16da70804e341893ae344d5cdcc6ee", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/platform/dnd/browser/dnd.ts b/src/vs/platform/dnd/browser/dnd.ts index f83103d27a6..f00c4d08a73 100644 --- a/src/vs/platform/dnd/browser/dnd.ts +++ b/src/vs/platform/dnd/browser/dnd.ts @@ -13,7 +13,7 @@ import { ResourceMap } from '../../../base/common/map.js'; import { parse } from '../../../base/common/marshalling.js'; import { Schemas } from '../../../base/common/network.js'; import { isNative, isWeb } from '../../../base/common/platform.js'; -import { URI } from '../../../base/common/uri.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IDialogService } from '../../dialogs/common/dialogs.js'; import { IBaseTextResourceEditorInput, ITextEditorSelection } from '../../editor/common/editor.js'; @@ -23,6 +23,7 @@ import { ByteSize, IFileService } from '../../files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js'; import { extractSelection } from '../../opener/common/opener.js'; import { Registry } from '../../registry/common/platform.js'; +import { IMarker } from '../../markers/common/markers.js'; //#region Editor / Resources DND @@ -30,7 +31,8 @@ import { Registry } from '../../registry/common/platform.js'; export const CodeDataTransfers = { EDITORS: 'CodeEditors', FILES: 'CodeFiles', - SYMBOLS: 'application/vnd.code.symbols' + SYMBOLS: 'application/vnd.code.symbols', + MARKERS: 'application/vnd.code.diagnostics', }; export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { @@ -414,8 +416,12 @@ export interface DocumentSymbolTransferData { kind: number; } -export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] { - const rawSymbolsData = e.dataTransfer?.getData(CodeDataTransfers.SYMBOLS); +function setDataAsJSON(e: DragEvent, kind: string, data: unknown) { + e.dataTransfer?.setData(kind, JSON.stringify(data)); +} + +function getDataAsJSON(e: DragEvent, kind: string, defaultValue: T): T { + const rawSymbolsData = e.dataTransfer?.getData(kind); if (rawSymbolsData) { try { return JSON.parse(rawSymbolsData); @@ -424,11 +430,25 @@ export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[ } } - return []; + return defaultValue; +} + +export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] { + return getDataAsJSON(e, CodeDataTransfers.SYMBOLS, []); } export function fillInSymbolsDragData(symbolsData: readonly DocumentSymbolTransferData[], e: DragEvent): void { - e.dataTransfer?.setData(CodeDataTransfers.SYMBOLS, JSON.stringify(symbolsData)); + setDataAsJSON(e, CodeDataTransfers.SYMBOLS, symbolsData); +} + +export type MarkerTransferData = IMarker | { uri: UriComponents }; + +export function extractMarkerDropData(e: DragEvent): MarkerTransferData[] | undefined { + return getDataAsJSON(e, CodeDataTransfers.MARKERS, undefined); +} + +export function fillInMarkersDragData(markerData: MarkerTransferData[], e: DragEvent): void { + setDataAsJSON(e, CodeDataTransfers.MARKERS, markerData); } /** diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 96770454dac..8273d3c17d4 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -44,6 +44,9 @@ const _allApiProposals = { chatReferenceBinaryData: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts', }, + chatReferenceDiagnostic: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts', + }, chatTab: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', }, diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 73cb120869a..602a67ea0d4 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -64,6 +64,15 @@ export namespace MarkerSeverity { return _displayStrings[a] || ''; } + const _displayStringsPlural: { [value: number]: string } = Object.create(null); + _displayStringsPlural[MarkerSeverity.Error] = localize('sev.errors', "Errors"); + _displayStringsPlural[MarkerSeverity.Warning] = localize('sev.warnings', "Warnings"); + _displayStringsPlural[MarkerSeverity.Info] = localize('sev.infos', "Infos"); + + export function toStringPlural(a: MarkerSeverity): string { + return _displayStringsPlural[a] || ''; + } + export function fromSeverity(severity: Severity): MarkerSeverity { switch (severity) { case Severity.Error: return MarkerSeverity.Error; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f926cde3ee2..3099a31228a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -211,7 +211,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); - const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels)); + const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics)); const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); @@ -1544,6 +1544,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResultFeedbackKind: extHostTypes.ChatResultFeedbackKind, ChatVariableLevel: extHostTypes.ChatVariableLevel, ChatCompletionItem: extHostTypes.ChatCompletionItem, + ChatReferenceDiagnostic: extHostTypes.ChatReferenceDiagnostic, CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index d579589cc8a..d053ad5e5ea 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -32,6 +32,7 @@ import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { isChatViewTitleActionContext } from '../../contrib/chat/common/chatActions.js'; import { IChatRelatedFile, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; +import { ExtHostDiagnostics } from './extHostDiagnostics.js'; class ChatAgentResponseStream { @@ -324,7 +325,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _logService: ILogService, private readonly _commands: ExtHostCommands, private readonly _documents: ExtHostDocuments, - private readonly _languageModels: ExtHostLanguageModels + private readonly _languageModels: ExtHostLanguageModels, + private readonly _diagnostics: ExtHostDiagnostics, ) { super(); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); @@ -402,7 +404,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); const model = await this.getModelForRequest(request, detector.extension); - const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference')); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference'), this.getDiagnosticsWhenEnabled(detector.extension)); return detector.provider.provideParticipantDetection( extRequest, @@ -486,7 +488,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); const model = await this.getModelForRequest(request, agent.extension); - const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference')); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference'), this.getDiagnosticsWhenEnabled(agent.extension)); inFlightRequest = { requestId: requestDto.requestId, extRequest }; this._inFlightRequests.add(inFlightRequest); @@ -538,6 +540,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } } + private getDiagnosticsWhenEnabled(extension: Readonly) { + if (!isProposedApiEnabled(extension, 'chatReferenceDiagnostic')) { + return []; + } + return this._diagnostics.getDiagnostics(); + } + private async prepareHistoryTurns(extension: Readonly, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = []; @@ -551,7 +560,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const hasReadonlyProposal = isProposedApiEnabled(extension, 'chatReadonlyPromptReference'); const varsWithoutTools = h.request.variables.variables .filter(v => !v.isTool) - .map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal)); + .map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal, this.getDiagnosticsWhenEnabled(extension))); const toolReferences = h.request.variables.variables .filter(v => v.isTool) .map(typeConvert.ChatLanguageModelToolReference.to); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1acf66bce5e..edd8590eedd 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2761,7 +2761,7 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.isTool); const variableReferences = request.variables.variables.filter(v => !v.isTool); return { @@ -2770,7 +2770,7 @@ export namespace ChatAgentRequest { attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, - references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal)), + references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal, diagnostics)), toolReferences: toolReferences.map(ChatLanguageModelToolReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, @@ -2814,19 +2814,48 @@ export namespace ChatLocation { } export namespace ChatPromptReference { - export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean): vscode.ChatPromptReference { - const value = variable.value; + export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatPromptReference { + let value: vscode.ChatPromptReference['value'] = variable.value; if (!value) { throw new Error('Invalid value reference'); } + if (isUriComponents(value)) { + value = URI.revive(value); + } else if (value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri)) { + value = Location.to(revive(value)); + } else if (variable.isImage) { + value = new types.ChatReferenceBinaryData( + variable.mimeType ?? 'image/png', + () => Promise.resolve(new Uint8Array(Object.values(variable.value as number[]))), + variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined + ); + } else if (variable.kind === 'diagnostic') { + const filterSeverity = variable.filterSeverity && DiagnosticSeverity.to(variable.filterSeverity); + const filterUri = variable.filterUri && URI.revive(variable.filterUri).toString(); + value = new types.ChatReferenceDiagnostic(diagnostics.map(([uri, d]): [vscode.Uri, vscode.Diagnostic[]] => { + if (variable.filterUri && uri.toString() !== filterUri) { + return [uri, []]; + } + + return [uri, d.filter(d => { + if (filterSeverity && d.severity > filterSeverity) { + return false; + } + if (variable.filterRange && !editorRange.Range.areIntersectingOrTouching(variable.filterRange, Range.from(d.range))) { + return false; + } + + return true; + })]; + }).filter(([, d]) => d.length > 0)); + } + return { id: variable.id, name: variable.name, range: variable.range && [variable.range.start, variable.range.endExclusive], - value: isUriComponents(value) ? URI.revive(value) : - value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ? - Location.to(revive(value)) : variable.isImage ? new types.ChatReferenceBinaryData(variable.mimeType ?? 'image/png', () => Promise.resolve(new Uint8Array(Object.values(value))), variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined) : value, + value, modelDescription: variable.modelDescription, isReadonly: hasReadonlyProposal ? variable.isMarkedReadonly : undefined, }; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index f27aac8e007..75d1061cfcc 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4716,6 +4716,10 @@ export class ChatReferenceBinaryData implements vscode.ChatReferenceBinaryData { } } +export class ChatReferenceDiagnostic implements vscode.ChatReferenceDiagnostic { + constructor(public readonly diagnostics: [vscode.Uri, vscode.Diagnostic[]][]) { } +} + export enum LanguageModelChatMessageRole { User = 1, Assistant = 2, diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 239e5bb89ac..72def8c4b99 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -646,18 +646,24 @@ export class ResourceListDnDHandler implements IListDragAndDrop { onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { const resources: URI[] = []; - for (const element of (data as ElementsDragAndDropData).elements) { + const elements = (data as ElementsDragAndDropData).elements; + for (const element of elements) { const resource = this.toResource(element); if (resource) { resources.push(resource); } } + this.onWillDragElements(elements, originalEvent); if (resources.length) { // Apply some datatransfer types to allow for dragging the element outside of the application this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent)); } } + protected onWillDragElements(elements: readonly T[], originalEvent: DragEvent): void { + // noop + } + onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { return false; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index de0ad326f51..5e958cb32dd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -49,14 +49,14 @@ import { SearchContext } from '../../../search/common/constants.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { IChatRequestVariableEntry } from '../../common/chatModel.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; -import { createFolderQuickPick } from '../contrib/chatDynamicVariables.js'; +import { createFolderQuickPick, createMarkersQuickPick } from '../contrib/chatDynamicVariables.js'; import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; import { resizeImage } from '../imageUtils.js'; import { CHAT_CATEGORY } from './chatActions.js'; @@ -75,7 +75,7 @@ export function registerChatContextActions() { */ type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | - IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem | IFolderQuickPickItem; + IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem; /** * These are the types that we can get out of the quick pick @@ -103,6 +103,12 @@ function isIFolderSearchResultQuickPickItem(obj: unknown): obj is IFolderResultQ && (obj as IFolderResultQuickPickItem).kind === 'folder-search-result'); } +function isIDiagnosticsQuickPickItemWithFilter(obj: unknown): obj is IDiagnosticsQuickPickItemWithFilter { + return ( + typeof obj === 'object' + && (obj as IDiagnosticsQuickPickItemWithFilter).kind === 'diagnostic-filter'); +} + function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource { return ( typeof obj === 'object' @@ -210,6 +216,19 @@ interface IScreenShotQuickPickItem extends IQuickPickItem { icon?: ThemeIcon; } +interface IDiagnosticsQuickPickItem extends IQuickPickItem { + kind: 'diagnostic'; + id: string; + icon?: ThemeIcon; +} + +interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem { + kind: 'diagnostic-filter'; + id: string; + filter: IDiagnosticVariableEntryFilterData; + icon?: ThemeIcon; +} + /** * Quick pick item for prompt instructions attachment. */ @@ -499,6 +518,14 @@ export class AttachContextAction extends Action2 { isFile: false, isDirectory: true, }); + } else if (isIDiagnosticsQuickPickItemWithFilter(pick)) { + toAttach.push({ + id: pick.id, + name: pick.label, + value: pick.filter, + kind: 'diagnostic', + ...pick.filter, + }); } else if (isIQuickPickItemWithResource(pick) && pick.resource) { if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) { // checks if the file is an image @@ -750,6 +777,13 @@ export class AttachContextAction extends Action2 { id: 'folder', }); + quickPickItems.push({ + kind: 'diagnostic', + label: localize('chatContext.diagnstic', 'Problem...'), + iconClass: ThemeIcon.asClassName(Codicon.error), + id: 'diagnostic' + }); + if (widget.location === ChatAgentLocation.Notebook) { quickPickItems.push({ kind: 'command', @@ -824,16 +858,33 @@ export class AttachContextAction extends Action2 { }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', context?.placeholder); } + private async _showDiagnosticsPick(instantiationService: IInstantiationService): Promise { + const filter = await instantiationService.invokeFunction(accessor => createMarkersQuickPick(accessor)); + if (!filter) { + return undefined; + } + + return { + kind: 'diagnostic-filter', + id: IDiagnosticVariableEntryFilterData.id(filter), + label: IDiagnosticVariableEntryFilterData.label(filter), + filter, + }; + } + private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, instantiationService: IInstantiationService, query: string = '', placeholder?: string) { const providerOptions: AnythingQuickAccessProviderRunOptions = { - handleAccept: async (item: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { + handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { + let item: IChatContextQuickPickItem | undefined = inputItem; if ('kind' in item && item.kind === 'folder') { - const folderItem = await this._showFolders(instantiationService); - if (!folderItem) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder); - return; - } - item = folderItem; + item = await this._showFolders(instantiationService); + } else if ('kind' in item && item.kind === 'diagnostic') { + item = await this._showDiagnosticsPick(instantiationService); + } + + if (!item) { + this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder); + return; } if ('prefix' in item) { diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 522a7257756..a6f55ecebbe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -11,21 +11,23 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../base/common/mime.js'; import { basename, joinPath } from '../../../../base/common/resources.js'; +import { Mutable } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { SymbolKinds } from '../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../nls.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractSymbolDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js'; +import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractMarkerDropData, extractSymbolDropData, IDraggedResourceEditorInput, MarkerTransferData } from '../../../../platform/dnd/browser/dnd.js'; import { FileType, IFileService, IFileSystemProvider } from '../../../../platform/files/common/files.js'; +import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { isUntitledResourceEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; -import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../common/chatModel.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry } from '../common/chatModel.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { IChatInputStyles } from './chatInputPart.js'; import { resizeImage } from './imageUtils.js'; @@ -35,7 +37,8 @@ enum ChatDragAndDropType { FILE_EXTERNAL, FOLDER, IMAGE, - SYMBOL + SYMBOL, + MARKER, } export class ChatDragAndDrop extends Themable { @@ -169,6 +172,8 @@ export class ChatDragAndDrop extends Themable { return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined; } else if (containsDragType(e, CodeDataTransfers.SYMBOLS)) { return ChatDragAndDropType.SYMBOL; + } else if (containsDragType(e, CodeDataTransfers.MARKERS)) { + return ChatDragAndDropType.MARKER; } else if (containsDragType(e, DataTransfers.FILES)) { return ChatDragAndDropType.FILE_EXTERNAL; } else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) { @@ -193,6 +198,7 @@ export class ChatDragAndDrop extends Themable { case ChatDragAndDropType.FOLDER: return localize('folder', 'Folder'); case ChatDragAndDropType.IMAGE: return localize('image', 'Image'); case ChatDragAndDropType.SYMBOL: return localize('symbol', 'Symbol'); + case ChatDragAndDropType.MARKER: return localize('problem', 'Problem'); } } @@ -224,6 +230,11 @@ export class ChatDragAndDrop extends Themable { return []; } + const markerData = extractMarkerDropData(e); + if (markerData) { + return this.resolveMarkerAttachContext(markerData); + } + if (containsDragType(e, CodeDataTransfers.SYMBOLS)) { const data = extractSymbolDropData(e); return this.resolveSymbolsAttachContext(data); @@ -304,6 +315,33 @@ export class ChatDragAndDrop extends Themable { }); } + private resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] { + return markers.map((marker): IDiagnosticVariableEntry => { + const filter: Mutable = {}; + if (!('severity' in marker)) { + filter.filterUri = URI.revive(marker.uri); + filter.filterSeverity = MarkerSeverity.Warning; + } else { + filter.filterUri = URI.revive(marker.resource); + filter.filterSeverity = marker.severity; + filter.filterRange = { + startLineNumber: marker.startLineNumber, + startColumn: marker.startColumn, + endLineNumber: marker.endLineNumber, + endColumn: marker.endColumn + }; + } + + return { + kind: 'diagnostic', + id: IDiagnosticVariableEntryFilterData.id(filter), + name: IDiagnosticVariableEntryFilterData.label(filter), + value: filter, + ...filter, + }; + }); + } + private setOverlay(target: HTMLElement, type: ChatDragAndDropType | undefined): void { // Remove any previous overlay text this.overlayText?.remove(); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 9c805598653..0bf919d40f0 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -4,11 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from '../../../../../base/common/arrays.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; +import * as glob from '../../../../../base/common/glob.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { basename, dirname, joinPath, relativePath } from '../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; @@ -22,20 +26,19 @@ import { FileType, IFileService } from '../../../../../platform/files/common/fil import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; +import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; import { IQuickAccessOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { getExcludes, ISearchConfiguration, IFileQuery, QueryType, ISearchComplete, ISearchService } from '../../../../services/search/common/search.js'; +import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js'; import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; +import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js'; import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; -import * as glob from '../../../../../base/common/glob.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; @@ -645,3 +648,57 @@ export class AddDynamicVariableAction extends Action2 { } } registerAction2(AddDynamicVariableAction); + +export async function createMarkersQuickPick(accessor: ServicesAccessor): Promise { + const markers = accessor.get(IMarkerService).read(); + if (!markers.length) { + return; + } + + const uriIdentityService = accessor.get(IUriIdentityService); + const labelService = accessor.get(ILabelService); + markers.sort((a, b) => uriIdentityService.extUri.compare(a.resource, b.resource) || b.severity - a.severity); + + const severities = new Set(); + type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; + const items: (MarkerPickItem | IQuickPickSeparator)[] = []; + for (const marker of markers) { + if (!uriIdentityService.extUri.isEqual(marker.resource, (items.at(-1) as MarkerPickItem)?.resource)) { + items.push({ type: 'separator', label: labelService.getUriLabel(marker.resource, { relative: true }) }); + } + + severities.add(marker.severity); + items.push({ + type: 'item', + resource: marker.resource, + label: marker.message, + description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), + entry: { filterUri: marker.resource, filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn } } + }); + } + + if (items.length === 2) { // single error in a URI + return (items[1] as MarkerPickItem).entry; + } + + if (items.length > 2) { + if (severities.has(MarkerSeverity.Error)) { + items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Errors'), entry: { filterSeverity: MarkerSeverity.Error } }); + } + if (severities.has(MarkerSeverity.Warning)) { + items.unshift({ type: 'item', label: localize('markers.panel.allWarnings', 'All Warnings'), entry: { filterSeverity: MarkerSeverity.Warning } }); + } + if (severities.has(MarkerSeverity.Info)) { + items.unshift({ type: 'item', label: localize('markers.panel.allInfos', 'All Infos'), entry: { filterSeverity: MarkerSeverity.Info } }); + } + } + + + const quickInputService = accessor.get(IQuickInputService); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); + quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); + quickPick.items = items; + + return quickInputService.pick(items, { canPickMany: false }).then(v => v?.entry); +} + diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index dec4623cd07..2eccb6163ef 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -21,6 +21,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent, reviveSerializedAgent } from './chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; @@ -87,7 +88,39 @@ export interface ILinkVariableEntry extends Omit, IDiagnosticVariableEntryFilterData { + readonly kind: 'diagnostic'; +} + +export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | ILinkVariableEntry | IBaseChatRequestVariableEntry | IDiagnosticVariableEntry; export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; @@ -101,6 +134,10 @@ export function isLinkVariableEntry(obj: IChatRequestVariableEntry): obj is ILin return obj.kind === 'link'; } +export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry { + return obj.kind === 'diagnostic'; +} + export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry { const entry = obj as IChatRequestVariableEntry; return typeof entry === 'object' && diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 85ad02f34be..f80cfdeebaa 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -5,57 +5,59 @@ import './media/markers.css'; -import { URI } from '../../../../base/common/uri.js'; import * as dom from '../../../../base/browser/dom.js'; -import { IAction, Separator } from '../../../../base/common/actions.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; -import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement, MarkerTableItem } from './markersModel.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { MarkersFilters, IMarkersFiltersChangeEvent } from './markersViewActions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import Messages from './messages.js'; -import { RangeHighlightDecorations } from '../../../browser/codeeditor.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { localize } from '../../../../nls.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer, ITreeEvent } from '../../../../base/browser/ui/tree/tree.js'; -import { Relay, Event } from '../../../../base/common/event.js'; -import { WorkbenchObjectTree, IListService, IWorkbenchObjectTreeOptions, IOpenEvent } from '../../../../platform/list/browser/listService.js'; -import { FilterOptions } from './markersFilterOptions.js'; -import { IExpression } from '../../../../base/common/glob.js'; -import { deepClone } from '../../../../base/common/objects.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersWidgetAccessibilityProvider, MarkersViewModel } from './markersTreeViewer.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { StandardKeyboardEvent, IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { ResourceLabels } from '../../../browser/labels.js'; -import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; -import { MementoObject, Memento } from '../../../common/memento.js'; -import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js'; -import { IViewDescriptorService } from '../../../common/views.js'; -import { IOpenerService, withSelection } from '../../../../platform/opener/common/opener.js'; +import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { groupBy } from '../../../../base/common/arrays.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js'; -import { IMarkersView } from './markers.js'; -import { ResourceListDnDHandler } from '../../../browser/dnd.js'; +import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { ITableContextMenuEvent, ITableEvent } from '../../../../base/browser/ui/table/table.js'; -import { MarkersTable } from './markersTable.js'; -import { Markers, MarkersContextKeys, MarkersViewMode } from '../common/markers.js'; -import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; +import { ITreeContextMenuEvent, ITreeElement, ITreeEvent, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; +import { IAction, Separator } from '../../../../base/common/actions.js'; +import { groupBy } from '../../../../base/common/arrays.js'; +import { Event, Relay } from '../../../../base/common/event.js'; +import { IExpression } from '../../../../base/common/glob.js'; +import { Iterable } from '../../../../base/common/iterator.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { deepClone } from '../../../../base/common/objects.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { localize } from '../../../../nls.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { fillInMarkersDragData, MarkerTransferData } from '../../../../platform/dnd/browser/dnd.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js'; +import { IListService, IOpenEvent, IWorkbenchObjectTreeOptions, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; +import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { IOpenerService, withSelection } from '../../../../platform/opener/common/opener.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; +import { RangeHighlightDecorations } from '../../../browser/codeeditor.js'; +import { ResourceListDnDHandler } from '../../../browser/dnd.js'; +import { ResourceLabels } from '../../../browser/labels.js'; +import { FilterViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js'; +import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js'; +import { Memento, MementoObject } from '../../../common/memento.js'; +import { IViewDescriptorService } from '../../../common/views.js'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { Markers, MarkersContextKeys, MarkersViewMode } from '../common/markers.js'; +import { IMarkersView } from './markers.js'; +import { FilterOptions } from './markersFilterOptions.js'; +import { compareMarkersByUri, Marker, MarkerChangesEvent, MarkerElement, MarkersModel, MarkerTableItem, RelatedInformation, ResourceMarkers } from './markersModel.js'; +import { MarkersTable } from './markersTable.js'; +import { Filter, FilterData, MarkerRenderer, MarkersViewModel, MarkersWidgetAccessibilityProvider, RelatedInformationRenderer, ResourceMarkersRenderer, VirtualDelegate } from './markersTreeViewer.js'; +import { IMarkersFiltersChangeEvent, MarkersFilters } from './markersViewActions.js'; +import Messages from './messages.js'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { @@ -495,18 +497,7 @@ export class MarkersView extends FilterViewPane implements IMarkersView { filter: this.filter, accessibilityProvider: this.widgetAccessibilityProvider, identityProvider: this.widgetIdentityProvider, - dnd: this.instantiationService.createInstance(ResourceListDnDHandler, (element) => { - if (element instanceof ResourceMarkers) { - return element.resource; - } - if (element instanceof Marker) { - return withSelection(element.resource, element.range); - } - if (element instanceof RelatedInformation) { - return withSelection(element.raw.resource, element.raw); - } - return null; - }), + dnd: this.instantiationService.createInstance(MarkersListDnDHandler), expandOnlyOnTwistieClick: (e: MarkerElement) => e instanceof Marker && e.relatedInformation.length > 0, overrideStyles: this.getLocationBasedColors().listOverrideStyles, selectionNavigation: true, @@ -1078,3 +1069,40 @@ class MarkersTree extends WorkbenchObjectTree impleme super.layout(height, width); } } + +class MarkersListDnDHandler extends ResourceListDnDHandler { + constructor( + @IInstantiationService instantiationService: IInstantiationService + ) { + super(element => { + if (element instanceof MarkerTableItem) { + return withSelection(element.resource, element.range); + } else if (element instanceof ResourceMarkers) { + return element.resource; + } else if (element instanceof Marker) { + return withSelection(element.resource, element.range); + } else if (element instanceof RelatedInformation) { + return withSelection(element.raw.resource, element.raw); + } + return null; + }, instantiationService); + } + + protected override onWillDragElements(elements: (MarkerElement | MarkerTableItem)[], originalEvent: DragEvent) { + const data = elements.map((e): MarkerTransferData | undefined => { + if (e instanceof RelatedInformation || e instanceof Marker) { + return e.marker; + } + if (e instanceof ResourceMarkers) { + return { uri: e.resource }; + } + return undefined; + }).filter(isDefined); + + if (!data.length) { + return; + } + + fillInMarkersDragData(data, originalEvent); + } +} diff --git a/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts b/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts new file mode 100644 index 00000000000..855015de891 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface ChatPromptReference { + /** + * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. + */ + readonly value: string | Uri | Location | ChatReferenceDiagnostic | unknown; + } + + export class ChatReferenceDiagnostic { + /** + * All attached diagnostics. An array of uri-diagnostics tuples or an empty array. + */ + readonly diagnostics: [Uri, Diagnostic[]][]; + + protected constructor(diagnostics: [Uri, Diagnostic[]][]); + } +} From 0629ca02c43b1508cb0824d37ca8ed0ea99b74af Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Feb 2025 14:03:16 +1100 Subject: [PATCH 15/36] Initial implementation of ModifiedNotebookEntry (#241275) * Initial implementation of ModifiedNotebookEntry * Updates * Ensure acceptAgentEdits is async * Udpates * Updates --- .../chatEditingModifiedNotebookEntry.ts | 183 ++++++++++++++++-- .../browser/chatEditing/chatEditingSession.ts | 15 +- .../chatEditingNotebookFileSystemProvider.ts | 121 ++++++++++++ .../browser/contrib/chatEdit/contribution.ts | 4 + 4 files changed, 303 insertions(+), 20 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index 5f6deab0d15..a36077d5d64 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -4,32 +4,125 @@ *--------------------------------------------------------------------------------------------*/ import { assert } from '../../../../../base/common/assert.js'; +import { decodeBase64, encodeBase64, streamToBuffer, VSBuffer } from '../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { DisposableStore, IReference } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { ITransaction, IObservable, constObservable } from '../../../../../base/common/observable.js'; +import { ITransaction, IObservable, observableValue } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; +import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IEditorPane } from '../../../../common/editor.js'; -import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; -import { IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js'; +import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; +import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js'; +import { ChatEditingNotebookFileSystemProvider } from '../../../notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.js'; +import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js'; +import { ICellEditOperation, IResolvedNotebookEditorModel, NotebookData, NotebookSetting, TransientOptions } from '../../../notebook/common/notebookCommon.js'; +import { INotebookEditorModelResolverService } from '../../../notebook/common/notebookEditorModelResolverService.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { ChatEditKind, IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; -import { AbstractChatEditingModifiedFileEntry, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { IChatService } from '../../common/chatService.js'; +import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingSnapshotTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifiedFileEntry { - override originalURI: URI = URI.parse('todo://todo/todo'); + private readonly modifiedModel: NotebookTextModel; + private readonly originalModel: NotebookTextModel; + override originalURI: URI; + override initialContent: string; - override initialContent: string = 'JSON.stringify(NotebookData)'; + private readonly _changesCount = observableValue(this, 0); + override changesCount: IObservable = this._changesCount; - override changesCount: IObservable = constObservable(Number.MAX_SAFE_INTEGER); + public static async create(uri: URI, _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, telemetryInfo: IModifiedEntryTelemetryInfo, chatKind: ChatEditKind, initialContent: string | undefined, instantiationService: IInstantiationService): Promise { + return instantiationService.invokeFunction(async accessor => { + const notebookService = accessor.get(INotebookService); + const resolver = accessor.get(INotebookEditorModelResolverService); + const configurationServie = accessor.get(IConfigurationService); + const resourceRef: IReference = await resolver.resolve(uri); + const notebook = resourceRef.object.notebook; + const originalUri = ChatEditingNotebookFileSystemProvider.getSnapshotFileURI(telemetryInfo.requestId, notebook.uri.path); - protected override _doAccept(tx: ITransaction | undefined): Promise { - throw new Error('Method not implemented.'); + const [options, buffer] = await Promise.all([ + notebookService.withNotebookDataProvider(resourceRef.object.notebook.notebookType), + notebookService.createNotebookTextDocumentSnapshot(notebook.uri, SnapshotContext.Backup, CancellationToken.None).then(s => streamToBuffer(s)) + ]); + const originalDisposables = new DisposableStore(); + originalDisposables.add(ChatEditingNotebookFileSystemProvider.registerFile(originalUri, buffer)); + + const originalRef = await resolver.resolve(originalUri, notebook.viewType); + originalDisposables.add(originalRef); + if (initialContent) { + restoreSnapshot(originalRef.object.notebook, initialContent); + } + initialContent = initialContent || createSnapshot(originalRef.object.notebook, options.serializer.options, configurationServie); + return instantiationService.createInstance(ChatEditingModifiedNotebookEntry, resourceRef, originalRef, _multiDiffEntryDelegate, options.serializer.options, telemetryInfo, chatKind, initialContent); + }); } - protected override _doReject(tx: ITransaction | undefined): Promise { - throw new Error('Method not implemented.'); + + constructor( + modifiedResourceRef: IReference, + originalResourceRef: IReference, + private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, + private readonly transientOptions: TransientOptions | undefined, + telemetryInfo: IModifiedEntryTelemetryInfo, + kind: ChatEditKind, + initialContent: string, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IFilesConfigurationService fileConfigService: IFilesConfigurationService, + @IChatService chatService: IChatService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService + + ) { + super(modifiedResourceRef.object.notebook.uri, telemetryInfo, kind, configurationService, fileConfigService, chatService, fileService, instantiationService); + this._register(modifiedResourceRef); + this._register(originalResourceRef); + this.modifiedModel = modifiedResourceRef.object.notebook; + this.originalModel = originalResourceRef.object.notebook; + this.originalURI = this.originalModel.uri; + this.initialContent = initialContent; + } + + protected override async _doAccept(tx: ITransaction | undefined): Promise { + const outputSizeLimit = this.configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; + const snapshot = this.modifiedModel.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions: this.transientOptions }); + this.originalModel.restoreSnapshot(snapshot, this.transientOptions); + this._changesCount.set(0, tx); + // this._diffInfo.set(nullDocumentDiff, tx); + // this._edit = OffsetEdit.empty; + await this._collapse(tx); + + } + + protected override async _doReject(tx: ITransaction | undefined): Promise { + if (this.createdInRequestId === this._telemetryInfo.requestId) { + // await this.docFileEditorModel.revert({ soft: true }); + await this._fileService.del(this.modifiedURI); + this._onDidDelete.fire(); + } else { + const outputSizeLimit = this.configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; + const snapshot = this.originalModel.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions: this.transientOptions }); + this.modifiedModel.restoreSnapshot(snapshot, this.transientOptions); + // if (this._allEditsAreFromUs) { + // // // save the file after discarding so that the dirty indicator goes away + // // // and so that an intermediate saved state gets reverted + // // await this.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); + // } + await this._collapse(tx); + } + + } + private async _collapse(transaction: ITransaction | undefined): Promise { + this._multiDiffEntryDelegate.collapse(transaction); } protected override _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration { @@ -48,18 +141,78 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } override createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry { - throw new Error('Method not implemented.'); + return { + resource: this.modifiedURI, + languageId: 'notebook', + snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(this._telemetryInfo.sessionId, requestId, undoStop, this.modifiedURI.path), + original: this.initialContent, + current: createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService), + originalToCurrentEdit: OffsetEdit.empty, + state: this.state.get(), + telemetryInfo: this.telemetryInfo, + }; } override equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean { - throw new Error('Method not implemented.'); + return !!snapshot && + this.modifiedURI.toString() === snapshot.resource.toString() && + this.state.get() === snapshot.state && + createSnapshot(this.originalModel, this.transientOptions, this.configurationService) === snapshot.original && + createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService) === snapshot.current; + } override restoreFromSnapshot(snapshot: ISnapshotEntry): void { - throw new Error('Method not implemented.'); + this._stateObs.set(snapshot.state, undefined); + restoreSnapshot(this.originalModel, snapshot.original); + restoreSnapshot(this.modifiedModel, snapshot.current); + // this._edit = snapshot.originalToCurrentEdit; + // this._updateDiffInfoSeq(); + restoreSnapshot(this.modifiedModel, snapshot.current); + } override resetToInitialContent(): void { - throw new Error('Method not implemented.'); + restoreSnapshot(this.modifiedModel, this.initialContent); } } + +const BufferMarker = 'ArrayBuffer-4f56482b-5a03-49ba-8356-210d3b0c1c3d'; +function createSnapshot(notebook: NotebookTextModel, transientOptions: TransientOptions | undefined, configurationService: IConfigurationService): string { + const outputSizeLimit = configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; + return serializeSnapshot(notebook.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions }), transientOptions); +} + +function restoreSnapshot(notebook: NotebookTextModel, snapshot: string): void { + const { transientOptions, data } = deserializeSnapshot(snapshot); + notebook.restoreSnapshot(data, transientOptions); +} + +function serializeSnapshot(data: NotebookData, transientOptions: TransientOptions | undefined): string { + return JSON.stringify([ + JSON.stringify(transientOptions) + , JSON.stringify(data, (_key, value) => { + if (value instanceof VSBuffer) { + return { + type: BufferMarker, + data: encodeBase64(value) + }; + } + return value; + }) + ]); +} + +function deserializeSnapshot(snapshot: string): { transientOptions: TransientOptions | undefined; data: NotebookData } { + const [transientOptionsStr, dataStr] = JSON.parse(snapshot); + const transientOptions = transientOptionsStr ? JSON.parse(transientOptionsStr) as TransientOptions : undefined; + + const data: NotebookData = JSON.parse(dataStr, (_key, value) => { + if (value && value.type === BufferMarker) { + return decodeBase64(value.data); + } + return value; + }); + + return { transientOptions, data }; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 281bf906fc3..077425865e0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -49,6 +49,7 @@ import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISna import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; @@ -1020,12 +1021,16 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { + const multiDiffEntryDelegate = { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }; + const chatKind = mustExist ? ChatEditKind.Created : ChatEditKind.Modified; try { - const ref = await this._textModelService.createModelReference(resource); - const ctor = this._notebookService.hasSupportedNotebooks(resource) - ? ChatEditingModifiedDocumentEntry // TODO@DonJayamanne use NotebookEntry once ready - : ChatEditingModifiedDocumentEntry; - return this._instantiationService.createInstance(ctor, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, telemetryInfo, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent); + const notebookUri = CellUri.parse(resource)?.notebook || resource; + if (this._notebookService.hasSupportedNotebooks(notebookUri)) { + return ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService); + } else { + const ref = await this._textModelService.createModelReference(resource); + return this._instantiationService.createInstance(ChatEditingModifiedDocumentEntry, ref, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent); + } } catch (err) { if (mustExist) { throw err; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts new file mode 100644 index 00000000000..0b3d8cd343d --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { ReadableStreamEvents } from '../../../../../../base/common/stream.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IFileService, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileReadStreamOptions, IFileOpenOptions, FileType } from '../../../../../../platform/files/common/files.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; + + +export class ChatEditingNotebookFileSystemProviderContrib extends Disposable implements IWorkbenchContribution { + static ID = 'chatEditingNotebookFileSystemProviderContribution'; + constructor( + @IFileService private readonly fileService: IFileService) { + + super(); + this._register(this.fileService.registerProvider(ChatEditingNotebookFileSystemProvider.scheme, new ChatEditingNotebookFileSystemProvider())); + } +} + +export class ChatEditingNotebookFileSystemProvider implements IFileSystemProvider { + public static readonly scheme = 'chat-editing-notebook-model'; + private static registeredFiles = new ResourceMap(); + + public static getEmptyFileURI(): URI { + return URI.from({ + scheme: ChatEditingNotebookFileSystemProvider.scheme, + query: JSON.stringify({ kind: 'empty' }), + }); + } + + public static getFileURI(documentId: string, path: string): URI { + return URI.from({ + scheme: ChatEditingNotebookFileSystemProvider.scheme, + path, + query: JSON.stringify({ kind: 'doc' }), + }); + } + public static getSnapshotFileURI(requestId: string | undefined, path: string): URI { + return URI.from({ + scheme: ChatEditingNotebookFileSystemProvider.scheme, + path, + query: JSON.stringify({ requestId: requestId ?? '' }), + }); + } + + public readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.FileAtomicRead | FileSystemProviderCapabilities.FileReadWrite; + public static registerFile(resource: URI, buffer: VSBuffer): IDisposable { + ChatEditingNotebookFileSystemProvider.registeredFiles.set(resource, buffer); + return { + dispose() { + if (ChatEditingNotebookFileSystemProvider.registeredFiles.get(resource) === buffer) { + ChatEditingNotebookFileSystemProvider.registeredFiles.delete(resource); + } + } + }; + } + + readonly onDidChangeCapabilities = Event.None; + readonly onDidChangeFile: Event = Event.None; + watch(_resource: URI, _opts: IWatchOptions): IDisposable { + return Disposable.None; + } + async stat(_resource: URI): Promise { + return { + type: FileType.File, + ctime: 0, + mtime: 0, + size: 0 + }; + } + mkdir(_resource: URI): Promise { + throw new Error('Method not implemented1.'); + } + readdir(_resource: URI): Promise<[string, FileType][]> { + throw new Error('Method not implemented2.'); + } + delete(_resource: URI, _opts: IFileDeleteOptions): Promise { + throw new Error('Method not implemented3.'); + } + rename(_from: URI, _to: URI, _opts: IFileOverwriteOptions): Promise { + throw new Error('Method not implemented4.'); + } + copy?(_from: URI, _to: URI, _opts: IFileOverwriteOptions): Promise { + throw new Error('Method not implemented5.'); + } + async readFile(resource: URI): Promise { + const buffer = ChatEditingNotebookFileSystemProvider.registeredFiles.get(resource); + if (!buffer) { + throw new Error('File not found'); + } + return buffer.buffer; + } + writeFile?(__resource: URI, _content: Uint8Array, _opts: IFileWriteOptions): Promise { + throw new Error('Method not implemented7.'); + } + readFileStream?(__resource: URI, _opts: IFileReadStreamOptions, _token: CancellationToken): ReadableStreamEvents { + throw new Error('Method not implemented8.'); + } + open?(__resource: URI, _opts: IFileOpenOptions): Promise { + throw new Error('Method not implemented9.'); + } + close?(_fd: number): Promise { + throw new Error('Method not implemented10.'); + } + read?(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise { + throw new Error('Method not implemented11.'); + } + write?(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise { + throw new Error('Method not implemented12.'); + } + cloneFile?(_from: URI, __to: URI): Promise { + throw new Error('Method not implemented13.'); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts index 6b8387bd908..b8123bc8c09 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts @@ -9,9 +9,13 @@ import { registerNotebookContribution } from '../../notebookEditorExtensions.js' import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js'; import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js'; import { NotebookChatEditorControllerContrib } from './notebookChatEditController.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; +import { ChatEditingNotebookFileSystemProviderContrib } from './chatEditingNotebookFileSystemProvider.js'; registerNotebookContribution(NotebookChatEditorControllerContrib.ID, NotebookChatEditorControllerContrib); registerSingleton(INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory, InstantiationType.Delayed); registerSingleton(INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory, InstantiationType.Delayed); registerSingleton(INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory, InstantiationType.Delayed); + +registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); From f23861839fb21c44102dac215998647b4cd02840 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 Feb 2025 20:25:51 -0800 Subject: [PATCH 16/36] Fix leaking previous ChatModel after clearing session (#241294) --- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 8 +++++--- src/vs/workbench/contrib/chat/browser/chat.ts | 2 +- .../browser/chatContentParts/chatMarkdownContentPart.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 6 ++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index baf3f39e7c0..aa90fc63047 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -450,8 +450,9 @@ export function registerChatCodeBlockActions() { const focused = !widget.inputEditor.hasWidgetFocus() && widget.getFocus(); const focusedResponse = isResponseVM(focused) ? focused : undefined; - const currentResponse = curCodeBlockInfo ? - curCodeBlockInfo.element : + const elementId = curCodeBlockInfo?.elementId; + const element = elementId ? widget.viewModel?.getItems().find(item => item.id === elementId) : undefined; + const currentResponse = element ?? (focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item))); if (!currentResponse || !isResponseVM(currentResponse)) { return; @@ -531,8 +532,9 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): return; } + const element = widget?.viewModel?.getItems().find(item => item.id === codeBlockInfo.elementId); return { - element: codeBlockInfo.element, + element, codeBlockIndex: codeBlockInfo.codeBlockIndex, code: editor.getValue(), languageId: editor.getModel()!.getLanguageId(), diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 982143743ce..0d3ff60ded2 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -136,7 +136,7 @@ export interface IChatAccessibilityService { export interface IChatCodeBlockInfo { readonly ownerMarkdownPartId: string; readonly codeBlockIndex: number; - readonly element: ChatTreeItem; + readonly elementId: string; readonly uri: URI | undefined; readonly uriPromise: Promise; codemapperUri: URI | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 215acff06b2..f97705b9303 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -130,7 +130,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const info: IChatCodeBlockInfo = new class { readonly ownerMarkdownPartId = ownerMarkdownPartId; readonly codeBlockIndex = globalIndex; - readonly element = element; + readonly elementId = element.id; readonly isStreaming = !rendererOptions.renderCodeBlockPills; codemapperUri = undefined; // will be set async public get uri() { @@ -165,7 +165,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const info: IChatCodeBlockInfo = new class { readonly ownerMarkdownPartId = ownerMarkdownPartId; readonly codeBlockIndex = globalIndex; - readonly element = element; + readonly elementId = element.id; readonly isStreaming = !isCodeBlockComplete; readonly codemapperUri = codemapperUri; public get uri() { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 8f5d0743d04..bfe7cad91f9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -1014,6 +1014,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IChatListItemTemplate): void { this.traceLayout('disposeElement', `Disposing element, index=${index}`); templateData.elementDisposables.clear(); + + // Don't retain the toolbar context which includes chat viewmodels + if (templateData.titleToolbar) { + templateData.titleToolbar.context = undefined; + } + templateData.footerToolbar.context = undefined; } disposeTemplate(templateData: IChatListItemTemplate): void { From ad9c5fda0c1004f9de462b9de01f5794a11990ee Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 19 Feb 2025 22:05:54 -0800 Subject: [PATCH 17/36] debt: dispose notification action runner (#241273) --- .../browser/parts/notifications/notificationsViewer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index c1880ef981b..389569cf5a7 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -491,7 +491,7 @@ export class NotificationTemplateRenderer extends Disposable { if (notification.expanded && isNonEmptyArray(primaryActions)) { const that = this; - const actionRunner: IActionRunner = new class extends ActionRunner { + const actionRunner: IActionRunner = this.inputDisposables.add(new class extends ActionRunner { protected override async runAction(action: IAction): Promise { // Run action @@ -502,7 +502,7 @@ export class NotificationTemplateRenderer extends Disposable { notification.close(); } } - }(); + }()); const buttonToolbar = this.inputDisposables.add(new ButtonBar(this.template.buttonsContainer)); for (let i = 0; i < primaryActions.length; i++) { From 6fdb41922c2ff82748ab95c2dc484bd78cd2b8f2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 20 Feb 2025 07:11:11 +0100 Subject: [PATCH 18/36] Git - remove jschardet (#241256) * Adopt API for stage * Adopt API for show * Remove package --- extensions/git/package-lock.json | 10 ---- extensions/git/package.json | 2 +- extensions/git/src/commands.ts | 19 ++++-- extensions/git/src/encoding.ts | 100 ------------------------------- extensions/git/src/git.ts | 13 ---- extensions/git/src/repository.ts | 26 ++------ extensions/git/tsconfig.json | 1 + 7 files changed, 23 insertions(+), 148 deletions(-) delete mode 100644 extensions/git/src/encoding.ts diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index bc150555c70..7a9cb2aded8 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -14,7 +14,6 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", - "jschardet": "3.1.4", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" @@ -279,15 +278,6 @@ "node": ">=16" } }, - "node_modules/jschardet": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", - "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", - "license": "LGPL-2.1+", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", diff --git a/extensions/git/package.json b/extensions/git/package.json index f5195eee8fb..06f40d948a1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -35,6 +35,7 @@ "statusBarItemTooltip", "tabInputMultiDiff", "tabInputTextMerge", + "textDocumentEncoding", "textEditorDiffInformation", "timeline" ], @@ -3566,7 +3567,6 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", - "jschardet": "3.1.4", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 94bcb143c58..b09e6d5a6c5 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1628,19 +1628,29 @@ export class CommandCenter { } let modifiedUri = changes.modifiedUri; + let modifiedDocument: TextDocument | undefined; + if (!modifiedUri) { const textEditor = window.activeTextEditor; if (!textEditor) { return; } - const modifiedDocument = textEditor.document; + + modifiedDocument = textEditor.document; modifiedUri = modifiedDocument.uri; } + if (modifiedUri.scheme !== 'file') { return; } + + if (!modifiedDocument) { + modifiedDocument = await workspace.openTextDocument(modifiedUri); + } + const result = changes.originalWithModifiedChanges; - await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result)); + await this.runByRepository(modifiedUri, async (repository, resource) => + await repository.stage(resource, result, modifiedDocument.encoding)); } @command('git.stageSelectedRanges', { diff: true }) @@ -1817,7 +1827,8 @@ export class CommandCenter { const originalDocument = await workspace.openTextDocument(originalUri); const result = applyLineChanges(originalDocument, modifiedDocument, changes); - await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result)); + await this.runByRepository(modifiedUri, async (repository, resource) => + await repository.stage(resource, result, modifiedDocument.encoding)); } @command('git.revertChange') @@ -1989,7 +2000,7 @@ export class CommandCenter { this.logger.trace(`[CommandCenter][unstageSelectedRanges] invertedDiffs: ${JSON.stringify(invertedDiffs)}`); const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); - await repository.stage(modifiedUri, result); + await repository.stage(modifiedUri, result, modifiedDocument.encoding); } @command('git.unstageFile') diff --git a/extensions/git/src/encoding.ts b/extensions/git/src/encoding.ts deleted file mode 100644 index c80fb6ee6d5..00000000000 --- a/extensions/git/src/encoding.ts +++ /dev/null @@ -1,100 +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 * as jschardet from 'jschardet'; - -function detectEncodingByBOM(buffer: Buffer): string | null { - if (!buffer || buffer.length < 2) { - return null; - } - - const b0 = buffer.readUInt8(0); - const b1 = buffer.readUInt8(1); - - // UTF-16 BE - if (b0 === 0xFE && b1 === 0xFF) { - return 'utf16be'; - } - - // UTF-16 LE - if (b0 === 0xFF && b1 === 0xFE) { - return 'utf16le'; - } - - if (buffer.length < 3) { - return null; - } - - const b2 = buffer.readUInt8(2); - - // UTF-8 - if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) { - return 'utf8'; - } - - return null; -} - -const IGNORE_ENCODINGS = [ - 'ascii', - 'utf-8', - 'utf-16', - 'utf-32' -]; - -const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { - 'ibm866': 'cp866', - 'big5': 'cp950' -}; - -const MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET: { [key: string]: string } = { - utf8: 'UTF-8', - utf16le: 'UTF-16LE', - utf16be: 'UTF-16BE', - windows1252: 'windows-1252', - windows1250: 'windows-1250', - iso88592: 'ISO-8859-2', - windows1251: 'windows-1251', - cp866: 'IBM866', - iso88595: 'ISO-8859-5', - koi8r: 'KOI8-R', - windows1253: 'windows-1253', - iso88597: 'ISO-8859-7', - windows1255: 'windows-1255', - iso88598: 'ISO-8859-8', - cp950: 'Big5', - shiftjis: 'SHIFT_JIS', - eucjp: 'EUC-JP', - euckr: 'EUC-KR', - gb2312: 'GB2312' -}; - -export function detectEncoding(buffer: Buffer, candidateGuessEncodings: string[]): string | null { - const result = detectEncodingByBOM(buffer); - - if (result) { - return result; - } - - candidateGuessEncodings = candidateGuessEncodings.map(e => MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET[e]).filter(e => !!e); - - const detected = jschardet.detect(buffer, candidateGuessEncodings.length > 0 ? { detectEncodings: candidateGuessEncodings } : undefined); - if (!detected || !detected.encoding) { - return null; - } - - const encoding = detected.encoding; - - // Ignore encodings that cannot guess correctly - // (http://chardet.readthedocs.io/en/latest/supported-encodings.html) - if (0 <= IGNORE_ENCODINGS.indexOf(encoding.toLowerCase())) { - return null; - } - - const normalizedEncodingName = encoding.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); - const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; - - return mapped || normalizedEncodingName; -} diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 2f0d11bd488..29fc38abfed 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -14,7 +14,6 @@ import * as iconv from '@vscode/iconv-lite-umd'; import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; -import { detectEncoding } from './encoding'; import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -1330,18 +1329,6 @@ export class Repository { .filter(entry => !!entry); } - async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false, candidateGuessEncodings: string[] = []): Promise { - const stdout = await this.buffer(object); - - if (autoGuessEncoding) { - encoding = detectEncoding(stdout, candidateGuessEncodings) || encoding; - } - - encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; - - return iconv.decode(stdout, encoding); - } - async buffer(object: string): Promise { const child = this.stream(['show', '--textconv', object]); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ca8974f2fc5..2f164ee5ede 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,7 +7,6 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import picomatch from 'picomatch'; -import * as iconv from '@vscode/iconv-lite-umd'; import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; @@ -25,7 +24,6 @@ import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, isRemote, Limiter, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; -import { detectEncoding } from './encoding'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -1222,19 +1220,9 @@ export class Repository implements Disposable { await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath))); } - async stage(resource: Uri, contents: string): Promise { - const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); + async stage(resource: Uri, contents: string, encoding: string): Promise { await this.run(Operation.Stage, async () => { - const configFiles = workspace.getConfiguration('files', Uri.file(resource.fsPath)); - let encoding = configFiles.get('encoding') ?? 'utf8'; - const autoGuessEncoding = configFiles.get('autoGuessEncoding') === true; - const candidateGuessEncodings = configFiles.get('candidateGuessEncodings') ?? []; - - if (autoGuessEncoding) { - encoding = detectEncoding(Buffer.from(contents), candidateGuessEncodings) ?? encoding; - } - - encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; + const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); await this.repository.stage(path, contents, encoding); this._onDidChangeOriginalResource.fire(resource); @@ -1980,17 +1968,15 @@ export class Repository implements Disposable { async show(ref: string, filePath: string): Promise { return await this.run(Operation.Show, async () => { const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); - const defaultEncoding = configFiles.get('encoding'); - const autoGuessEncoding = configFiles.get('autoGuessEncoding'); - const candidateGuessEncodings = configFiles.get('candidateGuessEncodings'); try { - return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); + const content = await this.repository.buffer(`${ref}:${path}`); + return await workspace.decode(content, Uri.file(filePath)); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { const gitRelativePath = await this.repository.getGitRelativePath(ref, path); - return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); + const content = await this.repository.buffer(`${ref}:${gitRelativePath}`); + return await workspace.decode(content, Uri.file(filePath)); } throw err; diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 42218d8decb..1d330ea2516 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -25,6 +25,7 @@ "../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", + "../../src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", "../types/lib.textEncoder.d.ts" From 2ffc73b962a6bf7df5dbd589a7ba11d540578b78 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 Feb 2025 22:41:46 -0800 Subject: [PATCH 19/36] Keyboard shortcut to accept tool confirmations (#241301) --- .../browser/actions/chatExecuteActions.ts | 3 +- .../chat/browser/actions/chatToolActions.ts | 50 +++++++++++++++++++ .../contrib/chat/browser/chat.contribution.ts | 2 + .../chatConfirmationWidget.ts | 3 +- .../chatToolInvocationPart.ts | 24 ++++++++- 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 59befe2e834..4118e2156ac 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -501,8 +501,9 @@ class SendToNewChatAction extends Action2 { } } +export const CancelChatActionId = 'workbench.action.chat.cancel'; export class CancelAction extends Action2 { - static readonly ID = 'workbench.action.chat.cancel'; + static readonly ID = CancelChatActionId; constructor() { super({ id: CancelAction.ID, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts new file mode 100644 index 00000000000..fdfbbf6a906 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { IChatToolInvocation } from '../../common/chatService.js'; +import { isResponseVM } from '../../common/chatViewModel.js'; +import { IChatWidgetService } from '../chat.js'; +import { CHAT_CATEGORY } from './chatActions.js'; + +export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool'; + +class AcceptToolConfirmation extends Action2 { + constructor() { + super({ + id: AcceptToolConfirmationActionId, + title: localize2('chat.accept', "Accept"), + f1: false, + category: CHAT_CATEGORY, + keybinding: { + when: ChatContextKeys.inChatInput, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + }, + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const chatWidgetService = accessor.get(IChatWidgetService); + const lastItem = chatWidgetService.lastFocusedWidget?.viewModel?.getItems().at(-1); + if (!isResponseVM(lastItem)) { + return; + } + + const unconfirmedToolInvocation = lastItem.model.response.value.find((item): item is IChatToolInvocation => item.kind === 'toolInvocation' && !item.isConfirmed); + if (unconfirmedToolInvocation) { + unconfirmedToolInvocation.confirmed.complete(true); + } + } +} + +export function registerChatToolActions() { + registerAction2(AcceptToolConfirmation); +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e1110b3e84f..d7ccf047998 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -95,6 +95,7 @@ import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorCon import { PromptsConfig } from '../../../../platform/prompts/common/config.js'; import { PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; import { DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; +import { registerChatToolActions } from './actions/chatToolActions.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -438,6 +439,7 @@ registerNewChatActions(); registerChatContextActions(); registerChatDeveloperActions(); registerChatEditorActions(); +registerChatToolActions(); registerEditorFeature(ChatPasteProvidersFeature); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts index b6f42ef60cc..620c1ba5cfa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts @@ -16,6 +16,7 @@ import { defaultButtonStyles } from '../../../../../platform/theme/browser/defau export interface IChatConfirmationButton { label: string; isSecondary?: boolean; + tooltip?: string; data: any; } @@ -63,7 +64,7 @@ export class ChatConfirmationWidget extends Disposable { elements.message.appendChild(renderedMessage.element); buttons.forEach(buttonData => { - const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary }); + const button = this._register(new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip })); button.label = buttonData.label; this._register(button.onDidClick(() => this._onDidClick.fire(buttonData))); }); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 0656658eff5..d5930da6340 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -12,9 +12,12 @@ import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownR import { localize } from '../../../../../nls.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IChatProgressMessage, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; import { IToolResult } from '../../common/languageModelToolsService.js'; +import { CancelChatActionId } from '../actions/chatExecuteActions.js'; +import { AcceptToolConfirmationActionId } from '../actions/chatToolActions.js'; import { ChatTreeItem } from '../chat.js'; import { ChatConfirmationWidget } from './chatConfirmationWidget.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; @@ -47,6 +50,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa const partStore = this._register(new DisposableStore()); const render = () => { dom.clearNode(this.domNode); + partStore.clear(); const subPart = partStore.add(instantiationService.createInstance(ChatToolInvocationSubPart, toolInvocation, context, renderer, listPool)); this.domNode.appendChild(subPart.domNode); @@ -84,6 +88,7 @@ class ChatToolInvocationSubPart extends Disposable { listPool: CollapsibleListPool, @IInstantiationService instantiationService: IInstantiationService, @IHoverService hoverService: IHoverService, + @IKeybindingService private readonly keybindingService: IKeybindingService, ) { super(); @@ -106,11 +111,28 @@ class ChatToolInvocationSubPart extends Disposable { } const title = toolInvocation.confirmationMessages.title; const message = toolInvocation.confirmationMessages.message; + const continueLabel = localize('continue', "Continue"); + const continueKeybinding = this.keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel(); + const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel; + const cancelLabel = localize('cancel', "Cancel"); + const cancelKeybinding = this.keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel(); + const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel; const confirmWidget = this._register(instantiationService.createInstance( ChatConfirmationWidget, title, message, - [{ label: localize('continue', "Continue"), data: true }, { label: localize('cancel', "Cancel"), data: false, isSecondary: true }] + [ + { + label: continueLabel, + data: true, + tooltip: continueTooltip + }, + { + label: cancelLabel, + data: false, + isSecondary: true, + tooltip: cancelTooltip + }] )); this._register(confirmWidget.onDidClick(button => { toolInvocation.confirmed.complete(button.data); From 303b4b7edeace74fa9a3f3d10cd8a578721631ba Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Feb 2025 17:50:42 +1100 Subject: [PATCH 20/36] Revert async AcceptAgentEdits (#241300) --- .../browser/chatEditing/chatEditingModifiedDocumentEntry.ts | 2 +- .../chat/browser/chatEditing/chatEditingModifiedFileEntry.ts | 2 +- .../browser/chatEditing/chatEditingModifiedNotebookEntry.ts | 2 +- .../contrib/chat/browser/chatEditing/chatEditingSession.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 1f90c36696c..6d8e6f6b27b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -272,7 +272,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } } - async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { + acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): void { assertType(textEdits.every(TextEdit.isTextEdit), 'INVALID args, can only handle text edits'); assert(isEqual(resource, this.modifiedURI), ' INVALID args, can only edit THIS document'); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 4cd72fcc07c..fb1b7d36274 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -225,7 +225,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im this._autoAcceptCtrl.get()?.cancel(); } - abstract acceptAgentEdits(uri: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise; + abstract acceptAgentEdits(uri: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): void; async acceptStreamingEditsEnd(tx: ITransaction) { this._resetEditsState(tx); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index a36077d5d64..2550dcb9291 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -129,7 +129,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie throw new Error('Method not implemented.'); } - override async acceptAgentEdits(resource: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { + override acceptAgentEdits(resource: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): void { const isCellUri = resource.scheme === Schemas.vscodeNotebookCell; assert(isCellUri || isEqual(resource, this.modifiedURI)); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 077425865e0..4751b8a379c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -931,7 +931,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private async _acceptEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { const entry = await this._getOrCreateModifiedFileEntry(resource, this._getTelemetryInfoForModel(responseModel)); - await entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel); + entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel); } private _getTelemetryInfoForModel(responseModel: IChatResponseModel): IModifiedEntryTelemetryInfo { From e81d4706a2a2f74a56e292cdb57c07cadeee3849 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 10:16:25 -0800 Subject: [PATCH 21/36] handle folder references in prompt file reference class --- .../chat/common/promptFileReferenceErrors.ts | 21 ++++++++++++++++++- .../filePromptContentsProvider.ts | 14 ++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index 1538f9e2316..22ee8fca39a 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -84,11 +84,30 @@ export class FileOpenFailed extends FailedToResolveContentsStream { super( uri, originalError, - `Failed to open file '${uri.toString()}': ${originalError}.`, + `Failed to open file '${uri.fsPath}': ${originalError}.`, ); } } +// /** +// * TODO: @legomushroom +// */ +// export class FolderReference extends FailedToResolveContentsStream { +// public override errorType = 'FolderReferenceError'; + +// constructor( +// uri: URI, +// originalError: unknown, +// ) { +// super( +// uri, +// originalError, +// `Entity at '${uri.fsPath}' is a folder.`, +// ); +// } +// } + + /** * Error that reflects the case when attempt resolve nested file * references failes due to a recursive reference, e.g., diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index 9bda1b1acf3..b287ba9ac80 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -11,7 +11,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { FileOpenFailed, NonPromptSnippetFile } from '../../promptFileReferenceErrors.js'; +import { FileOpenFailed, NonPromptSnippetFile, ParseError } from '../../promptFileReferenceErrors.js'; import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; /** @@ -64,8 +64,20 @@ export class FilePromptContentProvider extends PromptContentsProviderBase Date: Wed, 19 Feb 2025 11:00:51 -0800 Subject: [PATCH 22/36] update unit tests for the `PromptFileReference` class --- .../promptSyntax/promptFileReference.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 0a19b7d85e5..d4fb37363fc 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -147,6 +147,8 @@ class TestPromptFileReference extends Disposable { /** * Create expected file reference for testing purposes. * + * Note! This utility also use for `markdown links` at the moment. + * * @param filePath The expected path of the file reference (without the `#file:` prefix). * @param lineNumber The expected line number of the file reference. * @param startColumnNumber The expected start column number of the file reference. @@ -216,14 +218,14 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'file3.prompt.md', - contents: `\n\n\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents\n some more\t content`, + contents: `\n[](./some-other-folder/non-existing-folder)\n\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents\n some more\t content`, }, { name: 'some-other-folder', children: [ { name: 'file4.prompt.md', - contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\n\nand some\n non-prompt #file:./some-non-prompt-file.md', + contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\n\nand some\n non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', }, { name: 'file.txt', @@ -234,7 +236,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'another-file.prompt.md', - contents: 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + contents: `[](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`, }, { name: 'one_more_file_just_in_case.prompt.md', @@ -260,6 +262,18 @@ suite('PromptFileReference (Unix)', function () { rootUri, createTestFileReference('folder1/file3.prompt.md', 2, 14), ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `./some-other-folder/non-existing-folder`, + 2, + 1, + ), + new FileOpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), + 'Reference to non-existing file cannot be opened.', + ), + ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), createTestFileReference( @@ -268,9 +282,17 @@ suite('PromptFileReference (Unix)', function () { 26, ), ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('.', 1, 1), + new NonPromptSnippetFile( + URI.joinPath(rootUri, './folder1/some-other-folder'), + 'This folder is not a prompt file!', + ), + ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), - createTestFileReference('../file.txt', 1, 35), + createTestFileReference('../file.txt', 2, 35), new NonPromptSnippetFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), 'Ughh oh, that is not a prompt file!', @@ -296,6 +318,14 @@ suite('PromptFileReference (Unix)', function () { 'Oh no!', ), ), + new ExpectedReference( + URI.joinPath(rootUri, './some-other-folder/folder1'), + createTestFileReference('../../folder1', 5, 48), + new NonPromptSnippetFile( + URI.joinPath(rootUri, './folder1'), + 'Uggh ohh!', + ), + ), ] )); From 55c1d0428c006d7ed643577d0bf2ff413038fa25 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 11:04:40 -0800 Subject: [PATCH 23/36] improve parse error names --- .../chat/common/promptFileReferenceErrors.ts | 34 ++++--------------- .../filePromptContentsProvider.ts | 10 +++--- .../languageFeatures/promptLinkProvider.ts | 6 ++-- .../promptSyntax/parsers/basePromptParser.ts | 12 +++---- .../parsers/textModelPromptParser.test.ts | 16 ++++----- .../promptSyntax/promptFileReference.test.ts | 22 ++++++------ 6 files changed, 40 insertions(+), 60 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index 22ee8fca39a..fc3dbc993c1 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -74,8 +74,8 @@ export abstract class ResolveError extends ParseError { /** * Error that reflects the case when attempt to open target file fails. */ -export class FileOpenFailed extends FailedToResolveContentsStream { - public override errorType = 'FileOpenError'; +export class OpenFailed extends FailedToResolveContentsStream { + public override errorType = 'OpenError'; constructor( uri: URI, @@ -84,30 +84,11 @@ export class FileOpenFailed extends FailedToResolveContentsStream { super( uri, originalError, - `Failed to open file '${uri.fsPath}': ${originalError}.`, + `Failed to open '${uri.fsPath}': ${originalError}.`, ); } } -// /** -// * TODO: @legomushroom -// */ -// export class FolderReference extends FailedToResolveContentsStream { -// public override errorType = 'FolderReferenceError'; - -// constructor( -// uri: URI, -// originalError: unknown, -// ) { -// super( -// uri, -// originalError, -// `Entity at '${uri.fsPath}' is a folder.`, -// ); -// } -// } - - /** * Error that reflects the case when attempt resolve nested file * references failes due to a recursive reference, e.g., @@ -169,11 +150,10 @@ export class RecursiveReference extends ResolveError { } /** - * Error that reflects the case when resource URI does not point to - * a prompt snippet file, hence was not attempted to be resolved. + * Error for the case when a resource URI doesn't point to a prompt file. */ -export class NonPromptSnippetFile extends ResolveError { - public override errorType = 'NonPromptSnippetFileError'; +export class NotPromptFile extends ResolveError { + public override errorType = 'NotPromptFileError'; constructor( uri: URI, @@ -184,7 +164,7 @@ export class NonPromptSnippetFile extends ResolveError { super( uri, - `Resource at ${uri.path} is not a prompt snippet file${suffix}`, + `Resource at ${uri.path} is not a prompt file${suffix}`, ); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index b287ba9ac80..8f38013f7fb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -11,7 +11,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { FileOpenFailed, NonPromptSnippetFile, ParseError } from '../../promptFileReferenceErrors.js'; +import { OpenFailed, NotPromptFile, ParseError } from '../../promptFileReferenceErrors.js'; import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; /** @@ -69,7 +69,7 @@ export class FilePromptContentProvider extends PromptContentsProviderBase { const { linkRange } = reference; @@ -80,7 +81,6 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { 'Link range must be defined.', ); - return { range: linkRange, url: reference.uri, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 4e6ce0657b9..40c428068c4 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -23,7 +23,7 @@ import { ObservableDisposable } from '../../../../../../base/common/observableDi import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; -import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; +import { OpenFailed, NotPromptFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; /** * Well-known localized error messages. @@ -38,7 +38,7 @@ const errorMessages = { /** * Error conditions that may happen during the file reference resolution. */ -export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; +export type TErrorCondition = OpenFailed | RecursiveReference | NotPromptFile; /** * Base prompt parser class that provides a common interface for all @@ -376,7 +376,7 @@ export abstract class BasePromptParser extend .filter((reference) => { const { errorCondition } = reference; - return !errorCondition || (errorCondition instanceof NonPromptSnippetFile); + return !errorCondition || (errorCondition instanceof NotPromptFile); }); } @@ -404,7 +404,7 @@ export abstract class BasePromptParser extend .filter((childReference) => { const { errorCondition } = childReference; - return errorCondition && !(errorCondition instanceof NonPromptSnippetFile); + return errorCondition && !(errorCondition instanceof NotPromptFile); }) // map to error condition objects .map((childReference): ParseError => { @@ -471,7 +471,7 @@ export abstract class BasePromptParser extend * @returns Error message. */ protected getErrorMessage(error: TError): string { - if (error instanceof FileOpenFailed) { + if (error instanceof OpenFailed) { return `${errorMessages.fileOpenFailed} '${error.uri.path}'.`; } @@ -591,7 +591,7 @@ export class PromptFileReference extends BasePromptParser { startLine: 1, startColumn: 27, pathStartColumn: 33, - childrenOrError: new FileOpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'), }), new ExpectedReference({ uri: createURI('/foo/folder/binary.file'), @@ -152,7 +152,7 @@ suite('TextModelPromptParser', () => { startLine: 7, startColumn: 10, pathStartColumn: 16, - childrenOrError: new FileOpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'), }), new ExpectedReference({ uri: createURI('/etc/hosts/random-file.txt'), @@ -161,7 +161,7 @@ suite('TextModelPromptParser', () => { startLine: 7, startColumn: 81, pathStartColumn: 91, - childrenOrError: new FileOpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'), }), ]); }); @@ -196,7 +196,7 @@ suite('TextModelPromptParser', () => { startLine: 3, startColumn: 43, pathStartColumn: 55, - childrenOrError: new FileOpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), }), new ExpectedReference({ uri: createURI('/absolute/c/file_name.prompt.md'), @@ -205,7 +205,7 @@ suite('TextModelPromptParser', () => { startLine: 6, startColumn: 7, pathStartColumn: 17, - childrenOrError: new FileOpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'), }), new ExpectedReference({ uri: createURI('/absolute/folder/main.rs'), @@ -214,7 +214,7 @@ suite('TextModelPromptParser', () => { startLine: 11, startColumn: 36, pathStartColumn: 42, - childrenOrError: new FileOpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'), }), new ExpectedReference({ uri: createURI('/absolute/folder/and/a/samefile.jpeg'), @@ -223,7 +223,7 @@ suite('TextModelPromptParser', () => { startLine: 11, startColumn: 56, pathStartColumn: 62, - childrenOrError: new FileOpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), + childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), }), ]); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index d4fb37363fc..1a456bd9933 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -23,9 +23,9 @@ import { waitRandom, randomBoolean } from '../../../../../../base/test/common/te import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { NotPromptFile, RecursiveReference, OpenFailed } from '../../../common/promptFileReferenceErrors.js'; import { ConfigurationService } from '../../../../../../platform/configuration/common/configurationService.js'; import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; -import { NonPromptSnippetFile, RecursiveReference, FileOpenFailed } from '../../../common/promptFileReferenceErrors.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; /** @@ -189,7 +189,7 @@ suite('PromptFileReference (Unix)', function () { instantiationService.stub(IConfigurationService, nullConfigService); }); - test('resolves nested file references', async function () { + test('• resolves nested file references', async function () { if (isWindows) { this.skip(); } @@ -269,7 +269,7 @@ suite('PromptFileReference (Unix)', function () { 2, 1, ), - new FileOpenFailed( + new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), 'Reference to non-existing file cannot be opened.', ), @@ -285,7 +285,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('.', 1, 1), - new NonPromptSnippetFile( + new NotPromptFile( URI.joinPath(rootUri, './folder1/some-other-folder'), 'This folder is not a prompt file!', ), @@ -293,7 +293,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), createTestFileReference('../file.txt', 2, 35), - new NonPromptSnippetFile( + new NotPromptFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), 'Ughh oh, that is not a prompt file!', ), @@ -305,7 +305,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), - new FileOpenFailed( + new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), 'Failed to open non-existring prompt snippets file', ), @@ -313,7 +313,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('./some-non-prompt-file.md', 5, 13), - new FileOpenFailed( + new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), 'Oh no!', ), @@ -321,7 +321,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './some-other-folder/folder1'), createTestFileReference('../../folder1', 5, 48), - new NonPromptSnippetFile( + new NotPromptFile( URI.joinPath(rootUri, './folder1'), 'Uggh ohh!', ), @@ -332,7 +332,7 @@ suite('PromptFileReference (Unix)', function () { await test.run(); }); - test('does not fall into infinite reference recursion', async function () { + test('• does not fall into infinite reference recursion', async function () { if (isWindows) { this.skip(); } @@ -439,7 +439,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('../some-non-existing/file.prompt.md', 1, 30), - new FileOpenFailed( + new OpenFailed( URI.joinPath(rootUri, './folder1/some-non-existing/file.prompt.md'), 'Uggh ohh!', ), @@ -472,7 +472,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( rootUri, createTestFileReference('./file1.md', 6, 2), - new NonPromptSnippetFile( + new NotPromptFile( URI.joinPath(rootUri, './file1.md'), 'Uggh oh!', ), From 9533503d8677ed6087854d5ba1875440da6ec144 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 19 Feb 2025 15:22:45 -0800 Subject: [PATCH 24/36] add `FolderReference` error type, don't provide document links for folder references --- .../chat/common/promptFileReferenceErrors.ts | 20 +++++++++++++++++++ .../filePromptContentsProvider.ts | 11 ++++++++-- .../languageFeatures/promptLinkProvider.ts | 20 +++++++++++-------- .../promptSyntax/parsers/basePromptParser.ts | 17 +++++++++++++--- .../promptSyntax/promptFileReference.test.ts | 6 +++--- 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index fc3dbc993c1..84e7d62551b 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -168,3 +168,23 @@ export class NotPromptFile extends ResolveError { ); } } + +/** + * Error for the case when a resource URI points to a folder. + */ +export class FolderReference extends NotPromptFile { + public override errorType = 'FolderReferenceError'; + + constructor( + uri: URI, + message: string = '', + ) { + + const suffix = message ? `: ${message}` : ''; + + super( + uri, + `Entity at '${uri.path}' is a folder${suffix}`, + ); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index 8f38013f7fb..512c29e98c2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -11,7 +11,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { OpenFailed, NotPromptFile, ParseError } from '../../promptFileReferenceErrors.js'; +import { OpenFailed, NotPromptFile, ParseError, FolderReference } from '../../promptFileReferenceErrors.js'; import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; /** @@ -67,9 +67,16 @@ export class FilePromptContentProvider extends PromptContentsProviderBase { - const { linkRange } = reference; + const { uri, linkRange } = reference; // must always be true because of the filter above assertDefined( @@ -83,7 +87,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { return { range: linkRange, - url: reference.uri, + url: uri, }; }); @@ -94,5 +98,5 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { } // register the provider as a workbench contribution -Registry.as(WorkbenchExtensions.Workbench) +Registry.as(Extensions.Workbench) .registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 40c428068c4..937c7e6b1bb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -23,7 +23,7 @@ import { ObservableDisposable } from '../../../../../../base/common/observableDi import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; -import { OpenFailed, NotPromptFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; +import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; /** * Well-known localized error messages. @@ -38,7 +38,7 @@ const errorMessages = { /** * Error conditions that may happen during the file reference resolution. */ -export type TErrorCondition = OpenFailed | RecursiveReference | NotPromptFile; +export type TErrorCondition = OpenFailed | RecursiveReference | FolderReference | NotPromptFile; /** * Base prompt parser class that provides a common interface for all @@ -376,7 +376,18 @@ export abstract class BasePromptParser extend .filter((reference) => { const { errorCondition } = reference; - return !errorCondition || (errorCondition instanceof NotPromptFile); + // include all references without errors + if (!errorCondition) { + return true; + } + + // filter out folder references from the list + if (errorCondition instanceof FolderReference) { + return false; + } + + // include non-prompt file references + return (errorCondition instanceof NotPromptFile); }); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 1a456bd9933..f9c6214af67 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -23,10 +23,10 @@ import { waitRandom, randomBoolean } from '../../../../../../base/test/common/te import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { NotPromptFile, RecursiveReference, OpenFailed } from '../../../common/promptFileReferenceErrors.js'; import { ConfigurationService } from '../../../../../../platform/configuration/common/configurationService.js'; import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { NotPromptFile, RecursiveReference, OpenFailed, FolderReference } from '../../../common/promptFileReferenceErrors.js'; /** * Represents a file reference with an expected @@ -285,7 +285,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('.', 1, 1), - new NotPromptFile( + new FolderReference( URI.joinPath(rootUri, './folder1/some-other-folder'), 'This folder is not a prompt file!', ), @@ -321,7 +321,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './some-other-folder/folder1'), createTestFileReference('../../folder1', 5, 48), - new NotPromptFile( + new FolderReference( URI.joinPath(rootUri, './folder1'), 'Uggh ohh!', ), From 6367c86d6e07bb35365c42839380c801b9341d08 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 20 Feb 2025 09:58:46 +0100 Subject: [PATCH 25/36] make sure `Close` button is rendered in inline chat (#241309) https://github.com/microsoft/vscode/issues/239090 --- .../workbench/contrib/inlineChat/browser/inlineChatActions.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 8949a625751..b23098134a1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -386,8 +386,7 @@ export class CloseAction extends AbstractInline1ChatAction { group: '0_main', order: 1, when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages) + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() ), }] }); From bfa04d61958838a308238298e9a97f47e998e98a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:08:20 +0100 Subject: [PATCH 26/36] Fix ghost text screen reader accessibility issues (#241311) bug fix screen readers --- .../editor/contrib/inlineCompletions/browser/model/ghostText.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts index ac3939847e1..40411f027ef 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts @@ -47,7 +47,7 @@ export class GhostText { const text = new TextEdit([ ...this.parts.map(p => new SingleTextEdit( Range.fromPositions(new Position(1, p.column)), - p.lines.join('\n') + p.lines.map(line => line.line).join('\n') )), ]).applyToString(cappedLineText); From 55d4dffb18a37a56320bb84cd379768d4ce62493 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 20 Feb 2025 10:35:48 +0100 Subject: [PATCH 27/36] Revert "Fixing appearance of the double cursor" (#241315) Revert "Fixing appearance of the double cursor (#238949)" This reverts commit 978e8605ebf4fc4cb3188e9d677cc5b363f0543b. --- .../controller/editContext/native/nativeEditContextUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts index fd170ef812d..b3166de0437 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts @@ -30,11 +30,6 @@ export class FocusTracker extends Disposable { return; } this._isFocused = focused; - if (this._isFocused) { - this._domNode.focus(); - } else { - this._domNode.blur(); - } this._onFocusChange(this._isFocused); } From fc13789ac8cbca2a499384e200f5e299dd97f9d0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:40:40 +0100 Subject: [PATCH 28/36] Move collapsed mode option (#241316) Move collapsed mode down in menu --- .../view/inlineEdits/components/gutterIndicatorMenu.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 23860ee0574..6624abea5fa 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -70,14 +70,14 @@ export class GutterIndicatorMenuContent { })), option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: hideInlineCompletionId })), separator(), - this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ? - option(createOptionArgs({ id: 'showExpanded', title: localize('showExpanded', "Show Expanded"), icon: Codicon.expandAll, commandId: toggleShowCollapsedId })) : - option(createOptionArgs({ id: 'showCollapsed', title: localize('showCollapsed', "Show Collapsed"), icon: Codicon.collapseAll, commandId: toggleShowCollapsedId })) - ), this._host.extensionCommands?.map(c => c && c.length > 0 ? [ ...c.map((c, idx) => option(createOptionArgs({ id: c.id + '_' + idx, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))), separator() ] : []), + this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ? + option(createOptionArgs({ id: 'showExpanded', title: localize('showExpanded', "Show Expanded"), icon: Codicon.expandAll, commandId: toggleShowCollapsedId })) : + option(createOptionArgs({ id: 'showCollapsed', title: localize('showCollapsed', "Show Collapsed"), icon: Codicon.collapseAll, commandId: toggleShowCollapsedId })) + ), option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['@tag:nextEditSuggestions'] })), this._host.action.map(action => action ? [ separator(), From 4e2822d978f1135fcadac0537c6a4d289171aff2 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:56:12 +0100 Subject: [PATCH 29/36] Fix transient model watcher leak (#241320) fix transient model watcher leak --- .../services/abstractCodeEditorService.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 0743bcae74a..295d5bf1ca1 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -7,7 +7,7 @@ import * as dom from '../../../base/browser/dom.js'; import * as domStylesheets from '../../../base/browser/domStylesheets.js'; import * as cssJs from '../../../base/browser/cssValue.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { IDisposable, DisposableStore, Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { IDisposable, DisposableStore, Disposable, toDisposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { LinkedList } from '../../../base/common/linkedList.js'; import * as strings from '../../../base/common/strings.js'; import { URI } from '../../../base/common/uri.js'; @@ -210,7 +210,7 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC return provider.resolveDecorationCSSRules(); } - private readonly _transientWatchers: { [uri: string]: ModelTransientSettingWatcher } = {}; + private readonly _transientWatchers = this._register(new DisposableMap()); private readonly _modelProperties = new Map>(); public setModelProperty(resource: URI, key: string, value: any): void { @@ -238,12 +238,10 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC public setTransientModelProperty(model: ITextModel, key: string, value: any): void { const uri = model.uri.toString(); - let w: ModelTransientSettingWatcher; - if (this._transientWatchers.hasOwnProperty(uri)) { - w = this._transientWatchers[uri]; - } else { + let w = this._transientWatchers.get(uri); + if (!w) { w = new ModelTransientSettingWatcher(uri, model, this); - this._transientWatchers[uri] = w; + this._transientWatchers.set(uri, w); } const previousValue = w.get(key); @@ -256,25 +254,27 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC public getTransientModelProperty(model: ITextModel, key: string): any { const uri = model.uri.toString(); - if (!this._transientWatchers.hasOwnProperty(uri)) { + const watcher = this._transientWatchers.get(uri); + if (!watcher) { return undefined; } - return this._transientWatchers[uri].get(key); + return watcher.get(key); } public getTransientModelProperties(model: ITextModel): [string, any][] | undefined { const uri = model.uri.toString(); - if (!this._transientWatchers.hasOwnProperty(uri)) { + const watcher = this._transientWatchers.get(uri); + if (!watcher) { return undefined; } - return this._transientWatchers[uri].keys().map(key => [key, this._transientWatchers[uri].get(key)]); + return watcher.keys().map(key => [key, watcher.get(key)]); } _removeWatcher(w: ModelTransientSettingWatcher): void { - delete this._transientWatchers[w.uri]; + this._transientWatchers.deleteAndDispose(w.uri); } abstract getActiveCodeEditor(): ICodeEditor | null; @@ -295,14 +295,16 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC } } -export class ModelTransientSettingWatcher { +export class ModelTransientSettingWatcher extends Disposable { public readonly uri: string; private readonly _values: { [key: string]: any }; constructor(uri: string, model: ITextModel, owner: AbstractCodeEditorService) { + super(); + this.uri = uri; this._values = {}; - model.onWillDispose(() => owner._removeWatcher(this)); + this._register(model.onWillDispose(() => owner._removeWatcher(this))); } public set(key: string, value: any): void { From a29d55f66a130260eec9196a5dc86aec4b771778 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 20 Feb 2025 11:06:22 +0100 Subject: [PATCH 30/36] prepare February endgame (#241323) --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- .vscode/notebooks/my-work.github-issues | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d1c128a6d0a..9f92bf0b6a6 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index b16f0025f56..9d5c8ccdcc5 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index e8b184f8e57..c7674cef414 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"January 2025\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"February 2025\"\n" }, { "kind": 1, From 7e2db9ad92ba0e30532d58b0d8083e640eef8097 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 20 Feb 2025 11:07:55 +0100 Subject: [PATCH 31/36] debt - fix leaks around `ActionRunner` (#241325) --- .../browser/menuEntryActionViewItem.ts | 19 +++++++------------ .../parts/editor/multiEditorTabsControl.ts | 2 +- .../notifications/notificationsCommands.ts | 1 + .../comments/browser/commentFormActions.ts | 4 ++-- .../terminal/browser/terminalContextMenu.ts | 4 +++- .../terminal/browser/terminalTabsList.ts | 4 ++-- .../contrib/terminal/browser/terminalView.ts | 6 ++++-- .../testing/browser/testingExplorerView.ts | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 0581f04a644..8971195a1c9 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -14,7 +14,7 @@ import { Event } from '../../../base/common/event.js'; import { UILabelProvider } from '../../../base/common/keybindingLabels.js'; import { ResolvedKeybinding } from '../../../base/common/keybindings.js'; import { KeyCode } from '../../../base/common/keyCodes.js'; -import { combinedDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { combinedDisposable, DisposableStore, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { isLinux, isWindows, OS } from '../../../base/common/platform.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { assertType } from '../../../base/common/types.js'; @@ -429,6 +429,7 @@ export interface IDropdownWithDefaultActionViewItemOptions extends IDropdownMenu export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { private readonly _options: IDropdownWithDefaultActionViewItemOptions | undefined; private _defaultAction: ActionViewItem; + private readonly _defaultActionDisposables = this._register(new DisposableStore()); private readonly _dropdown: DropdownMenuActionViewItem; private _container: HTMLElement | null = null; private readonly _storageKey: string; @@ -471,7 +472,7 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { actionRunner: options?.actionRunner ?? this._register(new ActionRunner()), }; - this._dropdown = new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions); + this._dropdown = this._register(new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions)); this._register(this._dropdown.actionRunner.onDidRun((e: IRunEvent) => { if (e.action instanceof MenuItemAction) { this.update(e.action); @@ -484,13 +485,13 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - this._defaultAction.dispose(); - this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) }); - this._defaultAction.actionRunner = new class extends ActionRunner { + this._defaultActionDisposables.clear(); + this._defaultAction = this._defaultActionDisposables.add(this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) })); + this._defaultAction.actionRunner = this._defaultActionDisposables.add(new class extends ActionRunner { protected override async runAction(action: IAction, context?: unknown): Promise { await action.run(undefined); } - }(); + }()); if (this._container) { this._defaultAction.render(prepend(this._container, $('.action-container'))); @@ -567,12 +568,6 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { this._dropdown.setFocusable(false); } } - - override dispose() { - this._defaultAction.dispose(); - this._dropdown.dispose(); - super.dispose(); - } } class SubmenuEntrySelectActionViewItem extends SelectActionViewItem { diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 708f4d9f6d5..978c3f94059 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -837,7 +837,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } }); - const tabActionBarDisposable = combinedDisposable(tabActionBar, tabActionListener, toDisposable(insert(this.tabActionBars, tabActionBar))); + const tabActionBarDisposable = combinedDisposable(tabActionRunner, tabActionBar, tabActionListener, toDisposable(insert(this.tabActionBars, tabActionBar))); // Tab Fade Hider // Hides the tab fade to the right when tab action left and sizing shrink/fixed, ::after, ::before are already used diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 04980171865..d3fff55ef7c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -169,6 +169,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl } actionRunner.run(primaryAction, notification); notification.close(); + actionRunner.dispose(); } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index 9b559a15e10..f83a1b60a53 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -52,11 +52,11 @@ export class CommentFormActions implements IDisposable { const button = dropDownActions.length ? new ButtonWithDropdown(this.container, { contextMenuProvider: this.contextMenuService, actions: dropDownActions, - actionRunner: new class extends ActionRunner { + actionRunner: this._toDispose.add(new class extends ActionRunner { protected override async runAction(action: IAction, context?: unknown): Promise { return actionHandler(action); } - }, + }), secondary: !isPrimary, title, addPrimaryActionToDropdown: false, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts index 16af6fc45e2..e7c3aa505a7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts @@ -59,10 +59,12 @@ export function openContextMenu(targetWindow: Window, event: MouseEvent, context const context: InstanceContext[] = contextInstances ? asArray(contextInstances).map(e => new InstanceContext(e)) : []; + const actionRunner = new TerminalContextActionRunner(); contextMenuService.showContextMenu({ - actionRunner: new TerminalContextActionRunner(), + actionRunner, getAnchor: () => standardEvent, getActions: () => actions, getActionsContext: () => context, + onHide: () => actionRunner.dispose() }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index cde793d6a9a..5d8140d604b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -292,10 +292,10 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer action instanceof MenuItemAction - ? this._instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) + ? this._register(this._instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate })) : undefined })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index d9b37e594c8..07494bbbde3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -547,15 +547,17 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { } private _openContextMenu() { + const actionRunner = new TerminalContextActionRunner(); this._contextMenuService.showContextMenu({ - actionRunner: new TerminalContextActionRunner(), + actionRunner, getAnchor: () => this.element!, getActions: () => this._actions, // The context is always the active instance in the terminal view getActionsContext: () => { const instance = this._terminalGroupService.activeInstance; return instance ? [new InstanceContext(instance)] : []; - } + }, + onHide: () => actionRunner.dispose() }); } } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 1383e0061ec..7c9c38a0239 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -706,7 +706,7 @@ class TestingExplorerViewModel extends Disposable { private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService); private readonly _viewSorting = TestingContextKeys.viewSorting.bindTo(this.contextKeyService); private readonly welcomeVisibilityEmitter = new Emitter(); - private readonly actionRunner = new TestExplorerActionRunner(() => this.tree.getSelection().filter(isDefined)); + private readonly actionRunner = this._register(new TestExplorerActionRunner(() => this.tree.getSelection().filter(isDefined))); private readonly lastViewState = this._register(new StoredValue({ key: 'testing.treeState', scope: StorageScope.WORKSPACE, From 89fef848ef22db790fd00e42eba05d428ca94485 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 20 Feb 2025 12:04:34 +0100 Subject: [PATCH 32/36] Provide encoding-related APIs for editor extensions (#824) (#240804) --- .../src/singlefolder-tests/workspace.test.ts | 65 ++++++++++++- .../api/browser/mainThreadDocuments.ts | 33 ++++--- .../workbench/api/common/extHost.api.impl.ts | 10 +- .../workbench/api/common/extHost.protocol.ts | 4 +- .../workbench/api/common/extHostDocuments.ts | 17 +++- .../vscode.proposed.textDocumentEncoding.d.ts | 95 ++++++++++++++++++- 6 files changed, 196 insertions(+), 28 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 97a99fe5646..501efe7e230 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1311,7 +1311,7 @@ suite('vscode API - workspace', () => { return deleteFile(file); } - test('text document encodings', async () => { + test('encoding: text document encodings', async () => { const uri1 = await createRandomFile(); const uri2 = await createRandomFile(new Uint8Array([0xEF, 0xBB, 0xBF]) /* UTF-8 with BOM */); const uri3 = await createRandomFile(new Uint8Array([0xFF, 0xFE]) /* UTF-16 LE BOM */); @@ -1333,7 +1333,66 @@ suite('vscode API - workspace', () => { assert.strictEqual(doc5.encoding, 'utf8'); }); - test('fs.decode', async function () { + test('encoding: openTextDocument', async () => { + const uri1 = await createRandomFile(); + + let doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'cp1252' }); + assert.strictEqual(doc1.encoding, 'cp1252'); + + let listener: vscode.Disposable | undefined; + const documentChangePromise = new Promise(resolve => { + listener = vscode.workspace.onDidChangeTextDocument(e => { + if (e.document.uri.toString() === uri1.toString()) { + resolve(); + } + }); + }); + + doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'utf16le' }); + assert.strictEqual(doc1.encoding, 'utf16le'); + await documentChangePromise; + + const doc2 = await vscode.workspace.openTextDocument({ encoding: 'utf16be' }); + assert.strictEqual(doc2.encoding, 'utf16be'); + + const doc3 = await vscode.workspace.openTextDocument({ content: 'Hello World', encoding: 'utf16le' }); + assert.strictEqual(doc3.encoding, 'utf16le'); + + listener?.dispose(); + }); + + test('encoding: openTextDocument - throws for dirty documents', async () => { + const uri1 = await createRandomFile(); + + const doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'cp1252' }); + + const edit = new vscode.WorkspaceEdit(); + edit.insert(doc1.uri, new vscode.Position(0, 0), 'Hello World'); + await vscode.workspace.applyEdit(edit); + assert.strictEqual(doc1.isDirty, true); + + let err; + try { + await vscode.workspace.decode(new Uint8Array([0, 0, 0, 0]), doc1.uri); + } catch (e) { + err = e; + } + assert.ok(err); + }); + + test('encoding: openTextDocument - multiple requests with different encoding work', async () => { + const uri1 = await createRandomFile(); + + const doc1P = vscode.workspace.openTextDocument(uri1); + const doc2P = vscode.workspace.openTextDocument(uri1, { encoding: 'cp1252' }); + + const [doc1, doc2] = await Promise.all([doc1P, doc2P]); + + assert.strictEqual(doc1.encoding, 'cp1252'); + assert.strictEqual(doc2.encoding, 'cp1252'); + }); + + test('encoding: decode', async function () { const uri = root.with({ path: posix.join(root.path, 'file.txt') }); // without setting @@ -1375,7 +1434,7 @@ suite('vscode API - workspace', () => { assert.ok(err); }); - test('fs.encode', async function () { + test('encoding: encode', async function () { const uri = root.with({ path: posix.join(root.path, 'file.txt') }); // without setting diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index a882ce0beed..47925bd1c08 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -12,7 +12,7 @@ import { IModelService } from '../../../editor/common/services/model.js'; import { ITextModelService } from '../../../editor/common/services/resolverService.js'; import { IFileService, FileOperation } from '../../../platform/files/common/files.js'; import { ExtHostContext, ExtHostDocumentsShape, MainThreadDocumentsShape } from '../common/extHost.protocol.js'; -import { ITextFileEditorModel, ITextFileService } from '../../services/textfile/common/textfiles.js'; +import { EncodingMode, ITextFileEditorModel, ITextFileService, TextFileResolveReason } from '../../services/textfile/common/textfiles.js'; import { IUntitledTextEditorModel } from '../../services/untitled/common/untitledTextEditorModel.js'; import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js'; import { toLocalResource, extUri, IExtUri } from '../../../base/common/resources.js'; @@ -219,7 +219,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen return Boolean(target); } - async $tryOpenDocument(uriData: UriComponents): Promise { + async $tryOpenDocument(uriData: UriComponents, options?: { encoding?: string }): Promise { const inputUri = URI.revive(uriData); if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) { throw new ErrorNoTelemetry(`Invalid uri. Scheme and authority or path must be set.`); @@ -230,11 +230,11 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen let promise: Promise; switch (canonicalUri.scheme) { case Schemas.untitled: - promise = this._handleUntitledScheme(canonicalUri); + promise = this._handleUntitledScheme(canonicalUri, options); break; case Schemas.file: default: - promise = this._handleAsResourceInput(canonicalUri); + promise = this._handleAsResourceInput(canonicalUri, options); break; } @@ -255,31 +255,40 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen } } - $tryCreateDocument(options?: { language?: string; content?: string }): Promise { - return this._doCreateUntitled(undefined, options ? options.language : undefined, options ? options.content : undefined); + $tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise { + return this._doCreateUntitled(undefined, options); } - private async _handleAsResourceInput(uri: URI): Promise { + private async _handleAsResourceInput(uri: URI, options?: { encoding?: string }): Promise { + if (options?.encoding) { + const model = await this._textFileService.files.resolve(uri, { encoding: options.encoding, reason: TextFileResolveReason.REFERENCE }); + if (model.isDirty()) { + throw new ErrorNoTelemetry(`Cannot re-open a dirty text document with different encoding. Save it first.`); + } + await model.setEncoding(options.encoding, EncodingMode.Decode); + } + const ref = await this._textModelResolverService.createModelReference(uri); this._modelReferenceCollection.add(uri, ref, ref.object.textEditorModel.getValueLength()); return ref.object.textEditorModel.uri; } - private async _handleUntitledScheme(uri: URI): Promise { + private async _handleUntitledScheme(uri: URI, options?: { encoding?: string }): Promise { const asLocalUri = toLocalResource(uri, this._environmentService.remoteAuthority, this._pathService.defaultUriScheme); const exists = await this._fileService.exists(asLocalUri); if (exists) { // don't create a new file ontop of an existing file return Promise.reject(new Error('file already exists')); } - return await this._doCreateUntitled(Boolean(uri.path) ? uri : undefined); + return await this._doCreateUntitled(Boolean(uri.path) ? uri : undefined, options); } - private async _doCreateUntitled(associatedResource?: URI, languageId?: string, initialValue?: string): Promise { + private async _doCreateUntitled(associatedResource?: URI, options?: { language?: string; content?: string; encoding?: string }): Promise { const model = this._textFileService.untitled.create({ associatedResource, - languageId, - initialValue + languageId: options?.language, + initialValue: options?.content, + encoding: options?.encoding }); const resource = model.resource; const ref = await this._textModelResolverService.createModelReference(resource); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 3099a31228a..1dac783d0a4 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1024,10 +1024,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I set textDocuments(value) { throw new errors.ReadonlyError('textDocuments'); }, - openTextDocument(uriOrFileNameOrOptions?: vscode.Uri | string | { language?: string; content?: string }) { + openTextDocument(uriOrFileNameOrOptions?: vscode.Uri | string | { language?: string; content?: string; encoding?: string }, options?: { encoding?: string }) { let uriPromise: Thenable; - const options = uriOrFileNameOrOptions as { language?: string; content?: string }; + options = (options ?? uriOrFileNameOrOptions) as ({ language?: string; content?: string; encoding?: string } | undefined); + if (typeof options?.encoding === 'string') { + checkProposedApiEnabled(extension, 'textDocumentEncoding'); + } + if (typeof uriOrFileNameOrOptions === 'string') { uriPromise = Promise.resolve(URI.file(uriOrFileNameOrOptions)); } else if (URI.isUri(uriOrFileNameOrOptions)) { @@ -1043,7 +1047,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if (uri.scheme === Schemas.vscodeRemote && !uri.authority) { extHostApiDeprecation.report('workspace.openTextDocument', extension, `A URI of 'vscode-remote' scheme requires an authority.`); } - return extHostDocuments.ensureDocumentData(uri).then(documentData => { + return extHostDocuments.ensureDocumentData(uri, options).then(documentData => { return documentData.document; }); }); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0e4072c38d9..c8530c64ede 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -238,8 +238,8 @@ export interface MainThreadDocumentContentProvidersShape extends IDisposable { } export interface MainThreadDocumentsShape extends IDisposable { - $tryCreateDocument(options?: { language?: string; content?: string }): Promise; - $tryOpenDocument(uri: UriComponents): Promise; + $tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise; + $tryOpenDocument(uri: UriComponents, options?: { encoding?: string }): Promise; $trySaveDocument(uri: UriComponents): Promise; } diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index c0d0ca5ad9c..4e7dd522c51 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -76,16 +76,16 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { return data.document; } - public ensureDocumentData(uri: URI): Promise { + public ensureDocumentData(uri: URI, options?: { encoding?: string }): Promise { const cached = this._documentsAndEditors.getDocument(uri); - if (cached) { + if (cached && (!options?.encoding || cached.document.encoding === options.encoding)) { return Promise.resolve(cached); } let promise = this._documentLoader.get(uri.toString()); if (!promise) { - promise = this._proxy.$tryOpenDocument(uri).then(uriData => { + promise = this._proxy.$tryOpenDocument(uri, options).then(uriData => { this._documentLoader.delete(uri.toString()); const canonicalUri = URI.revive(uriData); return assertIsDefined(this._documentsAndEditors.getDocument(canonicalUri)); @@ -94,12 +94,21 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { return Promise.reject(err); }); this._documentLoader.set(uri.toString(), promise); + } else { + if (options?.encoding) { + promise = promise.then(data => { + if (data.document.encoding !== options.encoding) { + return this.ensureDocumentData(uri, options); + } + return data; + }); + } } return promise; } - public createDocumentData(options?: { language?: string; content?: string }): Promise { + public createDocumentData(options?: { language?: string; content?: string; encoding?: string }): Promise { return this._proxy.$tryCreateDocument(options).then(data => URI.revive(data)); } diff --git a/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts b/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts index 16c6b87ab3a..73ca9341b70 100644 --- a/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts +++ b/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts @@ -30,6 +30,91 @@ declare module 'vscode' { export namespace workspace { + /** + * Opens a document. Will return early if this document is already open. Otherwise + * the document is loaded and the {@link workspace.onDidOpenTextDocument didOpen}-event fires. + * + * The document is denoted by an {@link Uri}. Depending on the {@link Uri.scheme scheme} the + * following rules apply: + * * `file`-scheme: Open a file on disk (`openTextDocument(Uri.file(path))`). Will be rejected if the file + * does not exist or cannot be loaded. + * * `untitled`-scheme: Open a blank untitled file with associated path (`openTextDocument(Uri.file(path).with({ scheme: 'untitled' }))`). + * The language will be derived from the file name. + * * For all other schemes contributed {@link TextDocumentContentProvider text document content providers} and + * {@link FileSystemProvider file system providers} are consulted. + * + * *Note* that the lifecycle of the returned document is owned by the editor and not by the extension. That means an + * {@linkcode workspace.onDidCloseTextDocument onDidClose}-event can occur at any time after opening it. + * + * @throws This method will throw an error when an existing text document with the provided uri is dirty. + * + * @param uri Identifies the resource to open. + * @param options Options to control how the document will be opened. + * @returns A promise that resolves to a {@link TextDocument document}. + */ + export function openTextDocument(uri: Uri, options?: { + /** + * The {@link TextDocument.encoding encoding} of the document to use + * for decoding the underlying buffer to text. If omitted, the encoding + * will be guessed based on the file content and/or the editor settings. + * + * See {@link TextDocument.encoding} for more information about valid + * values for encoding. + * + * *Note* that opening a text document that was already opened with a + * different encoding has the potential of changing the text contents of + * the text document. + */ + encoding?: string; + }): Thenable; + + /** + * A short-hand for `openTextDocument(Uri.file(path))`. + * + * @see {@link workspace.openTextDocument} + * @param path A path of a file on disk. + * @param options Options to control how the document will be opened. + * @returns A promise that resolves to a {@link TextDocument document}. + */ + export function openTextDocument(path: string, options?: { + /** + * The {@link TextDocument.encoding encoding} of the document to use + * for decoding the underlying buffer to text. If omitted, the encoding + * will be guessed based on the file content and/or the editor settings. + * + * See {@link TextDocument.encoding} for more information about valid + * values for encoding. + * + * *Note* that opening a text document that was already opened with a + * different encoding has the potential of changing the text contents of + * the text document. + */ + encoding?: string; + }): Thenable; + + /** + * Opens an untitled text document. The editor will prompt the user for a file + * path when the document is to be saved. The `options` parameter allows to + * specify the *language*, *encoding* and/or the *content* of the document. + * + * @param options Options to control how the document will be created. + * @returns A promise that resolves to a {@link TextDocument document}. + */ + export function openTextDocument(options?: { + /** + * The {@link TextDocument.languageId language} of the document. + */ + language?: string; + /** + * The initial contents of the document. + */ + content?: string; + /** + * The {@link TextDocument.encoding encoding} of the document. + */ + encoding?: string; + }): Thenable; + /** * Decodes the content from a `Uint8Array` to a `string`. * @@ -42,10 +127,11 @@ declare module 'vscode' { * @param content The content to decode as a `Uint8Array`. * @param uri The URI that represents the file. This information * is used to figure out the encoding related configuration for the file. - * @param options Allows to explicitly pick the encoding to use. + * @param options Allows to explicitly pick the encoding to use. See {@link TextDocument.encoding} + * for more information about valid values for encoding. * @returns A thenable that resolves to the decoded `string`. */ - export function decode(content: Uint8Array, uri: Uri | undefined, options?: { readonly encoding: string }): Thenable; + export function decode(content: Uint8Array, uri: Uri | undefined, options?: { encoding: string }): Thenable; /** * Encodes the content of a `string` to a `Uint8Array`. @@ -56,9 +142,10 @@ declare module 'vscode' { * @param content The content to decode as a `string`. * @param uri The URI that represents the file. This information * is used to figure out the encoding related configuration for the file. - * @param options Allows to explicitly pick the encoding to use. + * @param options Allows to explicitly pick the encoding to use. See {@link TextDocument.encoding} + * for more information about valid values for encoding. * @returns A thenable that resolves to the encoded `Uint8Array`. */ - export function encode(content: string, uri: Uri | undefined, options?: { readonly encoding: string }): Thenable; + export function encode(content: string, uri: Uri | undefined, options?: { encoding: string }): Thenable; } } From 97f43d1c46a2e6be075251ce0d7c4cea67098233 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:20:15 +0100 Subject: [PATCH 33/36] Insertion view indentation trim fixes (#241329) render fixes --- src/vs/editor/browser/observableCodeEditor.ts | 1 + .../inlineEditsViews/inlineEditsInsertionView.ts | 7 ++++++- .../inlineEditsLineReplacementView.ts | 3 ++- .../browser/view/inlineEdits/utils/utils.ts | 16 ++++++++++++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 4940703fd42..d7093a0de5c 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -233,6 +233,7 @@ export class ObservableCodeEditor extends Disposable { public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); public readonly layoutInfoWidth = this.layoutInfo.map(l => l.width); public readonly layoutInfoMinimap = this.layoutInfo.map(l => l.minimap); + public readonly layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth); public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index 0815f76a3ae..622538332e0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -53,7 +53,12 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits const eol = textModel.getEOL(); const startsWithEol = state.text.startsWith(eol); const originalRange = new LineRange(state.lineNumber, state.lineNumber + (startsWithEol ? 0 : 1)); - const modifiedLines = state.text.split(eol).splice(startsWithEol ? 0 : 1); + let modifiedLines = state.text.split(eol); + if (startsWithEol) { + modifiedLines = modifiedLines.splice(1); + } else { + modifiedLines[0] = textModel.getLineContent(state.lineNumber) + modifiedLines[0]; + } return getPrefixTrim([], originalRange, modifiedLines, this._editor); }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts index 6b6bcd8d65c..e1279376dee 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts @@ -100,6 +100,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const verticalScrollbarWidth = this._editor.layoutInfoVerticalScrollbarWidth.read(reader); const scrollLeft = this._editor.scrollLeft.read(reader); const scrollTop = this._editor.scrollTop.read(reader); const editorLeftOffset = contentLeft - scrollLeft; @@ -140,7 +141,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin lowerBackground, lowerText, padding: PADDING, - minContentWidthRequired: maxLineWidth + PADDING * 2, + minContentWidthRequired: prefixLeftOffset + maxLineWidth + PADDING * 2 + verticalScrollbarWidth, }; }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 52b182c1282..9427e0b23a0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -81,10 +81,22 @@ export function getPrefixTrim(diffRanges: Range[], originalLinesRange: LineRange const replacementStart = diffRanges.map(r => r.isSingleLine() ? r.startColumn - 1 : 0); const originalIndents = originalLinesRange.mapToLineArray(line => indentOfLine(textModel.getLineContent(line))); - const modifiedIndents = modifiedLines.map(line => indentOfLine(line)); + const modifiedIndents = modifiedLines.filter(line => line !== '').map(line => indentOfLine(line)); const prefixTrim = Math.min(...replacementStart, ...originalIndents, ...modifiedIndents); - const prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1); + let prefixLeftOffset; + const startLineIndent = textModel.getLineIndentColumn(originalLinesRange.startLineNumber); + if (startLineIndent >= prefixTrim + 1) { + // We can use the editor to get the offset + prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1); + } else if (startLineIndent !== 1) { + // We need to approximate the offset as the editor does not contain the modified lines yet + const startLineIndentOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, startLineIndent); + prefixLeftOffset = startLineIndentOffset / (startLineIndent - 1) * prefixTrim; + } else { + // unable to approximate the offset + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } return { prefixTrim, prefixLeftOffset }; } From 82d32bf6b992c411b2e50245d3bb862b6588fbc3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 20 Feb 2025 13:23:16 +0100 Subject: [PATCH 34/36] The order of elements in the language status indicator are not fixed (fix #241246) (#241333) --- .../browser/parts/statusbar/statusbarModel.ts | 10 ++++-- .../parts/statusbar/statusbarModel.test.ts | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 82db371b426..4f4a138dbfb 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -332,7 +332,9 @@ export class StatusbarViewModel extends Disposable { // Fill relative entries to LEFT if (relativeEntries) { - sortedEntries.push(...relativeEntries.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT)); + sortedEntries.push(...relativeEntries + .filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT) + .sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary)); } // Fill referenced entry @@ -340,7 +342,9 @@ export class StatusbarViewModel extends Disposable { // Fill relative entries to RIGHT if (relativeEntries) { - sortedEntries.push(...relativeEntries.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT)); + sortedEntries.push(...relativeEntries + .filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT) + .sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary)); } // Delete from map to mark as handled @@ -350,7 +354,7 @@ export class StatusbarViewModel extends Disposable { // Finally, just append all entries that reference another entry // that does not exist to the end of the list for (const [, entries] of mapEntryWithRelativePriority) { - sortedEntries.push(...entries.values()); + sortedEntries.push(...Array.from(entries.values()).sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary)); } } diff --git a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts index 426ef2e55e1..1dad975266b 100644 --- a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts +++ b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts @@ -232,5 +232,37 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.getEntries(StatusbarAlignment.RIGHT).length, 3); }); + test('entry with reference to other entry respects secondary sorting (existent)', () => { + const container = document.createElement('div'); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); + + model.add({ id: 'ref', alignment: StatusbarAlignment.LEFT, name: 'ref', priority: { primary: 0, secondary: 0 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'entry2', alignment: StatusbarAlignment.RIGHT, name: '2', priority: { primary: { id: 'ref', alignment: StatusbarAlignment.RIGHT }, secondary: 2 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'entry1', alignment: StatusbarAlignment.RIGHT, name: '1', priority: { primary: { id: 'ref', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'entry3', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'ref', alignment: StatusbarAlignment.RIGHT }, secondary: 3 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + + const entries = model.entries; + assert.strictEqual(entries.length, 4); + assert.strictEqual(entries[0].id, 'ref'); + assert.strictEqual(entries[1].id, 'entry3'); + assert.strictEqual(entries[2].id, 'entry2'); + assert.strictEqual(entries[3].id, 'entry1'); + }); + + test('entry with reference to other entry respects secondary sorting (nonexistent)', () => { + const container = document.createElement('div'); + const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); + + model.add({ id: 'entry2', alignment: StatusbarAlignment.RIGHT, name: '2', priority: { primary: { id: 'ref', alignment: StatusbarAlignment.RIGHT }, secondary: 2 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'entry1', alignment: StatusbarAlignment.RIGHT, name: '1', priority: { primary: { id: 'ref', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'entry3', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'ref', alignment: StatusbarAlignment.RIGHT }, secondary: 3 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + + const entries = model.entries; + assert.strictEqual(entries.length, 3); + assert.strictEqual(entries[0].id, 'entry3'); + assert.strictEqual(entries[1].id, 'entry2'); + assert.strictEqual(entries[2].id, 'entry1'); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); From 3941518bee735f033b528cc90913c6b2a95cfa3b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 Feb 2025 05:25:14 -0800 Subject: [PATCH 35/36] Show close button regardless of code block Fixes microsoft/vscode-copilot-release#5393 --- .../contrib/terminalContrib/chat/browser/terminalChatActions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 935f2731a33..782aca3edb1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -71,7 +71,6 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 2, - when: TerminalChatContextKeys.responseContainsCodeBlock }], icon: Codicon.close, f1: true, From 24190e293c709bd95901a8b620c79da84b46adbb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 21 Feb 2025 00:48:53 +1100 Subject: [PATCH 36/36] Parse cell Uris to get notebook Uri when reading items in chat session (#241172) --- .../contrib/chat/browser/chatEditing/chatEditingSession.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 4751b8a379c..c90d301c459 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -240,6 +240,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _getEntry(uri: URI): AbstractChatEditingModifiedFileEntry | undefined { + uri = CellUri.parse(uri)?.notebook ?? uri; return this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri)); } @@ -248,6 +249,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } public readEntry(uri: URI, reader: IReader | undefined): IModifiedFileEntry | undefined { + uri = CellUri.parse(uri)?.notebook ?? uri; return this._entriesObs.read(reader).find(e => isEqual(e.modifiedURI, uri)); }