From a71da4b79942cc251ea345f5ac524fa0a47cf618 Mon Sep 17 00:00:00 2001
From: CoreyShay <11512658+CoreyShay@users.noreply.github.com>
Date: Mon, 24 Aug 2026 18:42:25 -0700
Subject: [PATCH] Don't overwrite debug settings the extension doesn't manage
When "Manage Working Directories" or "Manage Launch Application" is
enabled but a project has no WorkDir/LaunchApp item, the aggregation
returned an empty string instead of null. UpdateProjectConfig only skips
writing on null, so an empty and
were written into the .vcxproj.user.
Those empty elements override the values inherited from the .vcxproj,
so the Command and Working Directory shown in the project property pages
were blanked on every launch and debugging failed until they were
entered again by hand. The same applied to the debug environment.
Distinguish the two cases:
- no item of that type exists => null => setting is not managed,
leave the project alone
- items exist but all unchecked => "" => user cleared it on purpose
This keeps the existing "uncheck to clear" behaviour intact while no
longer touching settings the extension was never asked to manage.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Services/ItemAggregationService.cs | 70 +++++++++++-----
.../Services/ItemAggregationServiceTests.cs | 82 +++++++++++++++++++
2 files changed, 131 insertions(+), 21 deletions(-)
diff --git a/SmartCmdArgs/SmartCmdArgs.Shared/Services/ItemAggregationService.cs b/SmartCmdArgs/SmartCmdArgs.Shared/Services/ItemAggregationService.cs
index 45e435eb..8e67500d 100644
--- a/SmartCmdArgs/SmartCmdArgs.Shared/Services/ItemAggregationService.cs
+++ b/SmartCmdArgs/SmartCmdArgs.Shared/Services/ItemAggregationService.cs
@@ -34,7 +34,7 @@ public ItemAggregationService(
this.treeViewModel = treeViewModel;
}
- private TResult AggregateComamndLineItemsForProject(IVsHierarchyWrapper project, Func, Func, CmdContainer, TResult> joinItems)
+ private TResult AggregateComamndLineItemsForProject(IVsHierarchyWrapper project, Func, Func, CmdContainer, TResult> joinItems, bool includeUnchecked = false)
{
if (project == null)
return default;
@@ -54,8 +54,10 @@ private TResult AggregateComamndLineItemsForProject(IVsHierarchyWrapper
TResult JoinContainer(CmdContainer con)
{
- IEnumerable items = con.Items
- .Where(x => x.IsChecked != false);
+ IEnumerable items = con.Items;
+
+ if (!includeUnchecked)
+ items = items.Where(x => x.IsChecked != false);
if (projConfig != null)
items = items.Where(x => { var conf = x.UsedProjectConfig; return conf == null || conf == projConfig; });
@@ -73,6 +75,9 @@ TResult JoinContainer(CmdContainer con)
}
public IEnumerable GetAllComamndLineParamsForProject(IVsHierarchyWrapper project)
+ => GetAllComamndLineParamsForProject(project, includeUnchecked: false);
+
+ private IEnumerable GetAllComamndLineParamsForProject(IVsHierarchyWrapper project, bool includeUnchecked)
{
IEnumerable joinItems(IEnumerable items, Func> joinContainer, CmdContainer parentContainer)
{
@@ -90,7 +95,8 @@ IEnumerable joinItems(IEnumerable items, Func>(project, joinItems);
+ return AggregateComamndLineItemsForProject>(project, joinItems, includeUnchecked)
+ ?? Enumerable.Empty();
}
public string CreateCommandLineArgsForProject(IVsHierarchyWrapper project)
@@ -113,12 +119,20 @@ public string CreateCommandLineArgsForProject(IVsHierarchyWrapper project)
public IDictionary GetEnvVarsForProject(IVsHierarchyWrapper project)
{
+ // If the project has no environment variable items at all, this extension does not manage the
+ // environment for it. Returning null (instead of an empty dictionary) prevents the caller from
+ // overwriting the environment which is defined by the project itself.
+ var items = GetAllComamndLineParamsForProject(project, includeUnchecked: true)
+ .Where(x => x.ParamType == CmdParamType.EnvVar)
+ .ToList();
+
+ if (items.Count == 0)
+ return null;
+
var result = new Dictionary();
- foreach (var item in GetAllComamndLineParamsForProject(project))
+ foreach (var item in items.Where(x => x.IsChecked != false))
{
- if (item.ParamType != CmdParamType.EnvVar) continue;
-
if (itemEvaluation.TryParseEnvVar(item.Value, out EnvVar envVar))
{
result[envVar.Name] = itemEvaluation.EvaluateMacros(envVar.Value, project);
@@ -128,32 +142,46 @@ public IDictionary GetEnvVarsForProject(IVsHierarchyWrapper proj
return result;
}
- public string GetWorkDirForProject(IVsHierarchyWrapper project)
+ ///
+ /// Returns the value of the last active parameter of the given type or
+ /// null if the project has no parameter of that type at all.
+ ///
+ /// Returning null is important: it tells the caller that this extension does not manage
+ /// this setting for the project, so the project's own value must be left untouched. Returning an
+ /// empty string instead would overwrite (and thereby erase) settings like the debugger command or
+ /// the working directory that are defined by the project itself.
+ ///
+ ///
+ private string GetSingleParamValueForProject(IVsHierarchyWrapper project, CmdParamType paramType)
{
+ // Unchecked items are included here on purpose. If the project has items of this type but all
+ // of them are unchecked, the user explicitly disabled them and we return an empty string to
+ // clear the setting. Only if there is no item of this type at all we return null.
+ var items = GetAllComamndLineParamsForProject(project, includeUnchecked: true)
+ .Where(x => x.ParamType == paramType)
+ .ToList();
+
+ if (items.Count == 0)
+ return null;
+
var result = "";
- foreach (var item in GetAllComamndLineParamsForProject(project))
+ foreach (var item in items.Where(x => x.IsChecked != false))
{
- if (item.ParamType != CmdParamType.WorkDir) continue;
-
result = itemEvaluation.EvaluateMacros(item.Value, project);
}
return result;
}
- public string GetLaunchAppForProject(IVsHierarchyWrapper project)
+ public string GetWorkDirForProject(IVsHierarchyWrapper project)
{
- var result = "";
-
- foreach (var item in GetAllComamndLineParamsForProject(project))
- {
- if (item.ParamType != CmdParamType.LaunchApp) continue;
-
- result = itemEvaluation.EvaluateMacros(item.Value, project);
- }
+ return GetSingleParamValueForProject(project, CmdParamType.WorkDir);
+ }
- return result;
+ public string GetLaunchAppForProject(IVsHierarchyWrapper project)
+ {
+ return GetSingleParamValueForProject(project, CmdParamType.LaunchApp);
}
public string CreateCommandLineArgsForProject(Guid guid)
diff --git a/SmartCmdArgs/Testing/SmartCmdArgs.Tests/Services/ItemAggregationServiceTests.cs b/SmartCmdArgs/Testing/SmartCmdArgs.Tests/Services/ItemAggregationServiceTests.cs
index 99fcbc4a..3233c97a 100644
--- a/SmartCmdArgs/Testing/SmartCmdArgs.Tests/Services/ItemAggregationServiceTests.cs
+++ b/SmartCmdArgs/Testing/SmartCmdArgs.Tests/Services/ItemAggregationServiceTests.cs
@@ -141,6 +141,88 @@ public async Task GetWorkDirForProject_ShouldReturnWorkDir()
Assert.Equal("WorkDir", result);
}
+ [Fact]
+ public async Task GetWorkDirForProject_ShouldReturnNull_WhenProjectHasNoWorkDirItem()
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ // Arrange
+ var projectGuid = Guid.NewGuid();
+ var project = new Mock().Register(vsHelperServiceMock, projectGuid).Object;
+ var cmdArg = new CmdParameter(Guid.NewGuid(), CmdParamType.CmdArg, "arg1", isChecked: true);
+ var cmdProject = new CmdProject(projectGuid, Guid.Empty, "TestProject", new[] { cmdArg }, false, false, " ", "", "");
+
+ treeViewModel.Projects.Add(projectGuid, cmdProject);
+
+ // Act
+ var result = itemAggregationService.GetWorkDirForProject(project);
+
+ // Assert
+ // null means "not managed by this extension" so the project's own working directory is kept
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task GetWorkDirForProject_ShouldReturnEmpty_WhenWorkDirItemIsUnchecked()
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ // Arrange
+ var projectGuid = Guid.NewGuid();
+ var project = new Mock().Register(vsHelperServiceMock, projectGuid).Object;
+ var workDirArg = new CmdParameter(Guid.NewGuid(), CmdParamType.WorkDir, "WorkDir", isChecked: false);
+ var cmdProject = new CmdProject(projectGuid, Guid.Empty, "TestProject", new[] { workDirArg }, false, false, " ", "", "");
+
+ treeViewModel.Projects.Add(projectGuid, cmdProject);
+
+ // Act
+ var result = itemAggregationService.GetWorkDirForProject(project);
+
+ // Assert
+ // the item exists but was disabled by the user, so the setting is cleared
+ Assert.Equal("", result);
+ }
+
+ [Fact]
+ public async Task GetLaunchAppForProject_ShouldReturnNull_WhenProjectHasNoLaunchAppItem()
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ // Arrange
+ var projectGuid = Guid.NewGuid();
+ var project = new Mock().Register(vsHelperServiceMock, projectGuid).Object;
+ var cmdArg = new CmdParameter(Guid.NewGuid(), CmdParamType.CmdArg, "arg1", isChecked: true);
+ var cmdProject = new CmdProject(projectGuid, Guid.Empty, "TestProject", new[] { cmdArg }, false, false, " ", "", "");
+
+ treeViewModel.Projects.Add(projectGuid, cmdProject);
+
+ // Act
+ var result = itemAggregationService.GetLaunchAppForProject(project);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task GetEnvVarsForProject_ShouldReturnNull_WhenProjectHasNoEnvVarItem()
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ // Arrange
+ var projectGuid = Guid.NewGuid();
+ var project = new Mock().Register(vsHelperServiceMock, projectGuid).Object;
+ var cmdArg = new CmdParameter(Guid.NewGuid(), CmdParamType.CmdArg, "arg1", isChecked: true);
+ var cmdProject = new CmdProject(projectGuid, Guid.Empty, "TestProject", new[] { cmdArg }, false, false, " ", "", "");
+
+ treeViewModel.Projects.Add(projectGuid, cmdProject);
+
+ // Act
+ var result = itemAggregationService.GetEnvVarsForProject(project);
+
+ // Assert
+ Assert.Null(result);
+ }
+
[Fact]
public async Task CreateCommandLineArgsForProject_WithGuid_ShouldCreateCommandLineArgs()
{