Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';6import { Emitter, Event } from '../../../../../base/common/event.js';7import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';8import { Schemas } from '../../../../../base/common/network.js';9import { autorun } from '../../../../../base/common/observable.js';10import { basename, isEqual } from '../../../../../base/common/resources.js';11import { ThemeIcon } from '../../../../../base/common/themables.js';12import { URI } from '../../../../../base/common/uri.js';13import { getCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';14import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';15import { Location } from '../../../../../editor/common/languages.js';16import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';17import { IWorkbenchContribution } from '../../../../common/contributions.js';18import { EditorsOrder } from '../../../../common/editor.js';19import { IEditorService } from '../../../../services/editor/common/editorService.js';20import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';21import { WebviewEditor } from '../../../webviewPanel/browser/webviewEditor.js';22import { WebviewInput } from '../../../webviewPanel/browser/webviewEditorInput.js';23import { IChatEditingService } from '../../common/editing/chatEditingService.js';24import { IChatService } from '../../common/chatService/chatService.js';25import { IChatRequestImplicitVariableEntry, IChatRequestVariableEntry, isStringImplicitContextValue, StringChatContextValue } from '../../common/attachments/chatVariableEntries.js';26import { ChatAgentLocation } from '../../common/constants.js';27import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js';28import { getPromptsTypeForLanguageId } from '../../common/promptSyntax/promptTypes.js';29import { IChatWidget, IChatWidgetService } from '../chat.js';30import { IChatContextService } from '../contextContrib/chatContextService.js';31import { ITextModel } from '../../../../../editor/common/model.js';32import { IRange } from '../../../../../editor/common/core/range.js';3334export class ChatImplicitContextContribution extends Disposable implements IWorkbenchContribution {35static readonly ID = 'chat.implicitContext';3637private readonly _currentCancelTokenSource: MutableDisposable<CancellationTokenSource>;3839private _implicitContextEnablement: { [mode: string]: string };4041constructor(42@ICodeEditorService private readonly codeEditorService: ICodeEditorService,43@IEditorService private readonly editorService: IEditorService,44@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,45@IChatService private readonly chatService: IChatService,46@IChatEditingService private readonly chatEditingService: IChatEditingService,47@IConfigurationService private readonly configurationService: IConfigurationService,48@ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService,49@IChatContextService private readonly chatContextService: IChatContextService50) {51super();52this._currentCancelTokenSource = this._register(new MutableDisposable<CancellationTokenSource>());53this._implicitContextEnablement = this.configurationService.getValue<{ [mode: string]: string }>('chat.implicitContext.enabled');5455const activeEditorDisposables = this._register(new DisposableStore());5657this._register(Event.runAndSubscribe(58editorService.onDidActiveEditorChange,59(() => {60activeEditorDisposables.clear();61const codeEditor = this.findActiveCodeEditor();62if (codeEditor) {63activeEditorDisposables.add(Event.debounce(64Event.any(65codeEditor.onDidChangeModel,66codeEditor.onDidChangeModelLanguage,67codeEditor.onDidChangeCursorSelection,68codeEditor.onDidScrollChange),69() => undefined,70500)(() => this.updateImplicitContext()));71}7273const notebookEditor = this.findActiveNotebookEditor();74if (notebookEditor) {75const activeCellDisposables = activeEditorDisposables.add(new DisposableStore());76activeEditorDisposables.add(notebookEditor.onDidChangeActiveCell(() => {77activeCellDisposables.clear();78const codeEditor = this.codeEditorService.getActiveCodeEditor();79if (codeEditor && codeEditor.getModel()?.uri.scheme === Schemas.vscodeNotebookCell) {80activeCellDisposables.add(Event.debounce(81Event.any(82codeEditor.onDidChangeModel,83codeEditor.onDidChangeCursorSelection,84codeEditor.onDidScrollChange),85() => undefined,86500)(() => this.updateImplicitContext()));87}88}));8990activeEditorDisposables.add(Event.debounce(91Event.any(92notebookEditor.onDidChangeModel,93notebookEditor.onDidChangeActiveCell94),95() => undefined,96500)(() => this.updateImplicitContext()));97}98const webviewEditor = this.findActiveWebviewEditor();99if (webviewEditor) {100activeEditorDisposables.add(Event.debounce((webviewEditor.input as WebviewInput).webview.onMessage, () => undefined, 500)(() => {101this.updateImplicitContext();102}));103}104105this.updateImplicitContext();106})));107this._register(autorun((reader) => {108this.chatEditingService.editingSessionsObs.read(reader);109this.updateImplicitContext();110}));111this._register(this.configurationService.onDidChangeConfiguration(e => {112if (e.affectsConfiguration('chat.implicitContext.enabled')) {113this._implicitContextEnablement = this.configurationService.getValue<{ [mode: string]: string }>('chat.implicitContext.enabled');114this.updateImplicitContext();115}116}));117this._register(this.chatService.onDidSubmitRequest(({ chatSessionResource }) => {118const widget = this.chatWidgetService.getWidgetBySessionResource(chatSessionResource);119if (!widget?.input.implicitContext) {120return;121}122if (this._implicitContextEnablement[widget.location] === 'first' && widget.viewModel?.getItems().length !== 0) {123widget.input.implicitContext.setValue(undefined, false, undefined);124}125}));126this._register(this.chatWidgetService.onDidAddWidget(async (widget) => {127await this.updateImplicitContext(widget);128}));129}130131private findActiveCodeEditor(): ICodeEditor | undefined {132const codeEditor = this.codeEditorService.getActiveCodeEditor();133if (codeEditor) {134const model = codeEditor.getModel();135if (model?.uri.scheme === Schemas.vscodeNotebookCell) {136return undefined;137}138139if (model) {140return codeEditor;141}142}143for (const codeOrDiffEditor of this.editorService.getVisibleTextEditorControls(EditorsOrder.MOST_RECENTLY_ACTIVE)) {144const codeEditor = getCodeEditor(codeOrDiffEditor);145if (!codeEditor) {146continue;147}148149const model = codeEditor.getModel();150if (model) {151return codeEditor;152}153}154return undefined;155}156157private findActiveWebviewEditor(): WebviewEditor | undefined {158const activeEditorPane = this.editorService.activeEditorPane;159if (activeEditorPane?.input instanceof WebviewInput) {160return activeEditorPane as WebviewEditor;161}162return undefined;163}164165private findActiveNotebookEditor(): INotebookEditor | undefined {166return getNotebookEditorFromEditorPane(this.editorService.activeEditorPane);167}168169private async updateImplicitContext(updateWidget?: IChatWidget): Promise<void> {170const cancelTokenSource = this._currentCancelTokenSource.value = new CancellationTokenSource();171const codeEditor = this.findActiveCodeEditor();172const model = codeEditor?.getModel();173const selection = codeEditor?.getSelection();174let newValue: Location | URI | StringChatContextValue | undefined;175let isSelection = false;176177let languageId: string | undefined;178if (model) {179languageId = model.getLanguageId();180if (selection && !selection.isEmpty()) {181newValue = { uri: model.uri, range: selection } satisfies Location;182isSelection = true;183} else {184if (this.configurationService.getValue('chat.implicitContext.suggestedContext')) {185newValue = model.uri;186} else {187const visibleRanges = codeEditor?.getVisibleRanges();188if (visibleRanges && visibleRanges.length > 0) {189// Merge visible ranges. Maybe the reference value could actually be an array of Locations?190// Something like a Location with an array of Ranges?191let range = visibleRanges[0];192visibleRanges.slice(1).forEach(r => {193range = range.plusRange(r);194});195newValue = { uri: model.uri, range } satisfies Location;196} else {197newValue = model.uri;198}199}200}201}202203const notebookEditor = this.findActiveNotebookEditor();204if (notebookEditor?.isReplHistory) {205// The chat APIs don't work well with Interactive Windows206newValue = undefined;207} else if (notebookEditor) {208const activeCell = notebookEditor.getActiveCell();209if (activeCell) {210const codeEditor = this.codeEditorService.getActiveCodeEditor();211const selection = codeEditor?.getSelection();212const visibleRanges = codeEditor?.getVisibleRanges() || [];213newValue = activeCell.uri;214const cellModel = codeEditor?.getModel();215if (cellModel && isEqual(cellModel.uri, activeCell.uri)) {216if (selection && !selection.isEmpty()) {217newValue = { uri: activeCell.uri, range: selection } satisfies Location;218isSelection = true;219} else if (visibleRanges.length > 0) {220// If the entire cell is visible, just use the cell URI, no need to specify range.221if (!isEntireCellVisible(cellModel, visibleRanges)) {222// Merge visible ranges. Maybe the reference value could actually be an array of Locations?223// Something like a Location with an array of Ranges?224let range = visibleRanges[0];225visibleRanges.slice(1).forEach(r => {226range = range.plusRange(r);227});228newValue = { uri: activeCell.uri, range } satisfies Location;229}230}231}232} else {233newValue = notebookEditor.textModel?.uri;234}235}236237const webviewEditor = this.findActiveWebviewEditor();238if (webviewEditor?.input?.resource) {239const webviewContext = await this.chatContextService.contextForResource(webviewEditor.input.resource);240if (webviewContext) {241newValue = webviewContext;242}243}244245const uri = newValue instanceof URI ? newValue : (isStringImplicitContextValue(newValue) ? undefined : newValue?.uri);246if (uri && (247await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token) ||248uri.path.endsWith('.copilotmd'))249) {250newValue = undefined;251}252253if (cancelTokenSource.token.isCancellationRequested) {254return;255}256257const isPromptFile = languageId && getPromptsTypeForLanguageId(languageId) !== undefined;258259const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.EditorInline)];260for (const widget of widgets) {261if (!widget.input.implicitContext) {262continue;263}264const setting = this._implicitContextEnablement[widget.location];265const isFirstInteraction = widget.viewModel?.getItems().length === 0;266if ((setting === 'always' || setting === 'first' && isFirstInteraction) && !isPromptFile) { // disable implicit context for prompt files267widget.input.implicitContext.setValue(newValue, isSelection, languageId);268} else {269widget.input.implicitContext.setValue(undefined, false, undefined);270}271}272}273}274275function isEntireCellVisible(cellModel: ITextModel, visibleRanges: IRange[]): boolean {276if (visibleRanges.length === 1 && visibleRanges[0].startLineNumber === 1 && visibleRanges[0].startColumn === 1 && visibleRanges[0].endLineNumber === cellModel.getLineCount() && visibleRanges[0].endColumn === cellModel.getLineMaxColumn(visibleRanges[0].endLineNumber)) {277return true;278}279return false;280}281282export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry {283get id() {284if (URI.isUri(this.value)) {285return 'vscode.implicit.file';286} else if (isStringImplicitContextValue(this.value)) {287return 'vscode.implicit.string';288} else if (this.value) {289if (this._isSelection) {290return 'vscode.implicit.selection';291} else {292return 'vscode.implicit.viewport';293}294} else {295return 'vscode.implicit';296}297}298299get name(): string {300if (URI.isUri(this.value)) {301return `file:${basename(this.value)}`;302} else if (isStringImplicitContextValue(this.value)) {303return this.value.name;304} else if (this.value) {305return `file:${basename(this.value.uri)}`;306} else {307return 'implicit';308}309}310311readonly kind = 'implicit';312313get modelDescription(): string {314if (URI.isUri(this.value)) {315return `User's active file`;316} else if (isStringImplicitContextValue(this.value)) {317return this.value.modelDescription ?? `User's active context from ${this.value.name}`;318} else if (this._isSelection) {319return `User's active selection`;320} else {321return `User's current visible code`;322}323}324325readonly isFile = true;326327private _isSelection = false;328public get isSelection(): boolean {329return this._isSelection;330}331332private _onDidChangeValue = this._register(new Emitter<void>());333readonly onDidChangeValue = this._onDidChangeValue.event;334335private _value: Location | URI | StringChatContextValue | undefined;336get value() {337return this._value;338}339340private _enabled = true;341get enabled() {342return this._enabled;343}344345set enabled(value: boolean) {346this._enabled = value;347this._onDidChangeValue.fire();348}349350private _uri: URI | undefined;351get uri(): URI | undefined {352if (isStringImplicitContextValue(this.value)) {353return this.value.uri;354}355return this._uri;356}357358get icon(): ThemeIcon | undefined {359if (isStringImplicitContextValue(this.value)) {360return this.value.icon;361}362return undefined;363}364365setValue(value: Location | URI | StringChatContextValue | undefined, isSelection: boolean, languageId?: string): void {366if (isStringImplicitContextValue(value)) {367this._value = value;368} else {369this._value = value;370this._uri = URI.isUri(value) ? value : value?.uri;371}372this._isSelection = isSelection;373this._onDidChangeValue.fire();374}375376public toBaseEntries(): IChatRequestVariableEntry[] {377if (!this.value) {378return [];379}380381if (isStringImplicitContextValue(this.value)) {382return [383{384kind: 'string',385id: this.id,386name: this.name,387value: this.value.value ?? this.name,388modelDescription: this.modelDescription,389icon: this.value.icon,390uri: this.value.uri391}392];393}394395return [{396kind: 'file',397id: this.id,398name: this.name,399value: this.value,400modelDescription: this.modelDescription,401}];402}403404}405406407