Skip to content

Commit 6693b1d

Browse files
authored
Fix tooltip on inline references in chat (microsoft#203737)
1 parent 3be6ca6 commit 6693b1d

File tree

2 files changed

+86
-57
lines changed

2 files changed

+86
-57
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil
5151
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
5252
import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat';
5353
import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
54-
import { annotateSpecialMarkdownContent, convertParsedRequestToMarkdown, extractVulnerabilitiesFromText, walkTreeAndAnnotateReferenceLinks } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
54+
import { ChatMarkdownDecorationsRenderer, annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
5555
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
5656
import { CodeBlockPart, ICodeBlockData, ICodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
5757
import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents';
@@ -109,6 +109,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
109109
private readonly focusedFileTreesByResponseId = new Map<string, number>();
110110

111111
private readonly renderer: MarkdownRenderer;
112+
private readonly markdownDecorationsRenderer: ChatMarkdownDecorationsRenderer;
112113

113114
protected readonly _onDidClickFollowup = this._register(new Emitter<IChatReplyFollowup>());
114115
readonly onDidClickFollowup: Event<IChatReplyFollowup> = this._onDidClickFollowup.event;
@@ -142,6 +143,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
142143
) {
143144
super();
144145
this.renderer = this.instantiationService.createInstance(MarkdownRenderer, {});
146+
this.markdownDecorationsRenderer = this.instantiationService.createInstance(ChatMarkdownDecorationsRenderer);
145147
this._editorPool = this._register(this.instantiationService.createInstance(EditorPool, this.editorOptions));
146148
this._treePool = this._register(this.instantiationService.createInstance(TreePool, this._onDidChangeVisibility.event));
147149
this._contentReferencesListPool = this._register(this.instantiationService.createInstance(ContentReferencesListPool, this._onDidChangeVisibility.event));
@@ -339,7 +341,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
339341
} else if (isRequestVM(element)) {
340342
const markdown = 'kind' in element.message ?
341343
element.message.message :
342-
this.instantiationService.invokeFunction(convertParsedRequestToMarkdown, element.message);
344+
this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message);
343345
this.basicRenderElement([{ content: new MarkdownString(markdown), kind: 'markdownContent' }], element, index, templateData);
344346
} else {
345347
this.renderWelcomeMessage(element, templateData);
@@ -876,7 +878,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
876878
disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id)));
877879
}
878880

879-
this.instantiationService.invokeFunction(acc => walkTreeAndAnnotateReferenceLinks(acc, result.element));
881+
this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element);
880882

881883
orderedDisposablesList.reverse().forEach(d => disposables.add(d));
882884
return {

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

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,86 +4,113 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from 'vs/base/browser/dom';
7+
import { toErrorMessage } from 'vs/base/common/errorMessage';
78
import { MarkdownString } from 'vs/base/common/htmlContent';
89
import { revive } from 'vs/base/common/marshalling';
910
import { basename } from 'vs/base/common/resources';
1011
import { URI } from 'vs/base/common/uri';
1112
import { IRange } from 'vs/editor/common/core/range';
1213
import { Location } from 'vs/editor/common/languages';
13-
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1414
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1515
import { ILabelService } from 'vs/platform/label/common/label';
16+
import { ILogService } from 'vs/platform/log/common/log';
1617
import { IChatProgressRenderableResponseContent, IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
1718
import { ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
1819
import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference } from 'vs/workbench/contrib/chat/common/chatService';
1920

2021
const variableRefUrl = 'http://_vscodedecoration_';
2122

22-
export function convertParsedRequestToMarkdown(accessor: ServicesAccessor, parsedRequest: IParsedChatRequest): string {
23-
let result = '';
24-
for (const part of parsedRequest.parts) {
25-
if (part instanceof ChatRequestTextPart) {
26-
result += part.text;
27-
} else {
28-
const labelService = accessor.get(ILabelService);
29-
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
30-
|| undefined;
31-
const title = uri ? labelService.getUriLabel(uri, { relative: true }) : '';
23+
export class ChatMarkdownDecorationsRenderer {
24+
constructor(
25+
@IKeybindingService private readonly keybindingService: IKeybindingService,
26+
@ILabelService private readonly labelService: ILabelService,
27+
@ILogService private readonly logService: ILogService
28+
) {
3229

33-
result += `[${part.text}](${variableRefUrl}${title})`;
34-
}
3530
}
3631

37-
return result;
38-
}
32+
convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string {
33+
let result = '';
34+
for (const part of parsedRequest.parts) {
35+
if (part instanceof ChatRequestTextPart) {
36+
result += part.text;
37+
} else {
38+
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
39+
|| undefined;
40+
const title = uri ? this.labelService.getUriLabel(uri, { relative: true }) : '';
3941

40-
export function walkTreeAndAnnotateReferenceLinks(accessor: ServicesAccessor, element: HTMLElement): void {
41-
const keybindingService = accessor.get(IKeybindingService);
42-
43-
element.querySelectorAll('a').forEach(a => {
44-
const href = a.getAttribute('data-href');
45-
if (href) {
46-
if (href.startsWith(variableRefUrl)) {
47-
const title = href.slice(variableRefUrl.length);
48-
a.parentElement!.replaceChild(
49-
renderResourceWidget(a.textContent!, title),
50-
a);
51-
} else if (href.startsWith(contentRefUrl)) {
52-
renderFileWidget(href, a);
53-
} else if (href.startsWith('command:')) {
54-
injectKeybindingHint(a, href, keybindingService);
42+
result += `[${part.text}](${variableRefUrl}${title})`;
5543
}
5644
}
57-
});
58-
}
5945

60-
function injectKeybindingHint(a: HTMLAnchorElement, href: string, keybindingService: IKeybindingService): void {
61-
const command = href.match(/command:([^\)]+)/)?.[1];
62-
if (command) {
63-
const kb = keybindingService.lookupKeybinding(command);
64-
if (kb) {
65-
const keybinding = kb.getLabel();
66-
if (keybinding) {
67-
a.textContent = `${a.textContent} (${keybinding})`;
46+
return result;
47+
}
48+
49+
walkTreeAndAnnotateReferenceLinks(element: HTMLElement): void {
50+
element.querySelectorAll('a').forEach(a => {
51+
const href = a.getAttribute('data-href');
52+
if (href) {
53+
if (href.startsWith(variableRefUrl)) {
54+
const title = href.slice(variableRefUrl.length);
55+
a.parentElement!.replaceChild(
56+
this.renderResourceWidget(a.textContent!, title),
57+
a);
58+
} else if (href.startsWith(contentRefUrl)) {
59+
this.renderFileWidget(href, a);
60+
} else if (href.startsWith('command:')) {
61+
this.injectKeybindingHint(a, href, this.keybindingService);
62+
}
6863
}
64+
});
65+
}
66+
67+
private renderFileWidget(href: string, a: HTMLAnchorElement): void {
68+
// TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now.
69+
const fullUri = URI.parse(href);
70+
let location: Location | { uri: URI; range: undefined };
71+
try {
72+
location = revive(JSON.parse(fullUri.fragment));
73+
} catch (err) {
74+
this.logService.error('Invalid chat widget render data JSON', toErrorMessage(err));
75+
return;
6976
}
77+
78+
if (!location.uri || !URI.isUri(location.uri)) {
79+
this.logService.error(`Invalid chat widget render data: ${fullUri.fragment}`);
80+
return;
81+
}
82+
83+
const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : '';
84+
a.setAttribute('data-href', location.uri.with({ fragment }).toString());
85+
86+
const label = this.labelService.getUriLabel(location.uri, { relative: true });
87+
a.title = location.range ?
88+
`${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` :
89+
label;
7090
}
71-
}
7291

73-
function renderResourceWidget(name: string, title: string): HTMLElement {
74-
const container = dom.$('span.chat-resource-widget');
75-
const alias = dom.$('span', undefined, name);
76-
alias.title = title;
77-
container.appendChild(alias);
78-
return container;
79-
}
8092

81-
function renderFileWidget(href: string, a: HTMLAnchorElement): void {
82-
// TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now.
83-
const fullUri = URI.parse(href);
84-
const location: Location | { uri: URI; range: undefined } = revive(JSON.parse(fullUri.fragment));
85-
const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : '';
86-
a.setAttribute('data-href', location.uri.with({ fragment }).toString());
93+
private renderResourceWidget(name: string, title: string): HTMLElement {
94+
const container = dom.$('span.chat-resource-widget');
95+
const alias = dom.$('span', undefined, name);
96+
alias.title = title;
97+
container.appendChild(alias);
98+
return container;
99+
}
100+
101+
102+
private injectKeybindingHint(a: HTMLAnchorElement, href: string, keybindingService: IKeybindingService): void {
103+
const command = href.match(/command:([^\)]+)/)?.[1];
104+
if (command) {
105+
const kb = keybindingService.lookupKeybinding(command);
106+
if (kb) {
107+
const keybinding = kb.getLabel();
108+
if (keybinding) {
109+
a.textContent = `${a.textContent} (${keybinding})`;
110+
}
111+
}
112+
}
113+
}
87114
}
88115

89116
export interface IMarkdownVulnerability {

0 commit comments

Comments
 (0)