Path: blob/main/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts
5241 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 { addDisposableListener, isKeyboardEvent } from '../../../../base/browser/dom.js';6import { DomEmitter } from '../../../../base/browser/event.js';7import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';8import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';9import { RunOnceScheduler } from '../../../../base/common/async.js';10import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';11import { memoize } from '../../../../base/common/decorators.js';12import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js';13import { Event } from '../../../../base/common/event.js';14import { visit } from '../../../../base/common/json.js';15import { setProperty } from '../../../../base/common/jsonEdit.js';16import { KeyCode } from '../../../../base/common/keyCodes.js';17import { DisposableStore, IDisposable, MutableDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js';18import { clamp } from '../../../../base/common/numbers.js';19import { basename } from '../../../../base/common/path.js';20import * as env from '../../../../base/common/platform.js';21import * as strings from '../../../../base/common/strings.js';22import { assertType, isDefined } from '../../../../base/common/types.js';23import { Constants } from '../../../../base/common/uint.js';24import { URI } from '../../../../base/common/uri.js';25import { CoreEditingCommands } from '../../../../editor/browser/coreCommands.js';26import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';27import { EditorOption, IEditorHoverOptions } from '../../../../editor/common/config/editorOptions.js';28import { EditOperation } from '../../../../editor/common/core/editOperation.js';29import { Position } from '../../../../editor/common/core/position.js';30import { IRange, Range } from '../../../../editor/common/core/range.js';31import { DEFAULT_WORD_REGEXP } from '../../../../editor/common/core/wordHelper.js';32import { IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js';33import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js';34import { InlineValue, InlineValueContext } from '../../../../editor/common/languages.js';35import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from '../../../../editor/common/model.js';36import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../../editor/common/services/languageFeatureDebounce.js';37import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';38import { IModelService } from '../../../../editor/common/services/model.js';39import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js';40import { HoverStartMode, HoverStartSource } from '../../../../editor/contrib/hover/browser/hoverOperation.js';41import * as nls from '../../../../nls.js';42import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';43import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';44import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';45import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';46import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';47import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';48import { FloatingEditorClickWidget } from '../../../browser/codeeditor.js';49import { DebugHoverWidget, ShowDebugHoverResult } from './debugHover.js';50import { ExceptionWidget } from './exceptionWidget.js';51import { CONTEXT_EXCEPTION_WIDGET_VISIBLE, IDebugConfiguration, IDebugEditorContribution, IDebugService, IDebugSession, IExceptionInfo, IExpression, IStackFrame, State } from '../common/debug.js';52import { Expression } from '../common/debugModel.js';53import { IHostService } from '../../../services/host/browser/host.js';54import { IEditorService } from '../../../services/editor/common/editorService.js';55import { MarkdownString } from '../../../../base/common/htmlContent.js';56import { InsertLineAfterAction } from '../../../../editor/contrib/linesOperations/browser/linesOperations.js';5758const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons59const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added60const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped6162const DEAFULT_INLINE_DEBOUNCE_DELAY = 200;6364export const debugInlineForeground = registerColor('editor.inlineValuesForeground', {65dark: '#ffffff80',66light: '#00000080',67hcDark: '#ffffff80',68hcLight: '#00000080'69}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text."));7071export const debugInlineBackground = registerColor('editor.inlineValuesBackground', '#ffc80033', nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background."));7273class InlineSegment {74constructor(public column: number, public text: string) {75}76}7778export function formatHoverContent(contentText: string): MarkdownString {79if (contentText.includes(',') && contentText.includes('=')) {80// Custom split: for each equals sign after the first, backtrack to the nearest comma81const customSplit = (text: string): string[] => {82const splits: number[] = [];83let equalsFound = 0;84let start = 0;85for (let i = 0; i < text.length; i++) {86if (text[i] === '=') {87if (equalsFound === 0) {88equalsFound++;89continue;90}91const commaIndex = text.lastIndexOf(',', i);92if (commaIndex !== -1 && commaIndex >= start) {93splits.push(commaIndex);94start = commaIndex + 1;95}96equalsFound++;97}98}99const result: string[] = [];100let s = 0;101for (const index of splits) {102result.push(text.substring(s, index).trim());103s = index + 1;104}105if (s < text.length) {106result.push(text.substring(s).trim());107}108return result;109};110111const pairs = customSplit(contentText);112const formattedPairs = pairs.map(pair => {113const equalsIndex = pair.indexOf('=');114if (equalsIndex !== -1) {115const indent = ' '.repeat(equalsIndex + 2);116const [firstLine, ...restLines] = pair.split(/\r?\n/);117return [firstLine, ...restLines.map(line => indent + line)].join('\n');118}119return pair;120});121return new MarkdownString().appendCodeblock('', formattedPairs.join(',\n'));122}123return new MarkdownString().appendCodeblock('', contentText);124}125126export function createInlineValueDecoration(lineNumber: number, contentText: string, classNamePrefix: string, column = Constants.MAX_SAFE_SMALL_INTEGER, viewportMaxCol: number = MAX_INLINE_DECORATOR_LENGTH): IModelDeltaDecoration[] {127const rawText = contentText; // store raw text for hover message128129// Truncate contentText if it exceeds the viewport max column130if (contentText.length > viewportMaxCol) {131contentText = contentText.substring(0, viewportMaxCol) + '...';132}133134return [135{136range: {137startLineNumber: lineNumber,138endLineNumber: lineNumber,139startColumn: column,140endColumn: column141},142options: {143description: `${classNamePrefix}-inline-value-decoration-spacer`,144after: {145content: strings.noBreakWhitespace,146cursorStops: InjectedTextCursorStops.None147},148showIfCollapsed: true,149}150},151{152range: {153startLineNumber: lineNumber,154endLineNumber: lineNumber,155startColumn: column,156endColumn: column157},158options: {159description: `${classNamePrefix}-inline-value-decoration`,160after: {161content: replaceWsWithNoBreakWs(contentText),162inlineClassName: `${classNamePrefix}-inline-value`,163inlineClassNameAffectsLetterSpacing: true,164cursorStops: InjectedTextCursorStops.None165},166showIfCollapsed: true,167hoverMessage: formatHoverContent(rawText)168}169},170];171}172173function replaceWsWithNoBreakWs(str: string): string {174return str.replace(/[ \t\n]/g, strings.noBreakWhitespace);175}176177function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray<IExpression>, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map<string, number[]>) {178const nameValueMap = new Map<string, string>();179for (const expr of expressions) {180nameValueMap.set(expr.name, expr.value);181// Limit the size of map. Too large can have a perf impact182if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {183break;184}185}186187const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();188189// Compute unique set of names on each line190nameValueMap.forEach((_value, name) => {191const lineNumbers = wordToLineNumbersMap.get(name);192if (lineNumbers) {193for (const lineNumber of lineNumbers) {194if (ranges.some(r => lineNumber >= r.startLineNumber && lineNumber <= r.endLineNumber)) {195if (!lineToNamesMap.has(lineNumber)) {196lineToNamesMap.set(lineNumber, []);197}198199if (lineToNamesMap.get(lineNumber)!.indexOf(name) === -1) {200lineToNamesMap.get(lineNumber)!.push(name);201}202}203}204}205});206207// Compute decorators for each line208return [...lineToNamesMap].map(([line, names]) => ({209line,210variables: names.sort((first, second) => {211const content = model.getLineContent(line);212return content.indexOf(first) - content.indexOf(second);213}).map(name => ({ name, value: nameValueMap.get(name)! }))214}));215}216217function getWordToLineNumbersMap(model: ITextModel, lineNumber: number, result: Map<string, number[]>) {218const lineLength = model.getLineLength(lineNumber);219// If line is too long then skip the line220if (lineLength > MAX_TOKENIZATION_LINE_LEN) {221return;222}223224const lineContent = model.getLineContent(lineNumber);225model.tokenization.forceTokenization(lineNumber);226const lineTokens = model.tokenization.getLineTokens(lineNumber);227for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) {228const tokenType = lineTokens.getStandardTokenType(tokenIndex);229230// Token is a word and not a comment231if (tokenType === StandardTokenType.Other) {232DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match233234const tokenStartOffset = lineTokens.getStartOffset(tokenIndex);235const tokenEndOffset = lineTokens.getEndOffset(tokenIndex);236const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset);237const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr);238239if (wordMatch) {240241const word = wordMatch[0];242if (!result.has(word)) {243result.set(word, []);244}245246result.get(word)!.push(lineNumber);247}248}249}250}251252export class DebugEditorContribution implements IDebugEditorContribution {253254private toDispose: IDisposable[];255private hoverWidget: DebugHoverWidget;256private hoverPosition?: { position: Position; event: IMouseEvent };257private mouseDown = false;258private exceptionWidgetVisible: IContextKey<boolean>;259private gutterIsHovered = false;260261private exceptionWidget: ExceptionWidget | undefined;262private configurationWidget: FloatingEditorClickWidget | undefined;263private readonly altListener = new MutableDisposable();264private altPressed = false;265private oldDecorations: IEditorDecorationsCollection;266private readonly displayedStore = new DisposableStore();267private editorHoverOptions: IEditorHoverOptions | undefined;268private readonly debounceInfo: IFeatureDebounceInformation;269private allowScrollToExceptionWidget = true;270private shouldScrollToExceptionWidget = () => this.allowScrollToExceptionWidget;271272// Holds a Disposable that prevents the default editor hover behavior while it exists.273private readonly defaultHoverLockout = new MutableDisposable();274275constructor(276private editor: ICodeEditor,277@IDebugService private readonly debugService: IDebugService,278@IInstantiationService private readonly instantiationService: IInstantiationService,279@ICommandService private readonly commandService: ICommandService,280@IConfigurationService private readonly configurationService: IConfigurationService,281@IHostService private readonly hostService: IHostService,282@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,283@IContextKeyService contextKeyService: IContextKeyService,284@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,285@ILanguageFeatureDebounceService featureDebounceService: ILanguageFeatureDebounceService,286@IEditorService private readonly editorService: IEditorService287) {288this.oldDecorations = this.editor.createDecorationsCollection();289this.debounceInfo = featureDebounceService.for(languageFeaturesService.inlineValuesProvider, 'InlineValues', { min: DEAFULT_INLINE_DEBOUNCE_DELAY });290this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor);291this.toDispose = [this.defaultHoverLockout, this.altListener, this.displayedStore];292this.registerListeners();293this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService);294this.toggleExceptionWidget();295}296297private registerListeners(): void {298this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(e => this.onFocusStackFrame(e.stackFrame)));299300// hover listeners & hover widget301this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e)));302this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false));303this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e)));304this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => {305const hoverDomNode = this.hoverWidget.getDomNode();306if (!hoverDomNode) {307return;308}309310const rect = hoverDomNode.getBoundingClientRect();311// Only hide the hover widget if the editor mouse leave event is outside the hover widget #3528312if (e.event.posx < rect.left || e.event.posx > rect.right || e.event.posy < rect.top || e.event.posy > rect.bottom) {313this.hideHoverWidget();314}315}));316this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e)));317this.toDispose.push(this.editor.onDidChangeModelContent(() => {318this._wordToLineNumbersMap = undefined;319this.updateInlineValuesScheduler.schedule();320}));321this.toDispose.push(this.debugService.getViewModel().onWillUpdateViews(() => this.updateInlineValuesScheduler.schedule()));322this.toDispose.push(this.debugService.getViewModel().onDidEvaluateLazyExpression(() => this.updateInlineValuesScheduler.schedule()));323this.toDispose.push(this.editor.onDidChangeModel(async () => {324this.addDocumentListeners();325this.toggleExceptionWidget();326this.hideHoverWidget();327this._wordToLineNumbersMap = undefined;328const stackFrame = this.debugService.getViewModel().focusedStackFrame;329await this.updateInlineValueDecorations(stackFrame);330}));331this.toDispose.push(this.editor.onDidScrollChange(() => {332this.hideHoverWidget();333334// Inline value provider should get called on view port change335const model = this.editor.getModel();336if (model && this.languageFeaturesService.inlineValuesProvider.has(model)) {337this.updateInlineValuesScheduler.schedule();338}339}));340this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => {341if (e.affectsConfiguration('editor.hover')) {342this.updateHoverConfiguration();343}344}));345this.toDispose.push(this.debugService.onDidChangeState((state: State) => {346if (state !== State.Stopped) {347this.toggleExceptionWidget();348}349}));350351this.updateHoverConfiguration();352}353354private _wordToLineNumbersMap: WordsToLineNumbersCache | undefined;355356private updateHoverConfiguration(): void {357const model = this.editor.getModel();358if (model) {359this.editorHoverOptions = this.configurationService.getValue<IEditorHoverOptions>('editor.hover', {360resource: model.uri,361overrideIdentifier: model.getLanguageId()362});363}364}365366private addDocumentListeners(): void {367const stackFrame = this.debugService.getViewModel().focusedStackFrame;368const model = this.editor.getModel();369if (model) {370this.applyDocumentListeners(model, stackFrame);371}372}373374private applyDocumentListeners(model: ITextModel, stackFrame: IStackFrame | undefined): void {375if (!stackFrame || !this.uriIdentityService.extUri.isEqual(model.uri, stackFrame.source.uri)) {376this.altListener.clear();377return;378}379380const ownerDocument = this.editor.getContainerDomNode().ownerDocument;381382// When the alt key is pressed show regular editor hover and hide the debug hover #84561383this.altListener.value = addDisposableListener(ownerDocument, 'keydown', keydownEvent => {384const standardKeyboardEvent = new StandardKeyboardEvent(keydownEvent);385if (standardKeyboardEvent.keyCode === KeyCode.Alt) {386this.altPressed = true;387const debugHoverWasVisible = this.hoverWidget.isVisible();388this.hoverWidget.hide();389this.defaultHoverLockout.clear();390391if (debugHoverWasVisible && this.hoverPosition) {392// If the debug hover was visible immediately show the editor hover for the alt transition to be smooth393this.showEditorHover(this.hoverPosition.position, false);394}395396const onKeyUp = new DomEmitter(ownerDocument, 'keyup');397const listener = Event.any<KeyboardEvent | boolean>(this.hostService.onDidChangeFocus, onKeyUp.event)(keyupEvent => {398let standardKeyboardEvent = undefined;399if (isKeyboardEvent(keyupEvent)) {400standardKeyboardEvent = new StandardKeyboardEvent(keyupEvent);401}402if (!standardKeyboardEvent || standardKeyboardEvent.keyCode === KeyCode.Alt) {403this.altPressed = false;404this.preventDefaultEditorHover();405listener.dispose();406onKeyUp.dispose();407}408});409}410});411}412413async showHover(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise<void> {414// normally will already be set in `showHoverScheduler`, but public callers may hit this directly:415this.preventDefaultEditorHover();416417const sf = this.debugService.getViewModel().focusedStackFrame;418const model = this.editor.getModel();419if (sf && model && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) {420const result = await this.hoverWidget.showAt(position, focus, mouseEvent);421if (result === ShowDebugHoverResult.NOT_AVAILABLE) {422// When no expression available fallback to editor hover423this.showEditorHover(position, focus);424}425} else {426this.showEditorHover(position, focus);427}428}429430private preventDefaultEditorHover() {431if (this.defaultHoverLockout.value || this.editorHoverOptions?.enabled === 'off') {432return;433}434435const hoverController = this.editor.getContribution<ContentHoverController>(ContentHoverController.ID);436hoverController?.hideContentHover();437438this.editor.updateOptions({ hover: { enabled: 'off' } });439this.defaultHoverLockout.value = {440dispose: () => {441this.editor.updateOptions({442hover: { enabled: this.editorHoverOptions?.enabled ?? 'on' }443});444}445};446}447448private showEditorHover(position: Position, focus: boolean) {449const hoverController = this.editor.getContribution<ContentHoverController>(ContentHoverController.ID);450const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);451// enable the editor hover, otherwise the content controller will see it452// as disabled and hide it on the first mouse move (#193149)453this.defaultHoverLockout.clear();454hoverController?.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Mouse, focus);455}456457private async onFocusStackFrame(sf: IStackFrame | undefined): Promise<void> {458const model = this.editor.getModel();459if (model) {460this.applyDocumentListeners(model, sf);461if (sf && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) {462await this.toggleExceptionWidget();463} else {464this.hideHoverWidget();465}466}467468await this.updateInlineValueDecorations(sf);469}470471private get hoverDelay() {472const baseDelay = this.editorHoverOptions?.delay || 0;473474// heuristic to get a 'good' but configurable delay for evaluation. The475// debug hover can be very large, so we tend to be more conservative about476// when to show it (#180621). With this equation:477// - default 300ms hover => * 2 = 600ms478// - short 100ms hover => * 2 = 200ms479// - longer 600ms hover => * 1.5 = 900ms480// - long 1000ms hover => * 1.0 = 1000ms481const delayFactor = clamp(2 - (baseDelay - 300) / 600, 1, 2);482483return baseDelay * delayFactor;484}485486@memoize487private get showHoverScheduler() {488const scheduler = new RunOnceScheduler(() => {489if (this.hoverPosition && !this.altPressed) {490this.showHover(this.hoverPosition.position, false, this.hoverPosition.event);491}492}, this.hoverDelay);493this.toDispose.push(scheduler);494495return scheduler;496}497498private hideHoverWidget(): void {499if (this.hoverWidget.willBeVisible()) {500this.hoverWidget.hide();501}502this.showHoverScheduler.cancel();503this.defaultHoverLockout.clear();504}505506// hover business507508private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {509this.mouseDown = true;510if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === DebugHoverWidget.ID) {511return;512}513514this.hideHoverWidget();515}516517private onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {518if (this.debugService.state !== State.Stopped) {519return;520}521522const target = mouseEvent.target;523const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey';524525if (!this.altPressed) {526if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN) {527this.defaultHoverLockout.clear();528this.gutterIsHovered = true;529} else if (this.gutterIsHovered) {530this.gutterIsHovered = false;531this.updateHoverConfiguration();532}533}534535if (536(target.type === MouseTargetType.CONTENT_WIDGET && target.detail === DebugHoverWidget.ID)537|| this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)538) {539// mouse moved on top of debug hover widget540541const sticky = this.editorHoverOptions?.sticky ?? true;542if (sticky || this.hoverWidget.isShowingComplexValue || mouseEvent.event[stopKey]) {543return;544}545}546547if (target.type === MouseTargetType.CONTENT_TEXT) {548if (target.position && !Position.equals(target.position, this.hoverPosition?.position || null) && !this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)) {549this.hoverPosition = { position: target.position, event: mouseEvent.event };550// Disable the editor hover during the request to avoid flickering551this.preventDefaultEditorHover();552this.showHoverScheduler.schedule(this.hoverDelay);553}554} else if (!this.mouseDown) {555// Do not hide debug hover when the mouse is pressed because it usually leads to accidental closing #64620556this.hideHoverWidget();557}558}559560private onKeyDown(e: IKeyboardEvent): void {561const stopKey = env.isMacintosh ? KeyCode.Meta : KeyCode.Ctrl;562if (e.keyCode !== stopKey && e.keyCode !== KeyCode.Alt) {563// do not hide hover when Ctrl/Meta is pressed, and alt is handled separately564this.hideHoverWidget();565}566}567// end hover business568569// exception widget570private async toggleExceptionWidget(): Promise<void> {571// Toggles exception widget based on the state of the current editor model and debug stack frame572const model = this.editor.getModel();573const focusedSf = this.debugService.getViewModel().focusedStackFrame;574const callStack = focusedSf ? focusedSf.thread.getCallStack() : null;575if (!model || !focusedSf || !callStack || callStack.length === 0) {576this.closeExceptionWidget();577return;578}579580// First call stack frame that is available is the frame where exception has been thrown581const exceptionSf = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'));582if (!exceptionSf || exceptionSf !== focusedSf) {583this.closeExceptionWidget();584return;585}586587const sameUri = this.uriIdentityService.extUri.isEqual(exceptionSf.source.uri, model.uri);588if (this.exceptionWidget && !sameUri) {589this.closeExceptionWidget();590} else if (sameUri) {591// Show exception widget in all editors with the same file, but only scroll in the active editor592const activeControl = this.editorService.activeTextEditorControl;593const isActiveEditor = activeControl === this.editor;594const exceptionInfo = await focusedSf.thread.exceptionInfo;595596if (exceptionInfo) {597if (isActiveEditor) {598// Active editor: show widget and scroll to it599this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn);600} else {601// Inactive editor: show widget without scrolling602this.showExceptionWidgetWithoutScroll(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn);603}604}605}606}607608private showExceptionWidget(exceptionInfo: IExceptionInfo, debugSession: IDebugSession | undefined, lineNumber: number, column: number): void {609if (this.exceptionWidget) {610this.exceptionWidget.dispose();611}612613this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession, this.shouldScrollToExceptionWidget);614this.exceptionWidget.show({ lineNumber, column }, 0);615this.exceptionWidget.focus();616this.editor.revealRangeInCenter({617startLineNumber: lineNumber,618startColumn: column,619endLineNumber: lineNumber,620endColumn: column,621});622this.exceptionWidgetVisible.set(true);623}624625private showExceptionWidgetWithoutScroll(exceptionInfo: IExceptionInfo, debugSession: IDebugSession | undefined, lineNumber: number, column: number): void {626if (this.exceptionWidget) {627this.exceptionWidget.dispose();628}629630// Disable scrolling to exception widget631this.allowScrollToExceptionWidget = false;632633const currentScrollTop = this.editor.getScrollTop();634const visibleRanges = this.editor.getVisibleRanges();635if (visibleRanges.length === 0) {636// Editor not fully initialized or not visible; skip scroll adjustment637this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession, this.shouldScrollToExceptionWidget);638this.exceptionWidget.show({ lineNumber, column }, 0);639this.exceptionWidgetVisible.set(true);640this.allowScrollToExceptionWidget = true;641return;642}643644const firstVisibleLine = visibleRanges[0].startLineNumber;645646// Create widget - this may add a zone that pushes content down647this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession, this.shouldScrollToExceptionWidget);648this.exceptionWidget.show({ lineNumber, column }, 0);649this.exceptionWidgetVisible.set(true);650651// only adjust scroll if the exception widget is above the first visible line652if (lineNumber < firstVisibleLine) {653// Get the actual height of the widget that was just added from the whitespace654// The whitespace height is more accurate than the container height655const scrollAdjustment = this.exceptionWidget.getWhitespaceHeight();656657// Scroll down by the actual widget height to keep the first visible line the same658this.editor.setScrollTop(currentScrollTop + scrollAdjustment, ScrollType.Immediate);659}660661// Re-enable scrolling to exception widget662this.allowScrollToExceptionWidget = true;663}664665closeExceptionWidget(): void {666if (this.exceptionWidget) {667const shouldFocusEditor = this.exceptionWidget.hasFocus();668this.exceptionWidget.dispose();669this.exceptionWidget = undefined;670this.exceptionWidgetVisible.set(false);671if (shouldFocusEditor) {672this.editor.focus();673}674}675}676677async addLaunchConfiguration(): Promise<void> {678const model = this.editor.getModel();679if (!model) {680return;681}682683let configurationsArrayPosition: Position | undefined;684let lastProperty: string;685686const getConfigurationPosition = () => {687let depthInArray = 0;688visit(model.getValue(), {689onObjectProperty: (property: string) => {690lastProperty = property;691},692onArrayBegin: (offset: number) => {693if (lastProperty === 'configurations' && depthInArray === 0) {694configurationsArrayPosition = model.getPositionAt(offset + 1);695}696depthInArray++;697},698onArrayEnd: () => {699depthInArray--;700}701});702};703704getConfigurationPosition();705706if (!configurationsArrayPosition) {707// "configurations" array doesn't exist. Add it here.708const { tabSize, insertSpaces } = model.getOptions();709const eol = model.getEOL();710const edit = (basename(model.uri.fsPath) === 'launch.json') ?711setProperty(model.getValue(), ['configurations'], [], { tabSize, insertSpaces, eol })[0] :712setProperty(model.getValue(), ['launch'], { 'configurations': [] }, { tabSize, insertSpaces, eol })[0];713const startPosition = model.getPositionAt(edit.offset);714const lineNumber = startPosition.lineNumber;715const range = new Range(lineNumber, startPosition.column, lineNumber, model.getLineMaxColumn(lineNumber));716model.pushEditOperations(null, [EditOperation.replace(range, edit.content)], () => null);717// Go through the file again since we've edited it718getConfigurationPosition();719}720if (!configurationsArrayPosition) {721return;722}723724this.editor.focus();725726const insertLine = (position: Position): Promise<any> => {727// Check if there are more characters on a line after a "configurations": [, if yes enter a newline728if (model.getLineLastNonWhitespaceColumn(position.lineNumber) > position.column) {729this.editor.setPosition(position);730this.instantiationService.invokeFunction((accessor) => {731CoreEditingCommands.LineBreakInsert.runEditorCommand(accessor, this.editor, null);732});733}734this.editor.setPosition(position);735return this.commandService.executeCommand(InsertLineAfterAction.ID);736};737738await insertLine(configurationsArrayPosition);739await this.commandService.executeCommand('editor.action.triggerSuggest');740}741742// Inline Decorations743744@memoize745private get removeInlineValuesScheduler(): RunOnceScheduler {746return new RunOnceScheduler(747() => {748this.displayedStore.clear();749this.oldDecorations.clear();750},751100752);753}754755@memoize756private get updateInlineValuesScheduler(): RunOnceScheduler {757const model = this.editor.getModel();758return new RunOnceScheduler(759async () => await this.updateInlineValueDecorations(this.debugService.getViewModel().focusedStackFrame),760model ? this.debounceInfo.get(model) : DEAFULT_INLINE_DEBOUNCE_DELAY761);762}763764private async updateInlineValueDecorations(stackFrame: IStackFrame | undefined): Promise<void> {765766const var_value_format = '{0} = {1}';767const separator = ', ';768769const model = this.editor.getModel();770const inlineValuesSetting = this.configurationService.getValue<IDebugConfiguration>('debug').inlineValues;771const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model));772if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) {773if (!this.removeInlineValuesScheduler.isScheduled()) {774this.removeInlineValuesScheduler.schedule();775}776return;777}778779this.removeInlineValuesScheduler.cancel();780this.displayedStore.clear();781782const viewRanges = this.editor.getVisibleRangesPlusViewportAboveBelow();783let allDecorations: IModelDeltaDecoration[];784785const cts = new CancellationTokenSource();786this.displayedStore.add(toDisposable(() => cts.dispose(true)));787788if (this.languageFeaturesService.inlineValuesProvider.has(model)) {789790const findVariable = async (_key: string, caseSensitiveLookup: boolean): Promise<string | undefined> => {791const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range);792const key = caseSensitiveLookup ? _key : _key.toLowerCase();793for (const scope of scopes) {794const variables = await scope.getChildren();795const found = variables.find(v => caseSensitiveLookup ? (v.name === key) : (v.name.toLowerCase() === key));796if (found) {797return found.value;798}799}800return undefined;801};802803const ctx: InlineValueContext = {804frameId: stackFrame.frameId,805stoppedLocation: new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1, stackFrame.range.endLineNumber, stackFrame.range.endColumn + 1)806};807808const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse();809810allDecorations = [];811const lineDecorations = new Map<number, InlineSegment[]>();812813const promises = providers.flatMap(provider => viewRanges.map(range => Promise.resolve(provider.provideInlineValues(model, range, ctx, cts.token)).then(async (result) => {814if (result) {815for (const iv of result) {816817let text: string | undefined = undefined;818switch (iv.type) {819case 'text':820text = iv.text;821break;822case 'variable': {823let va = iv.variableName;824if (!va) {825const lineContent = model.getLineContent(iv.range.startLineNumber);826va = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1);827}828const value = await findVariable(va, iv.caseSensitiveLookup);829if (value) {830text = strings.format(var_value_format, va, value);831}832break;833}834case 'expression': {835let expr = iv.expression;836if (!expr) {837const lineContent = model.getLineContent(iv.range.startLineNumber);838expr = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1);839}840if (expr) {841const expression = new Expression(expr);842await expression.evaluate(stackFrame.thread.session, stackFrame, 'watch', true);843if (expression.available) {844text = strings.format(var_value_format, expr, expression.value);845}846}847break;848}849}850851if (text) {852const line = iv.range.startLineNumber;853let lineSegments = lineDecorations.get(line);854if (!lineSegments) {855lineSegments = [];856lineDecorations.set(line, lineSegments);857}858if (!lineSegments.some(iv => iv.text === text)) { // de-dupe859lineSegments.push(new InlineSegment(iv.range.startColumn, text));860}861}862}863}864}, err => {865onUnexpectedExternalError(err);866})));867868const startTime = Date.now();869870await Promise.all(promises);871872// update debounce info873this.updateInlineValuesScheduler.delay = this.debounceInfo.update(model, Date.now() - startTime);874875// sort line segments and concatenate them into a decoration876877lineDecorations.forEach((segments, line) => {878if (segments.length > 0) {879segments = segments.sort((a, b) => a.column - b.column);880const text = segments.map(s => s.text).join(separator);881const editorWidth = this.editor.getLayoutInfo().width;882const fontInfo = this.editor.getOption(EditorOption.fontInfo);883const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);884allDecorations.push(...createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol));885}886});887888} else {889// old "one-size-fits-all" strategy890891const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range);892const scopesWithVariables = await Promise.all(scopes.map(async scope =>893({ scope, variables: await scope.getChildren() })));894895// Map of inline values per line that's populated in scope order, from896// narrowest to widest. This is done to avoid duplicating values if897// they appear in multiple scopes or are shadowed (#129770, #217326)898const valuesPerLine = new Map</* line */number, Map</* var */string, /* value */ string>>();899900for (const { scope, variables } of scopesWithVariables) {901let scopeRange = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn);902if (scope.range) {903scopeRange = scopeRange.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);904}905906const ownRanges = viewRanges.map(r => r.intersectRanges(scopeRange)).filter(isDefined);907this._wordToLineNumbersMap ??= new WordsToLineNumbersCache(model);908for (const range of ownRanges) {909this._wordToLineNumbersMap.ensureRangePopulated(range);910}911912const mapped = createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value);913for (const { line, variables } of mapped) {914let values = valuesPerLine.get(line);915if (!values) {916values = new Map<string, string>();917valuesPerLine.set(line, values);918}919920for (const { name, value } of variables) {921if (!values.has(name)) {922values.set(name, value);923}924}925}926}927928allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) => {929const text = [...values].map(([n, v]) => `${n} = ${v}`).join(', ');930const editorWidth = this.editor.getLayoutInfo().width;931const fontInfo = this.editor.getOption(EditorOption.fontInfo);932const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);933return createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol);934});935}936937if (cts.token.isCancellationRequested) {938return;939}940941// If word wrap is on, application of inline decorations may change the scroll position.942// Ensure the cursor maintains its vertical position relative to the viewport when943// we apply decorations.944let preservePosition: { position: Position; top: number } | undefined;945if (this.editor.getOption(EditorOption.wordWrap) !== 'off') {946const position = this.editor.getPosition();947if (position && this.editor.getVisibleRanges().some(r => r.containsPosition(position))) {948preservePosition = { position, top: this.editor.getTopForPosition(position.lineNumber, position.column) };949}950}951952this.oldDecorations.set(allDecorations);953954if (preservePosition) {955const top = this.editor.getTopForPosition(preservePosition.position.lineNumber, preservePosition.position.column);956this.editor.setScrollTop(this.editor.getScrollTop() - (preservePosition.top - top), ScrollType.Immediate);957}958}959960dispose(): void {961if (this.hoverWidget) {962this.hoverWidget.dispose();963}964if (this.configurationWidget) {965this.configurationWidget.dispose();966}967this.toDispose = dispose(this.toDispose);968}969}970971class WordsToLineNumbersCache {972// we use this as an array of bits where each 1 bit is a line number that's been parsed973private readonly intervals: Uint8Array;974public readonly value = new Map<string, number[]>();975976constructor(private readonly model: ITextModel) {977this.intervals = new Uint8Array(Math.ceil(model.getLineCount() / 8));978}979980/** Ensures that variables names in the given range have been identified. */981public ensureRangePopulated(range: Range) {982for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {983const bin = lineNumber >> 3; /* Math.floor(i / 8) */984const bit = 1 << (lineNumber & 0b111); /* 1 << (i % 8) */985if (!(this.intervals[bin] & bit)) {986getWordToLineNumbersMap(this.model, lineNumber, this.value);987this.intervals[bin] |= bit;988}989}990}991}992993994CommandsRegistry.registerCommand(995'_executeInlineValueProvider',996async (997accessor: ServicesAccessor,998uri: URI,999iRange: IRange,1000context: InlineValueContext1001): Promise<InlineValue[] | null> => {1002assertType(URI.isUri(uri));1003assertType(Range.isIRange(iRange));10041005if (!context || typeof context.frameId !== 'number' || !Range.isIRange(context.stoppedLocation)) {1006throw illegalArgument('context');1007}10081009const model = accessor.get(IModelService).getModel(uri);1010if (!model) {1011throw illegalArgument('uri');1012}10131014const range = Range.lift(iRange);1015const { inlineValuesProvider } = accessor.get(ILanguageFeaturesService);1016const providers = inlineValuesProvider.ordered(model);1017const providerResults = await Promise.all(providers.map(provider => provider.provideInlineValues(model, range, context, CancellationToken.None)));1018return providerResults.flat().filter(isDefined);1019});102010211022