diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f03d68..118962e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Bug Fixes: - Fix "Makefile Tools" being translated in localized languages when it should remain as a proper noun. [#731](https://github.com/microsoft/vscode-makefile-tools/issues/731) - Fix binary path links in output log not being clickable on Windows when the path has no .exe extension. [#698](https://github.com/microsoft/vscode-makefile-tools/issues/698) - Fix `parseCompilerArgs.bat` failing when macro definitions contain numeric expressions like `-1` inside quoted values. [#755](https://github.com/microsoft/vscode-makefile-tools/issues/755) +- Fix launch target resetting to "Unset" when `binaryArgs` is added to `makefile.launchConfigurations` on Windows. [#737](https://github.com/microsoft/vscode-makefile-tools/issues/737) - Fix Project Outline showing "Unset" for launch configurations that have a `name` field. [#808](https://github.com/microsoft/vscode-makefile-tools/issues/808) ## 0.11 diff --git a/src/configuration.ts b/src/configuration.ts index 4028cf7a..6d8c7419 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -912,7 +912,12 @@ export async function setCurrentLaunchConfiguration( function getLaunchConfiguration(name: string): LaunchConfiguration | undefined { return launchConfigurations.find((k) => { - if (launchConfigurationToString(k) === name) { + if ( + util.areLaunchConfigurationStringsEqual( + launchConfigurationToString(k), + name + ) + ) { return { ...k, keep: true }; } }); @@ -2838,7 +2843,10 @@ export async function selectLaunchConfiguration(): Promise { getCurrentLaunchConfiguration(); if ( !currentLaunchConfiguration || - chosenId !== launchConfigurationToString(currentLaunchConfiguration) + !util.areLaunchConfigurationStringsEqual( + chosenId, + launchConfigurationToString(currentLaunchConfiguration) + ) ) { let telemetryProperties: telemetry.Properties | null = { state: "launchConfiguration", diff --git a/src/test/fakeSuite/extension.test.ts b/src/test/fakeSuite/extension.test.ts index bd8ee0ac..2589c883 100644 --- a/src/test/fakeSuite/extension.test.ts +++ b/src/test/fakeSuite/extension.test.ts @@ -255,6 +255,85 @@ suite("Configuration settings", () => { }); }); +suite("Launch configuration string comparison", () => { + test("areLaunchConfigurationStringsEqual - same strings", () => { + const str1 = "c:\\Users\\test\\project>out()"; + const str2 = "c:\\Users\\test\\project>out()"; + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.true; + }); + + test("areLaunchConfigurationStringsEqual - different strings", () => { + const str1 = "c:\\Users\\test\\project>out()"; + const str2 = "c:\\Users\\test\\project>out(arg1)"; + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.false; + }); + + test("areLaunchConfigurationStringsEqual - same strings with args", () => { + const str1 = "c:\\Users\\test\\project>out(arg1,arg2)"; + const str2 = "c:\\Users\\test\\project>out(arg1,arg2)"; + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.true; + }); + + test("areLaunchConfigurationStringsEqual - handles null/undefined", () => { + expect(util.areLaunchConfigurationStringsEqual(null as any, null as any)).to + .be.true; + expect( + util.areLaunchConfigurationStringsEqual(undefined as any, undefined as any) + ).to.be.true; + expect( + util.areLaunchConfigurationStringsEqual( + "c:\\Users\\test\\project>out()", + null as any + ) + ).to.be.false; + expect( + util.areLaunchConfigurationStringsEqual( + null as any, + "c:\\Users\\test\\project>out()" + ) + ).to.be.false; + }); + + // When the string doesn't match the expected cwd>binary(args) format, + // the function falls back to case-insensitive comparison on Windows. + if (process.platform === "win32") { + test("areLaunchConfigurationStringsEqual - fallback for non-standard format on Windows", () => { + const str1 = "some-random-string"; + const str2 = "Some-Random-String"; + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.true; + }); + + test("areLaunchConfigurationStringsEqual - fallback mismatch for non-standard format on Windows", () => { + const str1 = "some-random-string"; + const str2 = "different-string"; + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.false; + }); + } + + // On Windows, paths are case-insensitive but arguments are case-sensitive + if (process.platform === "win32") { + test("areLaunchConfigurationStringsEqual - different path case on Windows", () => { + const str1 = "c:\\Users\\test\\project>out()"; + const str2 = "C:\\Users\\Test\\Project>OUT()"; + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.true; + }); + + test("areLaunchConfigurationStringsEqual - different path case same args on Windows", () => { + const str1 = "c:\\users\\test\\project>out(arg1,arg2)"; + const str2 = "C:\\Users\\TEST\\Project>Out(arg1,arg2)"; + // Paths are compared case-insensitively, args are case-sensitive + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.true; + }); + + test("areLaunchConfigurationStringsEqual - different args case on Windows", () => { + const str1 = "c:\\users\\test\\project>out(arg1,arg2)"; + const str2 = "C:\\Users\\TEST\\Project>Out(Arg1,Arg2)"; + // Arguments are case-sensitive even on Windows + expect(util.areLaunchConfigurationStringsEqual(str1, str2)).to.be.false; + }); + } +}); + // TODO: refactor initialization and cleanup of each test suite("Fake dryrun parsing", () => { suiteSetup(async function (this: Mocha.Context) { diff --git a/src/util.ts b/src/util.ts index 41c6c953..b0b709d3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -864,6 +864,49 @@ export function hasProperties(obj: any): boolean { return props && props.length > 0; } +// Helper for comparing launch configuration strings. +// On Windows, paths are case-insensitive, so we need to do a case-insensitive comparison +// for the path portions (cwd and binaryPath), while keeping the arguments case-sensitive. +// Launch configuration string format: [CWD path]>[binaryPath]([binaryArg1,binaryArg2,...]) +export function areLaunchConfigurationStringsEqual( + str1: string, + str2: string +): boolean { + // Handle null/undefined inputs + if (str1 === null || str1 === undefined) { + return str2 === null || str2 === undefined; + } + if (str2 === null || str2 === undefined) { + return false; + } + + if (process.platform !== "win32") { + return str1 === str2; + } + + // On Windows, parse the launch configuration strings to compare + // paths case-insensitively while keeping arguments case-sensitive. + // Format: [CWD path]>[binaryPath]([binaryArg1,binaryArg2,...]) + const regexp: RegExp = /^(.*)\>(.*)\((.*)\)$/; + const match1: RegExpExecArray | null = regexp.exec(str1); + const match2: RegExpExecArray | null = regexp.exec(str2); + + // If either string doesn't match the expected format, fall back to case-insensitive comparison + if (!match1 || !match2) { + return str1.toLowerCase() === str2.toLowerCase(); + } + + // Compare cwd and binaryPath case-insensitively, and args case-sensitively + const cwd1: string = match1[1].toLowerCase(); + const cwd2: string = match2[1].toLowerCase(); + const binPath1: string = match1[2].toLowerCase(); + const binPath2: string = match2[2].toLowerCase(); + const args1: string = match1[3]; + const args2: string = match2[3]; + + return cwd1 === cwd2 && binPath1 === binPath2 && args1 === args2; +} + // Apply any properties from source to destination, logging for overwrite. // To make things simpler for the caller, create a valid dst if given null or undefined. export function mergeProperties(dst: any, src: any): any {