/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as env from 'vs/base/common/platform'; import { visit } from 'vs/base/common/json'; import { setProperty } from 'vs/base/common/jsonEdit'; import { Constants } from 'vs/base/common/uint'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { InlineValueContext, StandardTokenType } from 'vs/editor/common/languages'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { distinct, flatten } from 'vs/base/common/arrays'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/core/wordHelper'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugEditorContribution, IDebugService, State, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, IDebugSession, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { Position } from 'vs/editor/common/core/position'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { memoize } from 'vs/base/common/decorators'; import { IEditorHoverOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover'; import { IModelDeltaDecoration, InjectedTextCursorStops, ITextModel } from 'vs/editor/common/model'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { basename } from 'vs/base/common/path'; import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover'; import { HoverStartMode } from 'vs/editor/contrib/hover/browser/hoverOperation'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { Event } from 'vs/base/common/event'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { addDisposableListener } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; const LAUNCH_JSON_REGEX = /\.vscode\/launch\.json$/; const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped const DEAFULT_INLINE_DEBOUNCE_DELAY = 200; export const debugInlineForeground = registerColor('editor.inlineValuesForeground', { dark: '#ffffff80', light: '#00000080', hcDark: '#ffffff80', hcLight: '#00000080' }, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { dark: '#ffc80033', light: '#ffc80033', hcDark: '#ffc80033', hcLight: '#ffc80033' }, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); class InlineSegment { constructor(public column: number, public text: string) { } } function createInlineValueDecoration(lineNumber: number, contentText: string, column = Constants.MAX_SAFE_SMALL_INTEGER): IModelDeltaDecoration[] { // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { contentText = contentText.substring(0, MAX_INLINE_DECORATOR_LENGTH) + '...'; } return [ { range: { startLineNumber: lineNumber, endLineNumber: lineNumber, startColumn: column, endColumn: column }, options: { description: 'debug-inline-value-decoration-spacer', after: { content: strings.noBreakWhitespace, cursorStops: InjectedTextCursorStops.None }, showIfCollapsed: true, } }, { range: { startLineNumber: lineNumber, endLineNumber: lineNumber, startColumn: column, endColumn: column }, options: { description: 'debug-inline-value-decoration', after: { content: replaceWsWithNoBreakWs(contentText), inlineClassName: 'debug-inline-value', inlineClassNameAffectsLetterSpacing: true, cursorStops: InjectedTextCursorStops.None }, showIfCollapsed: true, } }, ]; } function replaceWsWithNoBreakWs(str: string): string { return str.replace(/[ \t]/g, strings.noBreakWhitespace); } function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, range: Range, model: ITextModel, wordToLineNumbersMap: Map): IModelDeltaDecoration[] { const nameValueMap = new Map(); for (const expr of expressions) { nameValueMap.set(expr.name, expr.value); // Limit the size of map. Too large can have a perf impact if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) { break; } } const lineToNamesMap: Map = new Map(); // Compute unique set of names on each line nameValueMap.forEach((_value, name) => { const lineNumbers = wordToLineNumbersMap.get(name); if (lineNumbers) { for (const lineNumber of lineNumbers) { if (range.containsPosition(new Position(lineNumber, 0))) { if (!lineToNamesMap.has(lineNumber)) { lineToNamesMap.set(lineNumber, []); } if (lineToNamesMap.get(lineNumber)!.indexOf(name) === -1) { lineToNamesMap.get(lineNumber)!.push(name); } } } } }); const decorations: IModelDeltaDecoration[] = []; // Compute decorators for each line lineToNamesMap.forEach((names, line) => { const contentText = names.sort((first, second) => { const content = model.getLineContent(line); return content.indexOf(first) - content.indexOf(second); }).map(name => `${name} = ${nameValueMap.get(name)}`).join(', '); decorations.push(...createInlineValueDecoration(line, contentText)); }); return decorations; } function getWordToLineNumbersMap(model: ITextModel | null): Map { const result = new Map(); if (!model) { return result; } // For every word in every line, map its ranges for fast lookup for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) { const lineContent = model.getLineContent(lineNumber); // If line is too long then skip the line if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) { continue; } model.tokenization.forceTokenization(lineNumber); const lineTokens = model.tokenization.getLineTokens(lineNumber); for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) { const tokenType = lineTokens.getStandardTokenType(tokenIndex); // Token is a word and not a comment if (tokenType === StandardTokenType.Other) { DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match const tokenStartOffset = lineTokens.getStartOffset(tokenIndex); const tokenEndOffset = lineTokens.getEndOffset(tokenIndex); const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset); const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr); if (wordMatch) { const word = wordMatch[0]; if (!result.has(word)) { result.set(word, []); } result.get(word)!.push(lineNumber); } } } } return result; } export class DebugEditorContribution implements IDebugEditorContribution { private toDispose: IDisposable[]; private hoverWidget: DebugHoverWidget; private hoverRange: Range | null = null; private mouseDown = false; private exceptionWidgetVisible: IContextKey; private exceptionWidget: ExceptionWidget | undefined; private configurationWidget: FloatingClickWidget | undefined; private altListener: IDisposable | undefined; private altPressed = false; private oldDecorations: string[] = []; private readonly debounceInfo: IFeatureDebounceInformation; constructor( private editor: ICodeEditor, @IDebugService private readonly debugService: IDebugService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IHostService private readonly hostService: IHostService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IContextKeyService contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ILanguageFeatureDebounceService featureDebounceService: ILanguageFeatureDebounceService ) { this.debounceInfo = featureDebounceService.for(languageFeaturesService.inlineValuesProvider, 'InlineValues', { min: DEAFULT_INLINE_DEBOUNCE_DELAY }); this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor); this.toDispose = []; this.registerListeners(); this.updateConfigurationWidgetVisibility(); this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService); this.toggleExceptionWidget(); } private registerListeners(): void { this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(e => this.onFocusStackFrame(e.stackFrame))); // hover listeners & hover widget this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e))); this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false)); this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e))); this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => { const hoverDomNode = this.hoverWidget.getDomNode(); if (!hoverDomNode) { return; } const rect = hoverDomNode.getBoundingClientRect(); // Only hide the hover widget if the editor mouse leave event is outside the hover widget #3528 if (e.event.posx < rect.left || e.event.posx > rect.right || e.event.posy < rect.top || e.event.posy > rect.bottom) { this.hideHoverWidget(); } })); this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e))); this.toDispose.push(this.editor.onDidChangeModelContent(() => { this._wordToLineNumbersMap = undefined; this.updateInlineValuesScheduler.schedule(); })); this.toDispose.push(this.debugService.getViewModel().onWillUpdateViews(() => this.updateInlineValuesScheduler.schedule())); this.toDispose.push(this.debugService.getViewModel().onDidEvaluateLazyExpression(() => this.updateInlineValuesScheduler.schedule())); this.toDispose.push(this.editor.onDidChangeModel(async () => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; const model = this.editor.getModel(); if (model) { this.applyHoverConfiguration(model, stackFrame); } this.toggleExceptionWidget(); this.hideHoverWidget(); this.updateConfigurationWidgetVisibility(); this._wordToLineNumbersMap = undefined; await this.updateInlineValueDecorations(stackFrame); })); this.toDispose.push(this.editor.onDidScrollChange(() => { this.hideHoverWidget(); // Inline value provider should get called on view port change const model = this.editor.getModel(); if (model && this.languageFeaturesService.inlineValuesProvider.has(model)) { this.updateInlineValuesScheduler.schedule(); } })); this.toDispose.push(this.debugService.onDidChangeState((state: State) => { if (state !== State.Stopped) { this.toggleExceptionWidget(); } })); } private _wordToLineNumbersMap: Map | undefined = undefined; private get wordToLineNumbersMap(): Map { if (!this._wordToLineNumbersMap) { this._wordToLineNumbersMap = getWordToLineNumbersMap(this.editor.getModel()); } return this._wordToLineNumbersMap; } private applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame | undefined): void { if (stackFrame && this.uriIdentityService.extUri.isEqual(model.uri, stackFrame.source.uri)) { if (this.altListener) { this.altListener.dispose(); } // When the alt key is pressed show regular editor hover and hide the debug hover #84561 this.altListener = addDisposableListener(document, 'keydown', keydownEvent => { const standardKeyboardEvent = new StandardKeyboardEvent(keydownEvent); if (standardKeyboardEvent.keyCode === KeyCode.Alt) { this.altPressed = true; const debugHoverWasVisible = this.hoverWidget.isVisible(); this.hoverWidget.hide(); this.enableEditorHover(); if (debugHoverWasVisible && this.hoverRange) { // If the debug hover was visible immediately show the editor hover for the alt transition to be smooth const hoverController = this.editor.getContribution(ModesHoverController.ID); hoverController?.showContentHover(this.hoverRange, HoverStartMode.Immediate, false); } const onKeyUp = new DomEmitter(document, 'keyup'); const listener = Event.any(this.hostService.onDidChangeFocus, onKeyUp.event)(keyupEvent => { let standardKeyboardEvent = undefined; if (keyupEvent instanceof KeyboardEvent) { standardKeyboardEvent = new StandardKeyboardEvent(keyupEvent); } if (!standardKeyboardEvent || standardKeyboardEvent.keyCode === KeyCode.Alt) { this.altPressed = false; this.editor.updateOptions({ hover: { enabled: false } }); listener.dispose(); onKeyUp.dispose(); } }); } }); this.editor.updateOptions({ hover: { enabled: false } }); } else { this.altListener?.dispose(); this.enableEditorHover(); } } private enableEditorHover(): void { if (this.editor.hasModel()) { const model = this.editor.getModel(); const overrides = { resource: model.uri, overrideIdentifier: model.getLanguageId() }; const defaultConfiguration = this.configurationService.getValue('editor.hover', overrides); this.editor.updateOptions({ hover: { enabled: defaultConfiguration.enabled, delay: defaultConfiguration.delay, sticky: defaultConfiguration.sticky } }); } } async showHover(range: Range, focus: boolean): Promise { const sf = this.debugService.getViewModel().focusedStackFrame; const model = this.editor.getModel(); if (sf && model && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri) && !this.altPressed) { return this.hoverWidget.showAt(range, focus); } } private async onFocusStackFrame(sf: IStackFrame | undefined): Promise { const model = this.editor.getModel(); if (model) { this.applyHoverConfiguration(model, sf); if (sf && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) { await this.toggleExceptionWidget(); } else { this.hideHoverWidget(); } } await this.updateInlineValueDecorations(sf); } @memoize private get showHoverScheduler(): RunOnceScheduler { const hoverOption = this.editor.getOption(EditorOption.hover); const scheduler = new RunOnceScheduler(() => { if (this.hoverRange) { this.showHover(this.hoverRange, false); } }, hoverOption.delay * 2); this.toDispose.push(scheduler); return scheduler; } @memoize private get hideHoverScheduler(): RunOnceScheduler { const scheduler = new RunOnceScheduler(() => { if (!this.hoverWidget.isHovered()) { this.hoverWidget.hide(); } }, 0); this.toDispose.push(scheduler); return scheduler; } private hideHoverWidget(): void { if (!this.hideHoverScheduler.isScheduled() && this.hoverWidget.willBeVisible()) { this.hideHoverScheduler.schedule(); } this.showHoverScheduler.cancel(); } // hover business private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { this.mouseDown = true; if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === DebugHoverWidget.ID) { return; } this.hideHoverWidget(); } private onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { if (this.debugService.state !== State.Stopped) { return; } const target = mouseEvent.target; const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey'; if (target.type === MouseTargetType.CONTENT_WIDGET && target.detail === DebugHoverWidget.ID && !(mouseEvent.event)[stopKey]) { // mouse moved on top of debug hover widget return; } if (target.type === MouseTargetType.CONTENT_TEXT) { if (target.range && !target.range.equalsRange(this.hoverRange)) { this.hoverRange = target.range; this.hideHoverScheduler.cancel(); this.showHoverScheduler.schedule(); } } else if (!this.mouseDown) { // Do not hide debug hover when the mouse is pressed because it usually leads to accidental closing #64620 this.hideHoverWidget(); } } private onKeyDown(e: IKeyboardEvent): void { const stopKey = env.isMacintosh ? KeyCode.Meta : KeyCode.Ctrl; if (e.keyCode !== stopKey) { // do not hide hover when Ctrl/Meta is pressed this.hideHoverWidget(); } } // end hover business // exception widget private async toggleExceptionWidget(): Promise { // Toggles exception widget based on the state of the current editor model and debug stack frame const model = this.editor.getModel(); const focusedSf = this.debugService.getViewModel().focusedStackFrame; const callStack = focusedSf ? focusedSf.thread.getCallStack() : null; if (!model || !focusedSf || !callStack || callStack.length === 0) { this.closeExceptionWidget(); return; } // First call stack frame that is available is the frame where exception has been thrown const exceptionSf = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); if (!exceptionSf || exceptionSf !== focusedSf) { this.closeExceptionWidget(); return; } const sameUri = this.uriIdentityService.extUri.isEqual(exceptionSf.source.uri, model.uri); if (this.exceptionWidget && !sameUri) { this.closeExceptionWidget(); } else if (sameUri) { const exceptionInfo = await focusedSf.thread.exceptionInfo; if (exceptionInfo) { this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn); } } } private showExceptionWidget(exceptionInfo: IExceptionInfo, debugSession: IDebugSession | undefined, lineNumber: number, column: number): void { if (this.exceptionWidget) { this.exceptionWidget.dispose(); } this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession); this.exceptionWidget.show({ lineNumber, column }, 0); this.exceptionWidget.focus(); this.editor.revealRangeInCenter({ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column, }); this.exceptionWidgetVisible.set(true); } closeExceptionWidget(): void { if (this.exceptionWidget) { const shouldFocusEditor = this.exceptionWidget.hasFocus(); this.exceptionWidget.dispose(); this.exceptionWidget = undefined; this.exceptionWidgetVisible.set(false); if (shouldFocusEditor) { this.editor.focus(); } } } // configuration widget private updateConfigurationWidgetVisibility(): void { const model = this.editor.getModel(); if (this.configurationWidget) { this.configurationWidget.dispose(); } if (model && LAUNCH_JSON_REGEX.test(model.uri.toString()) && !this.editor.getOption(EditorOption.readOnly)) { this.configurationWidget = this.instantiationService.createInstance(FloatingClickWidget, this.editor, nls.localize('addConfiguration', "Add Configuration..."), null); this.configurationWidget.render(); this.toDispose.push(this.configurationWidget.onClick(() => this.addLaunchConfiguration())); } } async addLaunchConfiguration(): Promise { const model = this.editor.getModel(); if (!model) { return; } let configurationsArrayPosition: Position | undefined; let lastProperty: string; const getConfigurationPosition = () => { let depthInArray = 0; visit(model.getValue(), { onObjectProperty: (property: string) => { lastProperty = property; }, onArrayBegin: (offset: number) => { if (lastProperty === 'configurations' && depthInArray === 0) { configurationsArrayPosition = model.getPositionAt(offset + 1); } depthInArray++; }, onArrayEnd: () => { depthInArray--; } }); }; getConfigurationPosition(); if (!configurationsArrayPosition) { // "configurations" array doesn't exist. Add it here. const { tabSize, insertSpaces } = model.getOptions(); const eol = model.getEOL(); const edit = (basename(model.uri.fsPath) === 'launch.json') ? setProperty(model.getValue(), ['configurations'], [], { tabSize, insertSpaces, eol })[0] : setProperty(model.getValue(), ['launch'], { 'configurations': [] }, { tabSize, insertSpaces, eol })[0]; const startPosition = model.getPositionAt(edit.offset); const lineNumber = startPosition.lineNumber; const range = new Range(lineNumber, startPosition.column, lineNumber, model.getLineMaxColumn(lineNumber)); model.pushEditOperations(null, [EditOperation.replace(range, edit.content)], () => null); // Go through the file again since we've edited it getConfigurationPosition(); } if (!configurationsArrayPosition) { return; } this.editor.focus(); const insertLine = (position: Position): Promise => { // Check if there are more characters on a line after a "configurations": [, if yes enter a newline if (model.getLineLastNonWhitespaceColumn(position.lineNumber) > position.column) { this.editor.setPosition(position); CoreEditingCommands.LineBreakInsert.runEditorCommand(null, this.editor, null); } this.editor.setPosition(position); return this.commandService.executeCommand('editor.action.insertLineAfter'); }; await insertLine(configurationsArrayPosition); await this.commandService.executeCommand('editor.action.triggerSuggest'); } // Inline Decorations @memoize private get removeInlineValuesScheduler(): RunOnceScheduler { return new RunOnceScheduler( () => { this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, []); }, 100 ); } @memoize private get updateInlineValuesScheduler(): RunOnceScheduler { const model = this.editor.getModel(); return new RunOnceScheduler( async () => await this.updateInlineValueDecorations(this.debugService.getViewModel().focusedStackFrame), model ? this.debounceInfo.get(model) : DEAFULT_INLINE_DEBOUNCE_DELAY ); } private async updateInlineValueDecorations(stackFrame: IStackFrame | undefined): Promise { const var_value_format = '{0} = {1}'; const separator = ', '; const model = this.editor.getModel(); const inlineValuesSetting = this.configurationService.getValue('debug').inlineValues; const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) { if (!this.removeInlineValuesScheduler.isScheduled()) { this.removeInlineValuesScheduler.schedule(); } return; } this.removeInlineValuesScheduler.cancel(); let allDecorations: IModelDeltaDecoration[]; if (this.languageFeaturesService.inlineValuesProvider.has(model)) { const findVariable = async (_key: string, caseSensitiveLookup: boolean): Promise => { const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range); const key = caseSensitiveLookup ? _key : _key.toLowerCase(); for (const scope of scopes) { const variables = await scope.getChildren(); const found = variables.find(v => caseSensitiveLookup ? (v.name === key) : (v.name.toLowerCase() === key)); if (found) { return found.value; } } return undefined; }; const ctx: InlineValueContext = { frameId: stackFrame.frameId, stoppedLocation: new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1, stackFrame.range.endLineNumber, stackFrame.range.endColumn + 1) }; const token = new CancellationTokenSource().token; const ranges = this.editor.getVisibleRangesPlusViewportAboveBelow(); const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse(); allDecorations = []; const lineDecorations = new Map(); const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlineValues(model, range, ctx, token)).then(async (result) => { if (result) { for (const iv of result) { let text: string | undefined = undefined; switch (iv.type) { case 'text': text = iv.text; break; case 'variable': { let va = iv.variableName; if (!va) { const lineContent = model.getLineContent(iv.range.startLineNumber); va = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1); } const value = await findVariable(va, iv.caseSensitiveLookup); if (value) { text = strings.format(var_value_format, va, value); } break; } case 'expression': { let expr = iv.expression; if (!expr) { const lineContent = model.getLineContent(iv.range.startLineNumber); expr = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1); } if (expr) { const expression = new Expression(expr); await expression.evaluate(stackFrame.thread.session, stackFrame, 'watch', true); if (expression.available) { text = strings.format(var_value_format, expr, expression.value); } } break; } } if (text) { const line = iv.range.startLineNumber; let lineSegments = lineDecorations.get(line); if (!lineSegments) { lineSegments = []; lineDecorations.set(line, lineSegments); } if (!lineSegments.some(iv => iv.text === text)) { // de-dupe lineSegments.push(new InlineSegment(iv.range.startColumn, text)); } } } } }, err => { onUnexpectedExternalError(err); })))); const startTime = Date.now(); await Promise.all(promises); // update debounce info this.updateInlineValuesScheduler.delay = this.debounceInfo.update(model, Date.now() - startTime); // sort line segments and concatenate them into a decoration lineDecorations.forEach((segments, line) => { if (segments.length > 0) { segments = segments.sort((a, b) => a.column - b.column); const text = segments.map(s => s.text).join(separator); allDecorations.push(...createInlineValueDecoration(line, text)); } }); } else { // old "one-size-fits-all" strategy const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range); // Get all top level variables in the scope chain const decorationsPerScope = await Promise.all(scopes.map(async scope => { const variables = await scope.getChildren(); let range = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn); if (scope.range) { range = range.setStartPosition(scope.range.startLineNumber, scope.range.startColumn); } return createInlineValueDecorationsInsideRange(variables, range, model, this.wordToLineNumbersMap); })); allDecorations = distinct(decorationsPerScope.reduce((previous, current) => previous.concat(current), []), // Deduplicate decorations since same variable can appear in multiple scopes, leading to duplicated decorations #129770 decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); } this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, allDecorations); } dispose(): void { if (this.hoverWidget) { this.hoverWidget.dispose(); } if (this.configurationWidget) { this.configurationWidget.dispose(); } this.toDispose = dispose(this.toDispose); this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, []); } }