Path: blob/main/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { 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 { MarkdownString } from '../../../../base/common/htmlContent.js';5556const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons57const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added58const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped5960const DEAFULT_INLINE_DEBOUNCE_DELAY = 200;6162export const debugInlineForeground = registerColor('editor.inlineValuesForeground', {63dark: '#ffffff80',64light: '#00000080',65hcDark: '#ffffff80',66hcLight: '#00000080'67}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text."));6869export const debugInlineBackground = registerColor('editor.inlineValuesBackground', '#ffc80033', nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background."));7071class InlineSegment {72constructor(public column: number, public text: string) {73}74}7576export function formatHoverContent(contentText: string): MarkdownString {77if (contentText.includes(',') && contentText.includes('=')) {78// Custom split: for each equals sign after the first, backtrack to the nearest comma79const customSplit = (text: string): string[] => {80const splits: number[] = [];81let equalsFound = 0;82let start = 0;83for (let i = 0; i < text.length; i++) {84if (text[i] === '=') {85if (equalsFound === 0) {86equalsFound++;87continue;88}89const commaIndex = text.lastIndexOf(',', i);90if (commaIndex !== -1 && commaIndex >= start) {91splits.push(commaIndex);92start = commaIndex + 1;93}94equalsFound++;95}96}97const result: string[] = [];98let s = 0;99for (const index of splits) {100result.push(text.substring(s, index).trim());101s = index + 1;102}103if (s < text.length) {104result.push(text.substring(s).trim());105}106return result;107};108109const pairs = customSplit(contentText);110const formattedPairs = pairs.map(pair => {111const equalsIndex = pair.indexOf('=');112if (equalsIndex !== -1) {113const indent = ' '.repeat(equalsIndex + 2);114const [firstLine, ...restLines] = pair.split(/\r?\n/);115return [firstLine, ...restLines.map(line => indent + line)].join('\n');116}117return pair;118});119return new MarkdownString().appendCodeblock('', formattedPairs.join(',\n'));120}121return new MarkdownString().appendCodeblock('', contentText);122}123124export function createInlineValueDecoration(lineNumber: number, contentText: string, classNamePrefix: string, column = Constants.MAX_SAFE_SMALL_INTEGER, viewportMaxCol: number = MAX_INLINE_DECORATOR_LENGTH): IModelDeltaDecoration[] {125const rawText = contentText; // store raw text for hover message126127// Truncate contentText if it exceeds the viewport max column128if (contentText.length > viewportMaxCol) {129contentText = contentText.substring(0, viewportMaxCol) + '...';130}131132return [133{134range: {135startLineNumber: lineNumber,136endLineNumber: lineNumber,137startColumn: column,138endColumn: column139},140options: {141description: `${classNamePrefix}-inline-value-decoration-spacer`,142after: {143content: strings.noBreakWhitespace,144cursorStops: InjectedTextCursorStops.None145},146showIfCollapsed: true,147}148},149{150range: {151startLineNumber: lineNumber,152endLineNumber: lineNumber,153startColumn: column,154endColumn: column155},156options: {157description: `${classNamePrefix}-inline-value-decoration`,158after: {159content: replaceWsWithNoBreakWs(contentText),160inlineClassName: `${classNamePrefix}-inline-value`,161inlineClassNameAffectsLetterSpacing: true,162cursorStops: InjectedTextCursorStops.None163},164showIfCollapsed: true,165hoverMessage: formatHoverContent(rawText)166}167},168];169}170171function replaceWsWithNoBreakWs(str: string): string {172return str.replace(/[ \t\n]/g, strings.noBreakWhitespace);173}174175function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray<IExpression>, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map<string, number[]>) {176const nameValueMap = new Map<string, string>();177for (const expr of expressions) {178nameValueMap.set(expr.name, expr.value);179// Limit the size of map. Too large can have a perf impact180if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {181break;182}183}184185const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();186187// Compute unique set of names on each line188nameValueMap.forEach((_value, name) => {189const lineNumbers = wordToLineNumbersMap.get(name);190if (lineNumbers) {191for (const lineNumber of lineNumbers) {192if (ranges.some(r => lineNumber >= r.startLineNumber && lineNumber <= r.endLineNumber)) {193if (!lineToNamesMap.has(lineNumber)) {194lineToNamesMap.set(lineNumber, []);195}196197if (lineToNamesMap.get(lineNumber)!.indexOf(name) === -1) {198lineToNamesMap.get(lineNumber)!.push(name);199}200}201}202}203});204205// Compute decorators for each line206return [...lineToNamesMap].map(([line, names]) => ({207line,208variables: names.sort((first, second) => {209const content = model.getLineContent(line);210return content.indexOf(first) - content.indexOf(second);211}).map(name => ({ name, value: nameValueMap.get(name)! }))212}));213}214215function getWordToLineNumbersMap(model: ITextModel, lineNumber: number, result: Map<string, number[]>) {216const lineLength = model.getLineLength(lineNumber);217// If line is too long then skip the line218if (lineLength > MAX_TOKENIZATION_LINE_LEN) {219return;220}221222const lineContent = model.getLineContent(lineNumber);223model.tokenization.forceTokenization(lineNumber);224const lineTokens = model.tokenization.getLineTokens(lineNumber);225for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) {226const tokenType = lineTokens.getStandardTokenType(tokenIndex);227228// Token is a word and not a comment229if (tokenType === StandardTokenType.Other) {230DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match231232const tokenStartOffset = lineTokens.getStartOffset(tokenIndex);233const tokenEndOffset = lineTokens.getEndOffset(tokenIndex);234const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset);235const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr);236237if (wordMatch) {238239const word = wordMatch[0];240if (!result.has(word)) {241result.set(word, []);242}243244result.get(word)!.push(lineNumber);245}246}247}248}249250export class DebugEditorContribution implements IDebugEditorContribution {251252private toDispose: IDisposable[];253private hoverWidget: DebugHoverWidget;254private hoverPosition?: { position: Position; event: IMouseEvent };255private mouseDown = false;256private exceptionWidgetVisible: IContextKey<boolean>;257private gutterIsHovered = false;258259private exceptionWidget: ExceptionWidget | undefined;260private configurationWidget: FloatingEditorClickWidget | undefined;261private readonly altListener = new MutableDisposable();262private altPressed = false;263private oldDecorations: IEditorDecorationsCollection;264private readonly displayedStore = new DisposableStore();265private editorHoverOptions: IEditorHoverOptions | undefined;266private readonly debounceInfo: IFeatureDebounceInformation;267268// Holds a Disposable that prevents the default editor hover behavior while it exists.269private readonly defaultHoverLockout = new MutableDisposable();270271constructor(272private editor: ICodeEditor,273@IDebugService private readonly debugService: IDebugService,274@IInstantiationService private readonly instantiationService: IInstantiationService,275@ICommandService private readonly commandService: ICommandService,276@IConfigurationService private readonly configurationService: IConfigurationService,277@IHostService private readonly hostService: IHostService,278@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,279@IContextKeyService contextKeyService: IContextKeyService,280@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,281@ILanguageFeatureDebounceService featureDebounceService: ILanguageFeatureDebounceService282) {283this.oldDecorations = this.editor.createDecorationsCollection();284this.debounceInfo = featureDebounceService.for(languageFeaturesService.inlineValuesProvider, 'InlineValues', { min: DEAFULT_INLINE_DEBOUNCE_DELAY });285this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor);286this.toDispose = [this.defaultHoverLockout, this.altListener, this.displayedStore];287this.registerListeners();288this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService);289this.toggleExceptionWidget();290}291292private registerListeners(): void {293this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(e => this.onFocusStackFrame(e.stackFrame)));294295// hover listeners & hover widget296this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e)));297this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false));298this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e)));299this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => {300const hoverDomNode = this.hoverWidget.getDomNode();301if (!hoverDomNode) {302return;303}304305const rect = hoverDomNode.getBoundingClientRect();306// Only hide the hover widget if the editor mouse leave event is outside the hover widget #3528307if (e.event.posx < rect.left || e.event.posx > rect.right || e.event.posy < rect.top || e.event.posy > rect.bottom) {308this.hideHoverWidget();309}310}));311this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e)));312this.toDispose.push(this.editor.onDidChangeModelContent(() => {313this._wordToLineNumbersMap = undefined;314this.updateInlineValuesScheduler.schedule();315}));316this.toDispose.push(this.debugService.getViewModel().onWillUpdateViews(() => this.updateInlineValuesScheduler.schedule()));317this.toDispose.push(this.debugService.getViewModel().onDidEvaluateLazyExpression(() => this.updateInlineValuesScheduler.schedule()));318this.toDispose.push(this.editor.onDidChangeModel(async () => {319this.addDocumentListeners();320this.toggleExceptionWidget();321this.hideHoverWidget();322this._wordToLineNumbersMap = undefined;323const stackFrame = this.debugService.getViewModel().focusedStackFrame;324await this.updateInlineValueDecorations(stackFrame);325}));326this.toDispose.push(this.editor.onDidScrollChange(() => {327this.hideHoverWidget();328329// Inline value provider should get called on view port change330const model = this.editor.getModel();331if (model && this.languageFeaturesService.inlineValuesProvider.has(model)) {332this.updateInlineValuesScheduler.schedule();333}334}));335this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => {336if (e.affectsConfiguration('editor.hover')) {337this.updateHoverConfiguration();338}339}));340this.toDispose.push(this.debugService.onDidChangeState((state: State) => {341if (state !== State.Stopped) {342this.toggleExceptionWidget();343}344}));345346this.updateHoverConfiguration();347}348349private _wordToLineNumbersMap: WordsToLineNumbersCache | undefined;350351private updateHoverConfiguration(): void {352const model = this.editor.getModel();353if (model) {354this.editorHoverOptions = this.configurationService.getValue<IEditorHoverOptions>('editor.hover', {355resource: model.uri,356overrideIdentifier: model.getLanguageId()357});358}359}360361private addDocumentListeners(): void {362const stackFrame = this.debugService.getViewModel().focusedStackFrame;363const model = this.editor.getModel();364if (model) {365this.applyDocumentListeners(model, stackFrame);366}367}368369private applyDocumentListeners(model: ITextModel, stackFrame: IStackFrame | undefined): void {370if (!stackFrame || !this.uriIdentityService.extUri.isEqual(model.uri, stackFrame.source.uri)) {371this.altListener.clear();372return;373}374375const ownerDocument = this.editor.getContainerDomNode().ownerDocument;376377// When the alt key is pressed show regular editor hover and hide the debug hover #84561378this.altListener.value = addDisposableListener(ownerDocument, 'keydown', keydownEvent => {379const standardKeyboardEvent = new StandardKeyboardEvent(keydownEvent);380if (standardKeyboardEvent.keyCode === KeyCode.Alt) {381this.altPressed = true;382const debugHoverWasVisible = this.hoverWidget.isVisible();383this.hoverWidget.hide();384this.defaultHoverLockout.clear();385386if (debugHoverWasVisible && this.hoverPosition) {387// If the debug hover was visible immediately show the editor hover for the alt transition to be smooth388this.showEditorHover(this.hoverPosition.position, false);389}390391const onKeyUp = new DomEmitter(ownerDocument, 'keyup');392const listener = Event.any<KeyboardEvent | boolean>(this.hostService.onDidChangeFocus, onKeyUp.event)(keyupEvent => {393let standardKeyboardEvent = undefined;394if (isKeyboardEvent(keyupEvent)) {395standardKeyboardEvent = new StandardKeyboardEvent(keyupEvent);396}397if (!standardKeyboardEvent || standardKeyboardEvent.keyCode === KeyCode.Alt) {398this.altPressed = false;399this.preventDefaultEditorHover();400listener.dispose();401onKeyUp.dispose();402}403});404}405});406}407408async showHover(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise<void> {409// normally will already be set in `showHoverScheduler`, but public callers may hit this directly:410this.preventDefaultEditorHover();411412const sf = this.debugService.getViewModel().focusedStackFrame;413const model = this.editor.getModel();414if (sf && model && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) {415const result = await this.hoverWidget.showAt(position, focus, mouseEvent);416if (result === ShowDebugHoverResult.NOT_AVAILABLE) {417// When no expression available fallback to editor hover418this.showEditorHover(position, focus);419}420} else {421this.showEditorHover(position, focus);422}423}424425private preventDefaultEditorHover() {426if (this.defaultHoverLockout.value || this.editorHoverOptions?.enabled === false) {427return;428}429430const hoverController = this.editor.getContribution<ContentHoverController>(ContentHoverController.ID);431hoverController?.hideContentHover();432433this.editor.updateOptions({ hover: { enabled: false } });434this.defaultHoverLockout.value = {435dispose: () => {436this.editor.updateOptions({437hover: { enabled: this.editorHoverOptions?.enabled ?? true }438});439}440};441}442443private showEditorHover(position: Position, focus: boolean) {444const hoverController = this.editor.getContribution<ContentHoverController>(ContentHoverController.ID);445const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);446// enable the editor hover, otherwise the content controller will see it447// as disabled and hide it on the first mouse move (#193149)448this.defaultHoverLockout.clear();449hoverController?.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Mouse, focus);450}451452private async onFocusStackFrame(sf: IStackFrame | undefined): Promise<void> {453const model = this.editor.getModel();454if (model) {455this.applyDocumentListeners(model, sf);456if (sf && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) {457await this.toggleExceptionWidget();458} else {459this.hideHoverWidget();460}461}462463await this.updateInlineValueDecorations(sf);464}465466private get hoverDelay() {467const baseDelay = this.editorHoverOptions?.delay || 0;468469// heuristic to get a 'good' but configurable delay for evaluation. The470// debug hover can be very large, so we tend to be more conservative about471// when to show it (#180621). With this equation:472// - default 300ms hover => * 2 = 600ms473// - short 100ms hover => * 2 = 200ms474// - longer 600ms hover => * 1.5 = 900ms475// - long 1000ms hover => * 1.0 = 1000ms476const delayFactor = clamp(2 - (baseDelay - 300) / 600, 1, 2);477478return baseDelay * delayFactor;479}480481@memoize482private get showHoverScheduler() {483const scheduler = new RunOnceScheduler(() => {484if (this.hoverPosition && !this.altPressed) {485this.showHover(this.hoverPosition.position, false, this.hoverPosition.event);486}487}, this.hoverDelay);488this.toDispose.push(scheduler);489490return scheduler;491}492493private hideHoverWidget(): void {494if (this.hoverWidget.willBeVisible()) {495this.hoverWidget.hide();496}497this.showHoverScheduler.cancel();498this.defaultHoverLockout.clear();499}500501// hover business502503private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {504this.mouseDown = true;505if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === DebugHoverWidget.ID) {506return;507}508509this.hideHoverWidget();510}511512private onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {513if (this.debugService.state !== State.Stopped) {514return;515}516517const target = mouseEvent.target;518const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey';519520if (!this.altPressed) {521if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN) {522this.defaultHoverLockout.clear();523this.gutterIsHovered = true;524} else if (this.gutterIsHovered) {525this.gutterIsHovered = false;526this.updateHoverConfiguration();527}528}529530if (531(target.type === MouseTargetType.CONTENT_WIDGET && target.detail === DebugHoverWidget.ID)532|| this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)533) {534// mouse moved on top of debug hover widget535536const sticky = this.editorHoverOptions?.sticky ?? true;537if (sticky || this.hoverWidget.isShowingComplexValue || mouseEvent.event[stopKey]) {538return;539}540}541542if (target.type === MouseTargetType.CONTENT_TEXT) {543if (target.position && !Position.equals(target.position, this.hoverPosition?.position || null) && !this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)) {544this.hoverPosition = { position: target.position, event: mouseEvent.event };545// Disable the editor hover during the request to avoid flickering546this.preventDefaultEditorHover();547this.showHoverScheduler.schedule(this.hoverDelay);548}549} else if (!this.mouseDown) {550// Do not hide debug hover when the mouse is pressed because it usually leads to accidental closing #64620551this.hideHoverWidget();552}553}554555private onKeyDown(e: IKeyboardEvent): void {556const stopKey = env.isMacintosh ? KeyCode.Meta : KeyCode.Ctrl;557if (e.keyCode !== stopKey && e.keyCode !== KeyCode.Alt) {558// do not hide hover when Ctrl/Meta is pressed, and alt is handled separately559this.hideHoverWidget();560}561}562// end hover business563564// exception widget565private async toggleExceptionWidget(): Promise<void> {566// Toggles exception widget based on the state of the current editor model and debug stack frame567const model = this.editor.getModel();568const focusedSf = this.debugService.getViewModel().focusedStackFrame;569const callStack = focusedSf ? focusedSf.thread.getCallStack() : null;570if (!model || !focusedSf || !callStack || callStack.length === 0) {571this.closeExceptionWidget();572return;573}574575// First call stack frame that is available is the frame where exception has been thrown576const exceptionSf = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'));577if (!exceptionSf || exceptionSf !== focusedSf) {578this.closeExceptionWidget();579return;580}581582const sameUri = this.uriIdentityService.extUri.isEqual(exceptionSf.source.uri, model.uri);583if (this.exceptionWidget && !sameUri) {584this.closeExceptionWidget();585} else if (sameUri) {586const exceptionInfo = await focusedSf.thread.exceptionInfo;587if (exceptionInfo) {588this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn);589}590}591}592593private showExceptionWidget(exceptionInfo: IExceptionInfo, debugSession: IDebugSession | undefined, lineNumber: number, column: number): void {594if (this.exceptionWidget) {595this.exceptionWidget.dispose();596}597598this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession);599this.exceptionWidget.show({ lineNumber, column }, 0);600this.exceptionWidget.focus();601this.editor.revealRangeInCenter({602startLineNumber: lineNumber,603startColumn: column,604endLineNumber: lineNumber,605endColumn: column,606});607this.exceptionWidgetVisible.set(true);608}609610closeExceptionWidget(): void {611if (this.exceptionWidget) {612const shouldFocusEditor = this.exceptionWidget.hasFocus();613this.exceptionWidget.dispose();614this.exceptionWidget = undefined;615this.exceptionWidgetVisible.set(false);616if (shouldFocusEditor) {617this.editor.focus();618}619}620}621622async addLaunchConfiguration(): Promise<void> {623const model = this.editor.getModel();624if (!model) {625return;626}627628let configurationsArrayPosition: Position | undefined;629let lastProperty: string;630631const getConfigurationPosition = () => {632let depthInArray = 0;633visit(model.getValue(), {634onObjectProperty: (property: string) => {635lastProperty = property;636},637onArrayBegin: (offset: number) => {638if (lastProperty === 'configurations' && depthInArray === 0) {639configurationsArrayPosition = model.getPositionAt(offset + 1);640}641depthInArray++;642},643onArrayEnd: () => {644depthInArray--;645}646});647};648649getConfigurationPosition();650651if (!configurationsArrayPosition) {652// "configurations" array doesn't exist. Add it here.653const { tabSize, insertSpaces } = model.getOptions();654const eol = model.getEOL();655const edit = (basename(model.uri.fsPath) === 'launch.json') ?656setProperty(model.getValue(), ['configurations'], [], { tabSize, insertSpaces, eol })[0] :657setProperty(model.getValue(), ['launch'], { 'configurations': [] }, { tabSize, insertSpaces, eol })[0];658const startPosition = model.getPositionAt(edit.offset);659const lineNumber = startPosition.lineNumber;660const range = new Range(lineNumber, startPosition.column, lineNumber, model.getLineMaxColumn(lineNumber));661model.pushEditOperations(null, [EditOperation.replace(range, edit.content)], () => null);662// Go through the file again since we've edited it663getConfigurationPosition();664}665if (!configurationsArrayPosition) {666return;667}668669this.editor.focus();670671const insertLine = (position: Position): Promise<any> => {672// Check if there are more characters on a line after a "configurations": [, if yes enter a newline673if (model.getLineLastNonWhitespaceColumn(position.lineNumber) > position.column) {674this.editor.setPosition(position);675this.instantiationService.invokeFunction((accessor) => {676CoreEditingCommands.LineBreakInsert.runEditorCommand(accessor, this.editor, null);677});678}679this.editor.setPosition(position);680return this.commandService.executeCommand('editor.action.insertLineAfter');681};682683await insertLine(configurationsArrayPosition);684await this.commandService.executeCommand('editor.action.triggerSuggest');685}686687// Inline Decorations688689@memoize690private get removeInlineValuesScheduler(): RunOnceScheduler {691return new RunOnceScheduler(692() => {693this.displayedStore.clear();694this.oldDecorations.clear();695},696100697);698}699700@memoize701private get updateInlineValuesScheduler(): RunOnceScheduler {702const model = this.editor.getModel();703return new RunOnceScheduler(704async () => await this.updateInlineValueDecorations(this.debugService.getViewModel().focusedStackFrame),705model ? this.debounceInfo.get(model) : DEAFULT_INLINE_DEBOUNCE_DELAY706);707}708709private async updateInlineValueDecorations(stackFrame: IStackFrame | undefined): Promise<void> {710711const var_value_format = '{0} = {1}';712const separator = ', ';713714const model = this.editor.getModel();715const inlineValuesSetting = this.configurationService.getValue<IDebugConfiguration>('debug').inlineValues;716const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model));717if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) {718if (!this.removeInlineValuesScheduler.isScheduled()) {719this.removeInlineValuesScheduler.schedule();720}721return;722}723724this.removeInlineValuesScheduler.cancel();725this.displayedStore.clear();726727const viewRanges = this.editor.getVisibleRangesPlusViewportAboveBelow();728let allDecorations: IModelDeltaDecoration[];729730const cts = new CancellationTokenSource();731this.displayedStore.add(toDisposable(() => cts.dispose(true)));732733if (this.languageFeaturesService.inlineValuesProvider.has(model)) {734735const findVariable = async (_key: string, caseSensitiveLookup: boolean): Promise<string | undefined> => {736const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range);737const key = caseSensitiveLookup ? _key : _key.toLowerCase();738for (const scope of scopes) {739const variables = await scope.getChildren();740const found = variables.find(v => caseSensitiveLookup ? (v.name === key) : (v.name.toLowerCase() === key));741if (found) {742return found.value;743}744}745return undefined;746};747748const ctx: InlineValueContext = {749frameId: stackFrame.frameId,750stoppedLocation: new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1, stackFrame.range.endLineNumber, stackFrame.range.endColumn + 1)751};752753const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse();754755allDecorations = [];756const lineDecorations = new Map<number, InlineSegment[]>();757758const promises = providers.flatMap(provider => viewRanges.map(range => Promise.resolve(provider.provideInlineValues(model, range, ctx, cts.token)).then(async (result) => {759if (result) {760for (const iv of result) {761762let text: string | undefined = undefined;763switch (iv.type) {764case 'text':765text = iv.text;766break;767case 'variable': {768let va = iv.variableName;769if (!va) {770const lineContent = model.getLineContent(iv.range.startLineNumber);771va = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1);772}773const value = await findVariable(va, iv.caseSensitiveLookup);774if (value) {775text = strings.format(var_value_format, va, value);776}777break;778}779case 'expression': {780let expr = iv.expression;781if (!expr) {782const lineContent = model.getLineContent(iv.range.startLineNumber);783expr = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1);784}785if (expr) {786const expression = new Expression(expr);787await expression.evaluate(stackFrame.thread.session, stackFrame, 'watch', true);788if (expression.available) {789text = strings.format(var_value_format, expr, expression.value);790}791}792break;793}794}795796if (text) {797const line = iv.range.startLineNumber;798let lineSegments = lineDecorations.get(line);799if (!lineSegments) {800lineSegments = [];801lineDecorations.set(line, lineSegments);802}803if (!lineSegments.some(iv => iv.text === text)) { // de-dupe804lineSegments.push(new InlineSegment(iv.range.startColumn, text));805}806}807}808}809}, err => {810onUnexpectedExternalError(err);811})));812813const startTime = Date.now();814815await Promise.all(promises);816817// update debounce info818this.updateInlineValuesScheduler.delay = this.debounceInfo.update(model, Date.now() - startTime);819820// sort line segments and concatenate them into a decoration821822lineDecorations.forEach((segments, line) => {823if (segments.length > 0) {824segments = segments.sort((a, b) => a.column - b.column);825const text = segments.map(s => s.text).join(separator);826const editorWidth = this.editor.getLayoutInfo().width;827const fontInfo = this.editor.getOption(EditorOption.fontInfo);828const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);829allDecorations.push(...createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol));830}831});832833} else {834// old "one-size-fits-all" strategy835836const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range);837const scopesWithVariables = await Promise.all(scopes.map(async scope =>838({ scope, variables: await scope.getChildren() })));839840// Map of inline values per line that's populated in scope order, from841// narrowest to widest. This is done to avoid duplicating values if842// they appear in multiple scopes or are shadowed (#129770, #217326)843const valuesPerLine = new Map</* line */number, Map</* var */string, /* value */ string>>();844845for (const { scope, variables } of scopesWithVariables) {846let scopeRange = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn);847if (scope.range) {848scopeRange = scopeRange.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);849}850851const ownRanges = viewRanges.map(r => r.intersectRanges(scopeRange)).filter(isDefined);852this._wordToLineNumbersMap ??= new WordsToLineNumbersCache(model);853for (const range of ownRanges) {854this._wordToLineNumbersMap.ensureRangePopulated(range);855}856857const mapped = createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value);858for (const { line, variables } of mapped) {859let values = valuesPerLine.get(line);860if (!values) {861values = new Map<string, string>();862valuesPerLine.set(line, values);863}864865for (const { name, value } of variables) {866if (!values.has(name)) {867values.set(name, value);868}869}870}871}872873allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) => {874const text = [...values].map(([n, v]) => `${n} = ${v}`).join(', ');875const editorWidth = this.editor.getLayoutInfo().width;876const fontInfo = this.editor.getOption(EditorOption.fontInfo);877const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);878return createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol);879});880}881882if (cts.token.isCancellationRequested) {883return;884}885886// If word wrap is on, application of inline decorations may change the scroll position.887// Ensure the cursor maintains its vertical position relative to the viewport when888// we apply decorations.889let preservePosition: { position: Position; top: number } | undefined;890if (this.editor.getOption(EditorOption.wordWrap) !== 'off') {891const position = this.editor.getPosition();892if (position && this.editor.getVisibleRanges().some(r => r.containsPosition(position))) {893preservePosition = { position, top: this.editor.getTopForPosition(position.lineNumber, position.column) };894}895}896897this.oldDecorations.set(allDecorations);898899if (preservePosition) {900const top = this.editor.getTopForPosition(preservePosition.position.lineNumber, preservePosition.position.column);901this.editor.setScrollTop(this.editor.getScrollTop() - (preservePosition.top - top), ScrollType.Immediate);902}903}904905dispose(): void {906if (this.hoverWidget) {907this.hoverWidget.dispose();908}909if (this.configurationWidget) {910this.configurationWidget.dispose();911}912this.toDispose = dispose(this.toDispose);913}914}915916class WordsToLineNumbersCache {917// we use this as an array of bits where each 1 bit is a line number that's been parsed918private readonly intervals: Uint8Array;919public readonly value = new Map<string, number[]>();920921constructor(private readonly model: ITextModel) {922this.intervals = new Uint8Array(Math.ceil(model.getLineCount() / 8));923}924925/** Ensures that variables names in the given range have been identified. */926public ensureRangePopulated(range: Range) {927for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {928const bin = lineNumber >> 3; /* Math.floor(i / 8) */929const bit = 1 << (lineNumber & 0b111); /* 1 << (i % 8) */930if (!(this.intervals[bin] & bit)) {931getWordToLineNumbersMap(this.model, lineNumber, this.value);932this.intervals[bin] |= bit;933}934}935}936}937938939CommandsRegistry.registerCommand(940'_executeInlineValueProvider',941async (942accessor: ServicesAccessor,943uri: URI,944iRange: IRange,945context: InlineValueContext946): Promise<InlineValue[] | null> => {947assertType(URI.isUri(uri));948assertType(Range.isIRange(iRange));949950if (!context || typeof context.frameId !== 'number' || !Range.isIRange(context.stoppedLocation)) {951throw illegalArgument('context');952}953954const model = accessor.get(IModelService).getModel(uri);955if (!model) {956throw illegalArgument('uri');957}958959const range = Range.lift(iRange);960const { inlineValuesProvider } = accessor.get(ILanguageFeaturesService);961const providers = inlineValuesProvider.ordered(model);962const providerResults = await Promise.all(providers.map(provider => provider.provideInlineValues(model, range, context, CancellationToken.None)));963return providerResults.flat().filter(isDefined);964});965966967