Path: blob/main/src/vs/workbench/contrib/debug/browser/repl.ts
5237 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 * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js';7import { IHistoryNavigationWidget } from '../../../../base/browser/history.js';8import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';9import * as aria from '../../../../base/browser/ui/aria/aria.js';10import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';11import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';12import { IAction } from '../../../../base/common/actions.js';13import { RunOnceScheduler, timeout } from '../../../../base/common/async.js';14import { CancellationToken } from '../../../../base/common/cancellation.js';15import { Codicon } from '../../../../base/common/codicons.js';16import { memoize } from '../../../../base/common/decorators.js';17import { Emitter } from '../../../../base/common/event.js';18import { FuzzyScore } from '../../../../base/common/filters.js';19import { HistoryNavigator } from '../../../../base/common/history.js';20import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';21import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';22import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';23import { ThemeIcon } from '../../../../base/common/themables.js';24import { URI as uri } from '../../../../base/common/uri.js';25import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js';26import { EditorAction, registerEditorAction } from '../../../../editor/browser/editorExtensions.js';27import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';28import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';29import { EditorOption } from '../../../../editor/common/config/editorOptions.js';30import { EDITOR_FONT_DEFAULTS } from '../../../../editor/common/config/fontInfo.js';31import { Position } from '../../../../editor/common/core/position.js';32import { Range } from '../../../../editor/common/core/range.js';33import { IDecorationOptions } from '../../../../editor/common/editorCommon.js';34import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';35import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemKinds, CompletionList } from '../../../../editor/common/languages.js';36import { ITextModel } from '../../../../editor/common/model.js';37import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';38import { IModelService } from '../../../../editor/common/services/model.js';39import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';40import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';41import { localize, localize2 } from '../../../../nls.js';42import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';43import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';44import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';45import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';46import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';47import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';48import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';49import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';50import { IHoverService } from '../../../../platform/hover/browser/hover.js';51import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';52import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';53import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';54import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';55import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';56import { ILogService } from '../../../../platform/log/common/log.js';57import { IOpenerService } from '../../../../platform/opener/common/opener.js';58import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';59import { editorForeground, resolveColorValue } from '../../../../platform/theme/common/colorRegistry.js';60import { IThemeService } from '../../../../platform/theme/common/themeService.js';61import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';62import { FilterViewPane, IViewPaneOptions, ViewAction } from '../../../browser/parts/views/viewPane.js';63import { IViewDescriptorService } from '../../../common/views.js';64import { IEditorService } from '../../../services/editor/common/editorService.js';65import { IViewsService } from '../../../services/views/common/viewsService.js';66import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';67import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';68import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';69import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, IDebugConfiguration, IDebugService, IDebugSession, IReplConfiguration, IReplElement, IReplOptions, REPL_VIEW_ID, State, getStateLabel } from '../common/debug.js';70import { Variable } from '../common/debugModel.js';71import { resolveChildSession } from '../common/debugUtils.js';72import { ReplEvaluationResult, ReplGroup } from '../common/replModel.js';73import { FocusSessionActionViewItem } from './debugActionViewItems.js';74import { DEBUG_COMMAND_CATEGORY, FOCUS_REPL_ID } from './debugCommands.js';75import { DebugExpressionRenderer } from './debugExpressionRenderer.js';76import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js';77import './media/repl.css';78import { ReplFilter } from './replFilter.js';79import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js';8081const $ = dom.$;8283const HISTORY_STORAGE_KEY = 'debug.repl.history';84const FILTER_HISTORY_STORAGE_KEY = 'debug.repl.filterHistory';85const FILTER_VALUE_STORAGE_KEY = 'debug.repl.filterValue';86const DECORATION_KEY = 'replinputdecoration';8788function revealLastElement(tree: WorkbenchAsyncDataTree<any, any, any>) {89tree.scrollTop = tree.scrollHeight - tree.renderHeight;90// tree.scrollTop = 1e6;91}9293const sessionsToIgnore = new Set<IDebugSession>();94const identityProvider = { getId: (element: IReplElement) => element.getId() };9596export class Repl extends FilterViewPane implements IHistoryNavigationWidget {97declare readonly _serviceBrand: undefined;9899private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show100private static readonly URI = uri.parse(`${DEBUG_SCHEME}:replinput`);101102private history: HistoryNavigator<string>;103private tree?: WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>;104private replOptions: ReplOptions;105private previousTreeScrollHeight: number = 0;106private replDelegate!: ReplDelegate;107private container!: HTMLElement;108private treeContainer!: HTMLElement;109private replInput!: CodeEditorWidget;110private replInputContainer!: HTMLElement;111private bodyContentDimension: dom.Dimension | undefined;112private model: ITextModel | undefined;113private setHistoryNavigationEnablement!: (enabled: boolean) => void;114private scopedInstantiationService!: IInstantiationService;115private replElementsChangeListener: IDisposable | undefined;116private styleElement: HTMLStyleElement | undefined;117private styleChangedWhenInvisible: boolean = false;118private completionItemProvider: IDisposable | undefined;119private modelChangeListener: IDisposable = Disposable.None;120private filter: ReplFilter;121private multiSessionRepl: IContextKey<boolean>;122private menu: IMenu;123private replDataSource: IAsyncDataSource<IDebugSession, IReplElement> | undefined;124private findIsOpen: boolean = false;125126constructor(127options: IViewPaneOptions,128@IDebugService private readonly debugService: IDebugService,129@IInstantiationService instantiationService: IInstantiationService,130@IStorageService private readonly storageService: IStorageService,131@IThemeService themeService: IThemeService,132@IModelService private readonly modelService: IModelService,133@IContextKeyService contextKeyService: IContextKeyService,134@ICodeEditorService codeEditorService: ICodeEditorService,135@IViewDescriptorService viewDescriptorService: IViewDescriptorService,136@IContextMenuService contextMenuService: IContextMenuService,137@IConfigurationService protected override readonly configurationService: IConfigurationService,138@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,139@IEditorService private readonly editorService: IEditorService,140@IKeybindingService protected override readonly keybindingService: IKeybindingService,141@IOpenerService openerService: IOpenerService,142@IHoverService hoverService: IHoverService,143@IMenuService menuService: IMenuService,144@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,145@ILogService private readonly logService: ILogService,146) {147const filterText = storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, '');148super({149...options,150filterOptions: {151placeholder: localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude, \\escape)"),152text: filterText,153history: JSON.parse(storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[],154}155}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);156157this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService);158this._register(this.menu);159this.history = this._register(new HistoryNavigator(new Set(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]'))), 100));160this.filter = new ReplFilter();161this.filter.filterQuery = filterText;162this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService);163this.replOptions = this._register(this.instantiationService.createInstance(ReplOptions, this.id, () => this.getLocationBasedColors().background));164this._register(this.replOptions.onDidChange(() => this.onDidStyleChange()));165166this._register(codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {}));167this.multiSessionRepl.set(this.isMultiSessionView);168this.registerListeners();169}170171private registerListeners(): void {172if (this.debugService.getViewModel().focusedSession) {173this.onDidFocusSession(this.debugService.getViewModel().focusedSession);174}175176this._register(this.debugService.getViewModel().onDidFocusSession(session => {177this.onDidFocusSession(session);178}));179this._register(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => {180if (e instanceof Variable && this.tree?.hasNode(e)) {181await this.tree.updateChildren(e, false, true);182await this.tree.expand(e);183}184}));185this._register(this.debugService.onWillNewSession(async newSession => {186// Need to listen to output events for sessions which are not yet fully initialised187const input = this.tree?.getInput();188if (!input || input.state === State.Inactive) {189await this.selectSession(newSession);190}191this.multiSessionRepl.set(this.isMultiSessionView);192}));193this._register(this.debugService.onDidEndSession(async () => {194// Update view, since orphaned sessions might now be separate195await Promise.resolve(); // allow other listeners to go first, so sessions can update parents196this.multiSessionRepl.set(this.isMultiSessionView);197}));198this._register(this.themeService.onDidColorThemeChange(() => {199this.refreshReplElements(false);200if (this.isVisible()) {201this.updateInputDecoration();202}203}));204this._register(this.onDidChangeBodyVisibility(visible => {205if (!visible) {206return;207}208if (!this.model) {209this.model = this.modelService.getModel(Repl.URI) || this.modelService.createModel('', null, Repl.URI, true);210}211212const focusedSession = this.debugService.getViewModel().focusedSession;213if (this.tree && this.tree.getInput() !== focusedSession) {214this.onDidFocusSession(focusedSession);215}216217this.setMode();218this.replInput.setModel(this.model);219this.updateInputDecoration();220this.refreshReplElements(true);221222if (this.styleChangedWhenInvisible) {223this.styleChangedWhenInvisible = false;224this.tree?.updateChildren(undefined, true, false);225this.onDidStyleChange();226}227}));228this._register(this.configurationService.onDidChangeConfiguration(e => {229if (e.affectsConfiguration('debug.console.wordWrap') && this.tree) {230this.tree.dispose();231this.treeContainer.innerText = '';232dom.clearNode(this.treeContainer);233this.createReplTree();234}235if (e.affectsConfiguration('debug.console.acceptSuggestionOnEnter')) {236const config = this.configurationService.getValue<IDebugConfiguration>('debug');237this.replInput.updateOptions({238acceptSuggestionOnEnter: config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off'239});240}241}));242243this._register(this.editorService.onDidActiveEditorChange(() => {244this.setMode();245}));246247this._register(this.filterWidget.onDidChangeFilterText(() => {248this.filter.filterQuery = this.filterWidget.getFilterText();249if (this.tree) {250this.tree.refilter();251revealLastElement(this.tree);252}253}));254}255256private async onDidFocusSession(session: IDebugSession | undefined): Promise<void> {257if (session) {258sessionsToIgnore.delete(session);259this.completionItemProvider?.dispose();260if (session.capabilities.supportsCompletionsRequest) {261this.completionItemProvider = this.languageFeaturesService.completionProvider.register({ scheme: DEBUG_SCHEME, pattern: '**/replinput', hasAccessToAllModels: true }, {262_debugDisplayName: 'debugConsole',263triggerCharacters: session.capabilities.completionTriggerCharacters || ['.'],264provideCompletionItems: async (_: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => {265// Disable history navigation because up and down are used to navigate through the suggest widget266this.setHistoryNavigationEnablement(false);267268const model = this.replInput.getModel();269if (model) {270const text = model.getValue();271const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;272const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined;273const response = await session.completions(frameId, focusedStackFrame?.thread.threadId || 0, text, position, token);274275const suggestions: CompletionItem[] = [];276const computeRange = (length: number) => Range.fromPositions(position.delta(0, -length), position);277if (response && response.body && response.body.targets) {278response.body.targets.forEach(item => {279if (item && item.label) {280let insertTextRules: CompletionItemInsertTextRule | undefined = undefined;281let insertText = item.text || item.label;282if (typeof item.selectionStart === 'number') {283// If a debug completion item sets a selection we need to use snippets to make sure the selection is selected #90974284insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;285const selectionLength = typeof item.selectionLength === 'number' ? item.selectionLength : 0;286const placeholder = selectionLength > 0 ? '${1:' + insertText.substring(item.selectionStart, item.selectionStart + selectionLength) + '}$0' : '$0';287insertText = insertText.substring(0, item.selectionStart) + placeholder + insertText.substring(item.selectionStart + selectionLength);288}289290suggestions.push({291label: item.label,292insertText,293detail: item.detail,294kind: CompletionItemKinds.fromString(item.type || 'property'),295filterText: (item.start && item.length) ? text.substring(item.start, item.start + item.length).concat(item.label) : undefined,296range: computeRange(item.length || 0),297sortText: item.sortText,298insertTextRules299});300}301});302}303304if (this.configurationService.getValue<IDebugConfiguration>('debug').console.historySuggestions) {305const history = this.history.getHistory();306const idxLength = String(history.length).length;307history.forEach((h, i) => suggestions.push({308label: h,309insertText: h,310kind: CompletionItemKind.Text,311range: computeRange(h.length),312sortText: 'ZZZ' + String(history.length - i).padStart(idxLength, '0')313}));314}315316return { suggestions };317}318319return Promise.resolve({ suggestions: [] });320}321});322}323}324325await this.selectSession();326}327328getFilterStats(): { total: number; filtered: number } {329// This could be called before the tree is created when setting this.filterState.filterText value330return {331total: this.tree?.getNode().children.length ?? 0,332filtered: this.tree?.getNode().children.filter(c => c.visible).length ?? 0333};334}335336get isReadonly(): boolean {337// Do not allow to edit inactive sessions338const session = this.tree?.getInput();339if (session && session.state !== State.Inactive) {340return false;341}342343return true;344}345346showPreviousValue(): void {347if (!this.isReadonly) {348this.navigateHistory(true);349}350}351352showNextValue(): void {353if (!this.isReadonly) {354this.navigateHistory(false);355}356}357358focusFilter(): void {359this.filterWidget.focus();360}361362openFind(): void {363this.tree?.openFind();364}365366private setMode(): void {367if (!this.isVisible()) {368return;369}370371const activeEditorControl = this.editorService.activeTextEditorControl;372if (isCodeEditor(activeEditorControl)) {373this.modelChangeListener.dispose();374this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode());375if (this.model && activeEditorControl.hasModel()) {376this.model.setLanguage(activeEditorControl.getModel().getLanguageId());377}378}379}380381private onDidStyleChange(): void {382if (!this.isVisible()) {383this.styleChangedWhenInvisible = true;384return;385}386if (this.styleElement) {387this.replInput.updateOptions({388fontSize: this.replOptions.replConfiguration.fontSize,389lineHeight: this.replOptions.replConfiguration.lineHeight,390fontFamily: this.replOptions.replConfiguration.fontFamily === 'default' ? EDITOR_FONT_DEFAULTS.fontFamily : this.replOptions.replConfiguration.fontFamily391});392393const replInputLineHeight = this.replInput.getOption(EditorOption.lineHeight);394395// Set the font size, font family, line height and align the twistie to be centered, and input theme color396this.styleElement.textContent = `397.repl .repl-input-wrapper .repl-input-chevron {398line-height: ${replInputLineHeight}px399}400401.repl .repl-input-wrapper .monaco-editor .lines-content {402background-color: ${this.replOptions.replConfiguration.backgroundColor};403}404`;405const cssFontFamily = this.replOptions.replConfiguration.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : this.replOptions.replConfiguration.fontFamily;406this.container.style.setProperty(`--vscode-repl-font-family`, cssFontFamily);407this.container.style.setProperty(`--vscode-repl-font-size`, `${this.replOptions.replConfiguration.fontSize}px`);408this.container.style.setProperty(`--vscode-repl-font-size-for-twistie`, `${this.replOptions.replConfiguration.fontSizeForTwistie}px`);409this.container.style.setProperty(`--vscode-repl-line-height`, this.replOptions.replConfiguration.cssLineHeight);410411this.tree?.rerender();412413if (this.bodyContentDimension) {414this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);415}416}417}418419private navigateHistory(previous: boolean): void {420const historyInput = (previous ?421(this.history.previous() ?? this.history.first()) : this.history.next())422?? '';423this.replInput.setValue(historyInput);424aria.status(historyInput);425// always leave cursor at the end.426this.replInput.setPosition({ lineNumber: 1, column: historyInput.length + 1 });427this.setHistoryNavigationEnablement(true);428}429430async selectSession(session?: IDebugSession): Promise<void> {431const treeInput = this.tree?.getInput();432if (!session) {433const focusedSession = this.debugService.getViewModel().focusedSession;434// If there is a focusedSession focus on that one, otherwise just show any other not ignored session435if (focusedSession) {436session = focusedSession;437} else if (!treeInput || sessionsToIgnore.has(treeInput)) {438session = this.debugService.getModel().getSessions(true).find(s => !sessionsToIgnore.has(s));439}440}441if (session) {442this.replElementsChangeListener?.dispose();443this.replElementsChangeListener = session.onDidChangeReplElements(() => {444this.refreshReplElements(session.getReplElements().length === 0);445});446447if (this.tree && treeInput !== session) {448try {449await this.tree.setInput(session);450} catch (err) {451// Ignore error because this may happen multiple times while refreshing,452// then changing the root may fail. Log to help with debugging if needed.453this.logService.error(err);454}455revealLastElement(this.tree);456}457}458459this.replInput?.updateOptions({ readOnly: this.isReadonly });460this.updateInputDecoration();461}462463async clearRepl(): Promise<void> {464const session = this.tree?.getInput();465if (session) {466session.removeReplExpressions();467if (session.state === State.Inactive) {468// Ignore inactive sessions which got cleared - so they are not shown any more469sessionsToIgnore.add(session);470await this.selectSession();471this.multiSessionRepl.set(this.isMultiSessionView);472}473}474this.replInput.focus();475}476477acceptReplInput(): void {478const session = this.tree?.getInput();479if (session && !this.isReadonly) {480session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, this.replInput.getValue());481revealLastElement(this.tree!);482this.history.add(this.replInput.getValue());483this.replInput.setValue('');484if (this.bodyContentDimension) {485// Trigger a layout to shrink a potential multi line input486this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);487}488}489}490491sendReplInput(input: string): void {492const session = this.tree?.getInput();493if (session && !this.isReadonly) {494session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, input);495revealLastElement(this.tree!);496this.history.add(input);497}498}499500getVisibleContent(): string {501let text = '';502if (this.model && this.tree) {503const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri);504const traverseAndAppend = (node: ITreeNode<IReplElement, FuzzyScore>) => {505node.children.forEach(child => {506if (child.visible) {507text += child.element.toString().trimRight() + lineDelimiter;508if (!child.collapsed && child.children.length) {509traverseAndAppend(child);510}511}512});513};514traverseAndAppend(this.tree.getNode());515}516517return removeAnsiEscapeCodes(text);518}519520protected layoutBodyContent(height: number, width: number): void {521this.bodyContentDimension = new dom.Dimension(width, height);522const replInputHeight = Math.min(this.replInput.getContentHeight(), height);523if (this.tree) {524const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;525const treeHeight = height - replInputHeight;526this.tree.getHTMLElement().style.height = `${treeHeight}px`;527this.tree.layout(treeHeight, width);528if (lastElementVisible) {529revealLastElement(this.tree);530}531}532this.replInputContainer.style.height = `${replInputHeight}px`;533534this.replInput.layout({ width: width - 30, height: replInputHeight });535}536537collapseAll(): void {538this.tree?.collapseAll();539}540541getDebugSession(): IDebugSession | undefined {542return this.tree?.getInput();543}544545getReplInput(): CodeEditorWidget {546return this.replInput;547}548549getReplDataSource(): IAsyncDataSource<IDebugSession, IReplElement> | undefined {550return this.replDataSource;551}552553getFocusedElement(): IReplElement | undefined {554return this.tree?.getFocus()?.[0];555}556557focusTree(): void {558this.tree?.domFocus();559}560561override async focus(): Promise<void> {562super.focus();563await timeout(0); // wait a task for the repl to get attached to the DOM, #83387564this.replInput.focus();565}566567override createActionViewItem(action: IAction): IActionViewItem | undefined {568if (action.id === selectReplCommandId) {569const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession;570return this.instantiationService.createInstance(SelectReplActionViewItem, action, session);571}572573return super.createActionViewItem(action);574}575576private get isMultiSessionView(): boolean {577return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1;578}579580// --- Cached locals581582@memoize583private get refreshScheduler(): RunOnceScheduler {584const autoExpanded = new Set<string>();585return new RunOnceScheduler(async () => {586if (!this.tree || !this.tree.getInput() || !this.isVisible()) {587return;588}589590await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider });591592const session = this.tree.getInput();593if (session) {594// Automatically expand repl group elements when specified595const autoExpandElements = async (elements: IReplElement[]) => {596for (const element of elements) {597if (element instanceof ReplGroup) {598if (element.autoExpand && !autoExpanded.has(element.getId())) {599autoExpanded.add(element.getId());600await this.tree!.expand(element);601}602if (!this.tree!.isCollapsed(element)) {603// Repl groups can have children which are repl groups thus we might need to expand those as well604await autoExpandElements(element.getChildren());605}606}607}608};609await autoExpandElements(session.getReplElements());610}611// Repl elements count changed, need to update filter stats on the badge612const { total, filtered } = this.getFilterStats();613this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : localize('showing filtered repl lines', "Showing {0} of {1}", filtered, total));614}, Repl.REFRESH_DELAY);615}616617// --- Creation618619override render(): void {620super.render();621this._register(registerNavigableContainer({622name: 'repl',623focusNotifiers: [this, this.filterWidget],624focusNextWidget: () => {625const element = this.tree?.getHTMLElement();626if (this.filterWidget.hasFocus()) {627this.tree?.domFocus();628} else if (element && dom.isActiveElement(element)) {629this.focus();630}631},632focusPreviousWidget: () => {633const element = this.tree?.getHTMLElement();634if (this.replInput.hasTextFocus()) {635this.tree?.domFocus();636} else if (element && dom.isActiveElement(element)) {637this.focusFilter();638}639}640}));641}642643protected override renderBody(parent: HTMLElement): void {644super.renderBody(parent);645this.container = dom.append(parent, $('.repl'));646this.treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`));647this.createReplInput(this.container);648this.createReplTree();649}650651private createReplTree(): void {652this.replDelegate = new ReplDelegate(this.configurationService, this.replOptions);653const wordWrap = this.configurationService.getValue<IDebugConfiguration>('debug').console.wordWrap;654this.treeContainer.classList.toggle('word-wrap', wordWrap);655const expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer);656this.replDataSource = new ReplDataSource();657658const tree = this.tree = this.instantiationService.createInstance(659WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>,660'DebugRepl',661this.treeContainer,662this.replDelegate,663[664this.instantiationService.createInstance(ReplVariablesRenderer, expressionRenderer),665this.instantiationService.createInstance(ReplOutputElementRenderer, expressionRenderer),666new ReplEvaluationInputsRenderer(),667this.instantiationService.createInstance(ReplGroupRenderer, expressionRenderer),668new ReplEvaluationResultsRenderer(expressionRenderer),669new ReplRawObjectsRenderer(expressionRenderer),670],671this.replDataSource,672{673filter: this.filter,674accessibilityProvider: new ReplAccessibilityProvider(),675identityProvider,676userSelection: true,677mouseSupport: false,678findWidgetEnabled: true,679keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e.toString(true) },680horizontalScrolling: !wordWrap,681setRowLineHeight: false,682supportDynamicHeights: wordWrap,683overrideStyles: this.getLocationBasedColors().listOverrideStyles684});685686this._register(tree.onDidChangeContentHeight(() => {687if (tree.scrollHeight !== this.previousTreeScrollHeight) {688// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.689// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.690const lastElementWasVisible = tree.scrollTop + tree.renderHeight >= this.previousTreeScrollHeight - 2;691if (lastElementWasVisible) {692setTimeout(() => {693// Can't set scrollTop during this event listener, the list might overwrite the change694revealLastElement(tree);695}, 0);696}697}698699this.previousTreeScrollHeight = tree.scrollHeight;700}));701702this._register(tree.onContextMenu(e => this.onContextMenu(e)));703this._register(tree.onDidChangeFindOpenState((open) => this.findIsOpen = open));704705let lastSelectedString: string;706this._register(tree.onMouseClick(() => {707if (this.findIsOpen) {708return;709}710const selection = dom.getWindow(this.treeContainer).getSelection();711if (!selection || selection.type !== 'Range' || lastSelectedString === selection.toString()) {712// only focus the input if the user is not currently selecting and find isn't open.713this.replInput.focus();714}715lastSelectedString = selection ? selection.toString() : '';716}));717// Make sure to select the session if debugging is already active718this.selectSession();719this.styleElement = domStylesheetsJs.createStyleSheet(this.container);720this.onDidStyleChange();721}722723private createReplInput(container: HTMLElement): void {724this.replInputContainer = dom.append(container, $('.repl-input-wrapper'));725dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt)));726727const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(this.scopedContextKeyService, this));728this.setHistoryNavigationEnablement = enabled => {729historyNavigationBackwardsEnablement.set(enabled);730historyNavigationForwardsEnablement.set(enabled);731};732CONTEXT_IN_DEBUG_REPL.bindTo(this.scopedContextKeyService).set(true);733734this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));735const options = getSimpleEditorOptions(this.configurationService);736options.readOnly = true;737options.suggest = { showStatusBar: true };738const config = this.configurationService.getValue<IDebugConfiguration>('debug');739options.acceptSuggestionOnEnter = config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off';740options.ariaLabel = this.getAriaLabel();741742this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions());743744let lastContentHeight = -1;745this._register(this.replInput.onDidChangeModelContent(() => {746const model = this.replInput.getModel();747this.setHistoryNavigationEnablement(!!model && model.getValue() === '');748749const contentHeight = this.replInput.getContentHeight();750if (contentHeight !== lastContentHeight) {751lastContentHeight = contentHeight;752if (this.bodyContentDimension) {753this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);754}755}756}));757// We add the input decoration only when the focus is in the input #61126758this._register(this.replInput.onDidFocusEditorText(() => this.updateInputDecoration()));759this._register(this.replInput.onDidBlurEditorText(() => this.updateInputDecoration()));760761this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.FOCUS, () => this.replInputContainer.classList.add('synthetic-focus')));762this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.BLUR, () => this.replInputContainer.classList.remove('synthetic-focus')));763}764765private getAriaLabel(): string {766let ariaLabel = localize('debugConsole', "Debug Console");767if (!this.configurationService.getValue(AccessibilityVerbositySettingId.Debug)) {768return ariaLabel;769}770const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel();771if (keybinding) {772ariaLabel = localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding);773} else {774ariaLabel = localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel);775}776777return ariaLabel;778}779780private onContextMenu(e: ITreeContextMenuEvent<IReplElement>): void {781const actions = getFlatContextMenuActions(this.menu.getActions({ arg: e.element, shouldForwardArgs: false }));782this.contextMenuService.showContextMenu({783getAnchor: () => e.anchor,784getActions: () => actions,785getActionsContext: () => e.element786});787}788789// --- Update790791private refreshReplElements(noDelay: boolean): void {792if (this.tree && this.isVisible()) {793if (this.refreshScheduler.isScheduled()) {794return;795}796797this.refreshScheduler.schedule(noDelay ? 0 : undefined);798}799}800801private updateInputDecoration(): void {802if (!this.replInput) {803return;804}805806const decorations: IDecorationOptions[] = [];807if (this.isReadonly && this.replInput.hasTextFocus() && !this.replInput.getValue()) {808const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4);809decorations.push({810range: {811startLineNumber: 0,812endLineNumber: 0,813startColumn: 0,814endColumn: 1815},816renderOptions: {817after: {818contentText: localize('startDebugFirst', "Please start a debug session to evaluate expressions"),819color: transparentForeground ? transparentForeground.toString() : undefined820}821}822});823}824825this.replInput.setDecorationsByType('repl-decoration', DECORATION_KEY, decorations);826}827828override saveState(): void {829const replHistory = this.history.getHistory();830if (replHistory.length) {831this.storageService.store(HISTORY_STORAGE_KEY, JSON.stringify(replHistory), StorageScope.WORKSPACE, StorageTarget.MACHINE);832} else {833this.storageService.remove(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);834}835const filterHistory = this.filterWidget.getHistory();836if (filterHistory.length) {837this.storageService.store(FILTER_HISTORY_STORAGE_KEY, JSON.stringify(filterHistory), StorageScope.WORKSPACE, StorageTarget.MACHINE);838} else {839this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);840}841const filterValue = this.filterWidget.getFilterText();842if (filterValue) {843this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.MACHINE);844} else {845this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE);846}847848super.saveState();849}850851override dispose(): void {852this.replInput?.dispose(); // Disposed before rendered? #174558853this.replElementsChangeListener?.dispose();854this.refreshScheduler.dispose();855this.modelChangeListener.dispose();856super.dispose();857}858}859860class ReplOptions extends Disposable implements IReplOptions {861private static readonly lineHeightEm = 1.4;862863private readonly _onDidChange = this._register(new Emitter<void>());864readonly onDidChange = this._onDidChange.event;865866private _replConfig!: IReplConfiguration;867public get replConfiguration(): IReplConfiguration {868return this._replConfig;869}870871constructor(872viewId: string,873private readonly backgroundColorDelegate: () => string,874@IConfigurationService private readonly configurationService: IConfigurationService,875@IThemeService private readonly themeService: IThemeService,876@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService877) {878super();879880this._register(this.themeService.onDidColorThemeChange(e => this.update()));881this._register(this.viewDescriptorService.onDidChangeLocation(e => {882if (e.views.some(v => v.id === viewId)) {883this.update();884}885}));886this._register(this.configurationService.onDidChangeConfiguration(e => {887if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) {888this.update();889}890}));891this.update();892}893894private update() {895const debugConsole = this.configurationService.getValue<IDebugConfiguration>('debug').console;896this._replConfig = {897fontSize: debugConsole.fontSize,898fontFamily: debugConsole.fontFamily,899lineHeight: debugConsole.lineHeight ? debugConsole.lineHeight : ReplOptions.lineHeightEm * debugConsole.fontSize,900cssLineHeight: debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : `${ReplOptions.lineHeightEm}em`,901backgroundColor: this.themeService.getColorTheme().getColor(this.backgroundColorDelegate()),902fontSizeForTwistie: debugConsole.fontSize * ReplOptions.lineHeightEm / 2 - 8903};904this._onDidChange.fire();905}906}907908// Repl actions and commands909910class AcceptReplInputAction extends EditorAction {911912constructor() {913super({914id: 'repl.action.acceptInput',915label: localize2({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "Debug Console: Accept Input"),916precondition: CONTEXT_IN_DEBUG_REPL,917kbOpts: {918kbExpr: EditorContextKeys.textInputFocus,919primary: KeyCode.Enter,920weight: KeybindingWeight.EditorContrib921}922});923}924925run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {926SuggestController.get(editor)?.cancelSuggestWidget();927const repl = getReplView(accessor.get(IViewsService));928repl?.acceptReplInput();929}930}931932class FilterReplAction extends ViewAction<Repl> {933934constructor() {935super({936viewId: REPL_VIEW_ID,937id: 'repl.action.filter',938title: localize('repl.action.filter', "Debug Console: Focus Filter"),939precondition: CONTEXT_IN_DEBUG_REPL,940keybinding: [{941when: EditorContextKeys.textInputFocus,942primary: KeyMod.CtrlCmd | KeyCode.KeyF,943weight: KeybindingWeight.EditorContrib944}]945});946}947948runInView(accessor: ServicesAccessor, repl: Repl): void | Promise<void> {949repl.focusFilter();950}951}952953954class FindReplAction extends ViewAction<Repl> {955956constructor() {957super({958viewId: REPL_VIEW_ID,959id: 'repl.action.find',960title: localize('repl.action.find', "Debug Console: Focus Find"),961precondition: CONTEXT_IN_DEBUG_REPL,962keybinding: [{963when: ContextKeyExpr.or(CONTEXT_IN_DEBUG_REPL, ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')),964primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF,965weight: KeybindingWeight.EditorContrib966}],967icon: Codicon.search,968menu: [{969id: MenuId.ViewTitle,970group: 'navigation',971when: ContextKeyExpr.equals('view', REPL_VIEW_ID),972order: 15973}, {974id: MenuId.DebugConsoleContext,975group: 'z_commands',976order: 25977}],978});979}980981runInView(accessor: ServicesAccessor, view: Repl): void | Promise<void> {982view.openFind();983}984}985986class ReplCopyAllAction extends EditorAction {987988constructor() {989super({990id: 'repl.action.copyAll',991label: localize('actions.repl.copyAll', "Debug: Console Copy All"),992alias: 'Debug Console Copy All',993precondition: CONTEXT_IN_DEBUG_REPL,994});995}996997run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {998const clipboardService = accessor.get(IClipboardService);999const repl = getReplView(accessor.get(IViewsService));1000if (repl) {1001return clipboardService.writeText(repl.getVisibleContent());1002}1003}1004}10051006registerEditorAction(AcceptReplInputAction);1007registerEditorAction(ReplCopyAllAction);1008registerAction2(FilterReplAction);1009registerAction2(FindReplAction);10101011class SelectReplActionViewItem extends FocusSessionActionViewItem {10121013protected override getSessions(): ReadonlyArray<IDebugSession> {1014return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s));1015}10161017protected override mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession {1018while (focusedSession.parentSession && !focusedSession.hasSeparateRepl()) {1019focusedSession = focusedSession.parentSession;1020}1021return focusedSession;1022}1023}10241025export function getReplView(viewsService: IViewsService): Repl | undefined {1026return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined;1027}10281029const selectReplCommandId = 'workbench.action.debug.selectRepl';1030registerAction2(class extends ViewAction<Repl> {1031constructor() {1032super({1033id: selectReplCommandId,1034viewId: REPL_VIEW_ID,1035title: localize('selectRepl', "Select Debug Console"),1036f1: false,1037menu: {1038id: MenuId.ViewTitle,1039group: 'navigation',1040when: ContextKeyExpr.and(ContextKeyExpr.equals('view', REPL_VIEW_ID), CONTEXT_MULTI_SESSION_REPL),1041order: 201042}1043});1044}10451046async runInView(accessor: ServicesAccessor, view: Repl, session: IDebugSession | undefined) {1047const debugService = accessor.get(IDebugService);1048// If session is already the focused session we need to manualy update the tree since view model will not send a focused change event1049if (session && session.state !== State.Inactive && session !== debugService.getViewModel().focusedSession) {1050session = resolveChildSession(session, debugService.getModel().getSessions());1051await debugService.focusStackFrame(undefined, undefined, session, { explicit: true });1052}1053// Need to select the session in the view since the focussed session might not have changed1054await view.selectSession(session);1055}1056});10571058registerAction2(class extends ViewAction<Repl> {1059constructor() {1060super({1061id: 'workbench.debug.panel.action.clearReplAction',1062viewId: REPL_VIEW_ID,1063title: localize2('clearRepl', 'Clear Console'),1064metadata: {1065description: localize2('clearRepl.descriotion', 'Clears all program output from your debug REPL')1066},1067f1: true,1068icon: debugConsoleClearAll,1069menu: [{1070id: MenuId.ViewTitle,1071group: 'navigation',1072when: ContextKeyExpr.equals('view', REPL_VIEW_ID),1073order: 301074}, {1075id: MenuId.DebugConsoleContext,1076group: 'z_commands',1077order: 201078}],1079keybinding: [{1080primary: 0,1081mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyK },1082// Weight is higher than work workbench contributions so the keybinding remains1083// highest priority when chords are registered afterwards1084weight: KeybindingWeight.WorkbenchContrib + 1,1085when: ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')1086}],1087});1088}10891090runInView(_accessor: ServicesAccessor, view: Repl): void {1091const accessibilitySignalService = _accessor.get(IAccessibilitySignalService);1092view.clearRepl();1093accessibilitySignalService.playSignal(AccessibilitySignal.clear);1094}1095});10961097registerAction2(class extends ViewAction<Repl> {1098constructor() {1099super({1100id: 'debug.collapseRepl',1101title: localize('collapse', "Collapse All"),1102viewId: REPL_VIEW_ID,1103menu: {1104id: MenuId.DebugConsoleContext,1105group: 'z_commands',1106order: 101107}1108});1109}11101111runInView(_accessor: ServicesAccessor, view: Repl): void {1112view.collapseAll();1113view.focus();1114}1115});11161117registerAction2(class extends ViewAction<Repl> {1118constructor() {1119super({1120id: 'debug.replPaste',1121title: localize('paste', "Paste"),1122viewId: REPL_VIEW_ID,1123precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Inactive)),1124menu: {1125id: MenuId.DebugConsoleContext,1126group: '2_cutcopypaste',1127order: 301128}1129});1130}11311132async runInView(accessor: ServicesAccessor, view: Repl): Promise<void> {1133const clipboardService = accessor.get(IClipboardService);1134const clipboardText = await clipboardService.readText();1135if (clipboardText) {1136const replInput = view.getReplInput();1137replInput.setValue(replInput.getValue().concat(clipboardText));1138view.focus();1139const model = replInput.getModel();1140const lineNumber = model ? model.getLineCount() : 0;1141const column = model?.getLineMaxColumn(lineNumber);1142if (typeof lineNumber === 'number' && typeof column === 'number') {1143replInput.setPosition({ lineNumber, column });1144}1145}1146}1147});11481149registerAction2(class extends ViewAction<Repl> {1150constructor() {1151super({1152id: 'workbench.debug.action.copyAll',1153title: localize('copyAll', "Copy All"),1154viewId: REPL_VIEW_ID,1155menu: {1156id: MenuId.DebugConsoleContext,1157group: '2_cutcopypaste',1158order: 201159}1160});1161}11621163async runInView(accessor: ServicesAccessor, view: Repl): Promise<void> {1164const clipboardService = accessor.get(IClipboardService);1165await clipboardService.writeText(view.getVisibleContent());1166}1167});11681169registerAction2(class extends Action2 {1170constructor() {1171super({1172id: 'debug.replCopy',1173title: localize('copy', "Copy"),1174menu: {1175id: MenuId.DebugConsoleContext,1176group: '2_cutcopypaste',1177order: 101178}1179});1180}11811182async run(accessor: ServicesAccessor, element: IReplElement): Promise<void> {1183const clipboardService = accessor.get(IClipboardService);1184const debugService = accessor.get(IDebugService);1185const nativeSelection = dom.getActiveWindow().getSelection();1186const selectedText = nativeSelection?.toString();1187if (selectedText && selectedText.length > 0) {1188return clipboardService.writeText(selectedText);1189} else if (element) {1190const retValue = await this.tryEvaluateAndCopy(debugService, element);1191const textToCopy = retValue || removeAnsiEscapeCodes(element.toString());1192return clipboardService.writeText(textToCopy);1193}1194}11951196private async tryEvaluateAndCopy(debugService: IDebugService, element: IReplElement): Promise<string | undefined> {1197// todo: we should expand DAP to allow copying more types here (#187784)1198if (!(element instanceof ReplEvaluationResult)) {1199return;1200}12011202const stackFrame = debugService.getViewModel().focusedStackFrame;1203const session = debugService.getViewModel().focusedSession;1204if (!stackFrame || !session || !session.capabilities.supportsClipboardContext) {1205return;1206}12071208try {1209const evaluation = await session.evaluate(element.originalExpression, stackFrame.frameId, 'clipboard');1210return evaluation?.body.result;1211} catch (e) {1212return;1213}1214}1215});12161217registerAction2(class extends Action2 {1218constructor() {1219super({1220id: FOCUS_REPL_ID,1221category: DEBUG_COMMAND_CATEGORY,1222title: localize2({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, "Focus on Debug Console View"),1223});1224}12251226override async run(accessor: ServicesAccessor) {1227const viewsService = accessor.get(IViewsService);1228const repl = await viewsService.openView<Repl>(REPL_VIEW_ID);1229await repl?.focus();1230}1231});123212331234