Optimize inline anchor widget

This commit is contained in:
Rob Lourens
2026-02-14 19:51:25 -08:00
parent 20c1301997
commit 90f268600a
2 changed files with 57 additions and 36 deletions

View File

@@ -614,8 +614,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {
}
if (attachment.kind === 'symbol') {
const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));
this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext));
this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, this.contextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext));
}
// Handle click for string context attachments with context commands
@@ -1115,19 +1114,15 @@ export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAcc
return store;
}
export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable {
export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, parentContextKeyService: IContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable {
const instantiationService = accessor.get(IInstantiationService);
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
const textModelService = accessor.get(ITextModelService);
const contextMenuService = accessor.get(IContextMenuService);
const menuService = accessor.get(IMenuService);
const store = new DisposableStore();
// Context
store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri));
const chatResourceContext = chatAttachmentResourceContextKey.bindTo(scopedContextKeyService);
chatResourceContext.set(attachment.value.uri.toString());
// Drag and drop
widget.draggable = true;
store.add(dom.addDisposableListener(widget, 'dragstart', e => {
@@ -1143,26 +1138,57 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces
e.dataTransfer?.setDragImage(widget, 0, 0);
}));
// Context menu
const providerContexts: ReadonlyArray<[IContextKey<boolean>, LanguageFeatureRegistry<unknown>]> = [
[EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider],
[EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider],
[EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider],
[EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider],
];
// Context menu (context key service created eagerly for keybinding preconditions,
// but resource context and provider contexts are initialized lazily on first use)
const scopedContextKeyService = store.add(parentContextKeyService.createScoped(widget));
chatAttachmentResourceContextKey.bindTo(scopedContextKeyService).set(attachment.value.uri.toString());
store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri));
let providerContexts: ReadonlyArray<[IContextKey<boolean>, LanguageFeatureRegistry<unknown>]> | undefined;
const ensureProviderContexts = () => {
if (!providerContexts) {
providerContexts = [
[EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider],
[EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider],
[EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider],
[EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider],
];
}
};
const updateContextKeys = async () => {
ensureProviderContexts();
const modelRef = await textModelService.createModelReference(attachment.value.uri);
try {
const model = modelRef.object.textEditorModel;
for (const [contextKey, registry] of providerContexts) {
for (const [contextKey, registry] of providerContexts!) {
contextKey.set(registry.has(model));
}
} finally {
modelRef.dispose();
}
};
store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, contextMenuId, attachment.value, updateContextKeys));
store.add(dom.addDisposableListener(widget, dom.EventType.CONTEXT_MENU, async domEvent => {
const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
dom.EventHelper.stop(domEvent, true);
try {
await updateContextKeys();
} catch (e) {
console.error(e);
}
contextMenuService.showContextMenu({
contextKeyService: scopedContextKeyService,
getAnchor: () => event,
getActions: () => {
const menu = menuService.getMenuActions(contextMenuId, scopedContextKeyService, { arg: attachment.value });
return getFlatContextMenuActions(menu);
},
});
}));
return store;
}

View File

@@ -23,7 +23,7 @@ import { getFlatContextMenuActions } from '../../../../../../platform/actions/br
import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js';
import { IClipboardService } from '../../../../../../platform/clipboard/common/clipboardService.js';
import { ICommandService } from '../../../../../../platform/commands/common/commands.js';
import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
import { IResourceStat } from '../../../../../../platform/dnd/browser/dnd.js';
import { ITextResourceEditorInput } from '../../../../../../platform/editor/common/editor.js';
@@ -125,8 +125,6 @@ export class InlineAnchorWidget extends Disposable {
public static readonly className = 'chat-inline-anchor-widget';
private readonly _chatResourceContext: IContextKey<string>;
readonly data: ContentRefData;
constructor(
@@ -158,9 +156,6 @@ export class InlineAnchorWidget extends Disposable {
? { kind: 'symbol', symbol: inlineReference.inlineReference }
: { uri: inlineReference.inlineReference };
const contextKeyService = this._register(originalContextKeyService.createScoped(element));
this._chatResourceContext = chatAttachmentResourceContextKey.bindTo(contextKeyService);
element.classList.add(InlineAnchorWidget.className, 'show-file-icons');
let iconText: Array<string | HTMLElement>;
@@ -168,7 +163,6 @@ export class InlineAnchorWidget extends Disposable {
let location: { readonly uri: URI; readonly range?: IRange };
let updateContextKeys: (() => Promise<void>) | undefined;
if (this.data.kind === 'symbol') {
const symbol = this.data.symbol;
@@ -176,7 +170,7 @@ export class InlineAnchorWidget extends Disposable {
iconText = [this.data.symbol.name];
iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(symbol.kind))];
this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, contextKeyService, { value: symbol.location, name: symbol.name, kind: symbol.kind }, MenuId.ChatInlineSymbolAnchorContext)));
this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, originalContextKeyService, { value: symbol.location, name: symbol.name, kind: symbol.kind }, MenuId.ChatInlineSymbolAnchorContext)));
} else {
location = this.data;
@@ -209,10 +203,10 @@ export class InlineAnchorWidget extends Disposable {
refreshIconClasses();
}));
const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService);
let isDirectory = false;
fileService.stat(location.uri)
.then(stat => {
isFolderContext.set(stat.isDirectory);
isDirectory = stat.isDirectory;
if (stat.isDirectory) {
fileKind = FileKind.FOLDER;
refreshIconClasses();
@@ -221,15 +215,20 @@ export class InlineAnchorWidget extends Disposable {
.catch(() => { });
// Context menu
const contextKeyService = this._register(originalContextKeyService.createScoped(element));
chatAttachmentResourceContextKey.bindTo(contextKeyService).set(location.uri.toString());
const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService);
let contextMenuInitialized = false;
this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, async domEvent => {
const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
dom.EventHelper.stop(domEvent, true);
try {
await updateContextKeys?.();
} catch (e) {
console.error(e);
if (!contextMenuInitialized) {
contextMenuInitialized = true;
const resourceContextKey = new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService);
resourceContextKey.set(location.uri);
}
isFolderContext.set(isDirectory);
if (this._store.isDisposed) {
return;
@@ -255,10 +254,6 @@ export class InlineAnchorWidget extends Disposable {
}
}
const resourceContextKey = new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService);
resourceContextKey.set(location.uri);
this._chatResourceContext.set(location.uri.toString());
const iconEl = dom.$('span.icon');
iconEl.classList.add(...iconClasses);
element.replaceChildren(iconEl, dom.$('span.icon-label', {}, ...iconText));