Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/repl.ts
5237 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 * as dom from '../../../../base/browser/dom.js';
7
import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js';
8
import { IHistoryNavigationWidget } from '../../../../base/browser/history.js';
9
import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';
10
import * as aria from '../../../../base/browser/ui/aria/aria.js';
11
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';
12
import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';
13
import { IAction } from '../../../../base/common/actions.js';
14
import { RunOnceScheduler, timeout } from '../../../../base/common/async.js';
15
import { CancellationToken } from '../../../../base/common/cancellation.js';
16
import { Codicon } from '../../../../base/common/codicons.js';
17
import { memoize } from '../../../../base/common/decorators.js';
18
import { Emitter } from '../../../../base/common/event.js';
19
import { FuzzyScore } from '../../../../base/common/filters.js';
20
import { HistoryNavigator } from '../../../../base/common/history.js';
21
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
22
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
23
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
24
import { ThemeIcon } from '../../../../base/common/themables.js';
25
import { URI as uri } from '../../../../base/common/uri.js';
26
import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
27
import { EditorAction, registerEditorAction } from '../../../../editor/browser/editorExtensions.js';
28
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
29
import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
30
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
31
import { EDITOR_FONT_DEFAULTS } from '../../../../editor/common/config/fontInfo.js';
32
import { Position } from '../../../../editor/common/core/position.js';
33
import { Range } from '../../../../editor/common/core/range.js';
34
import { IDecorationOptions } from '../../../../editor/common/editorCommon.js';
35
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
36
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemKinds, CompletionList } from '../../../../editor/common/languages.js';
37
import { ITextModel } from '../../../../editor/common/model.js';
38
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
39
import { IModelService } from '../../../../editor/common/services/model.js';
40
import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';
41
import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';
42
import { localize, localize2 } from '../../../../nls.js';
43
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
44
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
45
import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
46
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
47
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
48
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
49
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
50
import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
51
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
52
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
53
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
54
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
55
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
56
import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';
57
import { ILogService } from '../../../../platform/log/common/log.js';
58
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
59
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
60
import { editorForeground, resolveColorValue } from '../../../../platform/theme/common/colorRegistry.js';
61
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
62
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
63
import { FilterViewPane, IViewPaneOptions, ViewAction } from '../../../browser/parts/views/viewPane.js';
64
import { IViewDescriptorService } from '../../../common/views.js';
65
import { IEditorService } from '../../../services/editor/common/editorService.js';
66
import { IViewsService } from '../../../services/views/common/viewsService.js';
67
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
68
import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';
69
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';
70
import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, IDebugConfiguration, IDebugService, IDebugSession, IReplConfiguration, IReplElement, IReplOptions, REPL_VIEW_ID, State, getStateLabel } from '../common/debug.js';
71
import { Variable } from '../common/debugModel.js';
72
import { resolveChildSession } from '../common/debugUtils.js';
73
import { ReplEvaluationResult, ReplGroup } from '../common/replModel.js';
74
import { FocusSessionActionViewItem } from './debugActionViewItems.js';
75
import { DEBUG_COMMAND_CATEGORY, FOCUS_REPL_ID } from './debugCommands.js';
76
import { DebugExpressionRenderer } from './debugExpressionRenderer.js';
77
import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js';
78
import './media/repl.css';
79
import { ReplFilter } from './replFilter.js';
80
import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js';
81
82
const $ = dom.$;
83
84
const HISTORY_STORAGE_KEY = 'debug.repl.history';
85
const FILTER_HISTORY_STORAGE_KEY = 'debug.repl.filterHistory';
86
const FILTER_VALUE_STORAGE_KEY = 'debug.repl.filterValue';
87
const DECORATION_KEY = 'replinputdecoration';
88
89
function revealLastElement(tree: WorkbenchAsyncDataTree<any, any, any>) {
90
tree.scrollTop = tree.scrollHeight - tree.renderHeight;
91
// tree.scrollTop = 1e6;
92
}
93
94
const sessionsToIgnore = new Set<IDebugSession>();
95
const identityProvider = { getId: (element: IReplElement) => element.getId() };
96
97
export class Repl extends FilterViewPane implements IHistoryNavigationWidget {
98
declare readonly _serviceBrand: undefined;
99
100
private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show
101
private static readonly URI = uri.parse(`${DEBUG_SCHEME}:replinput`);
102
103
private history: HistoryNavigator<string>;
104
private tree?: WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>;
105
private replOptions: ReplOptions;
106
private previousTreeScrollHeight: number = 0;
107
private replDelegate!: ReplDelegate;
108
private container!: HTMLElement;
109
private treeContainer!: HTMLElement;
110
private replInput!: CodeEditorWidget;
111
private replInputContainer!: HTMLElement;
112
private bodyContentDimension: dom.Dimension | undefined;
113
private model: ITextModel | undefined;
114
private setHistoryNavigationEnablement!: (enabled: boolean) => void;
115
private scopedInstantiationService!: IInstantiationService;
116
private replElementsChangeListener: IDisposable | undefined;
117
private styleElement: HTMLStyleElement | undefined;
118
private styleChangedWhenInvisible: boolean = false;
119
private completionItemProvider: IDisposable | undefined;
120
private modelChangeListener: IDisposable = Disposable.None;
121
private filter: ReplFilter;
122
private multiSessionRepl: IContextKey<boolean>;
123
private menu: IMenu;
124
private replDataSource: IAsyncDataSource<IDebugSession, IReplElement> | undefined;
125
private findIsOpen: boolean = false;
126
127
constructor(
128
options: IViewPaneOptions,
129
@IDebugService private readonly debugService: IDebugService,
130
@IInstantiationService instantiationService: IInstantiationService,
131
@IStorageService private readonly storageService: IStorageService,
132
@IThemeService themeService: IThemeService,
133
@IModelService private readonly modelService: IModelService,
134
@IContextKeyService contextKeyService: IContextKeyService,
135
@ICodeEditorService codeEditorService: ICodeEditorService,
136
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
137
@IContextMenuService contextMenuService: IContextMenuService,
138
@IConfigurationService protected override readonly configurationService: IConfigurationService,
139
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,
140
@IEditorService private readonly editorService: IEditorService,
141
@IKeybindingService protected override readonly keybindingService: IKeybindingService,
142
@IOpenerService openerService: IOpenerService,
143
@IHoverService hoverService: IHoverService,
144
@IMenuService menuService: IMenuService,
145
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
146
@ILogService private readonly logService: ILogService,
147
) {
148
const filterText = storageService.get(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE, '');
149
super({
150
...options,
151
filterOptions: {
152
placeholder: localize({ key: 'workbench.debug.filter.placeholder', comment: ['Text in the brackets after e.g. is not localizable'] }, "Filter (e.g. text, !exclude, \\escape)"),
153
text: filterText,
154
history: JSON.parse(storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[],
155
}
156
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
157
158
this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService);
159
this._register(this.menu);
160
this.history = this._register(new HistoryNavigator(new Set(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]'))), 100));
161
this.filter = new ReplFilter();
162
this.filter.filterQuery = filterText;
163
this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService);
164
this.replOptions = this._register(this.instantiationService.createInstance(ReplOptions, this.id, () => this.getLocationBasedColors().background));
165
this._register(this.replOptions.onDidChange(() => this.onDidStyleChange()));
166
167
this._register(codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {}));
168
this.multiSessionRepl.set(this.isMultiSessionView);
169
this.registerListeners();
170
}
171
172
private registerListeners(): void {
173
if (this.debugService.getViewModel().focusedSession) {
174
this.onDidFocusSession(this.debugService.getViewModel().focusedSession);
175
}
176
177
this._register(this.debugService.getViewModel().onDidFocusSession(session => {
178
this.onDidFocusSession(session);
179
}));
180
this._register(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => {
181
if (e instanceof Variable && this.tree?.hasNode(e)) {
182
await this.tree.updateChildren(e, false, true);
183
await this.tree.expand(e);
184
}
185
}));
186
this._register(this.debugService.onWillNewSession(async newSession => {
187
// Need to listen to output events for sessions which are not yet fully initialised
188
const input = this.tree?.getInput();
189
if (!input || input.state === State.Inactive) {
190
await this.selectSession(newSession);
191
}
192
this.multiSessionRepl.set(this.isMultiSessionView);
193
}));
194
this._register(this.debugService.onDidEndSession(async () => {
195
// Update view, since orphaned sessions might now be separate
196
await Promise.resolve(); // allow other listeners to go first, so sessions can update parents
197
this.multiSessionRepl.set(this.isMultiSessionView);
198
}));
199
this._register(this.themeService.onDidColorThemeChange(() => {
200
this.refreshReplElements(false);
201
if (this.isVisible()) {
202
this.updateInputDecoration();
203
}
204
}));
205
this._register(this.onDidChangeBodyVisibility(visible => {
206
if (!visible) {
207
return;
208
}
209
if (!this.model) {
210
this.model = this.modelService.getModel(Repl.URI) || this.modelService.createModel('', null, Repl.URI, true);
211
}
212
213
const focusedSession = this.debugService.getViewModel().focusedSession;
214
if (this.tree && this.tree.getInput() !== focusedSession) {
215
this.onDidFocusSession(focusedSession);
216
}
217
218
this.setMode();
219
this.replInput.setModel(this.model);
220
this.updateInputDecoration();
221
this.refreshReplElements(true);
222
223
if (this.styleChangedWhenInvisible) {
224
this.styleChangedWhenInvisible = false;
225
this.tree?.updateChildren(undefined, true, false);
226
this.onDidStyleChange();
227
}
228
}));
229
this._register(this.configurationService.onDidChangeConfiguration(e => {
230
if (e.affectsConfiguration('debug.console.wordWrap') && this.tree) {
231
this.tree.dispose();
232
this.treeContainer.innerText = '';
233
dom.clearNode(this.treeContainer);
234
this.createReplTree();
235
}
236
if (e.affectsConfiguration('debug.console.acceptSuggestionOnEnter')) {
237
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
238
this.replInput.updateOptions({
239
acceptSuggestionOnEnter: config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off'
240
});
241
}
242
}));
243
244
this._register(this.editorService.onDidActiveEditorChange(() => {
245
this.setMode();
246
}));
247
248
this._register(this.filterWidget.onDidChangeFilterText(() => {
249
this.filter.filterQuery = this.filterWidget.getFilterText();
250
if (this.tree) {
251
this.tree.refilter();
252
revealLastElement(this.tree);
253
}
254
}));
255
}
256
257
private async onDidFocusSession(session: IDebugSession | undefined): Promise<void> {
258
if (session) {
259
sessionsToIgnore.delete(session);
260
this.completionItemProvider?.dispose();
261
if (session.capabilities.supportsCompletionsRequest) {
262
this.completionItemProvider = this.languageFeaturesService.completionProvider.register({ scheme: DEBUG_SCHEME, pattern: '**/replinput', hasAccessToAllModels: true }, {
263
_debugDisplayName: 'debugConsole',
264
triggerCharacters: session.capabilities.completionTriggerCharacters || ['.'],
265
provideCompletionItems: async (_: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => {
266
// Disable history navigation because up and down are used to navigate through the suggest widget
267
this.setHistoryNavigationEnablement(false);
268
269
const model = this.replInput.getModel();
270
if (model) {
271
const text = model.getValue();
272
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
273
const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined;
274
const response = await session.completions(frameId, focusedStackFrame?.thread.threadId || 0, text, position, token);
275
276
const suggestions: CompletionItem[] = [];
277
const computeRange = (length: number) => Range.fromPositions(position.delta(0, -length), position);
278
if (response && response.body && response.body.targets) {
279
response.body.targets.forEach(item => {
280
if (item && item.label) {
281
let insertTextRules: CompletionItemInsertTextRule | undefined = undefined;
282
let insertText = item.text || item.label;
283
if (typeof item.selectionStart === 'number') {
284
// If a debug completion item sets a selection we need to use snippets to make sure the selection is selected #90974
285
insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;
286
const selectionLength = typeof item.selectionLength === 'number' ? item.selectionLength : 0;
287
const placeholder = selectionLength > 0 ? '${1:' + insertText.substring(item.selectionStart, item.selectionStart + selectionLength) + '}$0' : '$0';
288
insertText = insertText.substring(0, item.selectionStart) + placeholder + insertText.substring(item.selectionStart + selectionLength);
289
}
290
291
suggestions.push({
292
label: item.label,
293
insertText,
294
detail: item.detail,
295
kind: CompletionItemKinds.fromString(item.type || 'property'),
296
filterText: (item.start && item.length) ? text.substring(item.start, item.start + item.length).concat(item.label) : undefined,
297
range: computeRange(item.length || 0),
298
sortText: item.sortText,
299
insertTextRules
300
});
301
}
302
});
303
}
304
305
if (this.configurationService.getValue<IDebugConfiguration>('debug').console.historySuggestions) {
306
const history = this.history.getHistory();
307
const idxLength = String(history.length).length;
308
history.forEach((h, i) => suggestions.push({
309
label: h,
310
insertText: h,
311
kind: CompletionItemKind.Text,
312
range: computeRange(h.length),
313
sortText: 'ZZZ' + String(history.length - i).padStart(idxLength, '0')
314
}));
315
}
316
317
return { suggestions };
318
}
319
320
return Promise.resolve({ suggestions: [] });
321
}
322
});
323
}
324
}
325
326
await this.selectSession();
327
}
328
329
getFilterStats(): { total: number; filtered: number } {
330
// This could be called before the tree is created when setting this.filterState.filterText value
331
return {
332
total: this.tree?.getNode().children.length ?? 0,
333
filtered: this.tree?.getNode().children.filter(c => c.visible).length ?? 0
334
};
335
}
336
337
get isReadonly(): boolean {
338
// Do not allow to edit inactive sessions
339
const session = this.tree?.getInput();
340
if (session && session.state !== State.Inactive) {
341
return false;
342
}
343
344
return true;
345
}
346
347
showPreviousValue(): void {
348
if (!this.isReadonly) {
349
this.navigateHistory(true);
350
}
351
}
352
353
showNextValue(): void {
354
if (!this.isReadonly) {
355
this.navigateHistory(false);
356
}
357
}
358
359
focusFilter(): void {
360
this.filterWidget.focus();
361
}
362
363
openFind(): void {
364
this.tree?.openFind();
365
}
366
367
private setMode(): void {
368
if (!this.isVisible()) {
369
return;
370
}
371
372
const activeEditorControl = this.editorService.activeTextEditorControl;
373
if (isCodeEditor(activeEditorControl)) {
374
this.modelChangeListener.dispose();
375
this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode());
376
if (this.model && activeEditorControl.hasModel()) {
377
this.model.setLanguage(activeEditorControl.getModel().getLanguageId());
378
}
379
}
380
}
381
382
private onDidStyleChange(): void {
383
if (!this.isVisible()) {
384
this.styleChangedWhenInvisible = true;
385
return;
386
}
387
if (this.styleElement) {
388
this.replInput.updateOptions({
389
fontSize: this.replOptions.replConfiguration.fontSize,
390
lineHeight: this.replOptions.replConfiguration.lineHeight,
391
fontFamily: this.replOptions.replConfiguration.fontFamily === 'default' ? EDITOR_FONT_DEFAULTS.fontFamily : this.replOptions.replConfiguration.fontFamily
392
});
393
394
const replInputLineHeight = this.replInput.getOption(EditorOption.lineHeight);
395
396
// Set the font size, font family, line height and align the twistie to be centered, and input theme color
397
this.styleElement.textContent = `
398
.repl .repl-input-wrapper .repl-input-chevron {
399
line-height: ${replInputLineHeight}px
400
}
401
402
.repl .repl-input-wrapper .monaco-editor .lines-content {
403
background-color: ${this.replOptions.replConfiguration.backgroundColor};
404
}
405
`;
406
const cssFontFamily = this.replOptions.replConfiguration.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : this.replOptions.replConfiguration.fontFamily;
407
this.container.style.setProperty(`--vscode-repl-font-family`, cssFontFamily);
408
this.container.style.setProperty(`--vscode-repl-font-size`, `${this.replOptions.replConfiguration.fontSize}px`);
409
this.container.style.setProperty(`--vscode-repl-font-size-for-twistie`, `${this.replOptions.replConfiguration.fontSizeForTwistie}px`);
410
this.container.style.setProperty(`--vscode-repl-line-height`, this.replOptions.replConfiguration.cssLineHeight);
411
412
this.tree?.rerender();
413
414
if (this.bodyContentDimension) {
415
this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);
416
}
417
}
418
}
419
420
private navigateHistory(previous: boolean): void {
421
const historyInput = (previous ?
422
(this.history.previous() ?? this.history.first()) : this.history.next())
423
?? '';
424
this.replInput.setValue(historyInput);
425
aria.status(historyInput);
426
// always leave cursor at the end.
427
this.replInput.setPosition({ lineNumber: 1, column: historyInput.length + 1 });
428
this.setHistoryNavigationEnablement(true);
429
}
430
431
async selectSession(session?: IDebugSession): Promise<void> {
432
const treeInput = this.tree?.getInput();
433
if (!session) {
434
const focusedSession = this.debugService.getViewModel().focusedSession;
435
// If there is a focusedSession focus on that one, otherwise just show any other not ignored session
436
if (focusedSession) {
437
session = focusedSession;
438
} else if (!treeInput || sessionsToIgnore.has(treeInput)) {
439
session = this.debugService.getModel().getSessions(true).find(s => !sessionsToIgnore.has(s));
440
}
441
}
442
if (session) {
443
this.replElementsChangeListener?.dispose();
444
this.replElementsChangeListener = session.onDidChangeReplElements(() => {
445
this.refreshReplElements(session.getReplElements().length === 0);
446
});
447
448
if (this.tree && treeInput !== session) {
449
try {
450
await this.tree.setInput(session);
451
} catch (err) {
452
// Ignore error because this may happen multiple times while refreshing,
453
// then changing the root may fail. Log to help with debugging if needed.
454
this.logService.error(err);
455
}
456
revealLastElement(this.tree);
457
}
458
}
459
460
this.replInput?.updateOptions({ readOnly: this.isReadonly });
461
this.updateInputDecoration();
462
}
463
464
async clearRepl(): Promise<void> {
465
const session = this.tree?.getInput();
466
if (session) {
467
session.removeReplExpressions();
468
if (session.state === State.Inactive) {
469
// Ignore inactive sessions which got cleared - so they are not shown any more
470
sessionsToIgnore.add(session);
471
await this.selectSession();
472
this.multiSessionRepl.set(this.isMultiSessionView);
473
}
474
}
475
this.replInput.focus();
476
}
477
478
acceptReplInput(): void {
479
const session = this.tree?.getInput();
480
if (session && !this.isReadonly) {
481
session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, this.replInput.getValue());
482
revealLastElement(this.tree!);
483
this.history.add(this.replInput.getValue());
484
this.replInput.setValue('');
485
if (this.bodyContentDimension) {
486
// Trigger a layout to shrink a potential multi line input
487
this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);
488
}
489
}
490
}
491
492
sendReplInput(input: string): void {
493
const session = this.tree?.getInput();
494
if (session && !this.isReadonly) {
495
session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, input);
496
revealLastElement(this.tree!);
497
this.history.add(input);
498
}
499
}
500
501
getVisibleContent(): string {
502
let text = '';
503
if (this.model && this.tree) {
504
const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri);
505
const traverseAndAppend = (node: ITreeNode<IReplElement, FuzzyScore>) => {
506
node.children.forEach(child => {
507
if (child.visible) {
508
text += child.element.toString().trimRight() + lineDelimiter;
509
if (!child.collapsed && child.children.length) {
510
traverseAndAppend(child);
511
}
512
}
513
});
514
};
515
traverseAndAppend(this.tree.getNode());
516
}
517
518
return removeAnsiEscapeCodes(text);
519
}
520
521
protected layoutBodyContent(height: number, width: number): void {
522
this.bodyContentDimension = new dom.Dimension(width, height);
523
const replInputHeight = Math.min(this.replInput.getContentHeight(), height);
524
if (this.tree) {
525
const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;
526
const treeHeight = height - replInputHeight;
527
this.tree.getHTMLElement().style.height = `${treeHeight}px`;
528
this.tree.layout(treeHeight, width);
529
if (lastElementVisible) {
530
revealLastElement(this.tree);
531
}
532
}
533
this.replInputContainer.style.height = `${replInputHeight}px`;
534
535
this.replInput.layout({ width: width - 30, height: replInputHeight });
536
}
537
538
collapseAll(): void {
539
this.tree?.collapseAll();
540
}
541
542
getDebugSession(): IDebugSession | undefined {
543
return this.tree?.getInput();
544
}
545
546
getReplInput(): CodeEditorWidget {
547
return this.replInput;
548
}
549
550
getReplDataSource(): IAsyncDataSource<IDebugSession, IReplElement> | undefined {
551
return this.replDataSource;
552
}
553
554
getFocusedElement(): IReplElement | undefined {
555
return this.tree?.getFocus()?.[0];
556
}
557
558
focusTree(): void {
559
this.tree?.domFocus();
560
}
561
562
override async focus(): Promise<void> {
563
super.focus();
564
await timeout(0); // wait a task for the repl to get attached to the DOM, #83387
565
this.replInput.focus();
566
}
567
568
override createActionViewItem(action: IAction): IActionViewItem | undefined {
569
if (action.id === selectReplCommandId) {
570
const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession;
571
return this.instantiationService.createInstance(SelectReplActionViewItem, action, session);
572
}
573
574
return super.createActionViewItem(action);
575
}
576
577
private get isMultiSessionView(): boolean {
578
return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1;
579
}
580
581
// --- Cached locals
582
583
@memoize
584
private get refreshScheduler(): RunOnceScheduler {
585
const autoExpanded = new Set<string>();
586
return new RunOnceScheduler(async () => {
587
if (!this.tree || !this.tree.getInput() || !this.isVisible()) {
588
return;
589
}
590
591
await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider });
592
593
const session = this.tree.getInput();
594
if (session) {
595
// Automatically expand repl group elements when specified
596
const autoExpandElements = async (elements: IReplElement[]) => {
597
for (const element of elements) {
598
if (element instanceof ReplGroup) {
599
if (element.autoExpand && !autoExpanded.has(element.getId())) {
600
autoExpanded.add(element.getId());
601
await this.tree!.expand(element);
602
}
603
if (!this.tree!.isCollapsed(element)) {
604
// Repl groups can have children which are repl groups thus we might need to expand those as well
605
await autoExpandElements(element.getChildren());
606
}
607
}
608
}
609
};
610
await autoExpandElements(session.getReplElements());
611
}
612
// Repl elements count changed, need to update filter stats on the badge
613
const { total, filtered } = this.getFilterStats();
614
this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : localize('showing filtered repl lines', "Showing {0} of {1}", filtered, total));
615
}, Repl.REFRESH_DELAY);
616
}
617
618
// --- Creation
619
620
override render(): void {
621
super.render();
622
this._register(registerNavigableContainer({
623
name: 'repl',
624
focusNotifiers: [this, this.filterWidget],
625
focusNextWidget: () => {
626
const element = this.tree?.getHTMLElement();
627
if (this.filterWidget.hasFocus()) {
628
this.tree?.domFocus();
629
} else if (element && dom.isActiveElement(element)) {
630
this.focus();
631
}
632
},
633
focusPreviousWidget: () => {
634
const element = this.tree?.getHTMLElement();
635
if (this.replInput.hasTextFocus()) {
636
this.tree?.domFocus();
637
} else if (element && dom.isActiveElement(element)) {
638
this.focusFilter();
639
}
640
}
641
}));
642
}
643
644
protected override renderBody(parent: HTMLElement): void {
645
super.renderBody(parent);
646
this.container = dom.append(parent, $('.repl'));
647
this.treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`));
648
this.createReplInput(this.container);
649
this.createReplTree();
650
}
651
652
private createReplTree(): void {
653
this.replDelegate = new ReplDelegate(this.configurationService, this.replOptions);
654
const wordWrap = this.configurationService.getValue<IDebugConfiguration>('debug').console.wordWrap;
655
this.treeContainer.classList.toggle('word-wrap', wordWrap);
656
const expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer);
657
this.replDataSource = new ReplDataSource();
658
659
const tree = this.tree = this.instantiationService.createInstance(
660
WorkbenchAsyncDataTree<IDebugSession, IReplElement, FuzzyScore>,
661
'DebugRepl',
662
this.treeContainer,
663
this.replDelegate,
664
[
665
this.instantiationService.createInstance(ReplVariablesRenderer, expressionRenderer),
666
this.instantiationService.createInstance(ReplOutputElementRenderer, expressionRenderer),
667
new ReplEvaluationInputsRenderer(),
668
this.instantiationService.createInstance(ReplGroupRenderer, expressionRenderer),
669
new ReplEvaluationResultsRenderer(expressionRenderer),
670
new ReplRawObjectsRenderer(expressionRenderer),
671
],
672
this.replDataSource,
673
{
674
filter: this.filter,
675
accessibilityProvider: new ReplAccessibilityProvider(),
676
identityProvider,
677
userSelection: true,
678
mouseSupport: false,
679
findWidgetEnabled: true,
680
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e.toString(true) },
681
horizontalScrolling: !wordWrap,
682
setRowLineHeight: false,
683
supportDynamicHeights: wordWrap,
684
overrideStyles: this.getLocationBasedColors().listOverrideStyles
685
});
686
687
this._register(tree.onDidChangeContentHeight(() => {
688
if (tree.scrollHeight !== this.previousTreeScrollHeight) {
689
// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.
690
// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.
691
const lastElementWasVisible = tree.scrollTop + tree.renderHeight >= this.previousTreeScrollHeight - 2;
692
if (lastElementWasVisible) {
693
setTimeout(() => {
694
// Can't set scrollTop during this event listener, the list might overwrite the change
695
revealLastElement(tree);
696
}, 0);
697
}
698
}
699
700
this.previousTreeScrollHeight = tree.scrollHeight;
701
}));
702
703
this._register(tree.onContextMenu(e => this.onContextMenu(e)));
704
this._register(tree.onDidChangeFindOpenState((open) => this.findIsOpen = open));
705
706
let lastSelectedString: string;
707
this._register(tree.onMouseClick(() => {
708
if (this.findIsOpen) {
709
return;
710
}
711
const selection = dom.getWindow(this.treeContainer).getSelection();
712
if (!selection || selection.type !== 'Range' || lastSelectedString === selection.toString()) {
713
// only focus the input if the user is not currently selecting and find isn't open.
714
this.replInput.focus();
715
}
716
lastSelectedString = selection ? selection.toString() : '';
717
}));
718
// Make sure to select the session if debugging is already active
719
this.selectSession();
720
this.styleElement = domStylesheetsJs.createStyleSheet(this.container);
721
this.onDidStyleChange();
722
}
723
724
private createReplInput(container: HTMLElement): void {
725
this.replInputContainer = dom.append(container, $('.repl-input-wrapper'));
726
dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt)));
727
728
const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(this.scopedContextKeyService, this));
729
this.setHistoryNavigationEnablement = enabled => {
730
historyNavigationBackwardsEnablement.set(enabled);
731
historyNavigationForwardsEnablement.set(enabled);
732
};
733
CONTEXT_IN_DEBUG_REPL.bindTo(this.scopedContextKeyService).set(true);
734
735
this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
736
const options = getSimpleEditorOptions(this.configurationService);
737
options.readOnly = true;
738
options.suggest = { showStatusBar: true };
739
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
740
options.acceptSuggestionOnEnter = config.console.acceptSuggestionOnEnter === 'on' ? 'on' : 'off';
741
options.ariaLabel = this.getAriaLabel();
742
743
this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions());
744
745
let lastContentHeight = -1;
746
this._register(this.replInput.onDidChangeModelContent(() => {
747
const model = this.replInput.getModel();
748
this.setHistoryNavigationEnablement(!!model && model.getValue() === '');
749
750
const contentHeight = this.replInput.getContentHeight();
751
if (contentHeight !== lastContentHeight) {
752
lastContentHeight = contentHeight;
753
if (this.bodyContentDimension) {
754
this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width);
755
}
756
}
757
}));
758
// We add the input decoration only when the focus is in the input #61126
759
this._register(this.replInput.onDidFocusEditorText(() => this.updateInputDecoration()));
760
this._register(this.replInput.onDidBlurEditorText(() => this.updateInputDecoration()));
761
762
this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.FOCUS, () => this.replInputContainer.classList.add('synthetic-focus')));
763
this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.BLUR, () => this.replInputContainer.classList.remove('synthetic-focus')));
764
}
765
766
private getAriaLabel(): string {
767
let ariaLabel = localize('debugConsole', "Debug Console");
768
if (!this.configurationService.getValue(AccessibilityVerbositySettingId.Debug)) {
769
return ariaLabel;
770
}
771
const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel();
772
if (keybinding) {
773
ariaLabel = localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding);
774
} else {
775
ariaLabel = localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel);
776
}
777
778
return ariaLabel;
779
}
780
781
private onContextMenu(e: ITreeContextMenuEvent<IReplElement>): void {
782
const actions = getFlatContextMenuActions(this.menu.getActions({ arg: e.element, shouldForwardArgs: false }));
783
this.contextMenuService.showContextMenu({
784
getAnchor: () => e.anchor,
785
getActions: () => actions,
786
getActionsContext: () => e.element
787
});
788
}
789
790
// --- Update
791
792
private refreshReplElements(noDelay: boolean): void {
793
if (this.tree && this.isVisible()) {
794
if (this.refreshScheduler.isScheduled()) {
795
return;
796
}
797
798
this.refreshScheduler.schedule(noDelay ? 0 : undefined);
799
}
800
}
801
802
private updateInputDecoration(): void {
803
if (!this.replInput) {
804
return;
805
}
806
807
const decorations: IDecorationOptions[] = [];
808
if (this.isReadonly && this.replInput.hasTextFocus() && !this.replInput.getValue()) {
809
const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4);
810
decorations.push({
811
range: {
812
startLineNumber: 0,
813
endLineNumber: 0,
814
startColumn: 0,
815
endColumn: 1
816
},
817
renderOptions: {
818
after: {
819
contentText: localize('startDebugFirst', "Please start a debug session to evaluate expressions"),
820
color: transparentForeground ? transparentForeground.toString() : undefined
821
}
822
}
823
});
824
}
825
826
this.replInput.setDecorationsByType('repl-decoration', DECORATION_KEY, decorations);
827
}
828
829
override saveState(): void {
830
const replHistory = this.history.getHistory();
831
if (replHistory.length) {
832
this.storageService.store(HISTORY_STORAGE_KEY, JSON.stringify(replHistory), StorageScope.WORKSPACE, StorageTarget.MACHINE);
833
} else {
834
this.storageService.remove(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
835
}
836
const filterHistory = this.filterWidget.getHistory();
837
if (filterHistory.length) {
838
this.storageService.store(FILTER_HISTORY_STORAGE_KEY, JSON.stringify(filterHistory), StorageScope.WORKSPACE, StorageTarget.MACHINE);
839
} else {
840
this.storageService.remove(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
841
}
842
const filterValue = this.filterWidget.getFilterText();
843
if (filterValue) {
844
this.storageService.store(FILTER_VALUE_STORAGE_KEY, filterValue, StorageScope.WORKSPACE, StorageTarget.MACHINE);
845
} else {
846
this.storageService.remove(FILTER_VALUE_STORAGE_KEY, StorageScope.WORKSPACE);
847
}
848
849
super.saveState();
850
}
851
852
override dispose(): void {
853
this.replInput?.dispose(); // Disposed before rendered? #174558
854
this.replElementsChangeListener?.dispose();
855
this.refreshScheduler.dispose();
856
this.modelChangeListener.dispose();
857
super.dispose();
858
}
859
}
860
861
class ReplOptions extends Disposable implements IReplOptions {
862
private static readonly lineHeightEm = 1.4;
863
864
private readonly _onDidChange = this._register(new Emitter<void>());
865
readonly onDidChange = this._onDidChange.event;
866
867
private _replConfig!: IReplConfiguration;
868
public get replConfiguration(): IReplConfiguration {
869
return this._replConfig;
870
}
871
872
constructor(
873
viewId: string,
874
private readonly backgroundColorDelegate: () => string,
875
@IConfigurationService private readonly configurationService: IConfigurationService,
876
@IThemeService private readonly themeService: IThemeService,
877
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService
878
) {
879
super();
880
881
this._register(this.themeService.onDidColorThemeChange(e => this.update()));
882
this._register(this.viewDescriptorService.onDidChangeLocation(e => {
883
if (e.views.some(v => v.id === viewId)) {
884
this.update();
885
}
886
}));
887
this._register(this.configurationService.onDidChangeConfiguration(e => {
888
if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) {
889
this.update();
890
}
891
}));
892
this.update();
893
}
894
895
private update() {
896
const debugConsole = this.configurationService.getValue<IDebugConfiguration>('debug').console;
897
this._replConfig = {
898
fontSize: debugConsole.fontSize,
899
fontFamily: debugConsole.fontFamily,
900
lineHeight: debugConsole.lineHeight ? debugConsole.lineHeight : ReplOptions.lineHeightEm * debugConsole.fontSize,
901
cssLineHeight: debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : `${ReplOptions.lineHeightEm}em`,
902
backgroundColor: this.themeService.getColorTheme().getColor(this.backgroundColorDelegate()),
903
fontSizeForTwistie: debugConsole.fontSize * ReplOptions.lineHeightEm / 2 - 8
904
};
905
this._onDidChange.fire();
906
}
907
}
908
909
// Repl actions and commands
910
911
class AcceptReplInputAction extends EditorAction {
912
913
constructor() {
914
super({
915
id: 'repl.action.acceptInput',
916
label: localize2({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "Debug Console: Accept Input"),
917
precondition: CONTEXT_IN_DEBUG_REPL,
918
kbOpts: {
919
kbExpr: EditorContextKeys.textInputFocus,
920
primary: KeyCode.Enter,
921
weight: KeybindingWeight.EditorContrib
922
}
923
});
924
}
925
926
run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
927
SuggestController.get(editor)?.cancelSuggestWidget();
928
const repl = getReplView(accessor.get(IViewsService));
929
repl?.acceptReplInput();
930
}
931
}
932
933
class FilterReplAction extends ViewAction<Repl> {
934
935
constructor() {
936
super({
937
viewId: REPL_VIEW_ID,
938
id: 'repl.action.filter',
939
title: localize('repl.action.filter', "Debug Console: Focus Filter"),
940
precondition: CONTEXT_IN_DEBUG_REPL,
941
keybinding: [{
942
when: EditorContextKeys.textInputFocus,
943
primary: KeyMod.CtrlCmd | KeyCode.KeyF,
944
weight: KeybindingWeight.EditorContrib
945
}]
946
});
947
}
948
949
runInView(accessor: ServicesAccessor, repl: Repl): void | Promise<void> {
950
repl.focusFilter();
951
}
952
}
953
954
955
class FindReplAction extends ViewAction<Repl> {
956
957
constructor() {
958
super({
959
viewId: REPL_VIEW_ID,
960
id: 'repl.action.find',
961
title: localize('repl.action.find', "Debug Console: Focus Find"),
962
precondition: CONTEXT_IN_DEBUG_REPL,
963
keybinding: [{
964
when: ContextKeyExpr.or(CONTEXT_IN_DEBUG_REPL, ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')),
965
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF,
966
weight: KeybindingWeight.EditorContrib
967
}],
968
icon: Codicon.search,
969
menu: [{
970
id: MenuId.ViewTitle,
971
group: 'navigation',
972
when: ContextKeyExpr.equals('view', REPL_VIEW_ID),
973
order: 15
974
}, {
975
id: MenuId.DebugConsoleContext,
976
group: 'z_commands',
977
order: 25
978
}],
979
});
980
}
981
982
runInView(accessor: ServicesAccessor, view: Repl): void | Promise<void> {
983
view.openFind();
984
}
985
}
986
987
class ReplCopyAllAction extends EditorAction {
988
989
constructor() {
990
super({
991
id: 'repl.action.copyAll',
992
label: localize('actions.repl.copyAll', "Debug: Console Copy All"),
993
alias: 'Debug Console Copy All',
994
precondition: CONTEXT_IN_DEBUG_REPL,
995
});
996
}
997
998
run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
999
const clipboardService = accessor.get(IClipboardService);
1000
const repl = getReplView(accessor.get(IViewsService));
1001
if (repl) {
1002
return clipboardService.writeText(repl.getVisibleContent());
1003
}
1004
}
1005
}
1006
1007
registerEditorAction(AcceptReplInputAction);
1008
registerEditorAction(ReplCopyAllAction);
1009
registerAction2(FilterReplAction);
1010
registerAction2(FindReplAction);
1011
1012
class SelectReplActionViewItem extends FocusSessionActionViewItem {
1013
1014
protected override getSessions(): ReadonlyArray<IDebugSession> {
1015
return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s));
1016
}
1017
1018
protected override mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession {
1019
while (focusedSession.parentSession && !focusedSession.hasSeparateRepl()) {
1020
focusedSession = focusedSession.parentSession;
1021
}
1022
return focusedSession;
1023
}
1024
}
1025
1026
export function getReplView(viewsService: IViewsService): Repl | undefined {
1027
return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined;
1028
}
1029
1030
const selectReplCommandId = 'workbench.action.debug.selectRepl';
1031
registerAction2(class extends ViewAction<Repl> {
1032
constructor() {
1033
super({
1034
id: selectReplCommandId,
1035
viewId: REPL_VIEW_ID,
1036
title: localize('selectRepl', "Select Debug Console"),
1037
f1: false,
1038
menu: {
1039
id: MenuId.ViewTitle,
1040
group: 'navigation',
1041
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', REPL_VIEW_ID), CONTEXT_MULTI_SESSION_REPL),
1042
order: 20
1043
}
1044
});
1045
}
1046
1047
async runInView(accessor: ServicesAccessor, view: Repl, session: IDebugSession | undefined) {
1048
const debugService = accessor.get(IDebugService);
1049
// If session is already the focused session we need to manualy update the tree since view model will not send a focused change event
1050
if (session && session.state !== State.Inactive && session !== debugService.getViewModel().focusedSession) {
1051
session = resolveChildSession(session, debugService.getModel().getSessions());
1052
await debugService.focusStackFrame(undefined, undefined, session, { explicit: true });
1053
}
1054
// Need to select the session in the view since the focussed session might not have changed
1055
await view.selectSession(session);
1056
}
1057
});
1058
1059
registerAction2(class extends ViewAction<Repl> {
1060
constructor() {
1061
super({
1062
id: 'workbench.debug.panel.action.clearReplAction',
1063
viewId: REPL_VIEW_ID,
1064
title: localize2('clearRepl', 'Clear Console'),
1065
metadata: {
1066
description: localize2('clearRepl.descriotion', 'Clears all program output from your debug REPL')
1067
},
1068
f1: true,
1069
icon: debugConsoleClearAll,
1070
menu: [{
1071
id: MenuId.ViewTitle,
1072
group: 'navigation',
1073
when: ContextKeyExpr.equals('view', REPL_VIEW_ID),
1074
order: 30
1075
}, {
1076
id: MenuId.DebugConsoleContext,
1077
group: 'z_commands',
1078
order: 20
1079
}],
1080
keybinding: [{
1081
primary: 0,
1082
mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyK },
1083
// Weight is higher than work workbench contributions so the keybinding remains
1084
// highest priority when chords are registered afterwards
1085
weight: KeybindingWeight.WorkbenchContrib + 1,
1086
when: ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view')
1087
}],
1088
});
1089
}
1090
1091
runInView(_accessor: ServicesAccessor, view: Repl): void {
1092
const accessibilitySignalService = _accessor.get(IAccessibilitySignalService);
1093
view.clearRepl();
1094
accessibilitySignalService.playSignal(AccessibilitySignal.clear);
1095
}
1096
});
1097
1098
registerAction2(class extends ViewAction<Repl> {
1099
constructor() {
1100
super({
1101
id: 'debug.collapseRepl',
1102
title: localize('collapse', "Collapse All"),
1103
viewId: REPL_VIEW_ID,
1104
menu: {
1105
id: MenuId.DebugConsoleContext,
1106
group: 'z_commands',
1107
order: 10
1108
}
1109
});
1110
}
1111
1112
runInView(_accessor: ServicesAccessor, view: Repl): void {
1113
view.collapseAll();
1114
view.focus();
1115
}
1116
});
1117
1118
registerAction2(class extends ViewAction<Repl> {
1119
constructor() {
1120
super({
1121
id: 'debug.replPaste',
1122
title: localize('paste', "Paste"),
1123
viewId: REPL_VIEW_ID,
1124
precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Inactive)),
1125
menu: {
1126
id: MenuId.DebugConsoleContext,
1127
group: '2_cutcopypaste',
1128
order: 30
1129
}
1130
});
1131
}
1132
1133
async runInView(accessor: ServicesAccessor, view: Repl): Promise<void> {
1134
const clipboardService = accessor.get(IClipboardService);
1135
const clipboardText = await clipboardService.readText();
1136
if (clipboardText) {
1137
const replInput = view.getReplInput();
1138
replInput.setValue(replInput.getValue().concat(clipboardText));
1139
view.focus();
1140
const model = replInput.getModel();
1141
const lineNumber = model ? model.getLineCount() : 0;
1142
const column = model?.getLineMaxColumn(lineNumber);
1143
if (typeof lineNumber === 'number' && typeof column === 'number') {
1144
replInput.setPosition({ lineNumber, column });
1145
}
1146
}
1147
}
1148
});
1149
1150
registerAction2(class extends ViewAction<Repl> {
1151
constructor() {
1152
super({
1153
id: 'workbench.debug.action.copyAll',
1154
title: localize('copyAll', "Copy All"),
1155
viewId: REPL_VIEW_ID,
1156
menu: {
1157
id: MenuId.DebugConsoleContext,
1158
group: '2_cutcopypaste',
1159
order: 20
1160
}
1161
});
1162
}
1163
1164
async runInView(accessor: ServicesAccessor, view: Repl): Promise<void> {
1165
const clipboardService = accessor.get(IClipboardService);
1166
await clipboardService.writeText(view.getVisibleContent());
1167
}
1168
});
1169
1170
registerAction2(class extends Action2 {
1171
constructor() {
1172
super({
1173
id: 'debug.replCopy',
1174
title: localize('copy', "Copy"),
1175
menu: {
1176
id: MenuId.DebugConsoleContext,
1177
group: '2_cutcopypaste',
1178
order: 10
1179
}
1180
});
1181
}
1182
1183
async run(accessor: ServicesAccessor, element: IReplElement): Promise<void> {
1184
const clipboardService = accessor.get(IClipboardService);
1185
const debugService = accessor.get(IDebugService);
1186
const nativeSelection = dom.getActiveWindow().getSelection();
1187
const selectedText = nativeSelection?.toString();
1188
if (selectedText && selectedText.length > 0) {
1189
return clipboardService.writeText(selectedText);
1190
} else if (element) {
1191
const retValue = await this.tryEvaluateAndCopy(debugService, element);
1192
const textToCopy = retValue || removeAnsiEscapeCodes(element.toString());
1193
return clipboardService.writeText(textToCopy);
1194
}
1195
}
1196
1197
private async tryEvaluateAndCopy(debugService: IDebugService, element: IReplElement): Promise<string | undefined> {
1198
// todo: we should expand DAP to allow copying more types here (#187784)
1199
if (!(element instanceof ReplEvaluationResult)) {
1200
return;
1201
}
1202
1203
const stackFrame = debugService.getViewModel().focusedStackFrame;
1204
const session = debugService.getViewModel().focusedSession;
1205
if (!stackFrame || !session || !session.capabilities.supportsClipboardContext) {
1206
return;
1207
}
1208
1209
try {
1210
const evaluation = await session.evaluate(element.originalExpression, stackFrame.frameId, 'clipboard');
1211
return evaluation?.body.result;
1212
} catch (e) {
1213
return;
1214
}
1215
}
1216
});
1217
1218
registerAction2(class extends Action2 {
1219
constructor() {
1220
super({
1221
id: FOCUS_REPL_ID,
1222
category: DEBUG_COMMAND_CATEGORY,
1223
title: localize2({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, "Focus on Debug Console View"),
1224
});
1225
}
1226
1227
override async run(accessor: ServicesAccessor) {
1228
const viewsService = accessor.get(IViewsService);
1229
const repl = await viewsService.openView<Repl>(REPL_VIEW_ID);
1230
await repl?.focus();
1231
}
1232
});
1233
1234