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/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/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); 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 */ 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);