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() {