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