Path: blob/main/src/vs/workbench/contrib/debug/browser/callStackView.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 { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';7import { AriaRole } from '../../../../base/browser/ui/aria/aria.js';8import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';9import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';10import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';11import { ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js';12import { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js';13import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js';14import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';15import { Action } from '../../../../base/common/actions.js';16import { RunOnceScheduler } from '../../../../base/common/async.js';17import { Codicon } from '../../../../base/common/codicons.js';18import { Event } from '../../../../base/common/event.js';19import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js';20import { DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js';21import { posix } from '../../../../base/common/path.js';22import { commonSuffixLength } from '../../../../base/common/strings.js';23import { localize } from '../../../../nls.js';24import { ICommandActionTitle, Icon } from '../../../../platform/action/common/action.js';25import { getActionBarActions, getContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';26import { IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';27import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';28import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';29import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';30import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';31import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';32import { ILabelService } from '../../../../platform/label/common/label.js';33import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js';34import { INotificationService } from '../../../../platform/notification/common/notification.js';35import { IOpenerService } from '../../../../platform/opener/common/opener.js';36import { asCssVariable, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';37import { IThemeService } from '../../../../platform/theme/common/themeService.js';38import { ThemeIcon } from '../../../../base/common/themables.js';39import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js';40import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';41import { IViewDescriptorService } from '../../../common/views.js';42import { renderViewTree } from './baseDebugView.js';43import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from './debugCommands.js';44import * as icons from './debugIcons.js';45import { createDisconnectMenuItemAction } from './debugToolBar.js';46import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_FOCUSED, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, isFrameDeemphasized, IStackFrame, IThread, State } from '../common/debug.js';47import { StackFrame, Thread, ThreadAndSessionIds } from '../common/debugModel.js';48import { isSessionAttach } from '../common/debugUtils.js';49import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';50import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';51import { IHoverService } from '../../../../platform/hover/browser/hover.js';5253const $ = dom.$;5455type CallStackItem = IStackFrame | IThread | IDebugSession | string | ThreadAndSessionIds | IStackFrame[];5657function assignSessionContext(element: IDebugSession, context: any) {58context.sessionId = element.getId();59return context;60}6162function assignThreadContext(element: IThread, context: any) {63context.threadId = element.getId();64assignSessionContext(element.session, context);65return context;66}6768function assignStackFrameContext(element: StackFrame, context: any) {69context.frameId = element.getId();70context.frameName = element.name;71context.frameLocation = { range: element.range, source: element.source.raw };72assignThreadContext(element.thread, context);73return context;74}7576export function getContext(element: CallStackItem | null) {77if (element instanceof StackFrame) {78return assignStackFrameContext(element, {});79} else if (element instanceof Thread) {80return assignThreadContext(element, {});81} else if (isDebugSession(element)) {82return assignSessionContext(element, {});83} else {84return undefined;85}86}8788// Extensions depend on this context, should not be changed even though it is not fully deterministic89export function getContextForContributedActions(element: CallStackItem | null): string | number {90if (element instanceof StackFrame) {91if (element.source.inMemory) {92return element.source.raw.path || element.source.reference || element.source.name;93}9495return element.source.uri.toString();96}97if (element instanceof Thread) {98return element.threadId;99}100if (isDebugSession(element)) {101return element.getId();102}103104return '';105}106107export function getSpecificSourceName(stackFrame: IStackFrame): string {108// To reduce flashing of the path name and the way we fetch stack frames109// We need to compute the source name based on the other frames in the stale call stack110let callStack = (<Thread>stackFrame.thread).getStaleCallStack();111callStack = callStack.length > 0 ? callStack : stackFrame.thread.getCallStack();112const otherSources = callStack.map(sf => sf.source).filter(s => s !== stackFrame.source);113let suffixLength = 0;114otherSources.forEach(s => {115if (s.name === stackFrame.source.name) {116suffixLength = Math.max(suffixLength, commonSuffixLength(stackFrame.source.uri.path, s.uri.path));117}118});119if (suffixLength === 0) {120return stackFrame.source.name;121}122123const from = Math.max(0, stackFrame.source.uri.path.lastIndexOf(posix.sep, stackFrame.source.uri.path.length - suffixLength - 1));124return (from > 0 ? '...' : '') + stackFrame.source.uri.path.substring(from);125}126127async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>): Promise<void> {128if (session.parentSession) {129await expandTo(session.parentSession, tree);130}131await tree.expand(session);132}133134export class CallStackView extends ViewPane {135private stateMessage!: HTMLSpanElement;136private stateMessageLabel!: HTMLSpanElement;137private stateMessageLabelHover!: IManagedHover;138private onCallStackChangeScheduler: RunOnceScheduler;139private needsRefresh = false;140private ignoreSelectionChangedEvent = false;141private ignoreFocusStackFrameEvent = false;142143private dataSource!: CallStackDataSource;144private tree!: WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>;145private autoExpandedSessions = new Set<IDebugSession>();146private selectionNeedsUpdate = false;147148constructor(149private options: IViewletViewOptions,150@IContextMenuService contextMenuService: IContextMenuService,151@IDebugService private readonly debugService: IDebugService,152@IKeybindingService keybindingService: IKeybindingService,153@IInstantiationService instantiationService: IInstantiationService,154@IViewDescriptorService viewDescriptorService: IViewDescriptorService,155@IConfigurationService configurationService: IConfigurationService,156@IContextKeyService contextKeyService: IContextKeyService,157@IOpenerService openerService: IOpenerService,158@IThemeService themeService: IThemeService,159@IHoverService hoverService: IHoverService,160@IMenuService private readonly menuService: IMenuService,161) {162super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);163164// Create scheduler to prevent unnecessary flashing of tree when reacting to changes165this.onCallStackChangeScheduler = this._register(new RunOnceScheduler(async () => {166// Only show the global pause message if we do not display threads.167// Otherwise there will be a pause message per thread and there is no need for a global one.168const sessions = this.debugService.getModel().getSessions();169if (sessions.length === 0) {170this.autoExpandedSessions.clear();171}172173const thread = sessions.length === 1 && sessions[0].getAllThreads().length === 1 ? sessions[0].getAllThreads()[0] : undefined;174const stoppedDetails = sessions.length === 1 ? sessions[0].getStoppedDetails() : undefined;175if (stoppedDetails && (thread || typeof stoppedDetails.threadId !== 'number')) {176this.stateMessageLabel.textContent = stoppedDescription(stoppedDetails);177this.stateMessageLabelHover.update(stoppedText(stoppedDetails));178this.stateMessageLabel.classList.toggle('exception', stoppedDetails.reason === 'exception');179this.stateMessage.hidden = false;180} else if (sessions.length === 1 && sessions[0].state === State.Running) {181this.stateMessageLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running");182this.stateMessageLabelHover.update(sessions[0].getLabel());183this.stateMessageLabel.classList.remove('exception');184this.stateMessage.hidden = false;185} else {186this.stateMessage.hidden = true;187}188this.updateActions();189190this.needsRefresh = false;191await this.tree.updateChildren();192try {193const toExpand = new Set<IDebugSession>();194sessions.forEach(s => {195// Automatically expand sessions that have children, but only do this once.196if (s.parentSession && !this.autoExpandedSessions.has(s.parentSession)) {197toExpand.add(s.parentSession);198}199});200for (const session of toExpand) {201await expandTo(session, this.tree);202this.autoExpandedSessions.add(session);203}204} catch (e) {205// Ignore tree expand errors if element no longer present206}207if (this.selectionNeedsUpdate) {208this.selectionNeedsUpdate = false;209await this.updateTreeSelection();210}211}, 50));212}213214protected override renderHeaderTitle(container: HTMLElement): void {215super.renderHeaderTitle(container, this.options.title);216217this.stateMessage = dom.append(container, $('span.call-stack-state-message'));218this.stateMessage.hidden = true;219this.stateMessageLabel = dom.append(this.stateMessage, $('span.label'));220this.stateMessageLabelHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.stateMessage, ''));221}222223protected override renderBody(container: HTMLElement): void {224super.renderBody(container);225this.element.classList.add('debug-pane');226container.classList.add('debug-call-stack');227const treeContainer = renderViewTree(container);228229this.dataSource = new CallStackDataSource(this.debugService);230this.tree = this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [231this.instantiationService.createInstance(SessionsRenderer),232this.instantiationService.createInstance(ThreadsRenderer),233this.instantiationService.createInstance(StackFramesRenderer),234this.instantiationService.createInstance(ErrorsRenderer),235new LoadMoreRenderer(),236new ShowMoreRenderer()237], this.dataSource, {238accessibilityProvider: new CallStackAccessibilityProvider(),239compressionEnabled: true,240autoExpandSingleChildren: true,241identityProvider: {242getId: (element: CallStackItem) => {243if (typeof element === 'string') {244return element;245}246if (element instanceof Array) {247return `showMore ${element[0].getId()}`;248}249250return element.getId();251}252},253keyboardNavigationLabelProvider: {254getKeyboardNavigationLabel: (e: CallStackItem) => {255if (isDebugSession(e)) {256return e.getLabel();257}258if (e instanceof Thread) {259return `${e.name} ${e.stateLabel}`;260}261if (e instanceof StackFrame || typeof e === 'string') {262return e;263}264if (e instanceof ThreadAndSessionIds) {265return LoadMoreRenderer.LABEL;266}267268return localize('showMoreStackFrames2', "Show More Stack Frames");269},270getCompressedNodeKeyboardNavigationLabel: (e: CallStackItem[]) => {271const firstItem = e[0];272if (isDebugSession(firstItem)) {273return firstItem.getLabel();274}275return '';276}277},278expandOnlyOnTwistieClick: true,279overrideStyles: this.getLocationBasedColors().listOverrideStyles280});281282CONTEXT_CALLSTACK_FOCUSED.bindTo(this.tree.contextKeyService);283284this.tree.setInput(this.debugService.getModel());285this._register(this.tree);286this._register(this.tree.onDidOpen(async e => {287if (this.ignoreSelectionChangedEvent) {288return;289}290291const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession, options: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean } = {}) => {292this.ignoreFocusStackFrameEvent = true;293try {294this.debugService.focusStackFrame(stackFrame, thread, session, { ...options, ...{ explicit: true } });295} finally {296this.ignoreFocusStackFrameEvent = false;297}298};299300const element = e.element;301if (element instanceof StackFrame) {302const opts = {303preserveFocus: e.editorOptions.preserveFocus,304sideBySide: e.sideBySide,305pinned: e.editorOptions.pinned306};307focusStackFrame(element, element.thread, element.thread.session, opts);308}309if (element instanceof Thread) {310focusStackFrame(undefined, element, element.session);311}312if (isDebugSession(element)) {313focusStackFrame(undefined, undefined, element);314}315if (element instanceof ThreadAndSessionIds) {316const session = this.debugService.getModel().getSession(element.sessionId);317const thread = session && session.getThread(element.threadId);318if (thread) {319const totalFrames = thread.stoppedDetails?.totalFrames;320const remainingFramesCount = typeof totalFrames === 'number' ? (totalFrames - thread.getCallStack().length) : undefined;321// Get all the remaining frames322await (<Thread>thread).fetchCallStack(remainingFramesCount);323await this.tree.updateChildren();324}325}326if (element instanceof Array) {327element.forEach(sf => this.dataSource.deemphasizedStackFramesToShow.add(sf));328this.tree.updateChildren();329}330}));331332this._register(this.debugService.getModel().onDidChangeCallStack(() => {333if (!this.isBodyVisible()) {334this.needsRefresh = true;335return;336}337338if (!this.onCallStackChangeScheduler.isScheduled()) {339this.onCallStackChangeScheduler.schedule();340}341}));342const onFocusChange = Event.any<any>(this.debugService.getViewModel().onDidFocusStackFrame, this.debugService.getViewModel().onDidFocusSession);343this._register(onFocusChange(async () => {344if (this.ignoreFocusStackFrameEvent) {345return;346}347if (!this.isBodyVisible()) {348this.needsRefresh = true;349this.selectionNeedsUpdate = true;350return;351}352if (this.onCallStackChangeScheduler.isScheduled()) {353this.selectionNeedsUpdate = true;354return;355}356357await this.updateTreeSelection();358}));359this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));360361// Schedule the update of the call stack tree if the viewlet is opened after a session started #14684362if (this.debugService.state === State.Stopped) {363this.onCallStackChangeScheduler.schedule(0);364}365366this._register(this.onDidChangeBodyVisibility(visible => {367if (visible && this.needsRefresh) {368this.onCallStackChangeScheduler.schedule();369}370}));371372this._register(this.debugService.onDidNewSession(s => {373const sessionListeners: IDisposable[] = [];374sessionListeners.push(s.onDidChangeName(() => {375// this.tree.updateChildren is called on a delay after a session is added,376// so don't rerender if the tree doesn't have the node yet377if (this.tree.hasNode(s)) {378this.tree.rerender(s);379}380}));381sessionListeners.push(s.onDidEndAdapter(() => dispose(sessionListeners)));382if (s.parentSession) {383// A session we already expanded has a new child session, allow to expand it again.384this.autoExpandedSessions.delete(s.parentSession);385}386}));387}388389protected override layoutBody(height: number, width: number): void {390super.layoutBody(height, width);391this.tree.layout(height, width);392}393394override focus(): void {395super.focus();396this.tree.domFocus();397}398399collapseAll(): void {400this.tree.collapseAll();401}402403private async updateTreeSelection(): Promise<void> {404if (!this.tree || !this.tree.getInput()) {405// Tree not initialized yet406return;407}408409const updateSelectionAndReveal = (element: IStackFrame | IDebugSession) => {410this.ignoreSelectionChangedEvent = true;411try {412this.tree.setSelection([element]);413// If the element is outside of the screen bounds,414// position it in the middle415if (this.tree.getRelativeTop(element) === null) {416this.tree.reveal(element, 0.5);417} else {418this.tree.reveal(element);419}420} catch (e) { }421finally {422this.ignoreSelectionChangedEvent = false;423}424};425426const thread = this.debugService.getViewModel().focusedThread;427const session = this.debugService.getViewModel().focusedSession;428const stackFrame = this.debugService.getViewModel().focusedStackFrame;429if (!thread) {430if (!session) {431this.tree.setSelection([]);432} else {433updateSelectionAndReveal(session);434}435} else {436// Ignore errors from this expansions because we are not aware if we rendered the threads and sessions or we hide them to declutter the view437try {438await expandTo(thread.session, this.tree);439} catch (e) { }440try {441await this.tree.expand(thread);442} catch (e) { }443444const toReveal = stackFrame || session;445if (toReveal) {446updateSelectionAndReveal(toReveal);447}448}449}450451private onContextMenu(e: ITreeContextMenuEvent<CallStackItem>): void {452const element = e.element;453let overlay: [string, any][] = [];454if (isDebugSession(element)) {455overlay = getSessionContextOverlay(element);456} else if (element instanceof Thread) {457overlay = getThreadContextOverlay(element);458} else if (element instanceof StackFrame) {459overlay = getStackFrameContextOverlay(element);460}461462const contextKeyService = this.contextKeyService.createOverlay(overlay);463const menu = this.menuService.getMenuActions(MenuId.DebugCallStackContext, contextKeyService, { arg: getContextForContributedActions(element), shouldForwardArgs: true });464const result = getContextMenuActions(menu, 'inline');465this.contextMenuService.showContextMenu({466getAnchor: () => e.anchor,467getActions: () => result.secondary,468getActionsContext: () => getContext(element)469});470}471}472473interface IThreadTemplateData {474thread: HTMLElement;475name: HTMLElement;476stateLabel: HTMLSpanElement;477label: HighlightedLabel;478actionBar: ActionBar;479elementDisposable: DisposableStore;480templateDisposable: IDisposable;481}482483interface ISessionTemplateData {484session: HTMLElement;485name: HTMLElement;486stateLabel: HTMLSpanElement;487label: HighlightedLabel;488actionBar: ActionBar;489elementDisposable: DisposableStore;490templateDisposable: IDisposable;491}492493interface IErrorTemplateData {494label: HTMLElement;495templateDisposable: DisposableStore;496}497498interface ILabelTemplateData {499label: HTMLElement;500}501502interface IStackFrameTemplateData {503stackFrame: HTMLElement;504file: HTMLElement;505fileName: HTMLElement;506lineNumber: HTMLElement;507label: HighlightedLabel;508actionBar: ActionBar;509templateDisposable: DisposableStore;510elementDisposables: DisposableStore;511}512513function getSessionContextOverlay(session: IDebugSession): [string, any][] {514return [515[CONTEXT_CALLSTACK_ITEM_TYPE.key, 'session'],516[CONTEXT_CALLSTACK_SESSION_IS_ATTACH.key, isSessionAttach(session)],517[CONTEXT_CALLSTACK_ITEM_STOPPED.key, session.state === State.Stopped],518[CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD.key, session.getAllThreads().length === 1],519];520}521522class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, FuzzyScore, ISessionTemplateData> {523static readonly ID = 'session';524525constructor(526@IInstantiationService private readonly instantiationService: IInstantiationService,527@IContextKeyService private readonly contextKeyService: IContextKeyService,528@IHoverService private readonly hoverService: IHoverService,529@IMenuService private readonly menuService: IMenuService,530) { }531532get templateId(): string {533return SessionsRenderer.ID;534}535536renderTemplate(container: HTMLElement): ISessionTemplateData {537const session = dom.append(container, $('.session'));538dom.append(session, $(ThemeIcon.asCSSSelector(icons.callstackViewSession)));539const name = dom.append(session, $('.name'));540const stateLabel = dom.append(session, $('span.state.label.monaco-count-badge.long'));541const templateDisposable = new DisposableStore();542const label = templateDisposable.add(new HighlightedLabel(name));543544const stopActionViewItemDisposables = templateDisposable.add(new DisposableStore());545const actionBar = templateDisposable.add(new ActionBar(session, {546actionViewItemProvider: (action, options) => {547if ((action.id === STOP_ID || action.id === DISCONNECT_ID) && action instanceof MenuItemAction) {548stopActionViewItemDisposables.clear();549const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, stopActionViewItemDisposables, accessor, { ...options, menuAsChild: false }));550if (item) {551return item;552}553}554555if (action instanceof MenuItemAction) {556return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });557} else if (action instanceof SubmenuItemAction) {558return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });559}560561return undefined;562}563}));564565const elementDisposable = templateDisposable.add(new DisposableStore());566return { session, name, stateLabel, label, actionBar, elementDisposable, templateDisposable };567}568569renderElement(element: ITreeNode<IDebugSession, FuzzyScore>, _: number, data: ISessionTemplateData): void {570this.doRenderElement(element.element, createMatches(element.filterData), data);571}572573renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IDebugSession>, FuzzyScore>, _index: number, templateData: ISessionTemplateData): void {574const lastElement = node.element.elements[node.element.elements.length - 1];575const matches = createMatches(node.filterData);576this.doRenderElement(lastElement, matches, templateData);577}578579private doRenderElement(session: IDebugSession, matches: IMatch[], data: ISessionTemplateData): void {580const sessionHover = data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.session, localize({ key: 'session', comment: ['Session is a noun'] }, "Session")));581data.label.set(session.getLabel(), matches);582const stoppedDetails = session.getStoppedDetails();583const thread = session.getAllThreads().find(t => t.stopped);584585const contextKeyService = this.contextKeyService.createOverlay(getSessionContextOverlay(session));586const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService));587588const setupActionBar = () => {589data.actionBar.clear();590591const { primary } = getActionBarActions(menu.getActions({ arg: getContextForContributedActions(session), shouldForwardArgs: true }), 'inline');592data.actionBar.push(primary, { icon: true, label: false });593// We need to set our internal context on the action bar, since our commands depend on that one594// While the external context our extensions rely on595data.actionBar.context = getContext(session);596};597data.elementDisposable.add(menu.onDidChange(() => setupActionBar()));598setupActionBar();599600data.stateLabel.style.display = '';601602if (stoppedDetails) {603data.stateLabel.textContent = stoppedDescription(stoppedDetails);604sessionHover.update(`${session.getLabel()}: ${stoppedText(stoppedDetails)}`);605data.stateLabel.classList.toggle('exception', stoppedDetails.reason === 'exception');606} else if (thread && thread.stoppedDetails) {607data.stateLabel.textContent = stoppedDescription(thread.stoppedDetails);608sessionHover.update(`${session.getLabel()}: ${stoppedText(thread.stoppedDetails)}`);609data.stateLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception');610} else {611data.stateLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running");612data.stateLabel.classList.remove('exception');613}614}615616disposeTemplate(templateData: ISessionTemplateData): void {617templateData.templateDisposable.dispose();618}619620disposeElement(_element: ITreeNode<IDebugSession, FuzzyScore>, _: number, templateData: ISessionTemplateData): void {621templateData.elementDisposable.clear();622}623624disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<IDebugSession>, FuzzyScore>, index: number, templateData: ISessionTemplateData): void {625templateData.elementDisposable.clear();626}627}628629function getThreadContextOverlay(thread: IThread): [string, any][] {630return [631[CONTEXT_CALLSTACK_ITEM_TYPE.key, 'thread'],632[CONTEXT_CALLSTACK_ITEM_STOPPED.key, thread.stopped]633];634}635636class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, IThreadTemplateData> {637static readonly ID = 'thread';638639constructor(640@IContextKeyService private readonly contextKeyService: IContextKeyService,641@IHoverService private readonly hoverService: IHoverService,642@IMenuService private readonly menuService: IMenuService,643) { }644645get templateId(): string {646return ThreadsRenderer.ID;647}648649renderTemplate(container: HTMLElement): IThreadTemplateData {650const thread = dom.append(container, $('.thread'));651const name = dom.append(thread, $('.name'));652const stateLabel = dom.append(thread, $('span.state.label.monaco-count-badge.long'));653654const templateDisposable = new DisposableStore();655const label = templateDisposable.add(new HighlightedLabel(name));656657const actionBar = templateDisposable.add(new ActionBar(thread));658const elementDisposable = templateDisposable.add(new DisposableStore());659660return { thread, name, stateLabel, label, actionBar, elementDisposable, templateDisposable };661}662663renderElement(element: ITreeNode<IThread, FuzzyScore>, _index: number, data: IThreadTemplateData): void {664const thread = element.element;665data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name));666data.label.set(thread.name, createMatches(element.filterData));667data.stateLabel.textContent = thread.stateLabel;668data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception');669670const contextKeyService = this.contextKeyService.createOverlay(getThreadContextOverlay(thread));671const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService));672673const setupActionBar = () => {674data.actionBar.clear();675676const { primary } = getActionBarActions(menu.getActions({ arg: getContextForContributedActions(thread), shouldForwardArgs: true }), 'inline');677data.actionBar.push(primary, { icon: true, label: false });678// We need to set our internal context on the action bar, since our commands depend on that one679// While the external context our extensions rely on680data.actionBar.context = getContext(thread);681};682data.elementDisposable.add(menu.onDidChange(() => setupActionBar()));683setupActionBar();684}685686renderCompressedElements(_node: ITreeNode<ICompressedTreeNode<IThread>, FuzzyScore>, _index: number, _templateData: IThreadTemplateData): void {687throw new Error('Method not implemented.');688}689690disposeElement(_element: any, _index: number, templateData: IThreadTemplateData): void {691templateData.elementDisposable.clear();692}693694disposeTemplate(templateData: IThreadTemplateData): void {695templateData.templateDisposable.dispose();696}697}698699function getStackFrameContextOverlay(stackFrame: IStackFrame): [string, any][] {700return [701[CONTEXT_CALLSTACK_ITEM_TYPE.key, 'stackFrame'],702[CONTEXT_STACK_FRAME_SUPPORTS_RESTART.key, stackFrame.canRestart]703];704}705706class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, FuzzyScore, IStackFrameTemplateData> {707static readonly ID = 'stackFrame';708709constructor(710@IHoverService private readonly hoverService: IHoverService,711@ILabelService private readonly labelService: ILabelService,712@INotificationService private readonly notificationService: INotificationService,713) { }714715get templateId(): string {716return StackFramesRenderer.ID;717}718719renderTemplate(container: HTMLElement): IStackFrameTemplateData {720const stackFrame = dom.append(container, $('.stack-frame'));721const labelDiv = dom.append(stackFrame, $('span.label.expression'));722const file = dom.append(stackFrame, $('.file'));723const fileName = dom.append(file, $('span.file-name'));724const wrapper = dom.append(file, $('span.line-number-wrapper'));725const lineNumber = dom.append(wrapper, $('span.line-number.monaco-count-badge'));726727const templateDisposable = new DisposableStore();728const elementDisposables = new DisposableStore();729templateDisposable.add(elementDisposables);730const label = templateDisposable.add(new HighlightedLabel(labelDiv));731const actionBar = templateDisposable.add(new ActionBar(stackFrame));732733return { file, fileName, label, lineNumber, stackFrame, actionBar, templateDisposable, elementDisposables };734}735736renderElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, data: IStackFrameTemplateData): void {737const stackFrame = element.element;738data.stackFrame.classList.toggle('disabled', !stackFrame.source || !stackFrame.source.available || isFrameDeemphasized(stackFrame));739data.stackFrame.classList.toggle('label', stackFrame.presentationHint === 'label');740const hasActions = !!stackFrame.thread.session.capabilities.supportsRestartFrame && stackFrame.presentationHint !== 'label' && stackFrame.presentationHint !== 'subtle' && stackFrame.canRestart;741data.stackFrame.classList.toggle('has-actions', hasActions);742743let title = stackFrame.source.inMemory ? stackFrame.source.uri.path : this.labelService.getUriLabel(stackFrame.source.uri);744if (stackFrame.source.raw.origin) {745title += `\n${stackFrame.source.raw.origin}`;746}747data.elementDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.file, title));748749data.label.set(stackFrame.name, createMatches(element.filterData), stackFrame.name);750data.fileName.textContent = getSpecificSourceName(stackFrame);751if (stackFrame.range.startLineNumber !== undefined) {752data.lineNumber.textContent = `${stackFrame.range.startLineNumber}`;753if (stackFrame.range.startColumn) {754data.lineNumber.textContent += `:${stackFrame.range.startColumn}`;755}756data.lineNumber.classList.remove('unavailable');757} else {758data.lineNumber.classList.add('unavailable');759}760761data.actionBar.clear();762if (hasActions) {763const action = data.elementDisposables.add(new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => {764try {765await stackFrame.restart();766} catch (e) {767this.notificationService.error(e);768}769}));770data.actionBar.push(action, { icon: true, label: false });771}772}773774renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IStackFrame>, FuzzyScore>, index: number, templateData: IStackFrameTemplateData): void {775throw new Error('Method not implemented.');776}777disposeElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, templateData: IStackFrameTemplateData): void {778templateData.elementDisposables.clear();779}780781disposeTemplate(templateData: IStackFrameTemplateData): void {782templateData.templateDisposable.dispose();783}784}785786class ErrorsRenderer implements ICompressibleTreeRenderer<string, FuzzyScore, IErrorTemplateData> {787static readonly ID = 'error';788789get templateId(): string {790return ErrorsRenderer.ID;791}792793constructor(794@IHoverService private readonly hoverService: IHoverService795) {796}797798renderTemplate(container: HTMLElement): IErrorTemplateData {799const label = dom.append(container, $('.error'));800801return { label, templateDisposable: new DisposableStore() };802}803804renderElement(element: ITreeNode<string, FuzzyScore>, index: number, data: IErrorTemplateData): void {805const error = element.element;806data.label.textContent = error;807data.templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.label, error));808}809810renderCompressedElements(node: ITreeNode<ICompressedTreeNode<string>, FuzzyScore>, index: number, templateData: IErrorTemplateData): void {811throw new Error('Method not implemented.');812}813814disposeTemplate(templateData: IErrorTemplateData): void {815// noop816}817}818819class LoadMoreRenderer implements ICompressibleTreeRenderer<ThreadAndSessionIds, FuzzyScore, ILabelTemplateData> {820static readonly ID = 'loadMore';821static readonly LABEL = localize('loadAllStackFrames', "Load More Stack Frames");822823constructor() { }824825get templateId(): string {826return LoadMoreRenderer.ID;827}828829renderTemplate(container: HTMLElement): ILabelTemplateData {830const label = dom.append(container, $('.load-all'));831label.style.color = asCssVariable(textLinkForeground);832return { label };833}834835renderElement(element: ITreeNode<ThreadAndSessionIds, FuzzyScore>, index: number, data: ILabelTemplateData): void {836data.label.textContent = LoadMoreRenderer.LABEL;837}838839renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ThreadAndSessionIds>, FuzzyScore>, index: number, templateData: ILabelTemplateData): void {840throw new Error('Method not implemented.');841}842843disposeTemplate(templateData: ILabelTemplateData): void {844// noop845}846}847848class ShowMoreRenderer implements ICompressibleTreeRenderer<IStackFrame[], FuzzyScore, ILabelTemplateData> {849static readonly ID = 'showMore';850851constructor() { }852853854get templateId(): string {855return ShowMoreRenderer.ID;856}857858renderTemplate(container: HTMLElement): ILabelTemplateData {859const label = dom.append(container, $('.show-more'));860label.style.color = asCssVariable(textLinkForeground);861return { label };862}863864renderElement(element: ITreeNode<IStackFrame[], FuzzyScore>, index: number, data: ILabelTemplateData): void {865const stackFrames = element.element;866if (stackFrames.every(sf => !!(sf.source && sf.source.origin && sf.source.origin === stackFrames[0].source.origin))) {867data.label.textContent = localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin);868} else {869data.label.textContent = localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length);870}871}872873renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IStackFrame[]>, FuzzyScore>, index: number, templateData: ILabelTemplateData): void {874throw new Error('Method not implemented.');875}876877disposeTemplate(templateData: ILabelTemplateData): void {878// noop879}880}881882class CallStackDelegate implements IListVirtualDelegate<CallStackItem> {883884getHeight(element: CallStackItem): number {885if (element instanceof StackFrame && element.presentationHint === 'label') {886return 16;887}888if (element instanceof ThreadAndSessionIds || element instanceof Array) {889return 16;890}891892return 22;893}894895getTemplateId(element: CallStackItem): string {896if (isDebugSession(element)) {897return SessionsRenderer.ID;898}899if (element instanceof Thread) {900return ThreadsRenderer.ID;901}902if (element instanceof StackFrame) {903return StackFramesRenderer.ID;904}905if (typeof element === 'string') {906return ErrorsRenderer.ID;907}908if (element instanceof ThreadAndSessionIds) {909return LoadMoreRenderer.ID;910}911912// element instanceof Array913return ShowMoreRenderer.ID;914}915}916917function stoppedText(stoppedDetails: IRawStoppedDetails): string {918return stoppedDetails.text ?? stoppedDescription(stoppedDetails);919}920921function stoppedDescription(stoppedDetails: IRawStoppedDetails): string {922return stoppedDetails.description ||923(stoppedDetails.reason ? localize({ key: 'pausedOn', comment: ['indicates reason for program being paused'] }, "Paused on {0}", stoppedDetails.reason) : localize('paused', "Paused"));924}925926function isDebugModel(obj: any): obj is IDebugModel {927return typeof obj.getSessions === 'function';928}929930function isDebugSession(obj: any): obj is IDebugSession {931return obj && typeof obj.getAllThreads === 'function';932}933934class CallStackDataSource implements IAsyncDataSource<IDebugModel, CallStackItem> {935deemphasizedStackFramesToShow = new WeakSet<IStackFrame>();936937constructor(private debugService: IDebugService) { }938939hasChildren(element: IDebugModel | CallStackItem): boolean {940if (isDebugSession(element)) {941const threads = element.getAllThreads();942return (threads.length > 1) || (threads.length === 1 && threads[0].stopped) || !!(this.debugService.getModel().getSessions().find(s => s.parentSession === element));943}944945return isDebugModel(element) || (element instanceof Thread && element.stopped);946}947948async getChildren(element: IDebugModel | CallStackItem): Promise<CallStackItem[]> {949if (isDebugModel(element)) {950const sessions = element.getSessions();951if (sessions.length === 0) {952return Promise.resolve([]);953}954if (sessions.length > 1 || this.debugService.getViewModel().isMultiSessionView()) {955return Promise.resolve(sessions.filter(s => !s.parentSession));956}957958const threads = sessions[0].getAllThreads();959// Only show the threads in the call stack if there is more than 1 thread.960return threads.length === 1 ? this.getThreadChildren(<Thread>threads[0]) : Promise.resolve(threads);961} else if (isDebugSession(element)) {962const childSessions = this.debugService.getModel().getSessions().filter(s => s.parentSession === element);963const threads: CallStackItem[] = element.getAllThreads();964if (threads.length === 1) {965// Do not show thread when there is only one to be compact.966const children = await this.getThreadChildren(<Thread>threads[0]);967return children.concat(childSessions);968}969970return Promise.resolve(threads.concat(childSessions));971} else {972return this.getThreadChildren(<Thread>element);973}974}975976private getThreadChildren(thread: Thread): Promise<CallStackItem[]> {977return this.getThreadCallstack(thread).then(children => {978// Check if some stack frames should be hidden under a parent element since they are deemphasized979const result: CallStackItem[] = [];980children.forEach((child, index) => {981if (child instanceof StackFrame && child.source && isFrameDeemphasized(child)) {982// Check if the user clicked to show the deemphasized source983if (!this.deemphasizedStackFramesToShow.has(child)) {984if (result.length) {985const last = result[result.length - 1];986if (last instanceof Array) {987// Collect all the stackframes that will be "collapsed"988last.push(child);989return;990}991}992993const nextChild = index < children.length - 1 ? children[index + 1] : undefined;994if (nextChild instanceof StackFrame && nextChild.source && isFrameDeemphasized(nextChild)) {995// Start collecting stackframes that will be "collapsed"996result.push([child]);997return;998}999}1000}10011002result.push(child);1003});10041005return result;1006});1007}10081009private async getThreadCallstack(thread: Thread): Promise<Array<IStackFrame | string | ThreadAndSessionIds>> {1010let callStack: any[] = thread.getCallStack();1011if (!callStack || !callStack.length) {1012await thread.fetchCallStack();1013callStack = thread.getCallStack();1014}10151016if (callStack.length === 1 && thread.session.capabilities.supportsDelayedStackTraceLoading && thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > 1) {1017// To reduce flashing of the call stack view simply append the stale call stack1018// once we have the correct data the tree will refresh and we will no longer display it.1019callStack = callStack.concat(thread.getStaleCallStack().slice(1));1020}10211022if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) {1023callStack = callStack.concat([thread.stoppedDetails.framesErrorMessage]);1024}1025if (!thread.reachedEndOfCallStack && thread.stoppedDetails) {1026callStack = callStack.concat([new ThreadAndSessionIds(thread.session.getId(), thread.threadId)]);1027}10281029return callStack;1030}1031}10321033class CallStackAccessibilityProvider implements IListAccessibilityProvider<CallStackItem> {10341035getWidgetAriaLabel(): string {1036return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack");1037}10381039getWidgetRole(): AriaRole {1040// Use treegrid as a role since each element can have additional actions inside #1462101041return 'treegrid';1042}10431044getRole(_element: CallStackItem): AriaRole | undefined {1045return 'row';1046}10471048getAriaLabel(element: CallStackItem): string {1049if (element instanceof Thread) {1050return localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel);1051}1052if (element instanceof StackFrame) {1053return localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element));1054}1055if (isDebugSession(element)) {1056const thread = element.getAllThreads().find(t => t.stopped);1057const state = thread ? thread.stateLabel : localize({ key: 'running', comment: ['indicates state'] }, "Running");1058return localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state);1059}1060if (typeof element === 'string') {1061return element;1062}1063if (element instanceof Array) {1064return localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length);1065}10661067// element instanceof ThreadAndSessionIds1068return LoadMoreRenderer.LABEL;1069}1070}10711072class CallStackCompressionDelegate implements ITreeCompressionDelegate<CallStackItem> {10731074constructor(private readonly debugService: IDebugService) { }10751076isIncompressible(stat: CallStackItem): boolean {1077if (isDebugSession(stat)) {1078if (stat.compact) {1079return false;1080}1081const sessions = this.debugService.getModel().getSessions();1082if (sessions.some(s => s.parentSession === stat && s.compact)) {1083return false;1084}10851086return true;1087}10881089return true;1090}1091}10921093registerAction2(class Collapse extends ViewAction<CallStackView> {1094constructor() {1095super({1096id: 'callStack.collapse',1097viewId: CALLSTACK_VIEW_ID,1098title: localize('collapse', "Collapse All"),1099f1: false,1100icon: Codicon.collapseAll,1101precondition: CONTEXT_DEBUG_STATE.isEqualTo(getStateLabel(State.Stopped)),1102menu: {1103id: MenuId.ViewTitle,1104order: 10,1105group: 'navigation',1106when: ContextKeyExpr.equals('view', CALLSTACK_VIEW_ID)1107}1108});1109}11101111runInView(_accessor: ServicesAccessor, view: CallStackView) {1112view.collapseAll();1113}1114});11151116function registerCallStackInlineMenuItem(id: string, title: string | ICommandActionTitle, icon: Icon, when: ContextKeyExpression, order: number, precondition?: ContextKeyExpression): void {1117MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, {1118group: 'inline',1119order,1120when,1121command: { id, title, icon, precondition }1122});1123}11241125const threadOrSessionWithOneThread = ContextKeyExpr.or(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD))!;1126registerCallStackInlineMenuItem(PAUSE_ID, PAUSE_LABEL, icons.debugPause, ContextKeyExpr.and(threadOrSessionWithOneThread, CONTEXT_CALLSTACK_ITEM_STOPPED.toNegated())!, 10, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG.toNegated());1127registerCallStackInlineMenuItem(CONTINUE_ID, CONTINUE_LABEL, icons.debugContinue, ContextKeyExpr.and(threadOrSessionWithOneThread, CONTEXT_CALLSTACK_ITEM_STOPPED)!, 10);1128registerCallStackInlineMenuItem(STEP_OVER_ID, STEP_OVER_LABEL, icons.debugStepOver, threadOrSessionWithOneThread, 20, CONTEXT_CALLSTACK_ITEM_STOPPED);1129registerCallStackInlineMenuItem(STEP_INTO_ID, STEP_INTO_LABEL, icons.debugStepInto, threadOrSessionWithOneThread, 30, CONTEXT_CALLSTACK_ITEM_STOPPED);1130registerCallStackInlineMenuItem(STEP_OUT_ID, STEP_OUT_LABEL, icons.debugStepOut, threadOrSessionWithOneThread, 40, CONTEXT_CALLSTACK_ITEM_STOPPED);1131registerCallStackInlineMenuItem(RESTART_SESSION_ID, RESTART_LABEL, icons.debugRestart, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), 50);1132registerCallStackInlineMenuItem(STOP_ID, STOP_LABEL, icons.debugStop, ContextKeyExpr.and(CONTEXT_CALLSTACK_SESSION_IS_ATTACH.toNegated(), CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'))!, 60);1133registerCallStackInlineMenuItem(DISCONNECT_ID, DISCONNECT_LABEL, icons.debugDisconnect, ContextKeyExpr.and(CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'))!, 60);113411351136