Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts
5241 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { addDisposableListener, isKeyboardEvent } from '../../../../base/browser/dom.js';
7
import { DomEmitter } from '../../../../base/browser/event.js';
8
import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
9
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
10
import { RunOnceScheduler } from '../../../../base/common/async.js';
11
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
12
import { memoize } from '../../../../base/common/decorators.js';
13
import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js';
14
import { Event } from '../../../../base/common/event.js';
15
import { visit } from '../../../../base/common/json.js';
16
import { setProperty } from '../../../../base/common/jsonEdit.js';
17
import { KeyCode } from '../../../../base/common/keyCodes.js';
18
import { DisposableStore, IDisposable, MutableDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js';
19
import { clamp } from '../../../../base/common/numbers.js';
20
import { basename } from '../../../../base/common/path.js';
21
import * as env from '../../../../base/common/platform.js';
22
import * as strings from '../../../../base/common/strings.js';
23
import { assertType, isDefined } from '../../../../base/common/types.js';
24
import { Constants } from '../../../../base/common/uint.js';
25
import { URI } from '../../../../base/common/uri.js';
26
import { CoreEditingCommands } from '../../../../editor/browser/coreCommands.js';
27
import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
28
import { EditorOption, IEditorHoverOptions } from '../../../../editor/common/config/editorOptions.js';
29
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
30
import { Position } from '../../../../editor/common/core/position.js';
31
import { IRange, Range } from '../../../../editor/common/core/range.js';
32
import { DEFAULT_WORD_REGEXP } from '../../../../editor/common/core/wordHelper.js';
33
import { IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js';
34
import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js';
35
import { InlineValue, InlineValueContext } from '../../../../editor/common/languages.js';
36
import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from '../../../../editor/common/model.js';
37
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../../editor/common/services/languageFeatureDebounce.js';
38
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
39
import { IModelService } from '../../../../editor/common/services/model.js';
40
import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js';
41
import { HoverStartMode, HoverStartSource } from '../../../../editor/contrib/hover/browser/hoverOperation.js';
42
import * as nls from '../../../../nls.js';
43
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
44
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
45
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
46
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
47
import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';
48
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
49
import { FloatingEditorClickWidget } from '../../../browser/codeeditor.js';
50
import { DebugHoverWidget, ShowDebugHoverResult } from './debugHover.js';
51
import { ExceptionWidget } from './exceptionWidget.js';
52
import { CONTEXT_EXCEPTION_WIDGET_VISIBLE, IDebugConfiguration, IDebugEditorContribution, IDebugService, IDebugSession, IExceptionInfo, IExpression, IStackFrame, State } from '../common/debug.js';
53
import { Expression } from '../common/debugModel.js';
54
import { IHostService } from '../../../services/host/browser/host.js';
55
import { IEditorService } from '../../../services/editor/common/editorService.js';
56
import { MarkdownString } from '../../../../base/common/htmlContent.js';
57
import { InsertLineAfterAction } from '../../../../editor/contrib/linesOperations/browser/linesOperations.js';
58
59
const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons
60
const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added
61
const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped
62
63
const DEAFULT_INLINE_DEBOUNCE_DELAY = 200;
64
65
export const debugInlineForeground = registerColor('editor.inlineValuesForeground', {
66
dark: '#ffffff80',
67
light: '#00000080',
68
hcDark: '#ffffff80',
69
hcLight: '#00000080'
70
}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text."));
71
72
export const debugInlineBackground = registerColor('editor.inlineValuesBackground', '#ffc80033', nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background."));
73
74
class InlineSegment {
75
constructor(public column: number, public text: string) {
76
}
77
}
78
79
export function formatHoverContent(contentText: string): MarkdownString {
80
if (contentText.includes(',') && contentText.includes('=')) {
81
// Custom split: for each equals sign after the first, backtrack to the nearest comma
82
const customSplit = (text: string): string[] => {
83
const splits: number[] = [];
84
let equalsFound = 0;
85
let start = 0;
86
for (let i = 0; i < text.length; i++) {
87
if (text[i] === '=') {
88
if (equalsFound === 0) {
89
equalsFound++;
90
continue;
91
}
92
const commaIndex = text.lastIndexOf(',', i);
93
if (commaIndex !== -1 && commaIndex >= start) {
94
splits.push(commaIndex);
95
start = commaIndex + 1;
96
}
97
equalsFound++;
98
}
99
}
100
const result: string[] = [];
101
let s = 0;
102
for (const index of splits) {
103
result.push(text.substring(s, index).trim());
104
s = index + 1;
105
}
106
if (s < text.length) {
107
result.push(text.substring(s).trim());
108
}
109
return result;
110
};
111
112
const pairs = customSplit(contentText);
113
const formattedPairs = pairs.map(pair => {
114
const equalsIndex = pair.indexOf('=');
115
if (equalsIndex !== -1) {
116
const indent = ' '.repeat(equalsIndex + 2);
117
const [firstLine, ...restLines] = pair.split(/\r?\n/);
118
return [firstLine, ...restLines.map(line => indent + line)].join('\n');
119
}
120
return pair;
121
});
122
return new MarkdownString().appendCodeblock('', formattedPairs.join(',\n'));
123
}
124
return new MarkdownString().appendCodeblock('', contentText);
125
}
126
127
export function createInlineValueDecoration(lineNumber: number, contentText: string, classNamePrefix: string, column = Constants.MAX_SAFE_SMALL_INTEGER, viewportMaxCol: number = MAX_INLINE_DECORATOR_LENGTH): IModelDeltaDecoration[] {
128
const rawText = contentText; // store raw text for hover message
129
130
// Truncate contentText if it exceeds the viewport max column
131
if (contentText.length > viewportMaxCol) {
132
contentText = contentText.substring(0, viewportMaxCol) + '...';
133
}
134
135
return [
136
{
137
range: {
138
startLineNumber: lineNumber,
139
endLineNumber: lineNumber,
140
startColumn: column,
141
endColumn: column
142
},
143
options: {
144
description: `${classNamePrefix}-inline-value-decoration-spacer`,
145
after: {
146
content: strings.noBreakWhitespace,
147
cursorStops: InjectedTextCursorStops.None
148
},
149
showIfCollapsed: true,
150
}
151
},
152
{
153
range: {
154
startLineNumber: lineNumber,
155
endLineNumber: lineNumber,
156
startColumn: column,
157
endColumn: column
158
},
159
options: {
160
description: `${classNamePrefix}-inline-value-decoration`,
161
after: {
162
content: replaceWsWithNoBreakWs(contentText),
163
inlineClassName: `${classNamePrefix}-inline-value`,
164
inlineClassNameAffectsLetterSpacing: true,
165
cursorStops: InjectedTextCursorStops.None
166
},
167
showIfCollapsed: true,
168
hoverMessage: formatHoverContent(rawText)
169
}
170
},
171
];
172
}
173
174
function replaceWsWithNoBreakWs(str: string): string {
175
return str.replace(/[ \t\n]/g, strings.noBreakWhitespace);
176
}
177
178
function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray<IExpression>, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map<string, number[]>) {
179
const nameValueMap = new Map<string, string>();
180
for (const expr of expressions) {
181
nameValueMap.set(expr.name, expr.value);
182
// Limit the size of map. Too large can have a perf impact
183
if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {
184
break;
185
}
186
}
187
188
const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
189
190
// Compute unique set of names on each line
191
nameValueMap.forEach((_value, name) => {
192
const lineNumbers = wordToLineNumbersMap.get(name);
193
if (lineNumbers) {
194
for (const lineNumber of lineNumbers) {
195
if (ranges.some(r => lineNumber >= r.startLineNumber && lineNumber <= r.endLineNumber)) {
196
if (!lineToNamesMap.has(lineNumber)) {
197
lineToNamesMap.set(lineNumber, []);
198
}
199
200
if (lineToNamesMap.get(lineNumber)!.indexOf(name) === -1) {
201
lineToNamesMap.get(lineNumber)!.push(name);
202
}
203
}
204
}
205
}
206
});
207
208
// Compute decorators for each line
209
return [...lineToNamesMap].map(([line, names]) => ({
210
line,
211
variables: names.sort((first, second) => {
212
const content = model.getLineContent(line);
213
return content.indexOf(first) - content.indexOf(second);
214
}).map(name => ({ name, value: nameValueMap.get(name)! }))
215
}));
216
}
217
218
function getWordToLineNumbersMap(model: ITextModel, lineNumber: number, result: Map<string, number[]>) {
219
const lineLength = model.getLineLength(lineNumber);
220
// If line is too long then skip the line
221
if (lineLength > MAX_TOKENIZATION_LINE_LEN) {
222
return;
223
}
224
225
const lineContent = model.getLineContent(lineNumber);
226
model.tokenization.forceTokenization(lineNumber);
227
const lineTokens = model.tokenization.getLineTokens(lineNumber);
228
for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) {
229
const tokenType = lineTokens.getStandardTokenType(tokenIndex);
230
231
// Token is a word and not a comment
232
if (tokenType === StandardTokenType.Other) {
233
DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match
234
235
const tokenStartOffset = lineTokens.getStartOffset(tokenIndex);
236
const tokenEndOffset = lineTokens.getEndOffset(tokenIndex);
237
const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset);
238
const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr);
239
240
if (wordMatch) {
241
242
const word = wordMatch[0];
243
if (!result.has(word)) {
244
result.set(word, []);
245
}
246
247
result.get(word)!.push(lineNumber);
248
}
249
}
250
}
251
}
252
253
export class DebugEditorContribution implements IDebugEditorContribution {
254
255
private toDispose: IDisposable[];
256
private hoverWidget: DebugHoverWidget;
257
private hoverPosition?: { position: Position; event: IMouseEvent };
258
private mouseDown = false;
259
private exceptionWidgetVisible: IContextKey<boolean>;
260
private gutterIsHovered = false;
261
262
private exceptionWidget: ExceptionWidget | undefined;
263
private configurationWidget: FloatingEditorClickWidget | undefined;
264
private readonly altListener = new MutableDisposable();
265
private altPressed = false;
266
private oldDecorations: IEditorDecorationsCollection;
267
private readonly displayedStore = new DisposableStore();
268
private editorHoverOptions: IEditorHoverOptions | undefined;
269
private readonly debounceInfo: IFeatureDebounceInformation;
270
private allowScrollToExceptionWidget = true;
271
private shouldScrollToExceptionWidget = () => this.allowScrollToExceptionWidget;
272
273
// Holds a Disposable that prevents the default editor hover behavior while it exists.
274
private readonly defaultHoverLockout = new MutableDisposable();
275
276
constructor(
277
private editor: ICodeEditor,
278
@IDebugService private readonly debugService: IDebugService,
279
@IInstantiationService private readonly instantiationService: IInstantiationService,
280
@ICommandService private readonly commandService: ICommandService,
281
@IConfigurationService private readonly configurationService: IConfigurationService,
282
@IHostService private readonly hostService: IHostService,
283
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
284
@IContextKeyService contextKeyService: IContextKeyService,
285
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
286
@ILanguageFeatureDebounceService featureDebounceService: ILanguageFeatureDebounceService,
287
@IEditorService private readonly editorService: IEditorService
288
) {
289
this.oldDecorations = this.editor.createDecorationsCollection();
290
this.debounceInfo = featureDebounceService.for(languageFeaturesService.inlineValuesProvider, 'InlineValues', { min: DEAFULT_INLINE_DEBOUNCE_DELAY });
291
this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor);
292
this.toDispose = [this.defaultHoverLockout, this.altListener, this.displayedStore];
293
this.registerListeners();
294
this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService);
295
this.toggleExceptionWidget();
296
}
297
298
private registerListeners(): void {
299
this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(e => this.onFocusStackFrame(e.stackFrame)));
300
301
// hover listeners & hover widget
302
this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e)));
303
this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false));
304
this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e)));
305
this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => {
306
const hoverDomNode = this.hoverWidget.getDomNode();
307
if (!hoverDomNode) {
308
return;
309
}
310
311
const rect = hoverDomNode.getBoundingClientRect();
312
// Only hide the hover widget if the editor mouse leave event is outside the hover widget #3528
313
if (e.event.posx < rect.left || e.event.posx > rect.right || e.event.posy < rect.top || e.event.posy > rect.bottom) {
314
this.hideHoverWidget();
315
}
316
}));
317
this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e)));
318
this.toDispose.push(this.editor.onDidChangeModelContent(() => {
319
this._wordToLineNumbersMap = undefined;
320
this.updateInlineValuesScheduler.schedule();
321
}));
322
this.toDispose.push(this.debugService.getViewModel().onWillUpdateViews(() => this.updateInlineValuesScheduler.schedule()));
323
this.toDispose.push(this.debugService.getViewModel().onDidEvaluateLazyExpression(() => this.updateInlineValuesScheduler.schedule()));
324
this.toDispose.push(this.editor.onDidChangeModel(async () => {
325
this.addDocumentListeners();
326
this.toggleExceptionWidget();
327
this.hideHoverWidget();
328
this._wordToLineNumbersMap = undefined;
329
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
330
await this.updateInlineValueDecorations(stackFrame);
331
}));
332
this.toDispose.push(this.editor.onDidScrollChange(() => {
333
this.hideHoverWidget();
334
335
// Inline value provider should get called on view port change
336
const model = this.editor.getModel();
337
if (model && this.languageFeaturesService.inlineValuesProvider.has(model)) {
338
this.updateInlineValuesScheduler.schedule();
339
}
340
}));
341
this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => {
342
if (e.affectsConfiguration('editor.hover')) {
343
this.updateHoverConfiguration();
344
}
345
}));
346
this.toDispose.push(this.debugService.onDidChangeState((state: State) => {
347
if (state !== State.Stopped) {
348
this.toggleExceptionWidget();
349
}
350
}));
351
352
this.updateHoverConfiguration();
353
}
354
355
private _wordToLineNumbersMap: WordsToLineNumbersCache | undefined;
356
357
private updateHoverConfiguration(): void {
358
const model = this.editor.getModel();
359
if (model) {
360
this.editorHoverOptions = this.configurationService.getValue<IEditorHoverOptions>('editor.hover', {
361
resource: model.uri,
362
overrideIdentifier: model.getLanguageId()
363
});
364
}
365
}
366
367
private addDocumentListeners(): void {
368
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
369
const model = this.editor.getModel();
370
if (model) {
371
this.applyDocumentListeners(model, stackFrame);
372
}
373
}
374
375
private applyDocumentListeners(model: ITextModel, stackFrame: IStackFrame | undefined): void {
376
if (!stackFrame || !this.uriIdentityService.extUri.isEqual(model.uri, stackFrame.source.uri)) {
377
this.altListener.clear();
378
return;
379
}
380
381
const ownerDocument = this.editor.getContainerDomNode().ownerDocument;
382
383
// When the alt key is pressed show regular editor hover and hide the debug hover #84561
384
this.altListener.value = addDisposableListener(ownerDocument, 'keydown', keydownEvent => {
385
const standardKeyboardEvent = new StandardKeyboardEvent(keydownEvent);
386
if (standardKeyboardEvent.keyCode === KeyCode.Alt) {
387
this.altPressed = true;
388
const debugHoverWasVisible = this.hoverWidget.isVisible();
389
this.hoverWidget.hide();
390
this.defaultHoverLockout.clear();
391
392
if (debugHoverWasVisible && this.hoverPosition) {
393
// If the debug hover was visible immediately show the editor hover for the alt transition to be smooth
394
this.showEditorHover(this.hoverPosition.position, false);
395
}
396
397
const onKeyUp = new DomEmitter(ownerDocument, 'keyup');
398
const listener = Event.any<KeyboardEvent | boolean>(this.hostService.onDidChangeFocus, onKeyUp.event)(keyupEvent => {
399
let standardKeyboardEvent = undefined;
400
if (isKeyboardEvent(keyupEvent)) {
401
standardKeyboardEvent = new StandardKeyboardEvent(keyupEvent);
402
}
403
if (!standardKeyboardEvent || standardKeyboardEvent.keyCode === KeyCode.Alt) {
404
this.altPressed = false;
405
this.preventDefaultEditorHover();
406
listener.dispose();
407
onKeyUp.dispose();
408
}
409
});
410
}
411
});
412
}
413
414
async showHover(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise<void> {
415
// normally will already be set in `showHoverScheduler`, but public callers may hit this directly:
416
this.preventDefaultEditorHover();
417
418
const sf = this.debugService.getViewModel().focusedStackFrame;
419
const model = this.editor.getModel();
420
if (sf && model && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) {
421
const result = await this.hoverWidget.showAt(position, focus, mouseEvent);
422
if (result === ShowDebugHoverResult.NOT_AVAILABLE) {
423
// When no expression available fallback to editor hover
424
this.showEditorHover(position, focus);
425
}
426
} else {
427
this.showEditorHover(position, focus);
428
}
429
}
430
431
private preventDefaultEditorHover() {
432
if (this.defaultHoverLockout.value || this.editorHoverOptions?.enabled === 'off') {
433
return;
434
}
435
436
const hoverController = this.editor.getContribution<ContentHoverController>(ContentHoverController.ID);
437
hoverController?.hideContentHover();
438
439
this.editor.updateOptions({ hover: { enabled: 'off' } });
440
this.defaultHoverLockout.value = {
441
dispose: () => {
442
this.editor.updateOptions({
443
hover: { enabled: this.editorHoverOptions?.enabled ?? 'on' }
444
});
445
}
446
};
447
}
448
449
private showEditorHover(position: Position, focus: boolean) {
450
const hoverController = this.editor.getContribution<ContentHoverController>(ContentHoverController.ID);
451
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);
452
// enable the editor hover, otherwise the content controller will see it
453
// as disabled and hide it on the first mouse move (#193149)
454
this.defaultHoverLockout.clear();
455
hoverController?.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Mouse, focus);
456
}
457
458
private async onFocusStackFrame(sf: IStackFrame | undefined): Promise<void> {
459
const model = this.editor.getModel();
460
if (model) {
461
this.applyDocumentListeners(model, sf);
462
if (sf && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) {
463
await this.toggleExceptionWidget();
464
} else {
465
this.hideHoverWidget();
466
}
467
}
468
469
await this.updateInlineValueDecorations(sf);
470
}
471
472
private get hoverDelay() {
473
const baseDelay = this.editorHoverOptions?.delay || 0;
474
475
// heuristic to get a 'good' but configurable delay for evaluation. The
476
// debug hover can be very large, so we tend to be more conservative about
477
// when to show it (#180621). With this equation:
478
// - default 300ms hover => * 2 = 600ms
479
// - short 100ms hover => * 2 = 200ms
480
// - longer 600ms hover => * 1.5 = 900ms
481
// - long 1000ms hover => * 1.0 = 1000ms
482
const delayFactor = clamp(2 - (baseDelay - 300) / 600, 1, 2);
483
484
return baseDelay * delayFactor;
485
}
486
487
@memoize
488
private get showHoverScheduler() {
489
const scheduler = new RunOnceScheduler(() => {
490
if (this.hoverPosition && !this.altPressed) {
491
this.showHover(this.hoverPosition.position, false, this.hoverPosition.event);
492
}
493
}, this.hoverDelay);
494
this.toDispose.push(scheduler);
495
496
return scheduler;
497
}
498
499
private hideHoverWidget(): void {
500
if (this.hoverWidget.willBeVisible()) {
501
this.hoverWidget.hide();
502
}
503
this.showHoverScheduler.cancel();
504
this.defaultHoverLockout.clear();
505
}
506
507
// hover business
508
509
private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
510
this.mouseDown = true;
511
if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === DebugHoverWidget.ID) {
512
return;
513
}
514
515
this.hideHoverWidget();
516
}
517
518
private onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
519
if (this.debugService.state !== State.Stopped) {
520
return;
521
}
522
523
const target = mouseEvent.target;
524
const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey';
525
526
if (!this.altPressed) {
527
if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN) {
528
this.defaultHoverLockout.clear();
529
this.gutterIsHovered = true;
530
} else if (this.gutterIsHovered) {
531
this.gutterIsHovered = false;
532
this.updateHoverConfiguration();
533
}
534
}
535
536
if (
537
(target.type === MouseTargetType.CONTENT_WIDGET && target.detail === DebugHoverWidget.ID)
538
|| this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)
539
) {
540
// mouse moved on top of debug hover widget
541
542
const sticky = this.editorHoverOptions?.sticky ?? true;
543
if (sticky || this.hoverWidget.isShowingComplexValue || mouseEvent.event[stopKey]) {
544
return;
545
}
546
}
547
548
if (target.type === MouseTargetType.CONTENT_TEXT) {
549
if (target.position && !Position.equals(target.position, this.hoverPosition?.position || null) && !this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)) {
550
this.hoverPosition = { position: target.position, event: mouseEvent.event };
551
// Disable the editor hover during the request to avoid flickering
552
this.preventDefaultEditorHover();
553
this.showHoverScheduler.schedule(this.hoverDelay);
554
}
555
} else if (!this.mouseDown) {
556
// Do not hide debug hover when the mouse is pressed because it usually leads to accidental closing #64620
557
this.hideHoverWidget();
558
}
559
}
560
561
private onKeyDown(e: IKeyboardEvent): void {
562
const stopKey = env.isMacintosh ? KeyCode.Meta : KeyCode.Ctrl;
563
if (e.keyCode !== stopKey && e.keyCode !== KeyCode.Alt) {
564
// do not hide hover when Ctrl/Meta is pressed, and alt is handled separately
565
this.hideHoverWidget();
566
}
567
}
568
// end hover business
569
570
// exception widget
571
private async toggleExceptionWidget(): Promise<void> {
572
// Toggles exception widget based on the state of the current editor model and debug stack frame
573
const model = this.editor.getModel();
574
const focusedSf = this.debugService.getViewModel().focusedStackFrame;
575
const callStack = focusedSf ? focusedSf.thread.getCallStack() : null;
576
if (!model || !focusedSf || !callStack || callStack.length === 0) {
577
this.closeExceptionWidget();
578
return;
579
}
580
581
// First call stack frame that is available is the frame where exception has been thrown
582
const exceptionSf = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'));
583
if (!exceptionSf || exceptionSf !== focusedSf) {
584
this.closeExceptionWidget();
585
return;
586
}
587
588
const sameUri = this.uriIdentityService.extUri.isEqual(exceptionSf.source.uri, model.uri);
589
if (this.exceptionWidget && !sameUri) {
590
this.closeExceptionWidget();
591
} else if (sameUri) {
592
// Show exception widget in all editors with the same file, but only scroll in the active editor
593
const activeControl = this.editorService.activeTextEditorControl;
594
const isActiveEditor = activeControl === this.editor;
595
const exceptionInfo = await focusedSf.thread.exceptionInfo;
596
597
if (exceptionInfo) {
598
if (isActiveEditor) {
599
// Active editor: show widget and scroll to it
600
this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn);
601
} else {
602
// Inactive editor: show widget without scrolling
603
this.showExceptionWidgetWithoutScroll(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn);
604
}
605
}
606
}
607
}
608
609
private showExceptionWidget(exceptionInfo: IExceptionInfo, debugSession: IDebugSession | undefined, lineNumber: number, column: number): void {
610
if (this.exceptionWidget) {
611
this.exceptionWidget.dispose();
612
}
613
614
this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession, this.shouldScrollToExceptionWidget);
615
this.exceptionWidget.show({ lineNumber, column }, 0);
616
this.exceptionWidget.focus();
617
this.editor.revealRangeInCenter({
618
startLineNumber: lineNumber,
619
startColumn: column,
620
endLineNumber: lineNumber,
621
endColumn: column,
622
});
623
this.exceptionWidgetVisible.set(true);
624
}
625
626
private showExceptionWidgetWithoutScroll(exceptionInfo: IExceptionInfo, debugSession: IDebugSession | undefined, lineNumber: number, column: number): void {
627
if (this.exceptionWidget) {
628
this.exceptionWidget.dispose();
629
}
630
631
// Disable scrolling to exception widget
632
this.allowScrollToExceptionWidget = false;
633
634
const currentScrollTop = this.editor.getScrollTop();
635
const visibleRanges = this.editor.getVisibleRanges();
636
if (visibleRanges.length === 0) {
637
// Editor not fully initialized or not visible; skip scroll adjustment
638
this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession, this.shouldScrollToExceptionWidget);
639
this.exceptionWidget.show({ lineNumber, column }, 0);
640
this.exceptionWidgetVisible.set(true);
641
this.allowScrollToExceptionWidget = true;
642
return;
643
}
644
645
const firstVisibleLine = visibleRanges[0].startLineNumber;
646
647
// Create widget - this may add a zone that pushes content down
648
this.exceptionWidget = this.instantiationService.createInstance(ExceptionWidget, this.editor, exceptionInfo, debugSession, this.shouldScrollToExceptionWidget);
649
this.exceptionWidget.show({ lineNumber, column }, 0);
650
this.exceptionWidgetVisible.set(true);
651
652
// only adjust scroll if the exception widget is above the first visible line
653
if (lineNumber < firstVisibleLine) {
654
// Get the actual height of the widget that was just added from the whitespace
655
// The whitespace height is more accurate than the container height
656
const scrollAdjustment = this.exceptionWidget.getWhitespaceHeight();
657
658
// Scroll down by the actual widget height to keep the first visible line the same
659
this.editor.setScrollTop(currentScrollTop + scrollAdjustment, ScrollType.Immediate);
660
}
661
662
// Re-enable scrolling to exception widget
663
this.allowScrollToExceptionWidget = true;
664
}
665
666
closeExceptionWidget(): void {
667
if (this.exceptionWidget) {
668
const shouldFocusEditor = this.exceptionWidget.hasFocus();
669
this.exceptionWidget.dispose();
670
this.exceptionWidget = undefined;
671
this.exceptionWidgetVisible.set(false);
672
if (shouldFocusEditor) {
673
this.editor.focus();
674
}
675
}
676
}
677
678
async addLaunchConfiguration(): Promise<void> {
679
const model = this.editor.getModel();
680
if (!model) {
681
return;
682
}
683
684
let configurationsArrayPosition: Position | undefined;
685
let lastProperty: string;
686
687
const getConfigurationPosition = () => {
688
let depthInArray = 0;
689
visit(model.getValue(), {
690
onObjectProperty: (property: string) => {
691
lastProperty = property;
692
},
693
onArrayBegin: (offset: number) => {
694
if (lastProperty === 'configurations' && depthInArray === 0) {
695
configurationsArrayPosition = model.getPositionAt(offset + 1);
696
}
697
depthInArray++;
698
},
699
onArrayEnd: () => {
700
depthInArray--;
701
}
702
});
703
};
704
705
getConfigurationPosition();
706
707
if (!configurationsArrayPosition) {
708
// "configurations" array doesn't exist. Add it here.
709
const { tabSize, insertSpaces } = model.getOptions();
710
const eol = model.getEOL();
711
const edit = (basename(model.uri.fsPath) === 'launch.json') ?
712
setProperty(model.getValue(), ['configurations'], [], { tabSize, insertSpaces, eol })[0] :
713
setProperty(model.getValue(), ['launch'], { 'configurations': [] }, { tabSize, insertSpaces, eol })[0];
714
const startPosition = model.getPositionAt(edit.offset);
715
const lineNumber = startPosition.lineNumber;
716
const range = new Range(lineNumber, startPosition.column, lineNumber, model.getLineMaxColumn(lineNumber));
717
model.pushEditOperations(null, [EditOperation.replace(range, edit.content)], () => null);
718
// Go through the file again since we've edited it
719
getConfigurationPosition();
720
}
721
if (!configurationsArrayPosition) {
722
return;
723
}
724
725
this.editor.focus();
726
727
const insertLine = (position: Position): Promise<any> => {
728
// Check if there are more characters on a line after a "configurations": [, if yes enter a newline
729
if (model.getLineLastNonWhitespaceColumn(position.lineNumber) > position.column) {
730
this.editor.setPosition(position);
731
this.instantiationService.invokeFunction((accessor) => {
732
CoreEditingCommands.LineBreakInsert.runEditorCommand(accessor, this.editor, null);
733
});
734
}
735
this.editor.setPosition(position);
736
return this.commandService.executeCommand(InsertLineAfterAction.ID);
737
};
738
739
await insertLine(configurationsArrayPosition);
740
await this.commandService.executeCommand('editor.action.triggerSuggest');
741
}
742
743
// Inline Decorations
744
745
@memoize
746
private get removeInlineValuesScheduler(): RunOnceScheduler {
747
return new RunOnceScheduler(
748
() => {
749
this.displayedStore.clear();
750
this.oldDecorations.clear();
751
},
752
100
753
);
754
}
755
756
@memoize
757
private get updateInlineValuesScheduler(): RunOnceScheduler {
758
const model = this.editor.getModel();
759
return new RunOnceScheduler(
760
async () => await this.updateInlineValueDecorations(this.debugService.getViewModel().focusedStackFrame),
761
model ? this.debounceInfo.get(model) : DEAFULT_INLINE_DEBOUNCE_DELAY
762
);
763
}
764
765
private async updateInlineValueDecorations(stackFrame: IStackFrame | undefined): Promise<void> {
766
767
const var_value_format = '{0} = {1}';
768
const separator = ', ';
769
770
const model = this.editor.getModel();
771
const inlineValuesSetting = this.configurationService.getValue<IDebugConfiguration>('debug').inlineValues;
772
const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model));
773
if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) {
774
if (!this.removeInlineValuesScheduler.isScheduled()) {
775
this.removeInlineValuesScheduler.schedule();
776
}
777
return;
778
}
779
780
this.removeInlineValuesScheduler.cancel();
781
this.displayedStore.clear();
782
783
const viewRanges = this.editor.getVisibleRangesPlusViewportAboveBelow();
784
let allDecorations: IModelDeltaDecoration[];
785
786
const cts = new CancellationTokenSource();
787
this.displayedStore.add(toDisposable(() => cts.dispose(true)));
788
789
if (this.languageFeaturesService.inlineValuesProvider.has(model)) {
790
791
const findVariable = async (_key: string, caseSensitiveLookup: boolean): Promise<string | undefined> => {
792
const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range);
793
const key = caseSensitiveLookup ? _key : _key.toLowerCase();
794
for (const scope of scopes) {
795
const variables = await scope.getChildren();
796
const found = variables.find(v => caseSensitiveLookup ? (v.name === key) : (v.name.toLowerCase() === key));
797
if (found) {
798
return found.value;
799
}
800
}
801
return undefined;
802
};
803
804
const ctx: InlineValueContext = {
805
frameId: stackFrame.frameId,
806
stoppedLocation: new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1, stackFrame.range.endLineNumber, stackFrame.range.endColumn + 1)
807
};
808
809
const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse();
810
811
allDecorations = [];
812
const lineDecorations = new Map<number, InlineSegment[]>();
813
814
const promises = providers.flatMap(provider => viewRanges.map(range => Promise.resolve(provider.provideInlineValues(model, range, ctx, cts.token)).then(async (result) => {
815
if (result) {
816
for (const iv of result) {
817
818
let text: string | undefined = undefined;
819
switch (iv.type) {
820
case 'text':
821
text = iv.text;
822
break;
823
case 'variable': {
824
let va = iv.variableName;
825
if (!va) {
826
const lineContent = model.getLineContent(iv.range.startLineNumber);
827
va = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1);
828
}
829
const value = await findVariable(va, iv.caseSensitiveLookup);
830
if (value) {
831
text = strings.format(var_value_format, va, value);
832
}
833
break;
834
}
835
case 'expression': {
836
let expr = iv.expression;
837
if (!expr) {
838
const lineContent = model.getLineContent(iv.range.startLineNumber);
839
expr = lineContent.substring(iv.range.startColumn - 1, iv.range.endColumn - 1);
840
}
841
if (expr) {
842
const expression = new Expression(expr);
843
await expression.evaluate(stackFrame.thread.session, stackFrame, 'watch', true);
844
if (expression.available) {
845
text = strings.format(var_value_format, expr, expression.value);
846
}
847
}
848
break;
849
}
850
}
851
852
if (text) {
853
const line = iv.range.startLineNumber;
854
let lineSegments = lineDecorations.get(line);
855
if (!lineSegments) {
856
lineSegments = [];
857
lineDecorations.set(line, lineSegments);
858
}
859
if (!lineSegments.some(iv => iv.text === text)) { // de-dupe
860
lineSegments.push(new InlineSegment(iv.range.startColumn, text));
861
}
862
}
863
}
864
}
865
}, err => {
866
onUnexpectedExternalError(err);
867
})));
868
869
const startTime = Date.now();
870
871
await Promise.all(promises);
872
873
// update debounce info
874
this.updateInlineValuesScheduler.delay = this.debounceInfo.update(model, Date.now() - startTime);
875
876
// sort line segments and concatenate them into a decoration
877
878
lineDecorations.forEach((segments, line) => {
879
if (segments.length > 0) {
880
segments = segments.sort((a, b) => a.column - b.column);
881
const text = segments.map(s => s.text).join(separator);
882
const editorWidth = this.editor.getLayoutInfo().width;
883
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
884
const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);
885
allDecorations.push(...createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol));
886
}
887
});
888
889
} else {
890
// old "one-size-fits-all" strategy
891
892
const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range);
893
const scopesWithVariables = await Promise.all(scopes.map(async scope =>
894
({ scope, variables: await scope.getChildren() })));
895
896
// Map of inline values per line that's populated in scope order, from
897
// narrowest to widest. This is done to avoid duplicating values if
898
// they appear in multiple scopes or are shadowed (#129770, #217326)
899
const valuesPerLine = new Map</* line */number, Map</* var */string, /* value */ string>>();
900
901
for (const { scope, variables } of scopesWithVariables) {
902
let scopeRange = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn);
903
if (scope.range) {
904
scopeRange = scopeRange.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);
905
}
906
907
const ownRanges = viewRanges.map(r => r.intersectRanges(scopeRange)).filter(isDefined);
908
this._wordToLineNumbersMap ??= new WordsToLineNumbersCache(model);
909
for (const range of ownRanges) {
910
this._wordToLineNumbersMap.ensureRangePopulated(range);
911
}
912
913
const mapped = createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value);
914
for (const { line, variables } of mapped) {
915
let values = valuesPerLine.get(line);
916
if (!values) {
917
values = new Map<string, string>();
918
valuesPerLine.set(line, values);
919
}
920
921
for (const { name, value } of variables) {
922
if (!values.has(name)) {
923
values.set(name, value);
924
}
925
}
926
}
927
}
928
929
allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) => {
930
const text = [...values].map(([n, v]) => `${n} = ${v}`).join(', ');
931
const editorWidth = this.editor.getLayoutInfo().width;
932
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
933
const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);
934
return createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol);
935
});
936
}
937
938
if (cts.token.isCancellationRequested) {
939
return;
940
}
941
942
// If word wrap is on, application of inline decorations may change the scroll position.
943
// Ensure the cursor maintains its vertical position relative to the viewport when
944
// we apply decorations.
945
let preservePosition: { position: Position; top: number } | undefined;
946
if (this.editor.getOption(EditorOption.wordWrap) !== 'off') {
947
const position = this.editor.getPosition();
948
if (position && this.editor.getVisibleRanges().some(r => r.containsPosition(position))) {
949
preservePosition = { position, top: this.editor.getTopForPosition(position.lineNumber, position.column) };
950
}
951
}
952
953
this.oldDecorations.set(allDecorations);
954
955
if (preservePosition) {
956
const top = this.editor.getTopForPosition(preservePosition.position.lineNumber, preservePosition.position.column);
957
this.editor.setScrollTop(this.editor.getScrollTop() - (preservePosition.top - top), ScrollType.Immediate);
958
}
959
}
960
961
dispose(): void {
962
if (this.hoverWidget) {
963
this.hoverWidget.dispose();
964
}
965
if (this.configurationWidget) {
966
this.configurationWidget.dispose();
967
}
968
this.toDispose = dispose(this.toDispose);
969
}
970
}
971
972
class WordsToLineNumbersCache {
973
// we use this as an array of bits where each 1 bit is a line number that's been parsed
974
private readonly intervals: Uint8Array;
975
public readonly value = new Map<string, number[]>();
976
977
constructor(private readonly model: ITextModel) {
978
this.intervals = new Uint8Array(Math.ceil(model.getLineCount() / 8));
979
}
980
981
/** Ensures that variables names in the given range have been identified. */
982
public ensureRangePopulated(range: Range) {
983
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
984
const bin = lineNumber >> 3; /* Math.floor(i / 8) */
985
const bit = 1 << (lineNumber & 0b111); /* 1 << (i % 8) */
986
if (!(this.intervals[bin] & bit)) {
987
getWordToLineNumbersMap(this.model, lineNumber, this.value);
988
this.intervals[bin] |= bit;
989
}
990
}
991
}
992
}
993
994
995
CommandsRegistry.registerCommand(
996
'_executeInlineValueProvider',
997
async (
998
accessor: ServicesAccessor,
999
uri: URI,
1000
iRange: IRange,
1001
context: InlineValueContext
1002
): Promise<InlineValue[] | null> => {
1003
assertType(URI.isUri(uri));
1004
assertType(Range.isIRange(iRange));
1005
1006
if (!context || typeof context.frameId !== 'number' || !Range.isIRange(context.stoppedLocation)) {
1007
throw illegalArgument('context');
1008
}
1009
1010
const model = accessor.get(IModelService).getModel(uri);
1011
if (!model) {
1012
throw illegalArgument('uri');
1013
}
1014
1015
const range = Range.lift(iRange);
1016
const { inlineValuesProvider } = accessor.get(ILanguageFeaturesService);
1017
const providers = inlineValuesProvider.ordered(model);
1018
const providerResults = await Promise.all(providers.map(provider => provider.provideInlineValues(model, range, context, CancellationToken.None)));
1019
return providerResults.flat().filter(isDefined);
1020
});
1021
1022