From 996b3cd6a8f747692581057e558f8cd9e5f066ee Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 13 Aug 2026 19:27:29 +0000 Subject: [PATCH] Fix configuration path diagnostic ranges --- .../src/LanguageServer/configurations.ts | 8 ++--- Extension/src/Utility/Text/escape.ts | 5 ++++ Extension/test/unit/escape.test.ts | 30 +++++++++++++++++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index dc52a6cdd..f2d1ec4f4 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -14,7 +14,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as which from 'which'; import { logAndReturn, returns } from '../Utility/Async/returns'; -import { escapePathForSquiggles } from '../Utility/Text/escape'; +import { escapePathForSquiggles, getTextMatchOffsets } from '../Utility/Text/escape'; import * as util from '../common'; import { isWindows } from '../constants'; import { getOutputChannelLogger } from '../logger'; @@ -2134,8 +2134,7 @@ export class CppProperties { expandedPaths = result ?? []; if (duration > 10 && configMatches) { newSquiggleMetrics.SlowPathResolution++; - const curOffset = curText.indexOf(configMatches[0]); - const endOffset = curOffset + curPath.length; + const [curOffset, endOffset] = getTextMatchOffsets(curText, configMatches[0]); const diagnostic: vscode.Diagnostic = new vscode.Diagnostic( new vscode.Range(document.positionAt(curTextStartOffset + curOffset), document.positionAt(curTextStartOffset + endOffset)), localize('resolve.path.took.too.long', "Path took {0}s to evaluate", duration), @@ -2145,8 +2144,7 @@ export class CppProperties { } catch (e) { expandedPaths = []; if (configMatches) { - const curOffset = curText.indexOf(configMatches[0]); - const endOffset = curOffset + curPath.length; + const [curOffset, endOffset] = getTextMatchOffsets(curText, configMatches[0]); const diagnostic: vscode.Diagnostic = new vscode.Diagnostic( new vscode.Range(document.positionAt(curTextStartOffset + curOffset), document.positionAt(curTextStartOffset + endOffset)), localize('resolve.path.failed', "Failed to resolve path {0}. Error: {1}", curPath, (e as Error).message), diff --git a/Extension/src/Utility/Text/escape.ts b/Extension/src/Utility/Text/escape.ts index d4e815ec8..243ac362b 100644 --- a/Extension/src/Utility/Text/escape.ts +++ b/Extension/src/Utility/Text/escape.ts @@ -7,3 +7,8 @@ export function escapePathForSquiggles(s: string): string { return s.replace(/[-"\/\\^$*+?.()|[\]{}]/g, (character: string): string => character === '"' ? '\\\\"' : `\\${character}`); } + +export function getTextMatchOffsets(text: string, match: string): [number, number] { + const startOffset: number = text.indexOf(match); + return [startOffset, startOffset + match.length]; +} diff --git a/Extension/test/unit/escape.test.ts b/Extension/test/unit/escape.test.ts index 716eab97a..2e3982e23 100644 --- a/Extension/test/unit/escape.test.ts +++ b/Extension/test/unit/escape.test.ts @@ -4,8 +4,8 @@ * ------------------------------------------------------------------------------------------ */ import { describe, it } from 'mocha'; -import { doesNotMatch, match, strictEqual } from 'node:assert'; -import { escapePathForSquiggles } from '../../src/Utility/Text/escape'; +import { deepStrictEqual, doesNotMatch, match, ok, strictEqual } from 'node:assert'; +import { escapePathForSquiggles, getTextMatchOffsets } from '../../src/Utility/Text/escape'; describe('Text escaping', () => { it('escapes paths for matching their JSON spelling', () => { @@ -29,4 +29,30 @@ describe('Text escaping', () => { doesNotMatch(String.raw`C:\sdk\[headers]+(x)?.h\say\"hello\"and\"goodbye`, pattern); doesNotMatch(String.raw`C:\\sdk\\headers+(x)?.h\\say\"hello\"and\"goodbye`, pattern); }); + + it('uses the full source match for a non-first semicolon-delimited path', () => { + const parsedPath: string = 'second'; + const sourceMatch: string = '"first;second;third"'; + const text: string = `"includePath": [${sourceMatch}]`; + const pattern: RegExp = new RegExp(`"[^"]*?(?<="|;)${escapePathForSquiggles(parsedPath)}(?="|;).*?"`, 'g'); + const matches: string[] | null = text.match(pattern); + + ok(matches); + strictEqual(matches?.[0], sourceMatch); + const startOffset: number = text.indexOf(sourceMatch); + deepStrictEqual(getTextMatchOffsets(text, matches[0]), [startOffset, startOffset + sourceMatch.length]); + }); + + it('uses the JSON source length when it differs from the parsed path', () => { + const parsedPath: string = 'folder"quoted"'; + const sourceMatch: string = String.raw`"folder\"quoted\""`; + const text: string = `"includePath": [${sourceMatch}]`; + const pattern: RegExp = new RegExp(`"[^"]*?(?<="|;)${escapePathForSquiggles(parsedPath)}(?="|;).*?"`, 'g'); + const matches: string[] | null = text.match(pattern); + + ok(matches); + strictEqual(matches?.[0], sourceMatch); + const startOffset: number = text.indexOf(sourceMatch); + deepStrictEqual(getTextMatchOffsets(text, matches[0]), [startOffset, startOffset + sourceMatch.length]); + }); });