Path: blob/main/src/vs/workbench/contrib/debug/browser/repl.ts
3296 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 { EDITOR_FONT_DEFAULTS, EditorOption } from '../../../../editor/common/config/editorOptions.js';30import { Position } from '../../../../editor/common/core/position.js';31import { Range } from '../../../../editor/common/core/range.js';32import { IDecorationOptions } from '../../../../editor/common/editorCommon.js';33import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';34import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemKinds, CompletionList } from '../../../../editor/common/languages.js';35import { ITextModel } from '../../../../editor/common/model.js';36import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';37import { IModelService } from '../../../../editor/common/services/model.js';38import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';39import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';40import { localize, localize2 } from '../../../../nls.js';41import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';42import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';43import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';44import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';45import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';46import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';47import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';48import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';49import { IHoverService } from '../../../../platform/hover/browser/hover.js';50import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';51import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';52import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';53import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';54import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';55import { ILogService } from '../../../../platform/log/common/log.js';56import { IOpenerService } from '../../../../platform/opener/common/opener.js';57import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';58import { editorForeground, resolveColorValue } from '../../../../platform/theme/common/colorRegistry.js';59import { IThemeService } from '../../../../platform/theme/common/themeService.js';60import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';61import { FilterViewPane, IViewPaneOptions, ViewAction } from '../../../browser/parts/views/viewPane.js';62import { IViewDescriptorService } from '../../../common/views.js';63import { IEditorService } from '../../../services/editor/common/editorService.js';64import { IViewsService } from '../../../services/views/common/viewsService.js';65import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';66import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';67import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';68import { 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';69import { Variable } from '../common/debugModel.js';70import { ReplEvaluationResult, ReplGroup } from '../common/replModel.js';71import { FocusSessionActionViewItem } from './debugActionViewItems.js';72import { DEBUG_COMMAND_CATEGORY, FOCUS_REPL_ID } from './debugCommands.js';73import { DebugExpressionRenderer } from './debugExpressionRenderer.js';74import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js';75import './media/repl.css';76import { ReplFilter } from './replFilter.js';77import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js';7879const $ = dom.$;8081const HISTORY_STORAGE_KEY = 'debug.repl.history';82const FILTER_HISTORY_STORAGE_KEY = 'debug.repl.filterHistory';83const FILTER_VALUE_STORAGE_KEY = 'debug.repl.filterValue';84const DECORATION_KEY = 'replinputdecoration';8586function revealLastElement(tree: WorkbenchAsyncDataTree<any, any, any>) {87tree.scrollTop = tree.scrollHeight - tree.renderHeight;88// tree.scrollTop = 1e6;89}9091const sessionsToIgnore = new Set<IDebugSession>();92const identityProvider = { getId: (element: IReplElement) => element.getId() };9394export class Repl extends FilterViewPane implements IHistoryNavigationWidget {95declare readonly _serviceBrand: undefined;9697private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show98private static readonly URI = uri.parse(`${DEBUG_SCHEME}:replinput`);99100private history: HistoryNavigator<string>;101private tree?: WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>;102private replOptions: ReplOptions;103private previousTreeScrollHeight: number = 0;104private replDelegate!: ReplDelegate;105private container!: HTMLElement;106private treeContainer!: HTMLElement;107private replInput!: CodeEditorWidget;108private replInputContainer!: HTMLElement;109private bodyContentDimension: dom.Dimension | undefined;110private model: ITextModel | undefined;111private setHistoryNavigationEnablement!: (enabled: boolean) => void;112private scopedInstantiationService!: IInstantiationService;113private replElementsChangeListener: IDisposable | undefined;114private styleElement: HTMLStyleElement | undefined;115private styleChangedWhenInvisible: boolean = false;116private completionItemProvider: IDisposable | undefined;117private modelChangeListener: IDisposable = Disposable.None;118private filter: ReplFilter;119private multiSessionRepl: IContextKey<boolean>;120private menu: IMenu;121private replDataSource: IAsyncDataSource<IDebugSession, IReplElement> | undefined;122private findIsOpen: boolean = false;123124constructor(125options: IViewPaneOptions,126@IDebugService private readonly debugService: IDebugService,127@IInstantiationService instantiationService: IInstantiationService,128@IStorageService private readonly storageService: IStorageService,129@IThemeService themeService: IThemeService,130@IModelService private readonly modelService: IModelService,131@IContextKeyService contextKeyService: IContextKeyService,132@ICodeEditorService codeEditorService: ICodeEditorService,133@IViewDescriptorService viewDescriptorService: IViewDescriptorService,134@IContextMenuService contextMenuService: IContextMenuService,135@IConfigurationService protected override readonly configurationService: IConfigurationService,136@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,137@IEditorService private readonly editorService: IEditorService,138@IKeybindingService protected override readonly keybindingService: IKeybindingService,139@IOpenerService openerService: IOpenerService,140@IHoverService hoverService: IHoverService,141@IMenuService menuService: IMenuService,142@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,143@ILogService private readonly logService: ILogService,144) {145const filterText = storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, '');146super({147...options,148filterOptions: {149placeholder: localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude, \\escape)"),150text: filterText,151history: JSON.parse(storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[],152}153}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);154155this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService);156this._register(this.menu);157this.history = this._register(new HistoryNavigator(new Set(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]'))), 100));158this.filter = new ReplFilter();159this.filter.filterQuery = filterText;160this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService);161this.replOptions = this._register(this.instantiationService.createInstance(ReplOptions, this.id, () => this.getLocationBasedColors().background));162this._register(this.replOptions.onDidChange(() => this.onDidStyleChange()));163164codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {});165this.multiSessionRepl.set(this.isMultiSessionView);166this.registerListeners();167}168169private registerListeners(): void {170if (this.debugService.getViewModel().focusedSession) {171this.onDidFocusSession(this.debugService.getViewModel().focusedSession);172}173174this._register(this.debugService.getViewModel().onDidFocusSession(session => {175this.onDidFocusSession(session);176}));177this._register(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => {178if (e instanceof Variable && this.tree?.hasNode(e)) {179await this.tree.updateChildren(e, false, true);180await this.tree.expand(e);181}182}));183this._register(this.debugService.onWillNewSession(async newSession => {184// Need to listen to output events for sessions which are not yet fully initialised185const input = this.tree?.getInput();186if (!input || input.state === State.Inactive) {187await this.selectSession(newSession);188}189this.multiSessionRepl.set(this.isMultiSessionView);190}));191this._register(this.debugService.onDidEndSession(async () => {192// Update view, since orphaned sessions might now be separate193await Promise.resolve(); // allow other listeners to go first, so sessions can update parents194this.multiSessionRepl.set(this.isMultiSessionView);195}));196this._register(this.themeService.onDidColorThemeChange(() => {197this.refreshReplElements(false);198if (this.isVisible()) {199this.updateInputDecoration();200}201}));202this._register(this.onDidChangeBodyVisibility(visible => {203if (!visible) {204return;205}206if (!this.model) {207this.model = this.modelService.getModel(Repl.URI) || this.modelService.createModel('', null, Repl.URI, true);208}209210const focusedSession = this.debugService.getViewModel().focusedSession;211if (this.tree && this.tree.getInput() !== focusedSession) {212this.onDidFocusSession(focusedSession);213}214215this.setMode();216this.replInput.setModel(this.model);217this.updateInputDecoration();218this.refreshReplElements(true);219220if (this.styleChangedWhenInvisible) {221this.styleChangedWhenInvisible = false;222this.tree?.updateChildren(undefined, true, false);223this.onDidStyleChange();224}225}));226this._register(this.configurationService.onDidChangeConfiguration(e => {227if (e.affectsConfiguration('debug.console.wordWrap') && this.tree) {228this.tree.dispose();229this.treeContainer.innerText = '';230dom.clearNode(this.treeContainer);231this.createReplTree();232}233if (e.affectsConfiguration('debug.console.acceptSuggestionOnEnter')) {234const config = this.configurationService.getValue<IDebugConfiguration>('debug');235this.replInput.updateOptions({236acceptSuggestionOnEnter: config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off'237});238}239}));240241this._register(this.editorService.onDidActiveEditorChange(() => {242this.setMode();243}));244245this._register(this.filterWidget.onDidChangeFilterText(() => {246this.filter.filterQuery = this.filterWidget.getFilterText();247if (this.tree) {248this.tree.refilter();249revealLastElement(this.tree);250}251}));252}253254private async onDidFocusSession(session: IDebugSession | undefined): Promise<void> {255if (session) {256sessionsToIgnore.delete(session);257this.completionItemProvider?.dispose();258if (session.capabilities.supportsCompletionsRequest) {259this.completionItemProvider = this.languageFeaturesService.completionProvider.register({ scheme: DEBUG_SCHEME, pattern: '**/replinput', hasAccessToAllModels: true }, {260_debugDisplayName: 'debugConsole',261triggerCharacters: session.capabilities.completionTriggerCharacters || ['.'],262provideCompletionItems: async (_: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => {263// Disable history navigation because up and down are used to navigate through the suggest widget264this.setHistoryNavigationEnablement(false);265266const model = this.replInput.getModel();267if (model) {268const text = model.getValue();269const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;270const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined;271const response = await session.completions(frameId, focusedStackFrame?.thread.threadId || 0, text, position, token);272273const suggestions: CompletionItem[] = [];274const computeRange = (length: number) => Range.fromPositions(position.delta(0, -length), position);275if (response && response.body && response.body.targets) {276response.body.targets.forEach(item => {277if (item && item.label) {278let insertTextRules: CompletionItemInsertTextRule | undefined = undefined;279let insertText = item.text || item.label;280if (typeof item.selectionStart === 'number') {281// If a debug completion item sets a selection we need to use snippets to make sure the selection is selected #90974282insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;283const selectionLength = typeof item.selectionLength === 'number' ? item.selectionLength : 0;284const placeholder = selectionLength > 0 ? '${1:' + insertText.substring(item.selectionStart, item.selectionStart + selectionLength) + '}$0' : '$0';285insertText = insertText.substring(0, item.selectionStart) + placeholder + insertText.substring(item.selectionStart + selectionLength);286}287288suggestions.push({289label: item.label,290insertText,291detail: item.detail,292kind: CompletionItemKinds.fromString(item.type || 'property'),293filterText: (item.start && item.length) ? text.substring(item.start, item.start + item.length).concat(item.label) : undefined,294range: computeRange(item.length || 0),295sortText: item.sortText,296insertTextRules297});298}299});300}301302if (this.configurationService.getValue<IDebugConfiguration>('debug').console.historySuggestions) {303const history = this.history.getHistory();304const idxLength = String(history.length).length;305history.forEach((h, i) => suggestions.push({306label: h,307insertText: h,308kind: CompletionItemKind.Text,309range: computeRange(h.length),310sortText: 'ZZZ' + String(history.length - i).padStart(idxLength, '0')311}));312}313314return { suggestions };315}316317return Promise.resolve({ suggestions: [] });318}319});320}321}322323await this.selectSession();324}325326getFilterStats(): { total: number; filtered: number } {327// This could be called before the tree is created when setting this.filterState.filterText value328return {329total: this.tree?.getNode().children.length ?? 0,330filtered: this.tree?.getNode().children.filter(c => c.visible).length ?? 0331};332}333334get isReadonly(): boolean {335// Do not allow to edit inactive sessions336const session = this.tree?.getInput();337if (session && session.state !== State.Inactive) {338return false;339}340341return true;342}343344showPreviousValue(): void {345if (!this.isReadonly) {346this.navigateHistory(true);347}348}349350showNextValue(): void {351if (!this.isReadonly) {352this.navigateHistory(false);353}354}355356focusFilter(): void {357this.filterWidget.focus();358}359360openFind(): void {361this.tree?.openFind();362}363364private setMode(): void {365if (!this.isVisible()) {366return;367}368369const activeEditorControl = this.editorService.activeTextEditorControl;370if (isCodeEditor(activeEditorControl)) {371this.modelChangeListener.dispose();372this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode());373if (this.model && activeEditorControl.hasModel()) {374this.model.setLanguage(activeEditorControl.getModel().getLanguageId());375}376}377}378379private onDidStyleChange(): void {380if (!this.isVisible()) {381this.styleChangedWhenInvisible = true;382return;383}384if (this.styleElement) {385this.replInput.updateOptions({386fontSize: this.replOptions.replConfiguration.fontSize,387lineHeight: this.replOptions.replConfiguration.lineHeight,388fontFamily: this.replOptions.replConfiguration.fontFamily === 'default' ? EDITOR_FONT_DEFAULTS.fontFamily : this.replOptions.replConfiguration.fontFamily389});390391const replInputLineHeight = this.replInput.getOption(EditorOption.lineHeight);392393// Set the font size, font family, line height and align the twistie to be centered, and input theme color394this.styleElement.textContent = `395.repl .repl-input-wrapper .repl-input-chevron {396line-height: ${replInputLineHeight}px397}398399.repl .repl-input-wrapper .monaco-editor .lines-content {400background-color: ${this.replOptions.replConfiguration.backgroundColor};401}402`;403const cssFontFamily = this.replOptions.replConfiguration.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : this.replOptions.replConfiguration.fontFamily;404this.container.style.setProperty(`--vscode-repl-font-family`, cssFontFamily);405this.container.style.setProperty(`--vscode-repl-font-size`, `${this.replOptions.replConfiguration.fontSize}px`);406this.container.style.setProperty(`--vscode-repl-font-size-for-twistie`, `${this.replOptions.replConfiguration.fontSizeForTwistie}px`);407this.container.style.setProperty(`--vscode-repl-line-height`, this.replOptions.replConfiguration.cssLineHeight);408409this.tree?.rerender();410411if (this.bodyContentDimension) {412this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);413}414}415}416417private navigateHistory(previous: boolean): void {418const historyInput = (previous ?419(this.history.previous() ?? this.history.first()) : this.history.next())420?? '';421this.replInput.setValue(historyInput);422aria.status(historyInput);423// always leave cursor at the end.424this.replInput.setPosition({ lineNumber: 1, column: historyInput.length + 1 });425this.setHistoryNavigationEnablement(true);426}427428async selectSession(session?: IDebugSession): Promise<void> {429const treeInput = this.tree?.getInput();430if (!session) {431const focusedSession = this.debugService.getViewModel().focusedSession;432// If there is a focusedSession focus on that one, otherwise just show any other not ignored session433if (focusedSession) {434session = focusedSession;435} else if (!treeInput || sessionsToIgnore.has(treeInput)) {436session = this.debugService.getModel().getSessions(true).find(s => !sessionsToIgnore.has(s));437}438}439if (session) {440this.replElementsChangeListener?.dispose();441this.replElementsChangeListener = session.onDidChangeReplElements(() => {442this.refreshReplElements(session.getReplElements().length === 0);443});444445if (this.tree && treeInput !== session) {446try {447await this.tree.setInput(session);448} catch (err) {449// Ignore error because this may happen multiple times while refreshing,450// then changing the root may fail. Log to help with debugging if needed.451this.logService.error(err);452}453revealLastElement(this.tree);454}455}456457this.replInput?.updateOptions({ readOnly: this.isReadonly });458this.updateInputDecoration();459}460461async clearRepl(): Promise<void> {462const session = this.tree?.getInput();463if (session) {464session.removeReplExpressions();465if (session.state === State.Inactive) {466// Ignore inactive sessions which got cleared - so they are not shown any more467sessionsToIgnore.add(session);468await this.selectSession();469this.multiSessionRepl.set(this.isMultiSessionView);470}471}472this.replInput.focus();473}474475acceptReplInput(): void {476const session = this.tree?.getInput();477if (session && !this.isReadonly) {478session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, this.replInput.getValue());479revealLastElement(this.tree!);480this.history.add(this.replInput.getValue());481this.replInput.setValue('');482if (this.bodyContentDimension) {483// Trigger a layout to shrink a potential multi line input484this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);485}486}487}488489sendReplInput(input: string): void {490const session = this.tree?.getInput();491if (session && !this.isReadonly) {492session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, input);493revealLastElement(this.tree!);494this.history.add(input);495}496}497498getVisibleContent(): string {499let text = '';500if (this.model && this.tree) {501const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri);502const traverseAndAppend = (node: ITreeNode<IReplElement, FuzzyScore>) => {503node.children.forEach(child => {504if (child.visible) {505text += child.element.toString().trimRight() + lineDelimiter;506if (!child.collapsed && child.children.length) {507traverseAndAppend(child);508}509}510});511};512traverseAndAppend(this.tree.getNode());513}514515return removeAnsiEscapeCodes(text);516}517518protected layoutBodyContent(height: number, width: number): void {519this.bodyContentDimension = new dom.Dimension(width, height);520const replInputHeight = Math.min(this.replInput.getContentHeight(), height);521if (this.tree) {522const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;523const treeHeight = height - replInputHeight;524this.tree.getHTMLElement().style.height = `${treeHeight}px`;525this.tree.layout(treeHeight, width);526if (lastElementVisible) {527revealLastElement(this.tree);528}529}530this.replInputContainer.style.height = `${replInputHeight}px`;531532this.replInput.layout({ width: width - 30, height: replInputHeight });533}534535collapseAll(): void {536this.tree?.collapseAll();537}538539getDebugSession(): IDebugSession | undefined {540return this.tree?.getInput();541}542543getReplInput(): CodeEditorWidget {544return this.replInput;545}546547getReplDataSource(): IAsyncDataSource<IDebugSession, IReplElement> | undefined {548return this.replDataSource;549}550551getFocusedElement(): IReplElement | undefined {552return this.tree?.getFocus()?.[0];553}554555focusTree(): void {556this.tree?.domFocus();557}558559override async focus(): Promise<void> {560super.focus();561await timeout(0); // wait a task for the repl to get attached to the DOM, #83387562this.replInput.focus();563}564565override createActionViewItem(action: IAction): IActionViewItem | undefined {566if (action.id === selectReplCommandId) {567const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession;568return this.instantiationService.createInstance(SelectReplActionViewItem, action, session);569}570571return super.createActionViewItem(action);572}573574private get isMultiSessionView(): boolean {575return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1;576}577578// --- Cached locals579580@memoize581private get refreshScheduler(): RunOnceScheduler {582const autoExpanded = new Set<string>();583return new RunOnceScheduler(async () => {584if (!this.tree || !this.tree.getInput() || !this.isVisible()) {585return;586}587588await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider });589590const session = this.tree.getInput();591if (session) {592// Automatically expand repl group elements when specified593const autoExpandElements = async (elements: IReplElement[]) => {594for (const element of elements) {595if (element instanceof ReplGroup) {596if (element.autoExpand && !autoExpanded.has(element.getId())) {597autoExpanded.add(element.getId());598await this.tree!.expand(element);599}600if (!this.tree!.isCollapsed(element)) {601// Repl groups can have children which are repl groups thus we might need to expand those as well602await autoExpandElements(element.getChildren());603}604}605}606};607await autoExpandElements(session.getReplElements());608}609// Repl elements count changed, need to update filter stats on the badge610const { total, filtered } = this.getFilterStats();611this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : localize('showing filtered repl lines', "Showing {0} of {1}", filtered, total));612}, Repl.REFRESH_DELAY);613}614615// --- Creation616617override render(): void {618super.render();619this._register(registerNavigableContainer({620name: 'repl',621focusNotifiers: [this, this.filterWidget],622focusNextWidget: () => {623const element = this.tree?.getHTMLElement();624if (this.filterWidget.hasFocus()) {625this.tree?.domFocus();626} else if (element && dom.isActiveElement(element)) {627this.focus();628}629},630focusPreviousWidget: () => {631const element = this.tree?.getHTMLElement();632if (this.replInput.hasTextFocus()) {633this.tree?.domFocus();634} else if (element && dom.isActiveElement(element)) {635this.focusFilter();636}637}638}));639}640641protected override renderBody(parent: HTMLElement): void {642super.renderBody(parent);643this.container = dom.append(parent, $('.repl'));644this.treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`));645this.createReplInput(this.container);646this.createReplTree();647}648649private createReplTree(): void {650this.replDelegate = new ReplDelegate(this.configurationService, this.replOptions);651const wordWrap = this.configurationService.getValue<IDebugConfiguration>('debug').console.wordWrap;652this.treeContainer.classList.toggle('word-wrap', wordWrap);653const expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer);654this.replDataSource = new ReplDataSource();655656const tree = this.tree = this.instantiationService.createInstance(657WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>,658'DebugRepl',659this.treeContainer,660this.replDelegate,661[662this.instantiationService.createInstance(ReplVariablesRenderer, expressionRenderer),663this.instantiationService.createInstance(ReplOutputElementRenderer, expressionRenderer),664new ReplEvaluationInputsRenderer(),665this.instantiationService.createInstance(ReplGroupRenderer, expressionRenderer),666new ReplEvaluationResultsRenderer(expressionRenderer),667new ReplRawObjectsRenderer(expressionRenderer),668],669this.replDataSource,670{671filter: this.filter,672accessibilityProvider: new ReplAccessibilityProvider(),673identityProvider,674userSelection: true,675mouseSupport: false,676findWidgetEnabled: true,677keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e.toString(true) },678horizontalScrolling: !wordWrap,679setRowLineHeight: false,680supportDynamicHeights: wordWrap,681overrideStyles: this.getLocationBasedColors().listOverrideStyles682});683684this._register(tree.onDidChangeContentHeight(() => {685if (tree.scrollHeight !== this.previousTreeScrollHeight) {686// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.687// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.688const lastElementWasVisible = tree.scrollTop + tree.renderHeight >= this.previousTreeScrollHeight - 2;689if (lastElementWasVisible) {690setTimeout(() => {691// Can't set scrollTop during this event listener, the list might overwrite the change692revealLastElement(tree);693}, 0);694}695}696697this.previousTreeScrollHeight = tree.scrollHeight;698}));699700this._register(tree.onContextMenu(e => this.onContextMenu(e)));701this._register(tree.onDidChangeFindOpenState((open) => this.findIsOpen = open));702703let lastSelectedString: string;704this._register(tree.onMouseClick(() => {705if (this.findIsOpen) {706return;707}708const selection = dom.getWindow(this.treeContainer).getSelection();709if (!selection || selection.type !== 'Range' || lastSelectedString === selection.toString()) {710// only focus the input if the user is not currently selecting and find isn't open.711this.replInput.focus();712}713lastSelectedString = selection ? selection.toString() : '';714}));715// Make sure to select the session if debugging is already active716this.selectSession();717this.styleElement = domStylesheetsJs.createStyleSheet(this.container);718this.onDidStyleChange();719}720721private createReplInput(container: HTMLElement): void {722this.replInputContainer = dom.append(container, $('.repl-input-wrapper'));723dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt)));724725const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(this.scopedContextKeyService, this));726this.setHistoryNavigationEnablement = enabled => {727historyNavigationBackwardsEnablement.set(enabled);728historyNavigationForwardsEnablement.set(enabled);729};730CONTEXT_IN_DEBUG_REPL.bindTo(this.scopedContextKeyService).set(true);731732this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));733const options = getSimpleEditorOptions(this.configurationService);734options.readOnly = true;735options.suggest = { showStatusBar: true };736const config = this.configurationService.getValue<IDebugConfiguration>('debug');737options.acceptSuggestionOnEnter = config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off';738options.ariaLabel = this.getAriaLabel();739740this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions());741742let lastContentHeight = -1;743this._register(this.replInput.onDidChangeModelContent(() => {744const model = this.replInput.getModel();745this.setHistoryNavigationEnablement(!!model && model.getValue() === '');746747const contentHeight = this.replInput.getContentHeight();748if (contentHeight !== lastContentHeight) {749lastContentHeight = contentHeight;750if (this.bodyContentDimension) {751this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);752}753}754}));755// We add the input decoration only when the focus is in the input #61126756this._register(this.replInput.onDidFocusEditorText(() => this.updateInputDecoration()));757this._register(this.replInput.onDidBlurEditorText(() => this.updateInputDecoration()));758759this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.FOCUS, () => this.replInputContainer.classList.add('synthetic-focus')));760this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.BLUR, () => this.replInputContainer.classList.remove('synthetic-focus')));761}762763private getAriaLabel(): string {764let ariaLabel = localize('debugConsole', "Debug Console");765if (!this.configurationService.getValue(AccessibilityVerbositySettingId.Debug)) {766return ariaLabel;767}768const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel();769if (keybinding) {770ariaLabel = localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding);771} else {772ariaLabel = localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel);773}774775return ariaLabel;776}777778private onContextMenu(e: ITreeContextMenuEvent<IReplElement>): void {779const actions = getFlatContextMenuActions(this.menu.getActions({ arg: e.element, shouldForwardArgs: false }));780this.contextMenuService.showContextMenu({781getAnchor: () => e.anchor,782getActions: () => actions,783getActionsContext: () => e.element784});785}786787// --- Update788789private refreshReplElements(noDelay: boolean): void {790if (this.tree && this.isVisible()) {791if (this.refreshScheduler.isScheduled()) {792return;793}794795this.refreshScheduler.schedule(noDelay ? 0 : undefined);796}797}798799private updateInputDecoration(): void {800if (!this.replInput) {801return;802}803804const decorations: IDecorationOptions[] = [];805if (this.isReadonly && this.replInput.hasTextFocus() && !this.replInput.getValue()) {806const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4);807decorations.push({808range: {809startLineNumber: 0,810endLineNumber: 0,811startColumn: 0,812endColumn: 1813},814renderOptions: {815after: {816contentText: localize('startDebugFirst', "Please start a debug session to evaluate expressions"),817color: transparentForeground ? transparentForeground.toString() : undefined818}819}820});821}822823this.replInput.setDecorationsByType('repl-decoration', DECORATION_KEY, decorations);824}825826override saveState(): void {827const replHistory = this.history.getHistory();828if (replHistory.length) {829this.storageService.store(HISTORY_STORAGE_KEY, JSON.stringify(replHistory), StorageScope.WORKSPACE, StorageTarget.MACHINE);830} else {831this.storageService.remove(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);832}833const filterHistory = this.filterWidget.getHistory();834if (filterHistory.length) {835this.storageService.store(FILTER_HISTORY_STORAGE_KEY, JSON.stringify(filterHistory), StorageScope.WORKSPACE, StorageTarget.MACHINE);836} else {837this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);838}839const filterValue = this.filterWidget.getFilterText();840if (filterValue) {841this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.MACHINE);842} else {843this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE);844}845846super.saveState();847}848849override dispose(): void {850this.replInput?.dispose(); // Disposed before rendered? #174558851this.replElementsChangeListener?.dispose();852this.refreshScheduler.dispose();853this.modelChangeListener.dispose();854super.dispose();855}856}857858class ReplOptions extends Disposable implements IReplOptions {859private static readonly lineHeightEm = 1.4;860861private readonly _onDidChange = this._register(new Emitter<void>());862readonly onDidChange = this._onDidChange.event;863864private _replConfig!: IReplConfiguration;865public get replConfiguration(): IReplConfiguration {866return this._replConfig;867}868869constructor(870viewId: string,871private readonly backgroundColorDelegate: () => string,872@IConfigurationService private readonly configurationService: IConfigurationService,873@IThemeService private readonly themeService: IThemeService,874@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService875) {876super();877878this._register(this.themeService.onDidColorThemeChange(e => this.update()));879this._register(this.viewDescriptorService.onDidChangeLocation(e => {880if (e.views.some(v => v.id === viewId)) {881this.update();882}883}));884this._register(this.configurationService.onDidChangeConfiguration(e => {885if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) {886this.update();887}888}));889this.update();890}891892private update() {893const debugConsole = this.configurationService.getValue<IDebugConfiguration>('debug').console;894this._replConfig = {895fontSize: debugConsole.fontSize,896fontFamily: debugConsole.fontFamily,897lineHeight: debugConsole.lineHeight ? debugConsole.lineHeight : ReplOptions.lineHeightEm * debugConsole.fontSize,898cssLineHeight: debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : `${ReplOptions.lineHeightEm}em`,899backgroundColor: this.themeService.getColorTheme().getColor(this.backgroundColorDelegate()),900fontSizeForTwistie: debugConsole.fontSize * ReplOptions.lineHeightEm / 2 - 8901};902this._onDidChange.fire();903}904}905906// Repl actions and commands907908class AcceptReplInputAction extends EditorAction {909910constructor() {911super({912id: 'repl.action.acceptInput',913label: localize2({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "Debug Console: Accept Input"),914precondition: CONTEXT_IN_DEBUG_REPL,915kbOpts: {916kbExpr: EditorContextKeys.textInputFocus,917primary: KeyCode.Enter,918weight: KeybindingWeight.EditorContrib919}920});921}922923run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {924SuggestController.get(editor)?.cancelSuggestWidget();925const repl = getReplView(accessor.get(IViewsService));926repl?.acceptReplInput();927}928}929930class FilterReplAction extends ViewAction<Repl> {931932constructor() {933super({934viewId: REPL_VIEW_ID,935id: 'repl.action.filter',936title: localize('repl.action.filter', "Debug Console: Focus Filter"),937precondition: CONTEXT_IN_DEBUG_REPL,938keybinding: [{939when: EditorContextKeys.textInputFocus,940primary: KeyMod.CtrlCmd | KeyCode.KeyF,941weight: KeybindingWeight.EditorContrib942}]943});944}945946runInView(accessor: ServicesAccessor, repl: Repl): void | Promise<void> {947repl.focusFilter();948}949}950951952class FindReplAction extends ViewAction<Repl> {953954constructor() {955super({956viewId: REPL_VIEW_ID,957id: 'repl.action.find',958title: localize('repl.action.find', "Debug Console: Focus Find"),959precondition: CONTEXT_IN_DEBUG_REPL,960keybinding: [{961when: ContextKeyExpr.or(CONTEXT_IN_DEBUG_REPL, ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')),962primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF,963weight: KeybindingWeight.EditorContrib964}],965icon: Codicon.search,966menu: [{967id: MenuId.ViewTitle,968group: 'navigation',969when: ContextKeyExpr.equals('view', REPL_VIEW_ID),970order: 15971}, {972id: MenuId.DebugConsoleContext,973group: 'z_commands',974order: 25975}],976});977}978979runInView(accessor: ServicesAccessor, view: Repl): void | Promise<void> {980view.openFind();981}982}983984class ReplCopyAllAction extends EditorAction {985986constructor() {987super({988id: 'repl.action.copyAll',989label: localize('actions.repl.copyAll', "Debug: Console Copy All"),990alias: 'Debug Console Copy All',991precondition: CONTEXT_IN_DEBUG_REPL,992});993}994995run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {996const clipboardService = accessor.get(IClipboardService);997const repl = getReplView(accessor.get(IViewsService));998if (repl) {999return clipboardService.writeText(repl.getVisibleContent());1000}1001}1002}10031004registerEditorAction(AcceptReplInputAction);1005registerEditorAction(ReplCopyAllAction);1006registerAction2(FilterReplAction);1007registerAction2(FindReplAction);10081009class SelectReplActionViewItem extends FocusSessionActionViewItem {10101011protected override getSessions(): ReadonlyArray<IDebugSession> {1012return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s));1013}10141015protected override mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession {1016while (focusedSession.parentSession && !focusedSession.hasSeparateRepl()) {1017focusedSession = focusedSession.parentSession;1018}1019return focusedSession;1020}1021}10221023export function getReplView(viewsService: IViewsService): Repl | undefined {1024return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined;1025}10261027const selectReplCommandId = 'workbench.action.debug.selectRepl';1028registerAction2(class extends ViewAction<Repl> {1029constructor() {1030super({1031id: selectReplCommandId,1032viewId: REPL_VIEW_ID,1033title: localize('selectRepl', "Select Debug Console"),1034f1: false,1035menu: {1036id: MenuId.ViewTitle,1037group: 'navigation',1038when: ContextKeyExpr.and(ContextKeyExpr.equals('view', REPL_VIEW_ID), CONTEXT_MULTI_SESSION_REPL),1039order: 201040}1041});1042}10431044async runInView(accessor: ServicesAccessor, view: Repl, session: IDebugSession | undefined) {1045const debugService = accessor.get(IDebugService);1046// If session is already the focused session we need to manualy update the tree since view model will not send a focused change event1047if (session && session.state !== State.Inactive && session !== debugService.getViewModel().focusedSession) {1048if (session.state !== State.Stopped) {1049// Focus child session instead if it is stopped #1125951050const stopppedChildSession = debugService.getModel().getSessions().find(s => s.parentSession === session && s.state === State.Stopped);1051if (stopppedChildSession) {1052session = stopppedChildSession;1053}1054}1055await debugService.focusStackFrame(undefined, undefined, session, { explicit: true });1056}1057// Need to select the session in the view since the focussed session might not have changed1058await view.selectSession(session);1059}1060});10611062registerAction2(class extends ViewAction<Repl> {1063constructor() {1064super({1065id: 'workbench.debug.panel.action.clearReplAction',1066viewId: REPL_VIEW_ID,1067title: localize2('clearRepl', 'Clear Console'),1068metadata: {1069description: localize2('clearRepl.descriotion', 'Clears all program output from your debug REPL')1070},1071f1: true,1072icon: debugConsoleClearAll,1073menu: [{1074id: MenuId.ViewTitle,1075group: 'navigation',1076when: ContextKeyExpr.equals('view', REPL_VIEW_ID),1077order: 301078}, {1079id: MenuId.DebugConsoleContext,1080group: 'z_commands',1081order: 201082}],1083keybinding: [{1084primary: 0,1085mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyK },1086// Weight is higher than work workbench contributions so the keybinding remains1087// highest priority when chords are registered afterwards1088weight: KeybindingWeight.WorkbenchContrib + 1,1089when: ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')1090}],1091});1092}10931094runInView(_accessor: ServicesAccessor, view: Repl): void {1095const accessibilitySignalService = _accessor.get(IAccessibilitySignalService);1096view.clearRepl();1097accessibilitySignalService.playSignal(AccessibilitySignal.clear);1098}1099});11001101registerAction2(class extends ViewAction<Repl> {1102constructor() {1103super({1104id: 'debug.collapseRepl',1105title: localize('collapse', "Collapse All"),1106viewId: REPL_VIEW_ID,1107menu: {1108id: MenuId.DebugConsoleContext,1109group: 'z_commands',1110order: 101111}1112});1113}11141115runInView(_accessor: ServicesAccessor, view: Repl): void {1116view.collapseAll();1117view.focus();1118}1119});11201121registerAction2(class extends ViewAction<Repl> {1122constructor() {1123super({1124id: 'debug.replPaste',1125title: localize('paste', "Paste"),1126viewId: REPL_VIEW_ID,1127precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Inactive)),1128menu: {1129id: MenuId.DebugConsoleContext,1130group: '2_cutcopypaste',1131order: 301132}1133});1134}11351136async runInView(accessor: ServicesAccessor, view: Repl): Promise<void> {1137const clipboardService = accessor.get(IClipboardService);1138const clipboardText = await clipboardService.readText();1139if (clipboardText) {1140const replInput = view.getReplInput();1141replInput.setValue(replInput.getValue().concat(clipboardText));1142view.focus();1143const model = replInput.getModel();1144const lineNumber = model ? model.getLineCount() : 0;1145const column = model?.getLineMaxColumn(lineNumber);1146if (typeof lineNumber === 'number' && typeof column === 'number') {1147replInput.setPosition({ lineNumber, column });1148}1149}1150}1151});11521153registerAction2(class extends ViewAction<Repl> {1154constructor() {1155super({1156id: 'workbench.debug.action.copyAll',1157title: localize('copyAll', "Copy All"),1158viewId: REPL_VIEW_ID,1159menu: {1160id: MenuId.DebugConsoleContext,1161group: '2_cutcopypaste',1162order: 201163}1164});1165}11661167async runInView(accessor: ServicesAccessor, view: Repl): Promise<void> {1168const clipboardService = accessor.get(IClipboardService);1169await clipboardService.writeText(view.getVisibleContent());1170}1171});11721173registerAction2(class extends Action2 {1174constructor() {1175super({1176id: 'debug.replCopy',1177title: localize('copy', "Copy"),1178menu: {1179id: MenuId.DebugConsoleContext,1180group: '2_cutcopypaste',1181order: 101182}1183});1184}11851186async run(accessor: ServicesAccessor, element: IReplElement): Promise<void> {1187const clipboardService = accessor.get(IClipboardService);1188const debugService = accessor.get(IDebugService);1189const nativeSelection = dom.getActiveWindow().getSelection();1190const selectedText = nativeSelection?.toString();1191if (selectedText && selectedText.length > 0) {1192return clipboardService.writeText(selectedText);1193} else if (element) {1194return clipboardService.writeText(await this.tryEvaluateAndCopy(debugService, element) || element.toString());1195}1196}11971198private async tryEvaluateAndCopy(debugService: IDebugService, element: IReplElement): Promise<string | undefined> {1199// todo: we should expand DAP to allow copying more types here (#187784)1200if (!(element instanceof ReplEvaluationResult)) {1201return;1202}12031204const stackFrame = debugService.getViewModel().focusedStackFrame;1205const session = debugService.getViewModel().focusedSession;1206if (!stackFrame || !session || !session.capabilities.supportsClipboardContext) {1207return;1208}12091210try {1211const evaluation = await session.evaluate(element.originalExpression, stackFrame.frameId, 'clipboard');1212return evaluation?.body.result;1213} catch (e) {1214return;1215}1216}1217});12181219registerAction2(class extends Action2 {1220constructor() {1221super({1222id: FOCUS_REPL_ID,1223category: DEBUG_COMMAND_CATEGORY,1224title: localize2({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, "Focus on Debug Console View"),1225});1226}12271228override async run(accessor: ServicesAccessor) {1229const viewsService = accessor.get(IViewsService);1230const repl = await viewsService.openView<Repl>(REPL_VIEW_ID);1231await repl?.focus();1232}1233});123412351236