Path: blob/main/src/vs/workbench/contrib/debug/browser/callStackView.ts
5221 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 type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';10import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';11import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';12import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';13import { ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js';14import { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js';15import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js';16import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';17import { Action } from '../../../../base/common/actions.js';18import { RunOnceScheduler } from '../../../../base/common/async.js';19import { Codicon } from '../../../../base/common/codicons.js';20import { Event } from '../../../../base/common/event.js';21import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js';22import { DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js';23import { posix } from '../../../../base/common/path.js';24import { commonSuffixLength } from '../../../../base/common/strings.js';25import { ThemeIcon } from '../../../../base/common/themables.js';26import { IRange } from '../../../../editor/common/core/range.js';27import { localize } from '../../../../nls.js';28import { ICommandActionTitle, Icon } from '../../../../platform/action/common/action.js';29import { getActionBarActions, getContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';30import { IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';31import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';32import { ContextKeyExpr, ContextKeyExpression, ContextKeyValue, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';33import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';34import { IHoverService } from '../../../../platform/hover/browser/hover.js';35import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';36import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';37import { ILabelService } from '../../../../platform/label/common/label.js';38import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js';39import { INotificationService } from '../../../../platform/notification/common/notification.js';40import { IOpenerService } from '../../../../platform/opener/common/opener.js';41import { asCssVariable, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';42import { IThemeService } from '../../../../platform/theme/common/themeService.js';43import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js';44import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';45import { IViewDescriptorService } from '../../../common/views.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 { renderViewTree } from './baseDebugView.js';50import { 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';51import * as icons from './debugIcons.js';52import { createDisconnectMenuItemAction } from './debugToolBar.js';5354const $ = dom.$;5556type CallStackItem = IStackFrame | IThread | IDebugSession | string | ThreadAndSessionIds | IStackFrame[];5758interface ICallStackItemContext {59sessionId: string;60threadId?: string;61frameId?: string;62frameName?: string;63frameLocation?: { range: IRange; source: DebugProtocol.Source };64}6566function getSessionContext(element: IDebugSession): ICallStackItemContext {67return {68sessionId: element.getId()69};70}7172function getThreadContext(element: IThread): ICallStackItemContext {73return {74...getSessionContext(element.session),75threadId: element.getId()76};77}7879function getStackFrameContext(element: StackFrame): ICallStackItemContext {80return {81...getThreadContext(element.thread),82frameId: element.getId(),83frameName: element.name,84frameLocation: { range: element.range, source: element.source.raw }85};86}8788export function getContext(element: CallStackItem | null): ICallStackItemContext | undefined {89if (element instanceof StackFrame) {90return getStackFrameContext(element);91} else if (element instanceof Thread) {92return getThreadContext(element);93} else if (isDebugSession(element)) {94return getSessionContext(element);95} else {96return undefined;97}98}99100// Extensions depend on this context, should not be changed even though it is not fully deterministic101export function getContextForContributedActions(element: CallStackItem | null): string | number {102if (element instanceof StackFrame) {103if (element.source.inMemory) {104return element.source.raw.path || element.source.reference || element.source.name;105}106107return element.source.uri.toString();108}109if (element instanceof Thread) {110return element.threadId;111}112if (isDebugSession(element)) {113return element.getId();114}115116return '';117}118119export function getSpecificSourceName(stackFrame: IStackFrame): string {120// To reduce flashing of the path name and the way we fetch stack frames121// We need to compute the source name based on the other frames in the stale call stack122let callStack = (<Thread>stackFrame.thread).getStaleCallStack();123callStack = callStack.length > 0 ? callStack : stackFrame.thread.getCallStack();124const otherSources = callStack.map(sf => sf.source).filter(s => s !== stackFrame.source);125let suffixLength = 0;126otherSources.forEach(s => {127if (s.name === stackFrame.source.name) {128suffixLength = Math.max(suffixLength, commonSuffixLength(stackFrame.source.uri.path, s.uri.path));129}130});131if (suffixLength === 0) {132return stackFrame.source.name;133}134135const from = Math.max(0, stackFrame.source.uri.path.lastIndexOf(posix.sep, stackFrame.source.uri.path.length - suffixLength - 1));136return (from > 0 ? '...' : '') + stackFrame.source.uri.path.substring(from);137}138139async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>): Promise<void> {140if (session.parentSession) {141await expandTo(session.parentSession, tree);142}143await tree.expand(session);144}145146export class CallStackView extends ViewPane {147private stateMessage!: HTMLSpanElement;148private stateMessageLabel!: HTMLSpanElement;149private stateMessageLabelHover!: IManagedHover;150private onCallStackChangeScheduler: RunOnceScheduler;151private needsRefresh = false;152private ignoreSelectionChangedEvent = false;153private ignoreFocusStackFrameEvent = false;154155private dataSource!: CallStackDataSource;156private tree!: WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>;157private autoExpandedSessions = new Set<IDebugSession>();158private selectionNeedsUpdate = false;159160constructor(161private options: IViewletViewOptions,162@IContextMenuService contextMenuService: IContextMenuService,163@IDebugService private readonly debugService: IDebugService,164@IKeybindingService keybindingService: IKeybindingService,165@IInstantiationService instantiationService: IInstantiationService,166@IViewDescriptorService viewDescriptorService: IViewDescriptorService,167@IConfigurationService configurationService: IConfigurationService,168@IContextKeyService contextKeyService: IContextKeyService,169@IOpenerService openerService: IOpenerService,170@IThemeService themeService: IThemeService,171@IHoverService hoverService: IHoverService,172@IMenuService private readonly menuService: IMenuService,173) {174super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);175176// Create scheduler to prevent unnecessary flashing of tree when reacting to changes177this.onCallStackChangeScheduler = this._register(new RunOnceScheduler(async () => {178// Only show the global pause message if we do not display threads.179// Otherwise there will be a pause message per thread and there is no need for a global one.180const sessions = this.debugService.getModel().getSessions();181if (sessions.length === 0) {182this.autoExpandedSessions.clear();183}184185const thread = sessions.length === 1 && sessions[0].getAllThreads().length === 1 ? sessions[0].getAllThreads()[0] : undefined;186const stoppedDetails = sessions.length === 1 ? sessions[0].getStoppedDetails() : undefined;187if (stoppedDetails && (thread || typeof stoppedDetails.threadId !== 'number')) {188this.stateMessageLabel.textContent = stoppedDescription(stoppedDetails);189this.stateMessageLabelHover.update(stoppedText(stoppedDetails));190this.stateMessageLabel.classList.toggle('exception', stoppedDetails.reason === 'exception');191this.stateMessage.hidden = false;192} else if (sessions.length === 1 && sessions[0].state === State.Running) {193this.stateMessageLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running");194this.stateMessageLabelHover.update(sessions[0].getLabel());195this.stateMessageLabel.classList.remove('exception');196this.stateMessage.hidden = false;197} else {198this.stateMessage.hidden = true;199}200this.updateActions();201202this.needsRefresh = false;203await this.tree.updateChildren();204try {205const toExpand = new Set<IDebugSession>();206sessions.forEach(s => {207// Automatically expand sessions that have children, but only do this once.208if (s.parentSession && !this.autoExpandedSessions.has(s.parentSession)) {209toExpand.add(s.parentSession);210}211});212for (const session of toExpand) {213await expandTo(session, this.tree);214this.autoExpandedSessions.add(session);215}216} catch (e) {217// Ignore tree expand errors if element no longer present218}219if (this.selectionNeedsUpdate) {220this.selectionNeedsUpdate = false;221await this.updateTreeSelection();222}223}, 50));224}225226protected override renderHeaderTitle(container: HTMLElement): void {227super.renderHeaderTitle(container, this.options.title);228229this.stateMessage = dom.append(container, $('span.call-stack-state-message'));230this.stateMessage.hidden = true;231this.stateMessageLabel = dom.append(this.stateMessage, $('span.label'));232this.stateMessageLabelHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.stateMessage, ''));233}234235protected override renderBody(container: HTMLElement): void {236super.renderBody(container);237this.element.classList.add('debug-pane');238container.classList.add('debug-call-stack');239const treeContainer = renderViewTree(container);240241this.dataSource = new CallStackDataSource(this.debugService);242this.tree = this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [243this.instantiationService.createInstance(SessionsRenderer),244this.instantiationService.createInstance(ThreadsRenderer),245this.instantiationService.createInstance(StackFramesRenderer),246this.instantiationService.createInstance(ErrorsRenderer),247new LoadMoreRenderer(),248new ShowMoreRenderer()249], this.dataSource, {250accessibilityProvider: new CallStackAccessibilityProvider(),251compressionEnabled: true,252autoExpandSingleChildren: true,253identityProvider: {254getId: (element: CallStackItem) => {255if (typeof element === 'string') {256return element;257}258if (element instanceof Array) {259return `showMore ${element[0].getId()}`;260}261262return element.getId();263}264},265keyboardNavigationLabelProvider: {266getKeyboardNavigationLabel: (e: CallStackItem) => {267if (isDebugSession(e)) {268return e.getLabel();269}270if (e instanceof Thread) {271return `${e.name} ${e.stateLabel}`;272}273if (e instanceof StackFrame || typeof e === 'string') {274return e;275}276if (e instanceof ThreadAndSessionIds) {277return LoadMoreRenderer.LABEL;278}279280return localize('showMoreStackFrames2', "Show More Stack Frames");281},282getCompressedNodeKeyboardNavigationLabel: (e: CallStackItem[]) => {283const firstItem = e[0];284if (isDebugSession(firstItem)) {285return firstItem.getLabel();286}287return '';288}289},290expandOnlyOnTwistieClick: true,291overrideStyles: this.getLocationBasedColors().listOverrideStyles292});293294CONTEXT_CALLSTACK_FOCUSED.bindTo(this.tree.contextKeyService);295296this.tree.setInput(this.debugService.getModel());297this._register(this.tree);298this._register(this.tree.onDidOpen(async e => {299if (this.ignoreSelectionChangedEvent) {300return;301}302303const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession, options: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean } = {}) => {304this.ignoreFocusStackFrameEvent = true;305try {306this.debugService.focusStackFrame(stackFrame, thread, session, { ...options, ...{ explicit: true } });307} finally {308this.ignoreFocusStackFrameEvent = false;309}310};311312const element = e.element;313if (element instanceof StackFrame) {314const opts = {315preserveFocus: e.editorOptions.preserveFocus,316sideBySide: e.sideBySide,317pinned: e.editorOptions.pinned318};319focusStackFrame(element, element.thread, element.thread.session, opts);320}321if (element instanceof Thread) {322focusStackFrame(undefined, element, element.session);323}324if (isDebugSession(element)) {325focusStackFrame(undefined, undefined, element);326}327if (element instanceof ThreadAndSessionIds) {328const session = this.debugService.getModel().getSession(element.sessionId);329const thread = session && session.getThread(element.threadId);330if (thread) {331const totalFrames = thread.stoppedDetails?.totalFrames;332const remainingFramesCount = typeof totalFrames === 'number' ? (totalFrames - thread.getCallStack().length) : undefined;333// Get all the remaining frames334await (<Thread>thread).fetchCallStack(remainingFramesCount);335await this.tree.updateChildren();336}337}338if (element instanceof Array) {339element.forEach(sf => this.dataSource.deemphasizedStackFramesToShow.add(sf));340this.tree.updateChildren();341}342}));343344this._register(this.debugService.getModel().onDidChangeCallStack(() => {345if (!this.isBodyVisible()) {346this.needsRefresh = true;347return;348}349350if (!this.onCallStackChangeScheduler.isScheduled()) {351this.onCallStackChangeScheduler.schedule();352}353}));354const onFocusChange = Event.any<unknown>(this.debugService.getViewModel().onDidFocusStackFrame, this.debugService.getViewModel().onDidFocusSession);355this._register(onFocusChange(async () => {356if (this.ignoreFocusStackFrameEvent) {357return;358}359if (!this.isBodyVisible()) {360this.needsRefresh = true;361this.selectionNeedsUpdate = true;362return;363}364if (this.onCallStackChangeScheduler.isScheduled()) {365this.selectionNeedsUpdate = true;366return;367}368369await this.updateTreeSelection();370}));371this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));372373// Schedule the update of the call stack tree if the viewlet is opened after a session started #14684374if (this.debugService.state === State.Stopped) {375this.onCallStackChangeScheduler.schedule(0);376}377378this._register(this.onDidChangeBodyVisibility(visible => {379if (visible && this.needsRefresh) {380this.onCallStackChangeScheduler.schedule();381}382}));383384this._register(this.debugService.onDidNewSession(s => {385const sessionListeners: IDisposable[] = [];386sessionListeners.push(s.onDidChangeName(() => {387// this.tree.updateChildren is called on a delay after a session is added,388// so don't rerender if the tree doesn't have the node yet389if (this.tree.hasNode(s)) {390this.tree.rerender(s);391}392}));393sessionListeners.push(s.onDidEndAdapter(() => dispose(sessionListeners)));394if (s.parentSession) {395// A session we already expanded has a new child session, allow to expand it again.396this.autoExpandedSessions.delete(s.parentSession);397}398}));399}400401protected override layoutBody(height: number, width: number): void {402super.layoutBody(height, width);403this.tree.layout(height, width);404}405406override focus(): void {407super.focus();408this.tree.domFocus();409}410411collapseAll(): void {412this.tree.collapseAll();413}414415private async updateTreeSelection(): Promise<void> {416if (!this.tree || !this.tree.getInput()) {417// Tree not initialized yet418return;419}420421const updateSelectionAndReveal = (element: IStackFrame | IDebugSession) => {422this.ignoreSelectionChangedEvent = true;423try {424this.tree.setSelection([element]);425// If the element is outside of the screen bounds,426// position it in the middle427if (this.tree.getRelativeTop(element) === null) {428this.tree.reveal(element, 0.5);429} else {430this.tree.reveal(element);431}432} catch (e) { }433finally {434this.ignoreSelectionChangedEvent = false;435}436};437438const thread = this.debugService.getViewModel().focusedThread;439const session = this.debugService.getViewModel().focusedSession;440const stackFrame = this.debugService.getViewModel().focusedStackFrame;441if (!thread) {442if (!session) {443this.tree.setSelection([]);444} else {445updateSelectionAndReveal(session);446}447} else {448// Ignore errors from this expansions because we are not aware if we rendered the threads and sessions or we hide them to declutter the view449try {450await expandTo(thread.session, this.tree);451} catch (e) { }452try {453await this.tree.expand(thread);454} catch (e) { }455456const toReveal = stackFrame || session;457if (toReveal) {458updateSelectionAndReveal(toReveal);459}460}461}462463private onContextMenu(e: ITreeContextMenuEvent<CallStackItem>): void {464const element = e.element;465let overlay: [string, ContextKeyValue][] = [];466if (isDebugSession(element)) {467overlay = getSessionContextOverlay(element);468} else if (element instanceof Thread) {469overlay = getThreadContextOverlay(element);470} else if (element instanceof StackFrame) {471overlay = getStackFrameContextOverlay(element);472}473474const contextKeyService = this.contextKeyService.createOverlay(overlay);475const menu = this.menuService.getMenuActions(MenuId.DebugCallStackContext, contextKeyService, { arg: getContextForContributedActions(element), shouldForwardArgs: true });476const result = getContextMenuActions(menu, 'inline');477this.contextMenuService.showContextMenu({478getAnchor: () => e.anchor,479getActions: () => result.secondary,480getActionsContext: () => getContext(element)481});482}483}484485interface IThreadTemplateData {486thread: HTMLElement;487name: HTMLElement;488stateLabel: HTMLSpanElement;489label: HighlightedLabel;490actionBar: ActionBar;491elementDisposable: DisposableStore;492templateDisposable: IDisposable;493}494495interface ISessionTemplateData {496session: HTMLElement;497name: HTMLElement;498stateLabel: HTMLSpanElement;499label: HighlightedLabel;500actionBar: ActionBar;501elementDisposable: DisposableStore;502templateDisposable: IDisposable;503}504505interface IErrorTemplateData {506label: HTMLElement;507templateDisposable: DisposableStore;508}509510interface ILabelTemplateData {511label: HTMLElement;512}513514interface IStackFrameTemplateData {515stackFrame: HTMLElement;516file: HTMLElement;517fileName: HTMLElement;518lineNumber: HTMLElement;519label: HighlightedLabel;520actionBar: ActionBar;521templateDisposable: DisposableStore;522elementDisposables: DisposableStore;523}524525function getSessionContextOverlay(session: IDebugSession): [string, ContextKeyValue][] {526return [527[CONTEXT_CALLSTACK_ITEM_TYPE.key, 'session'],528[CONTEXT_CALLSTACK_SESSION_IS_ATTACH.key, isSessionAttach(session)],529[CONTEXT_CALLSTACK_ITEM_STOPPED.key, session.state === State.Stopped],530[CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD.key, session.getAllThreads().length === 1],531];532}533534class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, FuzzyScore, ISessionTemplateData> {535static readonly ID = 'session';536537constructor(538@IInstantiationService private readonly instantiationService: IInstantiationService,539@IContextKeyService private readonly contextKeyService: IContextKeyService,540@IHoverService private readonly hoverService: IHoverService,541@IMenuService private readonly menuService: IMenuService,542) { }543544get templateId(): string {545return SessionsRenderer.ID;546}547548renderTemplate(container: HTMLElement): ISessionTemplateData {549const session = dom.append(container, $('.session'));550dom.append(session, $(ThemeIcon.asCSSSelector(icons.callstackViewSession)));551const name = dom.append(session, $('.name'));552const stateLabel = dom.append(session, $('span.state.label.monaco-count-badge.long'));553const templateDisposable = new DisposableStore();554const label = templateDisposable.add(new HighlightedLabel(name));555556const stopActionViewItemDisposables = templateDisposable.add(new DisposableStore());557const actionBar = templateDisposable.add(new ActionBar(session, {558actionViewItemProvider: (action, options) => {559if ((action.id === STOP_ID || action.id === DISCONNECT_ID) && action instanceof MenuItemAction) {560stopActionViewItemDisposables.clear();561const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, stopActionViewItemDisposables, accessor, { ...options, menuAsChild: false }));562if (item) {563return item;564}565}566567if (action instanceof MenuItemAction) {568return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });569} else if (action instanceof SubmenuItemAction) {570return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });571}572573return undefined;574}575}));576577const elementDisposable = templateDisposable.add(new DisposableStore());578return { session, name, stateLabel, label, actionBar, elementDisposable, templateDisposable };579}580581renderElement(element: ITreeNode<IDebugSession, FuzzyScore>, _: number, data: ISessionTemplateData): void {582this.doRenderElement(element.element, createMatches(element.filterData), data);583}584585renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IDebugSession>, FuzzyScore>, _index: number, templateData: ISessionTemplateData): void {586const lastElement = node.element.elements[node.element.elements.length - 1];587const matches = createMatches(node.filterData);588this.doRenderElement(lastElement, matches, templateData);589}590591private doRenderElement(session: IDebugSession, matches: IMatch[], data: ISessionTemplateData): void {592const sessionHover = data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.session, localize({ key: 'session', comment: ['Session is a noun'] }, "Session")));593data.label.set(session.getLabel(), matches);594const stoppedDetails = session.getStoppedDetails();595const thread = session.getAllThreads().find(t => t.stopped);596597const contextKeyService = this.contextKeyService.createOverlay(getSessionContextOverlay(session));598const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService));599600const setupActionBar = () => {601data.actionBar.clear();602603const { primary } = getActionBarActions(menu.getActions({ arg: getContextForContributedActions(session), shouldForwardArgs: true }), 'inline');604data.actionBar.push(primary, { icon: true, label: false });605// We need to set our internal context on the action bar, since our commands depend on that one606// While the external context our extensions rely on607data.actionBar.context = getContext(session);608};609data.elementDisposable.add(menu.onDidChange(() => setupActionBar()));610setupActionBar();611612data.stateLabel.style.display = '';613614if (stoppedDetails) {615data.stateLabel.textContent = stoppedDescription(stoppedDetails);616sessionHover.update(`${session.getLabel()}: ${stoppedText(stoppedDetails)}`);617data.stateLabel.classList.toggle('exception', stoppedDetails.reason === 'exception');618} else if (thread && thread.stoppedDetails) {619data.stateLabel.textContent = stoppedDescription(thread.stoppedDetails);620sessionHover.update(`${session.getLabel()}: ${stoppedText(thread.stoppedDetails)}`);621data.stateLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception');622} else {623data.stateLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running");624data.stateLabel.classList.remove('exception');625}626}627628disposeTemplate(templateData: ISessionTemplateData): void {629templateData.templateDisposable.dispose();630}631632disposeElement(_element: ITreeNode<IDebugSession, FuzzyScore>, _: number, templateData: ISessionTemplateData): void {633templateData.elementDisposable.clear();634}635636disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<IDebugSession>, FuzzyScore>, index: number, templateData: ISessionTemplateData): void {637templateData.elementDisposable.clear();638}639}640641function getThreadContextOverlay(thread: IThread): [string, ContextKeyValue][] {642return [643[CONTEXT_CALLSTACK_ITEM_TYPE.key, 'thread'],644[CONTEXT_CALLSTACK_ITEM_STOPPED.key, thread.stopped]645];646}647648class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, IThreadTemplateData> {649static readonly ID = 'thread';650651constructor(652@IContextKeyService private readonly contextKeyService: IContextKeyService,653@IHoverService private readonly hoverService: IHoverService,654@IMenuService private readonly menuService: IMenuService,655) { }656657get templateId(): string {658return ThreadsRenderer.ID;659}660661renderTemplate(container: HTMLElement): IThreadTemplateData {662const thread = dom.append(container, $('.thread'));663const name = dom.append(thread, $('.name'));664const stateLabel = dom.append(thread, $('span.state.label.monaco-count-badge.long'));665666const templateDisposable = new DisposableStore();667const label = templateDisposable.add(new HighlightedLabel(name));668669const actionBar = templateDisposable.add(new ActionBar(thread));670const elementDisposable = templateDisposable.add(new DisposableStore());671672return { thread, name, stateLabel, label, actionBar, elementDisposable, templateDisposable };673}674675renderElement(element: ITreeNode<IThread, FuzzyScore>, _index: number, data: IThreadTemplateData): void {676const thread = element.element;677data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name));678data.label.set(thread.name, createMatches(element.filterData));679data.stateLabel.textContent = thread.stateLabel;680data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception');681682const contextKeyService = this.contextKeyService.createOverlay(getThreadContextOverlay(thread));683const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService));684685const setupActionBar = () => {686data.actionBar.clear();687688const { primary } = getActionBarActions(menu.getActions({ arg: getContextForContributedActions(thread), shouldForwardArgs: true }), 'inline');689data.actionBar.push(primary, { icon: true, label: false });690// We need to set our internal context on the action bar, since our commands depend on that one691// While the external context our extensions rely on692data.actionBar.context = getContext(thread);693};694data.elementDisposable.add(menu.onDidChange(() => setupActionBar()));695setupActionBar();696}697698renderCompressedElements(_node: ITreeNode<ICompressedTreeNode<IThread>, FuzzyScore>, _index: number, _templateData: IThreadTemplateData): void {699throw new Error('Method not implemented.');700}701702disposeElement(_element: ITreeNode<IThread, FuzzyScore>, _index: number, templateData: IThreadTemplateData): void {703templateData.elementDisposable.clear();704}705706disposeTemplate(templateData: IThreadTemplateData): void {707templateData.templateDisposable.dispose();708}709}710711function getStackFrameContextOverlay(stackFrame: IStackFrame): [string, ContextKeyValue][] {712return [713[CONTEXT_CALLSTACK_ITEM_TYPE.key, 'stackFrame'],714[CONTEXT_STACK_FRAME_SUPPORTS_RESTART.key, stackFrame.canRestart]715];716}717718class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, FuzzyScore, IStackFrameTemplateData> {719static readonly ID = 'stackFrame';720721constructor(722@IHoverService private readonly hoverService: IHoverService,723@ILabelService private readonly labelService: ILabelService,724@INotificationService private readonly notificationService: INotificationService,725) { }726727get templateId(): string {728return StackFramesRenderer.ID;729}730731renderTemplate(container: HTMLElement): IStackFrameTemplateData {732const stackFrame = dom.append(container, $('.stack-frame'));733const labelDiv = dom.append(stackFrame, $('span.label.expression'));734const file = dom.append(stackFrame, $('.file'));735const fileName = dom.append(file, $('span.file-name'));736const wrapper = dom.append(file, $('span.line-number-wrapper'));737const lineNumber = dom.append(wrapper, $('span.line-number.monaco-count-badge'));738739const templateDisposable = new DisposableStore();740const elementDisposables = new DisposableStore();741templateDisposable.add(elementDisposables);742const label = templateDisposable.add(new HighlightedLabel(labelDiv));743const actionBar = templateDisposable.add(new ActionBar(stackFrame));744745return { file, fileName, label, lineNumber, stackFrame, actionBar, templateDisposable, elementDisposables };746}747748renderElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, data: IStackFrameTemplateData): void {749const stackFrame = element.element;750data.stackFrame.classList.toggle('disabled', !stackFrame.source || !stackFrame.source.available || isFrameDeemphasized(stackFrame));751data.stackFrame.classList.toggle('label', stackFrame.presentationHint === 'label');752const hasActions = !!stackFrame.thread.session.capabilities.supportsRestartFrame && stackFrame.presentationHint !== 'label' && stackFrame.presentationHint !== 'subtle' && stackFrame.canRestart;753data.stackFrame.classList.toggle('has-actions', hasActions);754755let title = stackFrame.source.inMemory ? stackFrame.source.uri.path : this.labelService.getUriLabel(stackFrame.source.uri);756if (stackFrame.source.raw.origin) {757title += `\n${stackFrame.source.raw.origin}`;758}759data.elementDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.file, title));760761data.label.set(stackFrame.name, createMatches(element.filterData), stackFrame.name);762data.fileName.textContent = getSpecificSourceName(stackFrame);763if (stackFrame.range.startLineNumber !== undefined) {764data.lineNumber.textContent = `${stackFrame.range.startLineNumber}`;765if (stackFrame.range.startColumn) {766data.lineNumber.textContent += `:${stackFrame.range.startColumn}`;767}768data.lineNumber.classList.remove('unavailable');769} else {770data.lineNumber.classList.add('unavailable');771}772773data.actionBar.clear();774if (hasActions) {775const action = data.elementDisposables.add(new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => {776try {777await stackFrame.restart();778} catch (e) {779this.notificationService.error(e);780}781}));782data.actionBar.push(action, { icon: true, label: false });783}784}785786renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IStackFrame>, FuzzyScore>, index: number, templateData: IStackFrameTemplateData): void {787throw new Error('Method not implemented.');788}789disposeElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, templateData: IStackFrameTemplateData): void {790templateData.elementDisposables.clear();791}792793disposeTemplate(templateData: IStackFrameTemplateData): void {794templateData.templateDisposable.dispose();795}796}797798class ErrorsRenderer implements ICompressibleTreeRenderer<string, FuzzyScore, IErrorTemplateData> {799static readonly ID = 'error';800801get templateId(): string {802return ErrorsRenderer.ID;803}804805constructor(806@IHoverService private readonly hoverService: IHoverService807) {808}809810renderTemplate(container: HTMLElement): IErrorTemplateData {811const label = dom.append(container, $('.error'));812813return { label, templateDisposable: new DisposableStore() };814}815816renderElement(element: ITreeNode<string, FuzzyScore>, index: number, data: IErrorTemplateData): void {817const error = element.element;818data.label.textContent = error;819data.templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.label, error));820}821822renderCompressedElements(node: ITreeNode<ICompressedTreeNode<string>, FuzzyScore>, index: number, templateData: IErrorTemplateData): void {823throw new Error('Method not implemented.');824}825826disposeTemplate(templateData: IErrorTemplateData): void {827// noop828}829}830831class LoadMoreRenderer implements ICompressibleTreeRenderer<ThreadAndSessionIds, FuzzyScore, ILabelTemplateData> {832static readonly ID = 'loadMore';833static readonly LABEL = localize('loadAllStackFrames', "Load More Stack Frames");834835constructor() { }836837get templateId(): string {838return LoadMoreRenderer.ID;839}840841renderTemplate(container: HTMLElement): ILabelTemplateData {842const label = dom.append(container, $('.load-all'));843label.style.color = asCssVariable(textLinkForeground);844return { label };845}846847renderElement(element: ITreeNode<ThreadAndSessionIds, FuzzyScore>, index: number, data: ILabelTemplateData): void {848data.label.textContent = LoadMoreRenderer.LABEL;849}850851renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ThreadAndSessionIds>, FuzzyScore>, index: number, templateData: ILabelTemplateData): void {852throw new Error('Method not implemented.');853}854855disposeTemplate(templateData: ILabelTemplateData): void {856// noop857}858}859860class ShowMoreRenderer implements ICompressibleTreeRenderer<IStackFrame[], FuzzyScore, ILabelTemplateData> {861static readonly ID = 'showMore';862863constructor() { }864865866get templateId(): string {867return ShowMoreRenderer.ID;868}869870renderTemplate(container: HTMLElement): ILabelTemplateData {871const label = dom.append(container, $('.show-more'));872label.style.color = asCssVariable(textLinkForeground);873return { label };874}875876renderElement(element: ITreeNode<IStackFrame[], FuzzyScore>, index: number, data: ILabelTemplateData): void {877const stackFrames = element.element;878if (stackFrames.every(sf => !!(sf.source && sf.source.origin && sf.source.origin === stackFrames[0].source.origin))) {879data.label.textContent = localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin);880} else {881data.label.textContent = localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length);882}883}884885renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IStackFrame[]>, FuzzyScore>, index: number, templateData: ILabelTemplateData): void {886throw new Error('Method not implemented.');887}888889disposeTemplate(templateData: ILabelTemplateData): void {890// noop891}892}893894class CallStackDelegate implements IListVirtualDelegate<CallStackItem> {895896getHeight(element: CallStackItem): number {897if (element instanceof StackFrame && element.presentationHint === 'label') {898return 16;899}900if (element instanceof ThreadAndSessionIds || element instanceof Array) {901return 16;902}903904return 22;905}906907getTemplateId(element: CallStackItem): string {908if (isDebugSession(element)) {909return SessionsRenderer.ID;910}911if (element instanceof Thread) {912return ThreadsRenderer.ID;913}914if (element instanceof StackFrame) {915return StackFramesRenderer.ID;916}917if (typeof element === 'string') {918return ErrorsRenderer.ID;919}920if (element instanceof ThreadAndSessionIds) {921return LoadMoreRenderer.ID;922}923924// element instanceof Array925return ShowMoreRenderer.ID;926}927}928929function stoppedText(stoppedDetails: IRawStoppedDetails): string {930return stoppedDetails.text ?? stoppedDescription(stoppedDetails);931}932933function stoppedDescription(stoppedDetails: IRawStoppedDetails): string {934return stoppedDetails.description ||935(stoppedDetails.reason ? localize({ key: 'pausedOn', comment: ['indicates reason for program being paused'] }, "Paused on {0}", stoppedDetails.reason) : localize('paused', "Paused"));936}937938function isDebugModel(obj: unknown): obj is IDebugModel {939return !!obj && typeof (obj as IDebugModel).getSessions === 'function';940}941942function isDebugSession(obj: unknown): obj is IDebugSession {943return !!obj && typeof (obj as IDebugSession).getAllThreads === 'function';944}945946class CallStackDataSource implements IAsyncDataSource<IDebugModel, CallStackItem> {947deemphasizedStackFramesToShow = new WeakSet<IStackFrame>();948949constructor(private debugService: IDebugService) { }950951hasChildren(element: IDebugModel | CallStackItem): boolean {952if (isDebugSession(element)) {953const threads = element.getAllThreads();954return (threads.length > 1) || (threads.length === 1 && threads[0].stopped) || !!(this.debugService.getModel().getSessions().find(s => s.parentSession === element));955}956957return isDebugModel(element) || (element instanceof Thread && element.stopped);958}959960async getChildren(element: IDebugModel | CallStackItem): Promise<CallStackItem[]> {961if (isDebugModel(element)) {962const sessions = element.getSessions();963if (sessions.length === 0) {964return Promise.resolve([]);965}966if (sessions.length > 1 || this.debugService.getViewModel().isMultiSessionView()) {967return Promise.resolve(sessions.filter(s => !s.parentSession));968}969970const threads = sessions[0].getAllThreads();971// Only show the threads in the call stack if there is more than 1 thread.972return threads.length === 1 ? this.getThreadChildren(<Thread>threads[0]) : Promise.resolve(threads);973} else if (isDebugSession(element)) {974const childSessions = this.debugService.getModel().getSessions().filter(s => s.parentSession === element);975const threads: CallStackItem[] = element.getAllThreads();976if (threads.length === 1) {977// Do not show thread when there is only one to be compact.978const children = await this.getThreadChildren(<Thread>threads[0]);979return children.concat(childSessions);980}981982return Promise.resolve(threads.concat(childSessions));983} else {984return this.getThreadChildren(<Thread>element);985}986}987988private getThreadChildren(thread: Thread): Promise<CallStackItem[]> {989return this.getThreadCallstack(thread).then(children => {990// Check if some stack frames should be hidden under a parent element since they are deemphasized991const result: CallStackItem[] = [];992children.forEach((child, index) => {993if (child instanceof StackFrame && child.source && isFrameDeemphasized(child)) {994// Check if the user clicked to show the deemphasized source995if (!this.deemphasizedStackFramesToShow.has(child)) {996if (result.length) {997const last = result[result.length - 1];998if (last instanceof Array) {999// Collect all the stackframes that will be "collapsed"1000last.push(child);1001return;1002}1003}10041005const nextChild = index < children.length - 1 ? children[index + 1] : undefined;1006if (nextChild instanceof StackFrame && nextChild.source && isFrameDeemphasized(nextChild)) {1007// Start collecting stackframes that will be "collapsed"1008result.push([child]);1009return;1010}1011}1012}10131014result.push(child);1015});10161017return result;1018});1019}10201021private async getThreadCallstack(thread: Thread): Promise<Array<IStackFrame | string | ThreadAndSessionIds>> {1022let callStack: Array<IStackFrame | string | ThreadAndSessionIds> = thread.getCallStack();1023if (!callStack || !callStack.length) {1024await thread.fetchCallStack();1025callStack = thread.getCallStack();1026}10271028if (callStack.length === 1 && thread.session.capabilities.supportsDelayedStackTraceLoading && thread.stoppedDetails && thread.stoppedDetails.totalFrames && thread.stoppedDetails.totalFrames > 1) {1029// To reduce flashing of the call stack view simply append the stale call stack1030// once we have the correct data the tree will refresh and we will no longer display it.1031callStack = callStack.concat(thread.getStaleCallStack().slice(1));1032}10331034if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) {1035callStack = callStack.concat([thread.stoppedDetails.framesErrorMessage]);1036}1037if (!thread.reachedEndOfCallStack && thread.stoppedDetails) {1038callStack = callStack.concat([new ThreadAndSessionIds(thread.session.getId(), thread.threadId)]);1039}10401041return callStack;1042}1043}10441045class CallStackAccessibilityProvider implements IListAccessibilityProvider<CallStackItem> {10461047getWidgetAriaLabel(): string {1048return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack");1049}10501051getWidgetRole(): AriaRole {1052// Use treegrid as a role since each element can have additional actions inside #1462101053return 'treegrid';1054}10551056getRole(_element: CallStackItem): AriaRole | undefined {1057return 'row';1058}10591060getAriaLabel(element: CallStackItem): string {1061if (element instanceof Thread) {1062return 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);1063}1064if (element instanceof StackFrame) {1065return localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element));1066}1067if (isDebugSession(element)) {1068const thread = element.getAllThreads().find(t => t.stopped);1069const state = thread ? thread.stateLabel : localize({ key: 'running', comment: ['indicates state'] }, "Running");1070return 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);1071}1072if (typeof element === 'string') {1073return element;1074}1075if (element instanceof Array) {1076return localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length);1077}10781079// element instanceof ThreadAndSessionIds1080return LoadMoreRenderer.LABEL;1081}1082}10831084class CallStackCompressionDelegate implements ITreeCompressionDelegate<CallStackItem> {10851086constructor(private readonly debugService: IDebugService) { }10871088isIncompressible(stat: CallStackItem): boolean {1089if (isDebugSession(stat)) {1090if (stat.compact) {1091return false;1092}1093const sessions = this.debugService.getModel().getSessions();1094if (sessions.some(s => s.parentSession === stat && s.compact)) {1095return false;1096}10971098return true;1099}11001101return true;1102}1103}11041105registerAction2(class Collapse extends ViewAction<CallStackView> {1106constructor() {1107super({1108id: 'callStack.collapse',1109viewId: CALLSTACK_VIEW_ID,1110title: localize('collapse', "Collapse All"),1111f1: false,1112icon: Codicon.collapseAll,1113precondition: CONTEXT_DEBUG_STATE.isEqualTo(getStateLabel(State.Stopped)),1114menu: {1115id: MenuId.ViewTitle,1116order: 10,1117group: 'navigation',1118when: ContextKeyExpr.equals('view', CALLSTACK_VIEW_ID)1119}1120});1121}11221123runInView(_accessor: ServicesAccessor, view: CallStackView) {1124view.collapseAll();1125}1126});11271128function registerCallStackInlineMenuItem(id: string, title: string | ICommandActionTitle, icon: Icon, when: ContextKeyExpression, order: number, precondition?: ContextKeyExpression): void {1129MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, {1130group: 'inline',1131order,1132when,1133command: { id, title, icon, precondition }1134});1135}11361137const threadOrSessionWithOneThread = ContextKeyExpr.or(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD))!;1138registerCallStackInlineMenuItem(PAUSE_ID, PAUSE_LABEL, icons.debugPause, ContextKeyExpr.and(threadOrSessionWithOneThread, CONTEXT_CALLSTACK_ITEM_STOPPED.toNegated())!, 10, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG.toNegated());1139registerCallStackInlineMenuItem(CONTINUE_ID, CONTINUE_LABEL, icons.debugContinue, ContextKeyExpr.and(threadOrSessionWithOneThread, CONTEXT_CALLSTACK_ITEM_STOPPED)!, 10);1140registerCallStackInlineMenuItem(STEP_OVER_ID, STEP_OVER_LABEL, icons.debugStepOver, threadOrSessionWithOneThread, 20, CONTEXT_CALLSTACK_ITEM_STOPPED);1141registerCallStackInlineMenuItem(STEP_INTO_ID, STEP_INTO_LABEL, icons.debugStepInto, threadOrSessionWithOneThread, 30, CONTEXT_CALLSTACK_ITEM_STOPPED);1142registerCallStackInlineMenuItem(STEP_OUT_ID, STEP_OUT_LABEL, icons.debugStepOut, threadOrSessionWithOneThread, 40, CONTEXT_CALLSTACK_ITEM_STOPPED);1143registerCallStackInlineMenuItem(RESTART_SESSION_ID, RESTART_LABEL, icons.debugRestart, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), 50);1144registerCallStackInlineMenuItem(STOP_ID, STOP_LABEL, icons.debugStop, ContextKeyExpr.and(CONTEXT_CALLSTACK_SESSION_IS_ATTACH.toNegated(), CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'))!, 60);1145registerCallStackInlineMenuItem(DISCONNECT_ID, DISCONNECT_LABEL, icons.debugDisconnect, ContextKeyExpr.and(CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'))!, 60);114611471148