Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/common/debugUtils.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 { equalsIgnoreCase } from '../../../../base/common/strings.js';
7
import { IDebuggerContribution, IDebugSession, IConfigPresentation } from './debug.js';
8
import { URI as uri } from '../../../../base/common/uri.js';
9
import { isAbsolute } from '../../../../base/common/path.js';
10
import { deepClone } from '../../../../base/common/objects.js';
11
import { Schemas } from '../../../../base/common/network.js';
12
import { IEditorService } from '../../../services/editor/common/editorService.js';
13
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
14
import { ITextModel } from '../../../../editor/common/model.js';
15
import { Position } from '../../../../editor/common/core/position.js';
16
import { IRange, Range } from '../../../../editor/common/core/range.js';
17
import { CancellationToken } from '../../../../base/common/cancellation.js';
18
import { coalesce } from '../../../../base/common/arrays.js';
19
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
20
21
const _formatPIIRegexp = /{([^}]+)}/g;
22
23
export function formatPII(value: string, excludePII: boolean, args: { [key: string]: string } | undefined): string {
24
return value.replace(_formatPIIRegexp, function (match, group) {
25
if (excludePII && group.length > 0 && group[0] !== '_') {
26
return match;
27
}
28
29
return args && args.hasOwnProperty(group) ?
30
args[group] :
31
match;
32
});
33
}
34
35
/**
36
* Filters exceptions (keys marked with "!") from the given object. Used to
37
* ensure exception data is not sent on web remotes, see #97628.
38
*/
39
export function filterExceptionsFromTelemetry<T extends { [key: string]: unknown }>(data: T): Partial<T> {
40
const output: Partial<T> = {};
41
for (const key of Object.keys(data) as (keyof T & string)[]) {
42
if (!key.startsWith('!')) {
43
output[key] = data[key];
44
}
45
}
46
47
return output;
48
}
49
50
51
export function isSessionAttach(session: IDebugSession): boolean {
52
return session.configuration.request === 'attach' && !getExtensionHostDebugSession(session) && (!session.parentSession || isSessionAttach(session.parentSession));
53
}
54
55
/**
56
* Returns the session or any parent which is an extension host debug session.
57
* Returns undefined if there's none.
58
*/
59
export function getExtensionHostDebugSession(session: IDebugSession): IDebugSession | void {
60
let type = session.configuration.type;
61
if (!type) {
62
return;
63
}
64
65
if (type === 'vslsShare') {
66
type = (<any>session.configuration).adapterProxy.configuration.type;
67
}
68
69
if (equalsIgnoreCase(type, 'extensionhost') || equalsIgnoreCase(type, 'pwa-extensionhost')) {
70
return session;
71
}
72
73
return session.parentSession ? getExtensionHostDebugSession(session.parentSession) : undefined;
74
}
75
76
// only a debugger contributions with a label, program, or runtime attribute is considered a "defining" or "main" debugger contribution
77
export function isDebuggerMainContribution(dbg: IDebuggerContribution) {
78
return dbg.type && (dbg.label || dbg.program || dbg.runtime);
79
}
80
81
/**
82
* Note- uses 1-indexed numbers
83
*/
84
export function getExactExpressionStartAndEnd(lineContent: string, looseStart: number, looseEnd: number): { start: number; end: number } {
85
let matchingExpression: string | undefined = undefined;
86
let startOffset = 0;
87
88
// Some example supported expressions: myVar.prop, a.b.c.d, myVar?.prop, myVar->prop, MyClass::StaticProp, *myVar, ...foo
89
// Match any character except a set of characters which often break interesting sub-expressions
90
const expression: RegExp = /([^()\[\]{}<>\s+\-/%~#^;=|,`!]|\->)+/g;
91
let result: RegExpExecArray | null = null;
92
93
// First find the full expression under the cursor
94
while (result = expression.exec(lineContent)) {
95
const start = result.index + 1;
96
const end = start + result[0].length;
97
98
if (start <= looseStart && end >= looseEnd) {
99
matchingExpression = result[0];
100
startOffset = start;
101
break;
102
}
103
}
104
105
// Handle spread syntax: if the expression starts with '...', extract just the identifier
106
if (matchingExpression) {
107
const spreadMatch = matchingExpression.match(/^\.\.\.(.+)/);
108
if (spreadMatch) {
109
matchingExpression = spreadMatch[1];
110
startOffset += 3; // Skip the '...' prefix
111
}
112
}
113
114
// If there are non-word characters after the cursor, we want to truncate the expression then.
115
// For example in expression 'a.b.c.d', if the focus was under 'b', 'a.b' would be evaluated.
116
if (matchingExpression) {
117
const subExpression: RegExp = /(\w|\p{L})+/gu;
118
let subExpressionResult: RegExpExecArray | null = null;
119
while (subExpressionResult = subExpression.exec(matchingExpression)) {
120
const subEnd = subExpressionResult.index + 1 + startOffset + subExpressionResult[0].length;
121
if (subEnd >= looseEnd) {
122
break;
123
}
124
}
125
126
if (subExpressionResult) {
127
matchingExpression = matchingExpression.substring(0, subExpression.lastIndex);
128
}
129
}
130
131
return matchingExpression ?
132
{ start: startOffset, end: startOffset + matchingExpression.length - 1 } :
133
{ start: 0, end: 0 };
134
}
135
136
export async function getEvaluatableExpressionAtPosition(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: Position, token?: CancellationToken): Promise<{ range: IRange; matchingExpression: string } | null> {
137
if (languageFeaturesService.evaluatableExpressionProvider.has(model)) {
138
const supports = languageFeaturesService.evaluatableExpressionProvider.ordered(model);
139
140
const results = coalesce(await Promise.all(supports.map(async support => {
141
try {
142
return await support.provideEvaluatableExpression(model, position, token ?? CancellationToken.None);
143
} catch (err) {
144
return undefined;
145
}
146
})));
147
148
if (results.length > 0) {
149
let matchingExpression = results[0].expression;
150
const range = results[0].range;
151
152
if (!matchingExpression) {
153
const lineContent = model.getLineContent(position.lineNumber);
154
matchingExpression = lineContent.substring(range.startColumn - 1, range.endColumn - 1);
155
}
156
157
return { range, matchingExpression };
158
}
159
} else { // old one-size-fits-all strategy
160
const lineContent = model.getLineContent(position.lineNumber);
161
const { start, end } = getExactExpressionStartAndEnd(lineContent, position.column, position.column);
162
163
// use regex to extract the sub-expression #9821
164
const matchingExpression = lineContent.substring(start - 1, end);
165
return {
166
matchingExpression,
167
range: new Range(position.lineNumber, start, position.lineNumber, start + matchingExpression.length)
168
};
169
}
170
171
return null;
172
}
173
174
// RFC 2396, Appendix A: https://www.ietf.org/rfc/rfc2396.txt
175
const _schemePattern = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/;
176
177
export function isUri(s: string | undefined): boolean {
178
// heuristics: a valid uri starts with a scheme and
179
// the scheme has at least 2 characters so that it doesn't look like a drive letter.
180
return !!(s && s.match(_schemePattern));
181
}
182
183
function stringToUri(source: PathContainer): string | undefined {
184
if (typeof source.path === 'string') {
185
if (typeof source.sourceReference === 'number' && source.sourceReference > 0) {
186
// if there is a source reference, don't touch path
187
} else {
188
if (isUri(source.path)) {
189
return <string><unknown>uri.parse(source.path);
190
} else {
191
// assume path
192
if (isAbsolute(source.path)) {
193
return <string><unknown>uri.file(source.path);
194
} else {
195
// leave relative path as is
196
}
197
}
198
}
199
}
200
return source.path;
201
}
202
203
function uriToString(source: PathContainer): string | undefined {
204
if (typeof source.path === 'object') {
205
const u = uri.revive(source.path);
206
if (u) {
207
if (u.scheme === Schemas.file) {
208
return u.fsPath;
209
} else {
210
return u.toString();
211
}
212
}
213
}
214
return source.path;
215
}
216
217
// path hooks helpers
218
219
interface PathContainer {
220
path?: string;
221
sourceReference?: number;
222
}
223
224
export function convertToDAPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage {
225
226
const fixPath = toUri ? stringToUri : uriToString;
227
228
// since we modify Source.paths in the message in place, we need to make a copy of it (see #61129)
229
const msg = deepClone(message);
230
231
convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => {
232
if (toDA && source) {
233
source.path = fixPath(source);
234
}
235
});
236
return msg;
237
}
238
239
export function convertToVSCPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage {
240
241
const fixPath = toUri ? stringToUri : uriToString;
242
243
// since we modify Source.paths in the message in place, we need to make a copy of it (see #61129)
244
const msg = deepClone(message);
245
246
convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => {
247
if (!toDA && source) {
248
source.path = fixPath(source);
249
}
250
});
251
return msg;
252
}
253
254
function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePath: (toDA: boolean, source: PathContainer | undefined) => void): void {
255
256
switch (msg.type) {
257
case 'event': {
258
const event = <DebugProtocol.Event>msg;
259
switch (event.event) {
260
case 'output':
261
fixSourcePath(false, (<DebugProtocol.OutputEvent>event).body.source);
262
break;
263
case 'loadedSource':
264
fixSourcePath(false, (<DebugProtocol.LoadedSourceEvent>event).body.source);
265
break;
266
case 'breakpoint':
267
fixSourcePath(false, (<DebugProtocol.BreakpointEvent>event).body.breakpoint.source);
268
break;
269
default:
270
break;
271
}
272
break;
273
}
274
case 'request': {
275
const request = <DebugProtocol.Request>msg;
276
switch (request.command) {
277
case 'setBreakpoints':
278
fixSourcePath(true, (<DebugProtocol.SetBreakpointsArguments>request.arguments).source);
279
break;
280
case 'breakpointLocations':
281
fixSourcePath(true, (<DebugProtocol.BreakpointLocationsArguments>request.arguments).source);
282
break;
283
case 'source':
284
fixSourcePath(true, (<DebugProtocol.SourceArguments>request.arguments).source);
285
break;
286
case 'gotoTargets':
287
fixSourcePath(true, (<DebugProtocol.GotoTargetsArguments>request.arguments).source);
288
break;
289
case 'launchVSCode':
290
request.arguments.args.forEach((arg: PathContainer | undefined) => fixSourcePath(false, arg));
291
break;
292
default:
293
break;
294
}
295
break;
296
}
297
case 'response': {
298
const response = <DebugProtocol.Response>msg;
299
if (response.success && response.body) {
300
switch (response.command) {
301
case 'stackTrace':
302
(<DebugProtocol.StackTraceResponse>response).body.stackFrames.forEach(frame => fixSourcePath(false, frame.source));
303
break;
304
case 'loadedSources':
305
(<DebugProtocol.LoadedSourcesResponse>response).body.sources.forEach(source => fixSourcePath(false, source));
306
break;
307
case 'scopes':
308
(<DebugProtocol.ScopesResponse>response).body.scopes.forEach(scope => fixSourcePath(false, scope.source));
309
break;
310
case 'setFunctionBreakpoints':
311
(<DebugProtocol.SetFunctionBreakpointsResponse>response).body.breakpoints.forEach(bp => fixSourcePath(false, bp.source));
312
break;
313
case 'setBreakpoints':
314
(<DebugProtocol.SetBreakpointsResponse>response).body.breakpoints.forEach(bp => fixSourcePath(false, bp.source));
315
break;
316
case 'disassemble':
317
{
318
const di = <DebugProtocol.DisassembleResponse>response;
319
di.body?.instructions.forEach(di => fixSourcePath(false, di.location));
320
}
321
break;
322
case 'locations':
323
fixSourcePath(false, (<DebugProtocol.LocationsResponse>response).body?.source);
324
break;
325
default:
326
break;
327
}
328
}
329
break;
330
}
331
}
332
}
333
334
export function getVisibleAndSorted<T extends { presentation?: IConfigPresentation }>(array: T[]): T[] {
335
return array.filter(config => !config.presentation?.hidden).sort((first, second) => {
336
if (!first.presentation) {
337
if (!second.presentation) {
338
return 0;
339
}
340
return 1;
341
}
342
if (!second.presentation) {
343
return -1;
344
}
345
if (!first.presentation.group) {
346
if (!second.presentation.group) {
347
return compareOrders(first.presentation.order, second.presentation.order);
348
}
349
return 1;
350
}
351
if (!second.presentation.group) {
352
return -1;
353
}
354
if (first.presentation.group !== second.presentation.group) {
355
return first.presentation.group.localeCompare(second.presentation.group);
356
}
357
358
return compareOrders(first.presentation.order, second.presentation.order);
359
});
360
}
361
362
function compareOrders(first: number | undefined, second: number | undefined): number {
363
if (typeof first !== 'number') {
364
if (typeof second !== 'number') {
365
return 0;
366
}
367
368
return 1;
369
}
370
if (typeof second !== 'number') {
371
return -1;
372
}
373
374
return first - second;
375
}
376
377
export async function saveAllBeforeDebugStart(configurationService: IConfigurationService, editorService: IEditorService): Promise<void> {
378
const saveBeforeStartConfig: string = configurationService.getValue('debug.saveBeforeStart', { overrideIdentifier: editorService.activeTextEditorLanguageId });
379
if (saveBeforeStartConfig !== 'none') {
380
await editorService.saveAll();
381
if (saveBeforeStartConfig === 'allEditorsInActiveGroup') {
382
const activeEditor = editorService.activeEditorPane;
383
if (activeEditor && activeEditor.input.resource?.scheme === Schemas.untitled) {
384
// Make sure to save the active editor in case it is in untitled file it wont be saved as part of saveAll #111850
385
await editorService.save({ editor: activeEditor.input, groupId: activeEditor.group.id });
386
}
387
}
388
}
389
await configurationService.reloadConfiguration();
390
}
391
392
export const sourcesEqual = (a: DebugProtocol.Source | undefined, b: DebugProtocol.Source | undefined): boolean =>
393
!a || !b ? a === b : a.name === b.name && a.path === b.path && a.sourceReference === b.sourceReference;
394
395