From 33c359fe6f8f47e6240d85364775f8bf5cd04eed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 22:09:09 +0000 Subject: [PATCH 1/3] Fix onSwitchHeaderSource path strip when workspace realpath is filesystem root The old code stripped the workspace's RootRealPath from a target filename by substring(RootRealPath.length), then prepended RootPath. When RootRealPath is a filesystem root (e.g. "/" or "C:\\"), the trailing separator that is part of that root is part of the prefix, so substring removes it and the resulting path is missing a separator (e.g. RootPath="/symlink", targetFileName="/foo.h" becomes "/symlinkfoo.h"). It also did a naive startsWith descendant check, which would falsely match unrelated siblings such as "/foo" vs "/foobar". Use path.relative + path.join so the descendant check and path reconstruction work correctly for any workspace root, including filesystem roots and on Windows drive roots. This mirrors the spirit of the fix in microsoft/vscode#317637. Co-authored-by: Franek Korta --- Extension/src/LanguageServer/extension.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 043678c63..4ba46726f 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -484,10 +484,17 @@ async function onSwitchHeaderSource(): Promise { // then replace the RootRealPath with the RootPath (the symlink path). let targetFileNameReplaced: boolean = false; clients.forEach(client => { - if (!targetFileNameReplaced && client.RootRealPath && client.RootPath !== client.RootRealPath - && targetFileName.startsWith(client.RootRealPath)) { - targetFileName = client.RootPath + targetFileName.substring(client.RootRealPath.length); - targetFileNameReplaced = true; + if (!targetFileNameReplaced && client.RootRealPath && client.RootPath !== client.RootRealPath) { + // Use path.relative + path.join so we correctly handle the case where + // RootRealPath is a filesystem root (e.g. "/" or "C:\"). A naive + // substring(RootRealPath.length) drops the trailing separator that is + // part of a root path and would produce a malformed path. path.relative + // also avoids false prefix matches such as "/foo" vs "/foobar". + const relativeToReal: string = path.relative(client.RootRealPath, targetFileName); + if (relativeToReal && !relativeToReal.startsWith('..' + path.sep) && relativeToReal !== '..' && !path.isAbsolute(relativeToReal)) { + targetFileName = path.join(client.RootPath, relativeToReal); + targetFileNameReplaced = true; + } } }); const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); From f2b545123c05578c9ae7bf1b5012629c99c5b2bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 22:09:20 +0000 Subject: [PATCH 2/3] Use path.join instead of fsPath + path.sep concatenation Several code paths in CppProperties built paths by manually concatenating rootUri.fsPath with path.sep: rootUri.fsPath + path.sep + something When the workspace folder is a filesystem root such as "/" or "C:\\", rootUri.fsPath already ends in (and is) a separator, so this concatenation produces malformed paths like "//something" or "C:\\\\something". On POSIX this is tolerated as equivalent to "/something", but it surfaces literally in user-visible error messages like "Cannot find: //usr/include/foo". Replace the manual concatenations with path.join, which normalizes correctly for any base directory including filesystem roots. checkPathExistsSync is refactored to also use path.join internally; its parameter that used to require a trailing-separator base directory ("relativePath") is renamed "baseDir" and no longer requires a trailing separator. Callers updated. Co-authored-by: Franek Korta --- .../src/LanguageServer/configurations.ts | 10 +++--- Extension/src/common.ts | 34 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index b4686505c..9af095f49 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -458,7 +458,7 @@ export class CppProperties { Object.assign(result, this.configurationJson.env); } - result["workspaceFolderBasename"] = this.rootUri ? path.basename(this.rootUri.fsPath) : ""; + result["workspaceFolderBasename"] = this.rootUri ? path.basename(this.rootUri.fsPath) || this.rootUri.fsPath : ""; result["execPath"] = process.execPath; result["pathSeparator"] = (os.platform() === 'win32') ? "\\" : "/"; result["/"] = (os.platform() === 'win32') ? "\\" : "/"; @@ -1680,7 +1680,7 @@ export class CppProperties { compilerPathAndArgs.compilerPath = pathLocation; } else if (rootUri) { // Test if it was a relative path. - const absolutePath: string = rootUri.fsPath + path.sep + resolvedCompilerPath; + const absolutePath: string = path.join(rootUri.fsPath, resolvedCompilerPath); if (!fs.existsSync(absolutePath)) { if (existsWithExeAdded(absolutePath)) { resolvedCompilerPath = absolutePath + ".exe"; @@ -1815,7 +1815,7 @@ export class CppProperties { pathExists = false; } else { // Check for relative path if resolved path does not exists - const relativePath: string = this.rootUri.fsPath + path.sep + resolvedPath; + const relativePath: string = path.join(this.rootUri.fsPath, resolvedPath); if (!fs.existsSync(relativePath)) { pathExists = false; } else { @@ -2085,7 +2085,7 @@ export class CppProperties { dotConfigPath = dotConfigPath !== '' ? dotConfigPath : undefined; if (dotConfigPath && this.rootUri) { - const checkPathExists: any = util.checkPathExistsSync(dotConfigPath, this.rootUri.fsPath + path.sep, isWindows, true); + const checkPathExists: any = util.checkPathExistsSync(dotConfigPath, this.rootUri.fsPath, isWindows, true); dotConfigPathExists = checkPathExists.pathExists; dotConfigPath = checkPathExists.path; } @@ -2169,7 +2169,7 @@ export class CppProperties { expandedPaths[index] = this.resolvePath(expandedPath); } - const checkPathExists: any = util.checkPathExistsSync(expandedPaths[index], this.rootUri.fsPath + path.sep, isWindows, false); + const checkPathExists: any = util.checkPathExistsSync(expandedPaths[index], this.rootUri.fsPath, isWindows, false); if (!checkPathExists.pathExists) { // If there are multiple paths, store any non-existing paths to squiggle later on. incorrectExpandedPaths.push(expandedPaths[index]); diff --git a/Extension/src/common.ts b/Extension/src/common.ts index c84f94290..86363fb1e 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -600,30 +600,36 @@ export function checkDirectoryExistsSync(dirPath: string): boolean { } } -/** Test whether a relative path exists */ -export function checkPathExistsSync(path: string, relativePath: string, _isWindows: boolean, isCompilerPath: boolean): { pathExists: boolean; path: string } { +/** Test whether a relative path exists. + * + * `inputPath` is checked first as-is. If it doesn't exist and `baseDir` is + * provided, `inputPath` is also tried joined with `baseDir`. Using path.join + * (rather than naive string concatenation with `path.sep`) ensures correct + * behavior when `baseDir` is a filesystem root such as "/" or "C:\\". + */ +export function checkPathExistsSync(inputPath: string, baseDir: string, _isWindows: boolean, isCompilerPath: boolean): { pathExists: boolean; path: string } { let pathExists: boolean = true; - const existsWithExeAdded: (path: string) => boolean = (path: string) => isCompilerPath && _isWindows && fs.existsSync(path + ".exe"); - if (!fs.existsSync(path)) { - if (existsWithExeAdded(path)) { - path += ".exe"; - } else if (!relativePath) { + const existsWithExeAdded: (p: string) => boolean = (p: string) => isCompilerPath && _isWindows && fs.existsSync(p + ".exe"); + if (!fs.existsSync(inputPath)) { + if (existsWithExeAdded(inputPath)) { + inputPath += ".exe"; + } else if (!baseDir) { pathExists = false; } else { - // Check again for a relative path. - relativePath = relativePath + path; - if (!fs.existsSync(relativePath)) { - if (existsWithExeAdded(path)) { - path += ".exe"; + // Check again as a path relative to baseDir. + const joinedPath: string = path.join(baseDir, inputPath); + if (!fs.existsSync(joinedPath)) { + if (existsWithExeAdded(inputPath)) { + inputPath += ".exe"; } else { pathExists = false; } } else { - path = relativePath; + inputPath = joinedPath; } } } - return { pathExists, path }; + return { pathExists, path: inputPath }; } /** Read the files in a directory */ From 81a4ccd9c0f6d92ba45ee66fc4cd16a23c7c002a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 22:09:35 +0000 Subject: [PATCH 3/3] Handle empty workspaceFolderBasename for root workspaces Three related issues affect ${workspaceFolderBasename} when the workspace folder is a filesystem root (path.basename("/") and path.basename("C:\\") both return ""): 1. expand.ts treated a defined-but-empty variable value as an invalid reference ("if (!repl)") and emitted an "Invalid variable reference" warning while leaving the literal ${...} in the output. Distinguish undefined (not defined) from empty string (defined as "") so empty substitutions are accepted. 2. CppProperties.ExtendedEnvironment computed workspaceFolderBasename as path.basename(rootUri.fsPath), yielding "" for root workspaces. Fall back to the fsPath itself so the value is never empty for a defined workspace folder. 3. DefaultClient.AdditionalEnvironment computed workspaceFolderBasename from this.Name (the WorkspaceFolder display name). For non-root workspaces this typically differs from VS Code's defined semantics for ${workspaceFolderBasename}, and is also "" for chroot-style root workspaces in many cases. Use path.basename(RootPath) with a fallback to the fsPath, and only fall back to this.Name when no RootPath is set, so the value is consistent with VS Code's own ${workspaceFolderBasename} and never empty for a root workspace. DebugConfigurationProvider.expand() applies the same path.basename fallback so debug configs see a non-empty substitution for root workspaces. Co-authored-by: Franek Korta --- Extension/src/Debugger/configurationProvider.ts | 5 ++++- Extension/src/LanguageServer/client.ts | 10 +++++++++- Extension/src/expand.ts | 10 +++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 5bc427759..48284ddef 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -1050,7 +1050,10 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv const folderPath: string | undefined = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0].uri.fsPath; const vars: ExpansionVars = config.variables ? config.variables : {}; vars.workspaceFolder = folderPath || '{workspaceFolder}'; - vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) : '{workspaceFolderBasename}'; + // path.basename returns "" for filesystem roots like "/" or "C:\\", so + // fall back to the folder path itself to keep ${workspaceFolderBasename} + // non-empty for root workspaces. + vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) || folderPath : '{workspaceFolderBasename}'; const expansionOptions: ExpansionOptions = { vars, recursive: true }; return expandAllStrings(config, expansionOptions); } diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index b6c00ab44..6b370d215 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -1007,8 +1007,16 @@ export class DefaultClient implements Client { } public get AdditionalEnvironment(): Record { + // workspaceFolderBasename mirrors VS Code's ${workspaceFolderBasename}, which + // is defined as path.basename(folder.uri.fsPath). Falling back to this.Name + // (the WorkspaceFolder display name, which may be user-customized in + // multi-root) keeps a sensible value when fsPath is unavailable; falling + // back to the fsPath itself ensures we don't return "" for root workspaces + // (path.basename("/") and path.basename("C:\\") return ""). + const rootFsPath: string = this.RootPath; + const baseName: string = rootFsPath ? path.basename(rootFsPath) || rootFsPath : this.Name; return { - workspaceFolderBasename: this.Name, + workspaceFolderBasename: baseName, workspaceStorage: this.workspaceStoragePath, execPath: process.execPath, pathSeparator: (os.platform() === 'win32') ? "\\" : "/", diff --git a/Extension/src/expand.ts b/Extension/src/expand.ts index f3586a54b..d7c6c1b47 100644 --- a/Extension/src/expand.ts +++ b/Extension/src/expand.ts @@ -78,9 +78,13 @@ async function expandStringImpl(input: string, options: ExpansionOptions): Promi const full: string = match[0]; const key: string = match[1]; if (key !== 'dollar') { - // Replace dollar sign at the very end of the expanding process - const repl: string = options.vars[key]; - if (!repl) { + // Replace dollar sign at the very end of the expanding process. + // Distinguish "variable not defined" (undefined) from "variable defined + // as empty string". A defined-but-empty value (e.g. workspaceFolderBasename + // when the workspace root is "/" or "C:\\") should substitute as "" rather + // than be reported as an invalid reference. + const repl: string | undefined = options.vars[key]; + if (repl === undefined) { void getOutputChannelLogger().showWarningMessage(localize('invalid.var.reference', 'Invalid variable reference {0} in string: {1}.', full, input)); } else { subs.set(full, repl);