Path: blob/main/src/vs/workbench/contrib/debug/common/debugUtils.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { equalsIgnoreCase } from '../../../../base/common/strings.js';6import { IDebuggerContribution, IDebugSession, IConfigPresentation } from './debug.js';7import { URI as uri } from '../../../../base/common/uri.js';8import { isAbsolute } from '../../../../base/common/path.js';9import { deepClone } from '../../../../base/common/objects.js';10import { Schemas } from '../../../../base/common/network.js';11import { IEditorService } from '../../../services/editor/common/editorService.js';12import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';13import { ITextModel } from '../../../../editor/common/model.js';14import { Position } from '../../../../editor/common/core/position.js';15import { IRange, Range } from '../../../../editor/common/core/range.js';16import { CancellationToken } from '../../../../base/common/cancellation.js';17import { coalesce } from '../../../../base/common/arrays.js';18import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';1920const _formatPIIRegexp = /{([^}]+)}/g;2122export function formatPII(value: string, excludePII: boolean, args: { [key: string]: string } | undefined): string {23return value.replace(_formatPIIRegexp, function (match, group) {24if (excludePII && group.length > 0 && group[0] !== '_') {25return match;26}2728return args && args.hasOwnProperty(group) ?29args[group] :30match;31});32}3334/**35* Filters exceptions (keys marked with "!") from the given object. Used to36* ensure exception data is not sent on web remotes, see #97628.37*/38export function filterExceptionsFromTelemetry<T extends { [key: string]: unknown }>(data: T): Partial<T> {39const output: Partial<T> = {};40for (const key of Object.keys(data) as (keyof T & string)[]) {41if (!key.startsWith('!')) {42output[key] = data[key];43}44}4546return output;47}484950export function isSessionAttach(session: IDebugSession): boolean {51return session.configuration.request === 'attach' && !getExtensionHostDebugSession(session) && (!session.parentSession || isSessionAttach(session.parentSession));52}5354/**55* Returns the session or any parent which is an extension host debug session.56* Returns undefined if there's none.57*/58export function getExtensionHostDebugSession(session: IDebugSession): IDebugSession | void {59let type = session.configuration.type;60if (!type) {61return;62}6364if (type === 'vslsShare') {65type = (<any>session.configuration).adapterProxy.configuration.type;66}6768if (equalsIgnoreCase(type, 'extensionhost') || equalsIgnoreCase(type, 'pwa-extensionhost')) {69return session;70}7172return session.parentSession ? getExtensionHostDebugSession(session.parentSession) : undefined;73}7475// only a debugger contributions with a label, program, or runtime attribute is considered a "defining" or "main" debugger contribution76export function isDebuggerMainContribution(dbg: IDebuggerContribution) {77return dbg.type && (dbg.label || dbg.program || dbg.runtime);78}7980/**81* Note- uses 1-indexed numbers82*/83export function getExactExpressionStartAndEnd(lineContent: string, looseStart: number, looseEnd: number): { start: number; end: number } {84let matchingExpression: string | undefined = undefined;85let startOffset = 0;8687// Some example supported expressions: myVar.prop, a.b.c.d, myVar?.prop, myVar->prop, MyClass::StaticProp, *myVar, ...foo88// Match any character except a set of characters which often break interesting sub-expressions89const expression: RegExp = /([^()\[\]{}<>\s+\-/%~#^;=|,`!]|\->)+/g;90let result: RegExpExecArray | null = null;9192// First find the full expression under the cursor93while (result = expression.exec(lineContent)) {94const start = result.index + 1;95const end = start + result[0].length;9697if (start <= looseStart && end >= looseEnd) {98matchingExpression = result[0];99startOffset = start;100break;101}102}103104// Handle spread syntax: if the expression starts with '...', extract just the identifier105if (matchingExpression) {106const spreadMatch = matchingExpression.match(/^\.\.\.(.+)/);107if (spreadMatch) {108matchingExpression = spreadMatch[1];109startOffset += 3; // Skip the '...' prefix110}111}112113// If there are non-word characters after the cursor, we want to truncate the expression then.114// For example in expression 'a.b.c.d', if the focus was under 'b', 'a.b' would be evaluated.115if (matchingExpression) {116const subExpression: RegExp = /(\w|\p{L})+/gu;117let subExpressionResult: RegExpExecArray | null = null;118while (subExpressionResult = subExpression.exec(matchingExpression)) {119const subEnd = subExpressionResult.index + 1 + startOffset + subExpressionResult[0].length;120if (subEnd >= looseEnd) {121break;122}123}124125if (subExpressionResult) {126matchingExpression = matchingExpression.substring(0, subExpression.lastIndex);127}128}129130return matchingExpression ?131{ start: startOffset, end: startOffset + matchingExpression.length - 1 } :132{ start: 0, end: 0 };133}134135export async function getEvaluatableExpressionAtPosition(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: Position, token?: CancellationToken): Promise<{ range: IRange; matchingExpression: string } | null> {136if (languageFeaturesService.evaluatableExpressionProvider.has(model)) {137const supports = languageFeaturesService.evaluatableExpressionProvider.ordered(model);138139const results = coalesce(await Promise.all(supports.map(async support => {140try {141return await support.provideEvaluatableExpression(model, position, token ?? CancellationToken.None);142} catch (err) {143return undefined;144}145})));146147if (results.length > 0) {148let matchingExpression = results[0].expression;149const range = results[0].range;150151if (!matchingExpression) {152const lineContent = model.getLineContent(position.lineNumber);153matchingExpression = lineContent.substring(range.startColumn - 1, range.endColumn - 1);154}155156return { range, matchingExpression };157}158} else { // old one-size-fits-all strategy159const lineContent = model.getLineContent(position.lineNumber);160const { start, end } = getExactExpressionStartAndEnd(lineContent, position.column, position.column);161162// use regex to extract the sub-expression #9821163const matchingExpression = lineContent.substring(start - 1, end);164return {165matchingExpression,166range: new Range(position.lineNumber, start, position.lineNumber, start + matchingExpression.length)167};168}169170return null;171}172173// RFC 2396, Appendix A: https://www.ietf.org/rfc/rfc2396.txt174const _schemePattern = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/;175176export function isUri(s: string | undefined): boolean {177// heuristics: a valid uri starts with a scheme and178// the scheme has at least 2 characters so that it doesn't look like a drive letter.179return !!(s && s.match(_schemePattern));180}181182function stringToUri(source: PathContainer): string | undefined {183if (typeof source.path === 'string') {184if (typeof source.sourceReference === 'number' && source.sourceReference > 0) {185// if there is a source reference, don't touch path186} else {187if (isUri(source.path)) {188return <string><unknown>uri.parse(source.path);189} else {190// assume path191if (isAbsolute(source.path)) {192return <string><unknown>uri.file(source.path);193} else {194// leave relative path as is195}196}197}198}199return source.path;200}201202function uriToString(source: PathContainer): string | undefined {203if (typeof source.path === 'object') {204const u = uri.revive(source.path);205if (u) {206if (u.scheme === Schemas.file) {207return u.fsPath;208} else {209return u.toString();210}211}212}213return source.path;214}215216// path hooks helpers217218interface PathContainer {219path?: string;220sourceReference?: number;221}222223export function convertToDAPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage {224225const fixPath = toUri ? stringToUri : uriToString;226227// since we modify Source.paths in the message in place, we need to make a copy of it (see #61129)228const msg = deepClone(message);229230convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => {231if (toDA && source) {232source.path = fixPath(source);233}234});235return msg;236}237238export function convertToVSCPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage {239240const fixPath = toUri ? stringToUri : uriToString;241242// since we modify Source.paths in the message in place, we need to make a copy of it (see #61129)243const msg = deepClone(message);244245convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => {246if (!toDA && source) {247source.path = fixPath(source);248}249});250return msg;251}252253function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePath: (toDA: boolean, source: PathContainer | undefined) => void): void {254255switch (msg.type) {256case 'event': {257const event = <DebugProtocol.Event>msg;258switch (event.event) {259case 'output':260fixSourcePath(false, (<DebugProtocol.OutputEvent>event).body.source);261break;262case 'loadedSource':263fixSourcePath(false, (<DebugProtocol.LoadedSourceEvent>event).body.source);264break;265case 'breakpoint':266fixSourcePath(false, (<DebugProtocol.BreakpointEvent>event).body.breakpoint.source);267break;268default:269break;270}271break;272}273case 'request': {274const request = <DebugProtocol.Request>msg;275switch (request.command) {276case 'setBreakpoints':277fixSourcePath(true, (<DebugProtocol.SetBreakpointsArguments>request.arguments).source);278break;279case 'breakpointLocations':280fixSourcePath(true, (<DebugProtocol.BreakpointLocationsArguments>request.arguments).source);281break;282case 'source':283fixSourcePath(true, (<DebugProtocol.SourceArguments>request.arguments).source);284break;285case 'gotoTargets':286fixSourcePath(true, (<DebugProtocol.GotoTargetsArguments>request.arguments).source);287break;288case 'launchVSCode':289request.arguments.args.forEach((arg: PathContainer | undefined) => fixSourcePath(false, arg));290break;291default:292break;293}294break;295}296case 'response': {297const response = <DebugProtocol.Response>msg;298if (response.success && response.body) {299switch (response.command) {300case 'stackTrace':301(<DebugProtocol.StackTraceResponse>response).body.stackFrames.forEach(frame => fixSourcePath(false, frame.source));302break;303case 'loadedSources':304(<DebugProtocol.LoadedSourcesResponse>response).body.sources.forEach(source => fixSourcePath(false, source));305break;306case 'scopes':307(<DebugProtocol.ScopesResponse>response).body.scopes.forEach(scope => fixSourcePath(false, scope.source));308break;309case 'setFunctionBreakpoints':310(<DebugProtocol.SetFunctionBreakpointsResponse>response).body.breakpoints.forEach(bp => fixSourcePath(false, bp.source));311break;312case 'setBreakpoints':313(<DebugProtocol.SetBreakpointsResponse>response).body.breakpoints.forEach(bp => fixSourcePath(false, bp.source));314break;315case 'disassemble':316{317const di = <DebugProtocol.DisassembleResponse>response;318di.body?.instructions.forEach(di => fixSourcePath(false, di.location));319}320break;321case 'locations':322fixSourcePath(false, (<DebugProtocol.LocationsResponse>response).body?.source);323break;324default:325break;326}327}328break;329}330}331}332333export function getVisibleAndSorted<T extends { presentation?: IConfigPresentation }>(array: T[]): T[] {334return array.filter(config => !config.presentation?.hidden).sort((first, second) => {335if (!first.presentation) {336if (!second.presentation) {337return 0;338}339return 1;340}341if (!second.presentation) {342return -1;343}344if (!first.presentation.group) {345if (!second.presentation.group) {346return compareOrders(first.presentation.order, second.presentation.order);347}348return 1;349}350if (!second.presentation.group) {351return -1;352}353if (first.presentation.group !== second.presentation.group) {354return first.presentation.group.localeCompare(second.presentation.group);355}356357return compareOrders(first.presentation.order, second.presentation.order);358});359}360361function compareOrders(first: number | undefined, second: number | undefined): number {362if (typeof first !== 'number') {363if (typeof second !== 'number') {364return 0;365}366367return 1;368}369if (typeof second !== 'number') {370return -1;371}372373return first - second;374}375376export async function saveAllBeforeDebugStart(configurationService: IConfigurationService, editorService: IEditorService): Promise<void> {377const saveBeforeStartConfig: string = configurationService.getValue('debug.saveBeforeStart', { overrideIdentifier: editorService.activeTextEditorLanguageId });378if (saveBeforeStartConfig !== 'none') {379await editorService.saveAll();380if (saveBeforeStartConfig === 'allEditorsInActiveGroup') {381const activeEditor = editorService.activeEditorPane;382if (activeEditor && activeEditor.input.resource?.scheme === Schemas.untitled) {383// Make sure to save the active editor in case it is in untitled file it wont be saved as part of saveAll #111850384await editorService.save({ editor: activeEditor.input, groupId: activeEditor.group.id });385}386}387}388await configurationService.reloadConfiguration();389}390391export const sourcesEqual = (a: DebugProtocol.Source | undefined, b: DebugProtocol.Source | undefined): boolean =>392!a || !b ? a === b : a.name === b.name && a.path === b.path && a.sourceReference === b.sourceReference;393394395