Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts
5263 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 * as dom from '../../../../../base/browser/dom.js';6import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';7import { localize2 } from '../../../../../nls.js';8import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';9import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';10import { katexContainerClassName, katexContainerLatexAttributeName } from '../../../markdown/common/markedKatexExtension.js';11import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';12import { IChatRequestViewModel, IChatResponseViewModel, isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';13import { ChatTreeItem, IChatWidgetService } from '../chat.js';14import { CHAT_CATEGORY, stringifyItem } from './chatActions.js';1516export function registerChatCopyActions() {17registerAction2(class CopyAllAction extends Action2 {18constructor() {19super({20id: 'workbench.action.chat.copyAll',21title: localize2('interactive.copyAll.label', "Copy All"),22f1: false,23category: CHAT_CATEGORY,24menu: {25id: MenuId.ChatContext,26when: ChatContextKeys.responseIsFiltered.negate(),27group: 'copy',28}29});30}3132run(accessor: ServicesAccessor, context?: ChatTreeItem) {33const clipboardService = accessor.get(IClipboardService);34const chatWidgetService = accessor.get(IChatWidgetService);35const widget = ((isRequestVM(context) || isResponseVM(context)) && chatWidgetService.getWidgetBySessionResource(context.sessionResource)) || chatWidgetService.lastFocusedWidget;36if (widget) {37const viewModel = widget.viewModel;38const sessionAsText = viewModel?.getItems()39.filter((item): item is (IChatRequestViewModel | IChatResponseViewModel) => isRequestVM(item) || (isResponseVM(item) && !item.errorDetails?.responseIsFiltered))40.map(item => stringifyItem(item))41.join('\n\n');42if (sessionAsText) {43clipboardService.writeText(sessionAsText);44}45}46}47});4849registerAction2(class CopyItemAction extends Action2 {50constructor() {51super({52id: 'workbench.action.chat.copyItem',53title: localize2('interactive.copyItem.label', "Copy"),54f1: false,55category: CHAT_CATEGORY,56menu: {57id: MenuId.ChatContext,58when: ChatContextKeys.responseIsFiltered.negate(),59group: 'copy',60}61});62}6364async run(accessor: ServicesAccessor, ...args: unknown[]) {65const chatWidgetService = accessor.get(IChatWidgetService);66const clipboardService = accessor.get(IClipboardService);6768const widget = chatWidgetService.lastFocusedWidget;69let item = args[0] as ChatTreeItem | undefined;70if (!isChatTreeItem(item)) {71item = widget?.getFocus();72if (!item) {73return;74}75}7677// If there is a text selection, and focus is inside the widget, copy the selected text.78// Otherwise, context menu with no selection -> copy the full item79const nativeSelection = dom.getActiveWindow().getSelection();80const selectedText = nativeSelection?.toString();81if (widget && selectedText && selectedText.length > 0 && dom.isAncestor(dom.getActiveElement(), widget.domNode)) {82await clipboardService.writeText(selectedText);83return;84}8586if (!isRequestVM(item) && !isResponseVM(item)) {87return;88}8990const text = stringifyItem(item, false);91await clipboardService.writeText(text);92}93});9495registerAction2(class CopyKatexMathSourceAction extends Action2 {96constructor() {97super({98id: 'workbench.action.chat.copyKatexMathSource',99title: localize2('chat.copyKatexMathSource.label', "Copy Math Source"),100f1: false,101category: CHAT_CATEGORY,102menu: {103id: MenuId.ChatContext,104group: 'copy',105when: ChatContextKeys.isKatexMathElement,106}107});108}109110async run(accessor: ServicesAccessor, ...args: unknown[]) {111const chatWidgetService = accessor.get(IChatWidgetService);112const clipboardService = accessor.get(IClipboardService);113114const widget = chatWidgetService.lastFocusedWidget;115let item = args[0] as ChatTreeItem | undefined;116if (!isChatTreeItem(item)) {117item = widget?.getFocus();118if (!item) {119return;120}121}122123// Try to find a KaTeX element from the selection or active element124let selectedElement: Node | null = null;125126// If there is a selection, and focus is inside the widget, extract the inner KaTeX element.127const activeElement = dom.getActiveElement();128const nativeSelection = dom.getActiveWindow().getSelection();129if (widget && nativeSelection && nativeSelection.rangeCount > 0 && dom.isAncestor(activeElement, widget.domNode)) {130const range = nativeSelection.getRangeAt(0);131selectedElement = range.commonAncestorContainer;132133// If it's a text node, get its parent element134if (selectedElement.nodeType === Node.TEXT_NODE) {135selectedElement = selectedElement.parentElement;136}137}138139// Otherwise, fallback to querying from the active element140if (!selectedElement) {141// eslint-disable-next-line no-restricted-syntax142selectedElement = activeElement?.querySelector(`.${katexContainerClassName}`) ?? null;143}144145// Extract the LaTeX source from the annotation element146const katexElement = dom.isHTMLElement(selectedElement) ? selectedElement.closest(`.${katexContainerClassName}`) : null;147const latexSource = katexElement?.getAttribute(katexContainerLatexAttributeName) || '';148if (latexSource) {149await clipboardService.writeText(latexSource);150}151}152});153}154155156