Skip to content

Commit 4034bad

Browse files
committed
Improve creation of text models for chat code blocks
Refactors the chat code block logic to better support cross code blocks IntelliSense. Previously we only created text models for the visible editors in chat. With this new approach, we instead create a unique text model for each code block in the conversation. This allows us our IntelliSense features to work even if a code block is not visible in chat Also uses this as a change to remove some duplicate I introduced to support local file editors in chat Still a draft as the text model creation should be moved out of the chat list renderer
1 parent a0b90ac commit 4034bad

File tree

4 files changed

+248
-288
lines changed

4 files changed

+248
-288
lines changed

src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

+84-65
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { Codicon } from 'vs/base/common/codicons';
2121
import { Emitter, Event } from 'vs/base/common/event';
2222
import { FuzzyScore } from 'vs/base/common/filters';
2323
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
24-
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
24+
import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle';
2525
import { ResourceMap } from 'vs/base/common/map';
2626
import { marked } from 'vs/base/common/marked/marked';
2727
import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network';
@@ -30,18 +30,16 @@ import { basename } from 'vs/base/common/path';
3030
import { equalsIgnoreCase } from 'vs/base/common/strings';
3131
import { ThemeIcon } from 'vs/base/common/themables';
3232
import { URI } from 'vs/base/common/uri';
33-
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
34-
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
3533
import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer';
3634
import { Range } from 'vs/editor/common/core/range';
35+
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
3736
import { localize } from 'vs/nls';
3837
import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
3938
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
4039
import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
4140
import { ICommandService } from 'vs/platform/commands/common/commands';
4241
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
4342
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
44-
import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor';
4543
import { FileKind, FileType } from 'vs/platform/files/common/files';
4644
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
4745
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
@@ -56,9 +54,9 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil
5654
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
5755
import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat';
5856
import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
59-
import { ChatMarkdownDecorationsRenderer, annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
57+
import { ChatMarkdownDecorationsRenderer, IMarkdownVulnerability, annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
6058
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
61-
import { ChatCodeBlockContentProvider, ICodeBlockData, ICodeBlockPart, LocalFileCodeBlockPart, SimpleCodeBlockPart, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
59+
import { ChatCodeBlockContentProvider, CodeBlockPart, ICodeBlockData, ICodeBlockPart, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
6260
import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents';
6361
import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
6462
import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
@@ -105,6 +103,7 @@ export interface IChatListItemRendererOptions {
105103
}
106104

107105
export class ChatListItemRenderer extends Disposable implements ITreeRenderer<ChatTreeItem, FuzzyScore, IChatListItemTemplate> {
106+
108107
static readonly ID = 'item';
109108

110109
private readonly codeBlocksByResponseId = new Map<string, IChatCodeBlockInfo[]>();
@@ -133,47 +132,29 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
133132
private _usedReferencesEnabled = false;
134133

135134
constructor(
136-
private readonly editorOptions: ChatEditorOptions,
135+
editorOptions: ChatEditorOptions,
137136
private readonly rendererOptions: IChatListItemRendererOptions,
138137
private readonly delegate: IChatRendererDelegate,
139-
overflowWidgetsDomNode: HTMLElement | undefined,
138+
private readonly codeBlockModelCollection: CodeBlockModelCollection,
140139
@IInstantiationService private readonly instantiationService: IInstantiationService,
141140
@IConfigurationService configService: IConfigurationService,
142141
@ILogService private readonly logService: ILogService,
143142
@IOpenerService private readonly openerService: IOpenerService,
144143
@IContextKeyService private readonly contextKeyService: IContextKeyService,
145-
@ICodeEditorService codeEditorService: ICodeEditorService,
146144
@IThemeService private readonly themeService: IThemeService,
147145
@ICommandService private readonly commandService: ICommandService,
146+
@ITextModelService private readonly textModelService: ITextModelService,
148147
) {
149148
super();
149+
this._editorPool = this._register(this.instantiationService.createInstance(EditorPool, editorOptions, delegate, undefined));
150+
150151
this.renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {}));
151152
this.markdownDecorationsRenderer = this.instantiationService.createInstance(ChatMarkdownDecorationsRenderer);
152-
this._editorPool = this._register(this.instantiationService.createInstance(EditorPool, this.editorOptions, delegate, overflowWidgetsDomNode));
153153
this._treePool = this._register(this.instantiationService.createInstance(TreePool, this._onDidChangeVisibility.event));
154154
this._contentReferencesListPool = this._register(this.instantiationService.createInstance(ContentReferencesListPool, this._onDidChangeVisibility.event));
155155

156156
this._register(this.instantiationService.createInstance(ChatCodeBlockContentProvider));
157157

158-
this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise<ICodeEditor | null> => {
159-
if (input.resource.scheme !== Schemas.vscodeChatCodeBlock) {
160-
return null;
161-
}
162-
const block = this._editorPool.find(input.resource);
163-
if (!block) {
164-
return null;
165-
}
166-
if (input.options?.selection) {
167-
block.editor.setSelection({
168-
startLineNumber: input.options.selection.startLineNumber,
169-
startColumn: input.options.selection.startColumn,
170-
endLineNumber: input.options.selection.startLineNumber ?? input.options.selection.endLineNumber,
171-
endColumn: input.options.selection.startColumn ?? input.options.selection.endColumn
172-
});
173-
}
174-
return block.editor;
175-
}));
176-
177158
this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? true;
178159
this._register(configService.onDidChangeConfiguration(e => {
179160
if (e.affectsConfiguration('chat.experimental.usedReferences')) {
@@ -186,6 +167,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
186167
return ChatListItemRenderer.ID;
187168
}
188169

170+
editorsInUse() {
171+
return this._editorPool.inUse();
172+
}
173+
189174
private traceLayout(method: string, message: string) {
190175
if (forceVerboseLayoutTracing) {
191176
this.logService.info(`ChatListItemRenderer#${method}: ${message}`);
@@ -858,7 +843,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
858843

859844
private renderMarkdown(markdown: IMarkdownString, element: ChatTreeItem, templateData: IChatListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult {
860845
const disposables = new DisposableStore();
861-
let codeBlockIndex = 0;
862846

863847
markdown = new MarkdownString(markdown.value, {
864848
isTrusted: {
@@ -870,25 +854,33 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
870854
// We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering
871855
const orderedDisposablesList: IDisposable[] = [];
872856
const codeblocks: IChatCodeBlockInfo[] = [];
857+
let codeBlockIndex = 0;
873858
const result = this.renderer.render(markdown, {
874859
fillInIncompleteTokens,
875860
codeBlockRendererSync: (languageId, text) => {
876-
let data: ICodeBlockData;
861+
const index = codeBlockIndex++;
862+
let textModel: Promise<IReference<IResolvedTextEditorModel>>;
863+
let range: Range | undefined;
864+
let vulns: readonly IMarkdownVulnerability[] | undefined;
877865
if (equalsIgnoreCase(languageId, localFileLanguageId)) {
878866
try {
879867
const parsedBody = parseLocalFileData(text);
880-
data = { type: 'localFile', uri: parsedBody.uri, range: parsedBody.range && Range.lift(parsedBody.range), codeBlockIndex: codeBlockIndex++, element, hideToolbar: false, parentContextKeyService: templateData.contextKeyService };
868+
range = parsedBody.range && Range.lift(parsedBody.range);
869+
textModel = this.textModelService.createModelReference(parsedBody.uri);
881870
} catch (e) {
882-
console.error(e);
883871
return $('div');
884872
}
885873
} else {
886-
const vulns = extractVulnerabilitiesFromText(text);
887-
const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered;
888-
data = { type: 'code', languageId, text: vulns.newText, codeBlockIndex: codeBlockIndex++, element, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns: vulns.vulnerabilities };
874+
// TODO: Creating the text models should be done in the model layer, not in the renderer
875+
// The current approach means that only code blocks that have been rendered can be referenced
876+
textModel = this.codeBlockModelCollection.getOrCreate(element, index);
877+
const extractedVulns = extractVulnerabilitiesFromText(text);
878+
vulns = extractedVulns.vulnerabilities;
879+
textModel.then(ref => ref.object.textEditorModel.setValue(extractedVulns.newText));
889880
}
890881

891-
const ref = this.renderCodeBlock(data);
882+
const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered;
883+
const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns });
892884

893885
// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)
894886
// not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render)
@@ -899,15 +891,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
899891

900892
if (isResponseVM(element)) {
901893
const info: IChatCodeBlockInfo = {
902-
codeBlockIndex: data.codeBlockIndex,
894+
codeBlockIndex: index,
903895
element,
904896
focus() {
905897
ref.object.focus();
906898
}
907899
};
908900
codeblocks.push(info);
909-
this.codeBlocksByEditorUri.set(ref.object.uri, info);
910-
disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(ref.object.uri)));
901+
if (ref.object.uri) {
902+
const uri = ref.object.uri;
903+
this.codeBlocksByEditorUri.set(uri, info);
904+
disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(uri)));
905+
}
911906
}
912907
orderedDisposablesList.push(ref);
913908
return ref.object.element;
@@ -933,7 +928,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
933928
}
934929

935930
private renderCodeBlock(data: ICodeBlockData): IDisposableReference<ICodeBlockPart> {
936-
const ref = this._editorPool.get(data);
931+
const ref = this._editorPool.get();
937932
const editorInfo = ref.object;
938933
editorInfo.render(data, this._currentLayoutWidth);
939934

@@ -1068,54 +1063,78 @@ interface IDisposableReference<T> extends IDisposable {
10681063
isStale: () => boolean;
10691064
}
10701065

1071-
class EditorPool extends Disposable {
1066+
export class EditorPool extends Disposable {
10721067

1073-
private readonly _simpleEditorPool: ResourcePool<SimpleCodeBlockPart>;
1074-
private readonly _localFileEditorPool: ResourcePool<LocalFileCodeBlockPart>;
1068+
private readonly _pool: ResourcePool<CodeBlockPart>;
10751069

1076-
public *inUse(): Iterable<ICodeBlockPart> {
1077-
yield* this._simpleEditorPool.inUse;
1078-
yield* this._localFileEditorPool.inUse;
1070+
public inUse(): Iterable<ICodeBlockPart> {
1071+
return this._pool.inUse;
10791072
}
10801073

10811074
constructor(
1082-
private readonly options: ChatEditorOptions,
1075+
options: ChatEditorOptions,
10831076
delegate: IChatRendererDelegate,
10841077
overflowWidgetsDomNode: HTMLElement | undefined,
1085-
@IInstantiationService private readonly instantiationService: IInstantiationService,
1078+
@IInstantiationService instantiationService: IInstantiationService,
10861079
) {
10871080
super();
1088-
this._simpleEditorPool = this._register(new ResourcePool(() => {
1089-
return this.instantiationService.createInstance(SimpleCodeBlockPart, this.options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode);
1090-
}));
1091-
this._localFileEditorPool = this._register(new ResourcePool(() => {
1092-
return this.instantiationService.createInstance(LocalFileCodeBlockPart, this.options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode);
1081+
this._pool = this._register(new ResourcePool(() => {
1082+
return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode);
10931083
}));
10941084
}
10951085

1096-
get(data: ICodeBlockData): IDisposableReference<ICodeBlockPart> {
1097-
return this.getFromPool(data.type === 'localFile' ? this._localFileEditorPool : this._simpleEditorPool);
1098-
}
1099-
1100-
find(resource: URI): SimpleCodeBlockPart | undefined {
1101-
return Array.from(this._simpleEditorPool.inUse).find(part => part.uri?.toString() === resource.toString());
1102-
}
1103-
1104-
private getFromPool(pool: ResourcePool<ICodeBlockPart>): IDisposableReference<ICodeBlockPart> {
1105-
const codeBlock = pool.get();
1086+
get(): IDisposableReference<ICodeBlockPart> {
1087+
const codeBlock = this._pool.get();
11061088
let stale = false;
11071089
return {
11081090
object: codeBlock,
11091091
isStale: () => stale,
11101092
dispose: () => {
11111093
codeBlock.reset();
11121094
stale = true;
1113-
pool.release(codeBlock);
1095+
this._pool.release(codeBlock);
11141096
}
11151097
};
11161098
}
11171099
}
11181100

1101+
export class CodeBlockModelCollection extends Disposable {
1102+
1103+
private readonly _models = new ResourceMap<Promise<IReference<IResolvedTextEditorModel>>>();
1104+
1105+
constructor(
1106+
@ITextModelService private readonly textModelService: ITextModelService,
1107+
) {
1108+
super();
1109+
}
1110+
1111+
public override dispose(): void {
1112+
super.dispose();
1113+
this.clear();
1114+
}
1115+
1116+
getOrCreate(element: ChatTreeItem, codeBlockIndex: number): Promise<IReference<IResolvedTextEditorModel>> {
1117+
const uri = this.getUri(element, codeBlockIndex);
1118+
const existing = this._models.get(uri);
1119+
if (existing) {
1120+
return existing;
1121+
}
1122+
1123+
const ref = this.textModelService.createModelReference(uri);
1124+
this._models.set(uri, ref);
1125+
return ref;
1126+
}
1127+
1128+
clear(): void {
1129+
this._models.forEach(async model => (await model).dispose());
1130+
this._models.clear();
1131+
}
1132+
1133+
private getUri(element: ChatTreeItem, index: number): URI {
1134+
return URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: `/${element.id}/${index}` });
1135+
}
1136+
}
1137+
11191138
class TreePool extends Disposable {
11201139
private _pool: ResourcePool<WorkbenchCompressibleAsyncDataTree<IChatResponseProgressFileTreeData, IChatResponseProgressFileTreeData, void>>;
11211140

0 commit comments

Comments
 (0)