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