diff --git a/src/ConnectedMode.UnitTests/UI/ConnectedModeUIManagerTests.cs b/src/ConnectedMode.UnitTests/UI/ConnectedModeUIManagerTests.cs new file mode 100644 index 0000000000..a01030637b --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ConnectedModeUIManagerTests.cs @@ -0,0 +1,41 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI +{ + [TestClass] + public class ConnectedModeUIManagerTests + { + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported<ConnectedModeUIManager, IConnectedModeUIManager>( + MefTestHelpers.CreateExport<IConnectedModeServices>(), + MefTestHelpers.CreateExport<IConnectedModeBindingServices>()); + } + + [TestMethod] + public void MefCtor_CheckIsNonShared() + => MefTestHelpers.CheckIsNonSharedMefComponent<ConnectedModeUIManager>(); + } +} diff --git a/src/ConnectedMode/UI/ConnectedModeUIManager.cs b/src/ConnectedMode/UI/ConnectedModeUIManager.cs new file mode 100644 index 0000000000..ffbac0d468 --- /dev/null +++ b/src/ConnectedMode/UI/ConnectedModeUIManager.cs @@ -0,0 +1,53 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Windows; +using SonarLint.VisualStudio.ConnectedMode.UI.ManageBinding; + +namespace SonarLint.VisualStudio.ConnectedMode.UI; + +public interface IConnectedModeUIManager +{ + void ShowManageBindingDialog(bool useSharedBindingOnInitialization = false); +} + +[Export(typeof(IConnectedModeUIManager))] +[PartCreationPolicy(CreationPolicy.NonShared)] +internal sealed class ConnectedModeUIManager : IConnectedModeUIManager +{ + private readonly IConnectedModeServices connectedModeServices; + private readonly IConnectedModeBindingServices connectedModeBindingServices; + + [ImportingConstructor] + public ConnectedModeUIManager(IConnectedModeServices connectedModeServices, IConnectedModeBindingServices connectedModeBindingServices) + { + this.connectedModeServices = connectedModeServices; + this.connectedModeBindingServices = connectedModeBindingServices; + } + + [ExcludeFromCodeCoverage] // UI, not really unit-testable + public void ShowManageBindingDialog(bool useSharedBindingOnInitialization = false) + { + var manageBindingDialog = new ManageBindingDialog(connectedModeServices, connectedModeBindingServices, useSharedBindingOnInitialization); + manageBindingDialog.ShowDialog(Application.Current.MainWindow); + } +} diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index ac945cbb14..440356b8c3 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,6 +9,6 @@ <EmbeddedSonarJSAnalyzerVersion>10.14.0.26080</EmbeddedSonarJSAnalyzerVersion> <EmbeddedSonarSecretsJarVersion>2.15.0.3845</EmbeddedSonarSecretsJarVersion> <!-- SLOOP: Binaries for SonarLint Out Of Process --> - <EmbeddedSloopVersion>10.6.0.79033</EmbeddedSloopVersion> + <EmbeddedSloopVersion>10.7.1.79146</EmbeddedSloopVersion> </PropertyGroup> </Project> \ No newline at end of file diff --git a/src/Integration.UnitTests/Binding/BindingSuggestionHandlerTests.cs b/src/Integration.UnitTests/Binding/BindingSuggestionHandlerTests.cs index ad5dca437a..0fe1167a30 100644 --- a/src/Integration.UnitTests/Binding/BindingSuggestionHandlerTests.cs +++ b/src/Integration.UnitTests/Binding/BindingSuggestionHandlerTests.cs @@ -19,11 +19,11 @@ */ using SonarLint.VisualStudio.ConnectedMode.Binding.Suggestion; +using SonarLint.VisualStudio.ConnectedMode.UI; using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.TestInfrastructure; using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Integration.TeamExplorer; using SonarLint.VisualStudio.Integration.Binding; namespace SonarLint.VisualStudio.Integration.UnitTests.Binding; @@ -31,6 +31,24 @@ namespace SonarLint.VisualStudio.Integration.UnitTests.Binding; [TestClass] public class BindingSuggestionHandlerTests { + private BindingSuggestionHandler testSubject; + private INotificationService notificationService; + private IActiveSolutionBoundTracker activeSolutionBoundTracker; + private IIDEWindowService ideWindowService; + private IConnectedModeUIManager connectedModeManager; + private IBrowserService browserService; + + [TestInitialize] + public void TestInitialize() + { + notificationService = Substitute.For<INotificationService>(); + activeSolutionBoundTracker = Substitute.For<IActiveSolutionBoundTracker>(); + ideWindowService = Substitute.For<IIDEWindowService>(); + connectedModeManager = Substitute.For<IConnectedModeUIManager>(); + browserService = Substitute.For<IBrowserService>(); + testSubject = new BindingSuggestionHandler(notificationService, activeSolutionBoundTracker, ideWindowService, connectedModeManager, browserService); + } + [TestMethod] public void MefCtor_CheckExports() { @@ -38,7 +56,7 @@ public void MefCtor_CheckExports() MefTestHelpers.CreateExport<INotificationService>(), MefTestHelpers.CreateExport<IActiveSolutionBoundTracker>(), MefTestHelpers.CreateExport<IIDEWindowService>(), - MefTestHelpers.CreateExport<ITeamExplorerController>(), + MefTestHelpers.CreateExport<IConnectedModeUIManager>(), MefTestHelpers.CreateExport<IBrowserService>()); } @@ -47,9 +65,8 @@ public void MefCtor_CheckExports() [DataRow(SonarLintMode.Connected)] public void Notify_BringsWindowToFront(SonarLintMode sonarLintMode) { - var ideWindowService = Substitute.For<IIDEWindowService>(); + MockCurrentConfiguration(sonarLintMode); - var testSubject = CreateTestSubject(sonarLintMode: sonarLintMode, ideWindowService: ideWindowService); testSubject.Notify(); ideWindowService.Received().BringToFront(); @@ -58,9 +75,8 @@ public void Notify_BringsWindowToFront(SonarLintMode sonarLintMode) [TestMethod] public void Notify_WithStandaloneProject_PromptsToConnect() { - var notificationService = Substitute.For<INotificationService>(); + MockCurrentConfiguration(SonarLintMode.Standalone); - var testSubject = CreateTestSubject(sonarLintMode: SonarLintMode.Standalone, notificationService: notificationService); testSubject.Notify(); notificationService.Received().ShowNotification(Arg.Is<INotification>( @@ -73,9 +89,8 @@ public void Notify_WithStandaloneProject_PromptsToConnect() [TestMethod] public void Notify_WithBoundProject_ShowsConflictMessage() { - var notificationService = Substitute.For<INotificationService>(); + MockCurrentConfiguration(SonarLintMode.Connected); - var testSubject = CreateTestSubject(sonarLintMode: SonarLintMode.Connected, notificationService: notificationService); testSubject.Notify(); notificationService.Received().ShowNotification(Arg.Is<INotification>( @@ -86,30 +101,28 @@ public void Notify_WithBoundProject_ShowsConflictMessage() } [TestMethod] - public void Notify_ConnectAction_OpensSonarQubePage() + public void Notify_ConnectAction_ShowsManageBindingDialog() { - var notificationService = Substitute.For<INotificationService>(); - var teamExplorerController = Substitute.For<ITeamExplorerController>(); + MockCurrentConfiguration(SonarLintMode.Standalone); - var testSubject = CreateTestSubject(sonarLintMode: SonarLintMode.Standalone, notificationService: notificationService, teamExplorerController: teamExplorerController); testSubject.Notify(); + var notification = (Notification)notificationService.ReceivedCalls().Single().GetArguments().Single(); var connectAction = notification.Actions.First(x => x.CommandText.Equals(BindingStrings.BindingSuggestionConnect)); - teamExplorerController.DidNotReceive().ShowSonarQubePage(); + connectedModeManager.DidNotReceive().ShowManageBindingDialog(); connectAction.Action(notification); - teamExplorerController.Received().ShowSonarQubePage(); + connectedModeManager.Received().ShowManageBindingDialog(); } [TestMethod] public void Notify_LearnMoreAction_OpensDocumentationInBrowser() { - var notificationService = Substitute.For<INotificationService>(); - var browserService = Substitute.For<IBrowserService>(); + MockCurrentConfiguration(SonarLintMode.Standalone); - var testSubject = CreateTestSubject(sonarLintMode: SonarLintMode.Standalone, notificationService: notificationService, browserService: browserService); testSubject.Notify(); + var notification = (Notification)notificationService.ReceivedCalls().Single().GetArguments().Single(); var connectAction = notification.Actions.First(x => x.CommandText.Equals(BindingStrings.BindingSuggestionLearnMore)); @@ -119,20 +132,10 @@ public void Notify_LearnMoreAction_OpensDocumentationInBrowser() browserService.Received().Navigate(DocumentationLinks.OpenInIdeBindingSetup); } - private BindingSuggestionHandler CreateTestSubject(SonarLintMode sonarLintMode, - INotificationService notificationService = null, - IIDEWindowService ideWindowService = null, - ITeamExplorerController teamExplorerController = null, - IBrowserService browserService = null) + private void MockCurrentConfiguration(SonarLintMode sonarLintMode) { - notificationService ??= Substitute.For<INotificationService>(); - var activeSolutionBoundTracker = Substitute.For<IActiveSolutionBoundTracker>(); - ideWindowService ??= Substitute.For<IIDEWindowService>(); - teamExplorerController ??= Substitute.For<ITeamExplorerController>(); - browserService ??= Substitute.For<IBrowserService>(); - - activeSolutionBoundTracker.CurrentConfiguration.Returns(new BindingConfiguration(new BoundServerProject("solution", "server project", new ServerConnection.SonarCloud("org")), sonarLintMode, "a-directory")); - - return new BindingSuggestionHandler(notificationService, activeSolutionBoundTracker, ideWindowService, teamExplorerController, browserService); + activeSolutionBoundTracker.CurrentConfiguration.Returns(new BindingConfiguration( + new BoundServerProject("solution", "server project", new ServerConnection.SonarCloud("org")), sonarLintMode, + "a-directory")); } } diff --git a/src/Integration.UnitTests/MefServices/SharedBindingSuggestionServiceTests.cs b/src/Integration.UnitTests/MefServices/SharedBindingSuggestionServiceTests.cs index e418188a31..b0500738ad 100644 --- a/src/Integration.UnitTests/MefServices/SharedBindingSuggestionServiceTests.cs +++ b/src/Integration.UnitTests/MefServices/SharedBindingSuggestionServiceTests.cs @@ -37,6 +37,7 @@ public class SharedBindingSuggestionServiceTests private IConnectedModeServices connectedModeServices; private IConnectedModeBindingServices connectedModeBindingServices; private IActiveSolutionTracker activeSolutionTracker; + private IConnectedModeUIManager connectedModeManager; [TestInitialize] public void TestInitialize() @@ -45,8 +46,9 @@ public void TestInitialize() connectedModeServices = Substitute.For<IConnectedModeServices>(); connectedModeBindingServices = Substitute.For<IConnectedModeBindingServices>(); activeSolutionTracker = Substitute.For<IActiveSolutionTracker>(); + connectedModeManager = Substitute.For<IConnectedModeUIManager>(); - testSubject = new SharedBindingSuggestionService(suggestSharedBindingGoldBar, connectedModeServices, connectedModeBindingServices, activeSolutionTracker); + testSubject = new SharedBindingSuggestionService(suggestSharedBindingGoldBar, connectedModeServices, connectedModeBindingServices, connectedModeManager, activeSolutionTracker); } [TestMethod] @@ -56,6 +58,7 @@ public void MefCtor_CheckExports() MefTestHelpers.CreateExport<ISuggestSharedBindingGoldBar>(), MefTestHelpers.CreateExport<IConnectedModeServices>(), MefTestHelpers.CreateExport<IConnectedModeBindingServices>(), + MefTestHelpers.CreateExport<IConnectedModeUIManager>(), MefTestHelpers.CreateExport<IActiveSolutionTracker>()); } @@ -127,6 +130,24 @@ public void Dispose_UnsubscribesFromActiveSolutionChanged() activeSolutionTracker.Received(1).ActiveSolutionChanged -= Arg.Any<EventHandler<ActiveSolutionChangedEventArgs>>(); } + [TestMethod] + public void ActiveSolutionChanged_SolutionIsOpened_ShowsGoldBarAndShowManageBindingDialog() + { + MockSharedBindingConfigExists(); + MockSolutionMode(SonarLintMode.Standalone); + Action showAction = null; + suggestSharedBindingGoldBar.When(x => x.Show(ServerType.SonarQube, Arg.Any<Action>())).Do(callInfo => + { + showAction = callInfo.Arg<Action>(); + }); + + RaiseActiveSolutionChanged(true); + showAction(); + + showAction.Should().NotBeNull(); + connectedModeManager.Received(1).ShowManageBindingDialog(true); + } + private void RaiseActiveSolutionChanged(bool isSolutionOpened) { activeSolutionTracker.ActiveSolutionChanged += Raise.EventWith(new ActiveSolutionChangedEventArgs(isSolutionOpened)); diff --git a/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs b/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs index b830489f3f..75150fc50d 100644 --- a/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs +++ b/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs @@ -66,7 +66,7 @@ public void ClientConstants_ShouldBeExpected() public void FeatureFlags_ShouldBeExpected() { var testSubject = CreateTestSubject(); - var expectedFeatureFlags = new FeatureFlagsDto(true, true, true, true, false, false, true, true); + var expectedFeatureFlags = new FeatureFlagsDto(true, true, true, true, false, false, true, true, true); var actual = testSubject.FeatureFlags; actual.Should().BeEquivalentTo(expectedFeatureFlags); diff --git a/src/Integration.Vsix.UnitTests/Commands/PackageCommandManagerTests.cs b/src/Integration.Vsix.UnitTests/Commands/PackageCommandManagerTests.cs index 2d760d38e7..f9b7a00f49 100644 --- a/src/Integration.Vsix.UnitTests/Commands/PackageCommandManagerTests.cs +++ b/src/Integration.Vsix.UnitTests/Commands/PackageCommandManagerTests.cs @@ -56,10 +56,10 @@ public void PackageCommandManager_Initialize() Mock.Of<IProjectPropertyManager>(), Mock.Of<IOutputWindowService>(), Mock.Of<IShowInBrowserService>(), - Mock.Of<IBrowserService>(), Mock.Of<PackageCommandManager.ShowOptionsPage>(), Mock.Of<IConnectedModeServices>(), -Mock.Of<IConnectedModeBindingServices>()); +Mock.Of<IConnectedModeBindingServices>(), + Mock.Of<IConnectedModeUIManager>()); // Assert menuService.Commands.Should().HaveCountGreaterOrEqualTo(allCommands.Count, "Unexpected number of commands"); diff --git a/src/Integration.Vsix/Commands/ConnectedModeMenu/ManageConnectionsCommand.cs b/src/Integration.Vsix/Commands/ConnectedModeMenu/ManageConnectionsCommand.cs index 00e50d7136..fa6210d877 100644 --- a/src/Integration.Vsix/Commands/ConnectedModeMenu/ManageConnectionsCommand.cs +++ b/src/Integration.Vsix/Commands/ConnectedModeMenu/ManageConnectionsCommand.cs @@ -18,28 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Windows; using SonarLint.VisualStudio.ConnectedMode.UI; -using SonarLint.VisualStudio.ConnectedMode.UI.ManageBinding; namespace SonarLint.VisualStudio.Integration.Vsix.Commands.ConnectedModeMenu { [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal class ManageConnectionsCommand : VsCommandBase { - private readonly IConnectedModeServices connectedModeServices; - private readonly IConnectedModeBindingServices connectedModeBindingServices; + private readonly IConnectedModeUIManager connectedModeUiManager; internal const int Id = 0x102; - public ManageConnectionsCommand(IConnectedModeServices connectedModeServices, IConnectedModeBindingServices connectedModeBindingServices) + public ManageConnectionsCommand(IConnectedModeUIManager connectedModeUiManager) { - this.connectedModeServices = connectedModeServices; - this.connectedModeBindingServices = connectedModeBindingServices; + this.connectedModeUiManager = connectedModeUiManager; } protected override void InvokeInternal() { - new ManageBindingDialog(connectedModeServices, connectedModeBindingServices).ShowDialog(Application.Current.MainWindow); + connectedModeUiManager.ShowManageBindingDialog(); } } } diff --git a/src/Integration.Vsix/Commands/PackageCommandManager.cs b/src/Integration.Vsix/Commands/PackageCommandManager.cs index f64268c804..ac92f70ecf 100644 --- a/src/Integration.Vsix/Commands/PackageCommandManager.cs +++ b/src/Integration.Vsix/Commands/PackageCommandManager.cs @@ -46,10 +46,10 @@ public void Initialize( IProjectPropertyManager projectPropertyManager, IOutputWindowService outputWindowService, IShowInBrowserService showInBrowserService, - IBrowserService browserService, ShowOptionsPage showOptionsPage, IConnectedModeServices connectedModeServices, - IConnectedModeBindingServices connectedModeBindingServices) + IConnectedModeBindingServices connectedModeBindingServices, + IConnectedModeUIManager connectedModeManager) { RegisterCommand((int)PackageCommandId.ProjectExcludePropertyToggle, new ProjectExcludePropertyToggleCommand(projectPropertyManager)); RegisterCommand((int)PackageCommandId.ProjectTestPropertyAuto, new ProjectTestPropertySetCommand(projectPropertyManager, null)); @@ -65,11 +65,11 @@ public void Initialize( // Help menu buttons RegisterCommand(CommonGuids.HelpMenuCommandSet, ShowLogsCommand.Id, new ShowLogsCommand(outputWindowService)); RegisterCommand(CommonGuids.HelpMenuCommandSet, ViewDocumentationCommand.Id, new ViewDocumentationCommand(showInBrowserService)); - RegisterCommand(CommonGuids.HelpMenuCommandSet, AboutCommand.Id, new AboutCommand(browserService)); + RegisterCommand(CommonGuids.HelpMenuCommandSet, AboutCommand.Id, new AboutCommand(connectedModeServices.BrowserService)); RegisterCommand(CommonGuids.HelpMenuCommandSet, ShowCommunityPageCommand.Id, new ShowCommunityPageCommand(showInBrowserService)); // Connected mode buttons - RegisterCommand(CommonGuids.ConnectedModeMenuCommandSet, ManageConnectionsCommand.Id, new ManageConnectionsCommand(connectedModeServices, connectedModeBindingServices)); + RegisterCommand(CommonGuids.ConnectedModeMenuCommandSet, ManageConnectionsCommand.Id, new ManageConnectionsCommand(connectedModeManager)); RegisterCommand(CommonGuids.ConnectedModeMenuCommandSet, SaveSharedConnectionCommand.Id, new SaveSharedConnectionCommand(connectedModeServices.ConfigurationProvider, connectedModeBindingServices.SharedBindingConfigProvider)); } diff --git a/src/Integration.Vsix/SonarLintIntegrationPackage.cs b/src/Integration.Vsix/SonarLintIntegrationPackage.cs index e90c261661..83c6f039a3 100644 --- a/src/Integration.Vsix/SonarLintIntegrationPackage.cs +++ b/src/Integration.Vsix/SonarLintIntegrationPackage.cs @@ -96,10 +96,10 @@ private async Task InitOnUIThreadAsync() serviceProvider.GetMefService<IProjectPropertyManager>(), serviceProvider.GetMefService<IOutputWindowService>(), serviceProvider.GetMefService<IShowInBrowserService>(), - serviceProvider.GetMefService<IBrowserService>(), ShowOptionPage, serviceProvider.GetMefService<IConnectedModeServices>(), - serviceProvider.GetMefService<IConnectedModeBindingServices>()); + serviceProvider.GetMefService<IConnectedModeBindingServices>(), + serviceProvider.GetMefService<IConnectedModeUIManager>()); this.roslynSettingsFileSynchronizer = await this.GetMefServiceAsync<IRoslynSettingsFileSynchronizer>(); roslynSettingsFileSynchronizer.UpdateFileStorageAsync().Forget(); // don't wait for it to finish diff --git a/src/Integration/Binding/BindingSuggestionHandler.cs b/src/Integration/Binding/BindingSuggestionHandler.cs index 3be011760a..004eba1e98 100644 --- a/src/Integration/Binding/BindingSuggestionHandler.cs +++ b/src/Integration/Binding/BindingSuggestionHandler.cs @@ -20,10 +20,10 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.ConnectedMode.Binding.Suggestion; +using SonarLint.VisualStudio.ConnectedMode.UI; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Integration.TeamExplorer; namespace SonarLint.VisualStudio.Integration.Binding { @@ -34,20 +34,20 @@ internal class BindingSuggestionHandler : IBindingSuggestionHandler private readonly INotificationService notificationService; private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; private readonly IIDEWindowService ideWindowService; - private readonly ITeamExplorerController teamExplorerController; + private readonly IConnectedModeUIManager connectedModeUiManager; private readonly IBrowserService browserService; [ImportingConstructor] public BindingSuggestionHandler(INotificationService notificationService, IActiveSolutionBoundTracker activeSolutionBoundTracker, IIDEWindowService ideWindowService, - ITeamExplorerController teamExplorerController, + IConnectedModeUIManager connectedModeUiManager, IBrowserService browserService) { this.notificationService = notificationService; this.activeSolutionBoundTracker = activeSolutionBoundTracker; this.ideWindowService = ideWindowService; - this.teamExplorerController = teamExplorerController; + this.connectedModeUiManager = connectedModeUiManager; this.browserService = browserService; } @@ -62,7 +62,7 @@ public void Notify() : BindingStrings.BindingSuggetsionBindingConflict; var connectAction = new NotificationAction(BindingStrings.BindingSuggestionConnect, - _ => teamExplorerController.ShowSonarQubePage(), + _ => connectedModeUiManager.ShowManageBindingDialog(), true); var learnMoreAction = new NotificationAction(BindingStrings.BindingSuggestionLearnMore, _ => browserService.Navigate(DocumentationLinks.OpenInIdeBindingSetup), diff --git a/src/Integration/MefServices/SharedBindingSuggestionService.cs b/src/Integration/MefServices/SharedBindingSuggestionService.cs index 89cbf795ed..49b0b53fa8 100644 --- a/src/Integration/MefServices/SharedBindingSuggestionService.cs +++ b/src/Integration/MefServices/SharedBindingSuggestionService.cs @@ -41,6 +41,7 @@ internal sealed class SharedBindingSuggestionService : ISharedBindingSuggestionS private readonly ISuggestSharedBindingGoldBar suggestSharedBindingGoldBar; private readonly IConnectedModeServices connectedModeServices; private readonly IConnectedModeBindingServices connectedModeBindingServices; + private readonly IConnectedModeUIManager connectedModeUiManager; private readonly IActiveSolutionTracker activeSolutionTracker; [ImportingConstructor] @@ -48,11 +49,13 @@ public SharedBindingSuggestionService( ISuggestSharedBindingGoldBar suggestSharedBindingGoldBar, IConnectedModeServices connectedModeServices, IConnectedModeBindingServices connectedModeBindingServices, + IConnectedModeUIManager connectedModeUiManager, IActiveSolutionTracker activeSolutionTracker) { this.suggestSharedBindingGoldBar = suggestSharedBindingGoldBar; this.connectedModeServices = connectedModeServices; this.connectedModeBindingServices = connectedModeBindingServices; + this.connectedModeUiManager = connectedModeUiManager; this.activeSolutionTracker = activeSolutionTracker; this.activeSolutionTracker.ActiveSolutionChanged += OnActiveSolutionChanged; @@ -76,8 +79,7 @@ public void Dispose() private void ShowManageBindingDialog() { - var manageBindingDialog = new ManageBindingDialog(connectedModeServices, connectedModeBindingServices, useSharedBindingOnInitialization:true); - manageBindingDialog.ShowDialog(Application.Current.MainWindow); + connectedModeUiManager.ShowManageBindingDialog(useSharedBindingOnInitialization:true); } private void OnActiveSolutionChanged(object sender, ActiveSolutionChangedEventArgs e) diff --git a/src/Integration/SLCore/SLCoreConstantsProvider.cs b/src/Integration/SLCore/SLCoreConstantsProvider.cs index 9b367e29b8..a72338a417 100644 --- a/src/Integration/SLCore/SLCoreConstantsProvider.cs +++ b/src/Integration/SLCore/SLCoreConstantsProvider.cs @@ -42,7 +42,7 @@ public SLCoreConstantsProvider(IVsInfoProvider vsInfoProvider) public ClientConstantsDto ClientConstants => new(vsInfoProvider.Name, $"SonarLint Visual Studio/{VersionHelper.SonarLintVersion}", Process.GetCurrentProcess().Id); - public FeatureFlagsDto FeatureFlags => new(true, true, true, true, false, false, true, true); + public FeatureFlagsDto FeatureFlags => new(true, true, true, true, false, false, true, true, true); public TelemetryClientConstantAttributesDto TelemetryConstants => new("visualstudio", "SonarLint Visual Studio", VersionHelper.SonarLintVersion, VisualStudioHelpers.VisualStudioVersion, new Dictionary<string, object> { diff --git a/src/IssueViz.UnitTests/Editor/IssueSpanCalculatorTests.cs b/src/IssueViz.UnitTests/Editor/IssueSpanCalculatorTests.cs index 8a0b324721..a193695461 100644 --- a/src/IssueViz.UnitTests/Editor/IssueSpanCalculatorTests.cs +++ b/src/IssueViz.UnitTests/Editor/IssueSpanCalculatorTests.cs @@ -18,12 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.Text; using Moq; -using SonarLint.VisualStudio.TestInfrastructure; using SonarLint.VisualStudio.IssueVisualization.Editor; +using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Editor @@ -34,6 +32,8 @@ public class IssueSpanCalculatorTests private Mock<IChecksumCalculator> checksumCalculatorMock; private IssueSpanCalculator testSubject; + private const int SnapshotLength = 10000; + private const int SnapshotLineCount = 10000; [TestInitialize] public void TestInitialize() @@ -331,6 +331,83 @@ public void CalculateSpan_TextRangeNull_ReturnsNull() checksumCalculatorMock.VerifyNoOtherCalls(); } + [TestMethod] + public void CalculateSpan_ForStartAndEndLines_GetsPositionOfCorrectLines() + { + var startLine = CreateLineMock(lineNumber: 66, startPos: 1, endPos: 2); + var endLine = CreateLineMock(lineNumber: 224, startPos: 13, endPos: 23); + var textSnapshotMock = MockTextSnapshotForLines(startLine, endLine); + + testSubject.CalculateSpan(textSnapshotMock.Object, startLine.LineNumber, endLine.LineNumber); + + textSnapshotMock.Verify(mock => mock.GetLineFromLineNumber(startLine.LineNumber - 1), Times.Once); + textSnapshotMock.Verify(mock => mock.GetLineFromLineNumber(endLine.LineNumber - 1), Times.Once); + } + + [TestMethod] + public void CalculateSpan_ForStartAndEndLines_ReturnsSnapshotSpanWithCorrectStartAndEnd() + { + var startLine = CreateLineMock(lineNumber:66, startPos:1, endPos:2); + var endLine = CreateLineMock(lineNumber:224, startPos:13, endPos:23); + var textSnapshotMock = MockTextSnapshotForLines(startLine, endLine); + + var snapshotSpan = testSubject.CalculateSpan(textSnapshotMock.Object, startLine.LineNumber, endLine.LineNumber); + + snapshotSpan.HasValue.Should().BeTrue(); + snapshotSpan.Value.Start.Position.Should().Be(startLine.Start.Position); + snapshotSpan.Value.End.Position.Should().Be(endLine.End.Position); + } + + [TestMethod] + [DataRow(0, 1)] + [DataRow(1, 0)] + [DataRow(SnapshotLineCount + 1, 1)] + [DataRow(1, SnapshotLineCount + 1)] + [DataRow(4, 1)] + public void CalculateSpan_ForInvalidStartAndEndLines_ReturnsNull(int startLineNumber, int endLineNumber) + { + var startLine = CreateLineMock(lineNumber:startLineNumber, startPos:1, endPos:2); + var endLine = CreateLineMock(lineNumber:endLineNumber, startPos:13, endPos:23); + var textSnapshotMock = MockTextSnapshotForLines(startLine, endLine); + + var result = testSubject.CalculateSpan(textSnapshotMock.Object, startLine.LineNumber, endLine.LineNumber); + + result.Should().BeNull(); + } + + [TestMethod] + public void IsSameHash_SnapshotIsSameAsText_ReturnsTrue() + { + var textSnapshotMock = new SnapshotSpan(CreateSnapshotMock().Object, 1, 1); + checksumCalculatorMock.Setup(mock => mock.Calculate(It.IsAny<string>())).Returns("sameHash"); + + var result = testSubject.IsSameHash(textSnapshotMock, "test"); + + result.Should().BeTrue(); + } + + [TestMethod] + public void IsSameHash_SnapshotIsSameAsText_ReturnsFalse() + { + var textSnapshotMock = new SnapshotSpan(CreateSnapshotMock().Object, 1, 1); + checksumCalculatorMock.SetupSequence(mock => mock.Calculate(It.IsAny<string>())).Returns("hash1").Returns("hash2"); + + var result = testSubject.IsSameHash(textSnapshotMock, "test"); + + result.Should().BeFalse(); + } + + private static Mock<ITextSnapshot> MockTextSnapshotForLines(ITextSnapshotLine startLine, ITextSnapshotLine endLine) + { + var textSnapshot = new Mock<ITextSnapshot>(); + textSnapshot.SetupGet(x => x.LineCount).Returns(SnapshotLineCount); + textSnapshot.SetupGet(x => x.Length).Returns(SnapshotLength); + textSnapshot.Setup(x => x.GetLineFromLineNumber(startLine.LineNumber - 1)).Returns(startLine); + textSnapshot.Setup(x => x.GetLineFromLineNumber(endLine.LineNumber - 1)).Returns(endLine); + + return textSnapshot; + } + private class VSLineDescription { public int ZeroBasedLineNumber { get; set; } @@ -339,7 +416,7 @@ private class VSLineDescription public string Text { get; set; } } - private static Mock<ITextSnapshot> CreateSnapshotMock(int bufferLineCount = 1000, int snapShotLength = 10000, params VSLineDescription[] lines) + private static Mock<ITextSnapshot> CreateSnapshotMock(int bufferLineCount = 1000, int snapShotLength = SnapshotLength, params VSLineDescription[] lines) { var textSnapshotMock = new Mock<ITextSnapshot>(); @@ -378,5 +455,19 @@ private static ITextSnapshotLine CreateLineMock(ITextSnapshot textSnapshot, VSLi return startLineMock.Object; } + + private static ITextSnapshotLine CreateLineMock(int lineNumber, int startPos, int endPos) + { + var startLineMock = new Mock<ITextSnapshotLine>(); + var textSnapshot = new Mock<ITextSnapshot>(); + + textSnapshot.SetupGet(x => x.Length).Returns(() => endPos +1); + + startLineMock.SetupGet(x => x.LineNumber).Returns(() => lineNumber); + startLineMock.SetupGet(x => x.Start).Returns(() => new SnapshotPoint(textSnapshot.Object, startPos)); + startLineMock.SetupGet(x => x.End).Returns(() => new SnapshotPoint(textSnapshot.Object, endPos)); + + return startLineMock.Object; + } } } diff --git a/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs b/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs new file mode 100644 index 0000000000..ec14ff22c1 --- /dev/null +++ b/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs @@ -0,0 +1,378 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using NSubstitute.ExceptionExtensions; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.IssueVisualization.Editor; +using SonarLint.VisualStudio.IssueVisualization.FixSuggestion; +using SonarLint.VisualStudio.IssueVisualization.OpenInIde; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using FileEditDto = SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models.FileEditDto; + +namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.FixSuggestion; + +[TestClass] +public class FixSuggestionHandlerTests +{ + private const string ConfigurationScopeRoot = @"C:\"; + private readonly ShowFixSuggestionParams suggestionWithOneChange = CreateFixSuggestionParams(changes: CreateChangesDto(1, 1, "var a=1;")); + private FixSuggestionHandler testSubject; + private IThreadHandling threadHandling; + private ILogger logger; + private IDocumentNavigator documentNavigator; + private IIssueSpanCalculator issueSpanCalculator; + private IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator; + private IIDEWindowService ideWindowService; + private IFixSuggestionNotification fixSuggestionNotification; + + [TestInitialize] + public void TestInitialize() + { + threadHandling = new NoOpThreadHandler(); + logger = Substitute.For<ILogger>(); + documentNavigator = Substitute.For<IDocumentNavigator>(); + issueSpanCalculator = Substitute.For<IIssueSpanCalculator>(); + openInIdeConfigScopeValidator = Substitute.For<IOpenInIdeConfigScopeValidator>(); + ideWindowService = Substitute.For<IIDEWindowService>(); + fixSuggestionNotification = Substitute.For<IFixSuggestionNotification>(); + + testSubject = new FixSuggestionHandler( + threadHandling, + logger, + documentNavigator, + issueSpanCalculator, + openInIdeConfigScopeValidator, + ideWindowService, + fixSuggestionNotification); + MockConfigScopeRoot(); + issueSpanCalculator.IsSameHash(Arg.Any<SnapshotSpan>(), Arg.Any<string>()).Returns(true); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent<FixSuggestionHandler>(); + } + + [TestMethod] + public void ApplyFixSuggestion_RunsOnUIThread() + { + var threadHandlingMock = Substitute.For<IThreadHandling>(); + var fixSuggestionHandler = new FixSuggestionHandler( + threadHandlingMock, + logger, + documentNavigator, + issueSpanCalculator, + openInIdeConfigScopeValidator, + ideWindowService, + fixSuggestionNotification); + + fixSuggestionHandler.ApplyFixSuggestion(suggestionWithOneChange); + + threadHandlingMock.ReceivedWithAnyArgs().RunOnUIThread(default); + } + + [TestMethod] + public void ApplyFixSuggestion_OneChange_AppliesChange() + { + var suggestedChange = suggestionWithOneChange.fixSuggestion.fileEdit.changes[0]; + MockCalculateSpan(suggestedChange); + var textView = MockOpenFile(); + var edit = MockTextEdit(textView); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + Received.InOrder(() => + { + logger.WriteLine(FixSuggestionResources.ProcessingRequest, suggestionWithOneChange.configurationScopeId, suggestionWithOneChange.fixSuggestion.suggestionId); + ideWindowService.BringToFront(); + fixSuggestionNotification.ClearAsync(); + documentNavigator.Open(@"C:\myFile.cs"); + textView.TextBuffer.CreateEdit(); + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), suggestedChange.beforeLineRange.startLine, suggestedChange.beforeLineRange.endLine); + edit.Replace(Arg.Any<Span>(), suggestedChange.after); + edit.Apply(); + logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, suggestionWithOneChange.configurationScopeId, suggestionWithOneChange.fixSuggestion.suggestionId); + }); + } + + [TestMethod] + public void ApplyFixSuggestion_TwoChanges_AppliesChangeOnce() + { + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()).Returns(new SnapshotSpan()); + var suggestionWithTwoChanges = CreateFixSuggestionParams(changes: [CreateChangesDto(1, 1, "var a=1;"), CreateChangesDto(2, 2, "var b=0;")]); + var textView = MockOpenFile(); + var edit = MockTextEdit(textView); + + testSubject.ApplyFixSuggestion(suggestionWithTwoChanges); + + issueSpanCalculator.Received(2).CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()); + edit.Received(2).Replace(Arg.Any<Span>(), Arg.Any<string>()); + edit.Received(1).Apply(); + } + + /// <summary> + /// The changes are applied from bottom to top to avoid changing the line numbers + /// of the changes that are below the current change. + /// + /// This is important when the change is more lines than the original line range. + /// </summary> + [TestMethod] + public void ApplyFixSuggestion_WhenMoreThanOneFixes_ApplyThemFromBottomToTop() + { + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()).Returns(new SnapshotSpan()); + MockOpenFile(); + ChangesDto[] changes = [CreateChangesDto(1, 1, "var a=1;"), CreateChangesDto(3, 3, "var b=0;")]; + var suggestionParams = CreateFixSuggestionParams(changes: changes); + + testSubject.ApplyFixSuggestion(suggestionParams); + + Received.InOrder(() => + { + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), 3, 3); + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), 1, 1); + }); + } + + [TestMethod] + public void ApplyFixSuggestion_WhenApplyingChange_BringWindowToFront() + { + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + ideWindowService.Received().BringToFront(); + } + + + [TestMethod] + public void ApplyFixSuggestion_WhenApplyingChange_BringFocusToFirstChangedLines() + { + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()).Returns(new SnapshotSpan()); + ChangesDto[] changes = [CreateChangesDto(1, 1, "var a=1;"), CreateChangesDto(3, 3, "var a=1;")]; + var suggestionParams = CreateFixSuggestionParams(changes: changes); + var firstSuggestedChange = suggestionParams.fixSuggestion.fileEdit.changes[0]; + var firstAffectedSnapshot = MockCalculateSpan(firstSuggestedChange); + var textView = MockOpenFile(); + MockConfigScopeRoot(); + + testSubject.ApplyFixSuggestion(suggestionParams); + + textView.ViewScroller.ReceivedWithAnyArgs(1).EnsureSpanVisible(Arg.Any<SnapshotSpan>(), default); + textView.ViewScroller.Received(1).EnsureSpanVisible(firstAffectedSnapshot, EnsureSpanVisibleOptions.AlwaysCenter); + } + + [TestMethod] + public void ApplyFixSuggestion_Throws_Logs() + { + var exceptionMsg = "error"; + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()).Throws(new Exception(exceptionMsg)); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + Received.InOrder(() => + { + logger.WriteLine(FixSuggestionResources.ProcessingRequest, suggestionWithOneChange.configurationScopeId, suggestionWithOneChange.fixSuggestion.suggestionId); + logger.WriteLine(FixSuggestionResources.ProcessingRequestFailed, suggestionWithOneChange.configurationScopeId, suggestionWithOneChange.fixSuggestion.suggestionId, exceptionMsg); + }); + logger.DidNotReceive().WriteLine(FixSuggestionResources.DoneProcessingRequest, suggestionWithOneChange.configurationScopeId, suggestionWithOneChange.fixSuggestion.suggestionId); + } + + [TestMethod] + public void ApplyFixSuggestion_WhenConfigRootScopeNotFound_ShouldLogFailureAndShowNotification() + { + var reason = "Scope not found"; + MockFailedConfigScopeRoot(reason); + var suggestionParams = CreateFixSuggestionParams("SpecificConfigScopeId"); + + testSubject.ApplyFixSuggestion(suggestionParams); + + logger.Received().WriteLine(FixSuggestionResources.GetConfigScopeRootPathFailed, "SpecificConfigScopeId", "Scope not found"); + fixSuggestionNotification.Received(1).InvalidRequestAsync(reason); + } + + [TestMethod] + public void ApplyFixSuggestion_WhenLineNumbersDoNotMatch_ShouldLogFailure() + { + FailWhenApplyingEdit("Line numbers do not match"); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + logger.Received().WriteLine(FixSuggestionResources.ProcessingRequestFailed, + suggestionWithOneChange.configurationScopeId, suggestionWithOneChange.fixSuggestion.suggestionId, + "Line numbers do not match"); + } + + [TestMethod] + public void ApplyFixSuggestion_WhenApplyingChangeAndExceptionIsThrown_ShouldCancelEdit() + { + var edit = FailWhenApplyingEdit(); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + edit.DidNotReceiveWithAnyArgs().Replace(default, default); + edit.Received().Cancel(); + } + + [TestMethod] + public void ApplyFixSuggestion_FileCanNotBeOpened_LogsAndShowsNotification() + { + var errorMessage = "error"; + documentNavigator.Open(Arg.Any<string>()).Throws(new Exception(errorMessage)); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + logger.Received().WriteLine(Resources.ERR_OpenDocumentException, GetAbsolutePathOfFile(suggestionWithOneChange), errorMessage); + fixSuggestionNotification.Received(1).UnableToOpenFileAsync(Arg.Is<string>(msg => msg == GetAbsolutePathOfFile(suggestionWithOneChange))); + } + + [TestMethod] + public void ApplyFixSuggestion_FileContentIsNull_LogsAndShowsNotification() + { + documentNavigator.Open(Arg.Any<string>()).Returns((ITextView)null); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + logger.DidNotReceive().WriteLine(Resources.ERR_OpenDocumentException, Arg.Any<string>(), Arg.Any<string>()); + fixSuggestionNotification.Received(1).UnableToOpenFileAsync(Arg.Is<string>(msg => msg == GetAbsolutePathOfFile(suggestionWithOneChange))); + } + + [TestMethod] + public void ApplyFixSuggestion_ClearsPreviousNotification() + { + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + fixSuggestionNotification.Received(1).ClearAsync(); + } + + [TestMethod] + public void ApplyFixSuggestion_OneChange_IssueCanNotBeLocated_ShowsNotificationAndDoesNotApplySuggestion() + { + var edit = MockTextEdit(); + issueSpanCalculator.IsSameHash(Arg.Any<SnapshotSpan>(), Arg.Any<string>()).Returns(false); + + testSubject.ApplyFixSuggestion(suggestionWithOneChange); + + VerifyFixSuggestionNotApplied(edit); + } + + [TestMethod] + public void ApplyFixSuggestion_TwoChanges_OneIssueCanNotBeLocated_ShowsNotificationAndDoesNotApplySuggestion() + { + var suggestionParams = CreateFixSuggestionParams(changes: [CreateChangesDto(1, 1, "var a=1;"), CreateChangesDto(2, 2, "var b=0;")]); + var edit = MockTextEdit(); + issueSpanCalculator.IsSameHash(Arg.Any<SnapshotSpan>(), Arg.Any<string>()).Returns( + _ => true, + _ => false); + + testSubject.ApplyFixSuggestion(suggestionParams); + + VerifyFixSuggestionNotApplied(edit); + } + + private void MockConfigScopeRoot() + { + openInIdeConfigScopeValidator.TryGetConfigurationScopeRoot(Arg.Any<string>(), out Arg.Any<string>(), out Arg.Any<string>()).Returns( + x => + { + x[1] = ConfigurationScopeRoot; + return true; + }); + } + + private void MockFailedConfigScopeRoot(string failureReason) + { + openInIdeConfigScopeValidator.TryGetConfigurationScopeRoot(Arg.Any<string>(), out Arg.Any<string>(), out Arg.Any<string>()).Returns( + x => + { + x[2] = failureReason; + return false; + }); + } + + private ITextView MockOpenFile() + { + var textView = Substitute.For<ITextView>(); + documentNavigator.Open(Arg.Any<string>()).Returns(textView); + return textView; + } + + private SnapshotSpan MockCalculateSpan(ChangesDto suggestedChange) + { + return MockCalculateSpan(suggestedChange.before, suggestedChange.beforeLineRange.startLine, suggestedChange.beforeLineRange.endLine); + } + + private SnapshotSpan MockCalculateSpan(string text, int startLine, int endLine) + { + var affectedSnapshot = new SnapshotSpan(); + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), startLine, endLine).Returns(affectedSnapshot); + issueSpanCalculator.IsSameHash(affectedSnapshot, text).Returns(true); + return affectedSnapshot; + } + + private ITextEdit FailWhenApplyingEdit(string reason = "") + { + var edit = Substitute.For<ITextEdit>(); + var textView = MockOpenFile(); + textView.TextBuffer.CreateEdit().Returns(edit); + issueSpanCalculator.CalculateSpan(Arg.Any<ITextSnapshot>(), Arg.Any<int>(), Arg.Any<int>()) + .Throws(new Exception(reason)); + return edit; + } + + private static ShowFixSuggestionParams CreateFixSuggestionParams(string scopeId = "scopeId", + string suggestionKey = "suggestionKey", + string idePath = @"myFile.cs", + params ChangesDto[] changes) + { + var fixSuggestion = new FixSuggestionDto(suggestionKey, "refactor", new FileEditDto(idePath, changes.ToList())); + var suggestionParams = new ShowFixSuggestionParams(scopeId, "key", fixSuggestion); + return suggestionParams; + } + + private static ChangesDto CreateChangesDto(int startLine, int endLine, string before) + { + return new ChangesDto(new LineRangeDto(startLine, endLine), before, ""); + } + + private static string GetAbsolutePathOfFile(ShowFixSuggestionParams suggestionParams) => + Path.Combine(ConfigurationScopeRoot, suggestionParams.fixSuggestion.fileEdit.idePath); + + private ITextEdit MockTextEdit(ITextView textView = null) + { + var edit = Substitute.For<ITextEdit>(); + textView ??= MockOpenFile(); + textView.TextBuffer.CreateEdit().Returns(edit); + return edit; + } + + private void VerifyFixSuggestionNotApplied(ITextEdit edit) + { + Received.InOrder(() => + { + fixSuggestionNotification.UnableToLocateIssueAsync(Arg.Is<string>(msg => msg == GetAbsolutePathOfFile(suggestionWithOneChange))); + edit.Cancel(); + }); + edit.DidNotReceive().Apply(); + } +} diff --git a/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionNotificationTests.cs b/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionNotificationTests.cs new file mode 100644 index 0000000000..0bd1f84c36 --- /dev/null +++ b/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionNotificationTests.cs @@ -0,0 +1,299 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.InfoBar; +using SonarLint.VisualStudio.IssueVisualization.FixSuggestion; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.FixSuggestion; + +[TestClass] +public class FixSuggestionNotificationTests +{ + private FixSuggestionNotification testSubject; + private IInfoBarManager infoBarManager; + private IOutputWindowService outputWindowService; + private IBrowserService browserService; + private IThreadHandling threadHandler; + + [TestInitialize] + public void TestInitialize() + { + infoBarManager = Substitute.For<IInfoBarManager>(); + outputWindowService = Substitute.For<IOutputWindowService>(); + threadHandler = new NoOpThreadHandler(); + browserService = Substitute.For<IBrowserService>(); + + testSubject = new FixSuggestionNotification(infoBarManager, outputWindowService, browserService, threadHandler); + } + + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported<FixSuggestionNotification, IFixSuggestionNotification>( + MefTestHelpers.CreateExport<IInfoBarManager>(), + MefTestHelpers.CreateExport<IOutputWindowService>(), + MefTestHelpers.CreateExport<IBrowserService>(), + MefTestHelpers.CreateExport<IThreadHandling>()); + } + + [TestMethod] + public async Task Clear_NoPreviousInfoBar_NoException() + { + await testSubject.ClearAsync(); + + infoBarManager.ReceivedCalls().Should().BeEmpty(); + } + + [TestMethod] + public async Task Clear_HasPreviousInfoBar_InfoBarCleared() + { + var infoBar = MockInfoBar(); + await MockPreviousInfoBar(infoBar); + + await testSubject.ClearAsync(); + + CheckInfoBarWithEventsRemoved(infoBar); + } + + [TestMethod] + public async Task Show_NoPreviousInfoBar_InfoBarIsShown() + { + var infoBarText = "info bar text"; + var infoBar = MockInfoBar(); + MockAttachInfoBarToMainWindow(infoBar); + + await testSubject.ShowAsync(infoBarText); + + CheckInfoBarWithEventsAdded(infoBar, infoBarText); + } + + [TestMethod] + public async Task Show_NoCustomText_InfoBarWithDefaultTextIsShown() + { + var infoBar = MockInfoBar(); + MockAttachInfoBarToMainWindow(infoBar); + + await testSubject.ShowAsync(null); + + CheckInfoBarWithEventsAdded(infoBar, FixSuggestionResources.InfoBarDefaultMessage); + } + + [TestMethod] + public async Task Show_HasPreviousInfoBar_InfoBarReplaced() + { + var firstInfoBar = MockInfoBar(); + var secondInfoBar = MockInfoBar(); + infoBarManager + .AttachInfoBarToMainWindow(Arg.Any<string>(), SonarLintImageMoniker.OfficialSonarLintMoniker, Arg.Any<string[]>()) + .Returns(x => firstInfoBar, + x=> secondInfoBar); + + var text1 = "text1"; + await testSubject.ShowAsync(text1); // show first bar + + CheckInfoBarWithEventsAdded(firstInfoBar, text1); + infoBarManager.ClearReceivedCalls(); + + var text2 = "text2"; + await testSubject.ShowAsync(text2); // show second bar + + CheckInfoBarWithEventsRemoved(firstInfoBar); + CheckInfoBarWithEventsAdded(secondInfoBar, text2); + } + + [TestMethod] + public async Task Dispose_HasPreviousInfoBar_InfoBarRemoved() + { + var infoBar = MockInfoBar(); + await MockPreviousInfoBar(infoBar); + + testSubject.Dispose(); + + CheckInfoBarWithEventsRemoved(infoBar); + } + + [TestMethod] + public void Dispose_NoPreviousInfoBar_NoException() + { + Action act = () => testSubject.Dispose(); + + act.Should().NotThrow(); + } + + [TestMethod] + public async Task InfoBarIsManuallyClosed_InfoBarDetachedFromToolWindow() + { + var infoBar = MockInfoBar(); + await MockPreviousInfoBar(infoBar); + + infoBar.Closed += Raise.EventWith(EventArgs.Empty); + + CheckInfoBarWithEventsRemoved(infoBar); + } + + [TestMethod] + public async Task InfoBarShowLogsButtonClicked_OutputWindowIsShown() + { + var infoBar = MockInfoBar(); + await MockPreviousInfoBar(infoBar); + + infoBar.ButtonClick += Raise.EventWith(new InfoBarButtonClickedEventArgs(FixSuggestionResources.InfoBarButtonShowLogs)); + + CheckInfoBarNotRemoved(infoBar); + outputWindowService.Received(1).Show(); + } + + [TestMethod] + public async Task InfoBarMoreInfoButtonClicked_DocumentationOpenInBrowser() + { + var infoBar = MockInfoBar(); + await MockPreviousInfoBar(infoBar); + + infoBar.ButtonClick += Raise.EventWith(new InfoBarButtonClickedEventArgs(FixSuggestionResources.InfoBarButtonMoreInfo)); + + CheckInfoBarNotRemoved(infoBar); + browserService.Received(1).Navigate(DocumentationLinks.OpenInIdeIssueLocation); + } + + [TestMethod] + public async Task ShowAsync_VerifySwitchesToUiThreadIsCalled() + { + MockAttachInfoBarToMainWindow(Substitute.For<IInfoBar>()); + var threadHandling = Substitute.For<IThreadHandling>(); + threadHandling + .When(x => x.RunOnUIThreadAsync(Arg.Any<Action>())) + .Do(callInfo => + { + infoBarManager.DidNotReceive().AttachInfoBarToMainWindow(Arg.Any<string>(), SonarLintImageMoniker.OfficialSonarLintMoniker, Arg.Any<string[]>()); + callInfo.Arg<Action>().Invoke(); + infoBarManager.Received(1).AttachInfoBarToMainWindow(Arg.Any<string>(), SonarLintImageMoniker.OfficialSonarLintMoniker, Arg.Any<string[]>()); + }); + var fixSuggestionNotification = new FixSuggestionNotification(infoBarManager, + outputWindowService, + browserService, + threadHandling); + + await fixSuggestionNotification.ShowAsync( "some text"); + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any<Action>()); + } + + [TestMethod] + public async Task ClearAsync_VerifySwitchesToUiThreadIsCalled() + { + MockAttachInfoBarToMainWindow(Substitute.For<IInfoBar>()); + var threadHandling = Substitute.For<IThreadHandling>(); + var fixSuggestionNotification = new FixSuggestionNotification(infoBarManager, + outputWindowService, + browserService, + threadHandling); + + await fixSuggestionNotification.ClearAsync(); + + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any<Action>()); + } + + [TestMethod] + public async Task UnableToOpenFileAsync_CallsShowAsyncWithCorrectMessage() + { + var infoBar = MockInfoBar(); + MockAttachInfoBarToMainWindow(infoBar); + var myPath = "c://myFile.cs"; + + await testSubject.UnableToOpenFileAsync(myPath); + + CheckInfoBarWithEventsAdded(infoBar, string.Format(FixSuggestionResources.InfoBarUnableToOpenFile, myPath)); + } + + [TestMethod] + public async Task InvalidRequestAsync_CallsShowAsyncWithCorrectMessage() + { + var infoBar = MockInfoBar(); + MockAttachInfoBarToMainWindow(infoBar); + var reason = "wrong config scope"; + + await testSubject.InvalidRequestAsync(reason); + + CheckInfoBarWithEventsAdded(infoBar, string.Format(FixSuggestionResources.InfoBarInvalidRequest, reason)); + } + + [TestMethod] + public async Task UnableToLocateIssueAsync_CallsShowAsyncWithCorrectMessage() + { + var infoBar = MockInfoBar(); + MockAttachInfoBarToMainWindow(infoBar); + var myPath = "c://myFile.cs"; + + await testSubject.UnableToLocateIssueAsync(myPath); + + CheckInfoBarWithEventsAdded(infoBar, string.Format(FixSuggestionResources.InfoBarUnableToLocateFixSuggestion, myPath)); + } + + private async Task MockPreviousInfoBar(IInfoBar infoBar = null) + { + infoBar ??= MockInfoBar(); + MockAttachInfoBarToMainWindow(infoBar); + var someText = "some text"; + + await testSubject.ShowAsync(someText); + + CheckInfoBarWithEventsAdded(infoBar, someText); + outputWindowService.ReceivedCalls().Should().BeEmpty(); + } + + private void MockAttachInfoBarToMainWindow(IInfoBar infoBar) + { + infoBarManager + .AttachInfoBarToMainWindow(Arg.Any<string>(), SonarLintImageMoniker.OfficialSonarLintMoniker, Arg.Any<string[]>()) + .Returns(infoBar); + } + + private static IInfoBar MockInfoBar() + { + return Substitute.For<IInfoBar>(); + } + + private void CheckInfoBarWithEventsRemoved(IInfoBar infoBar) + { + infoBarManager.Received(1).DetachInfoBar(infoBar); + + infoBar.Received(1).Closed -= Arg.Any<EventHandler>(); + infoBar.Received(1).ButtonClick -= Arg.Any<EventHandler<InfoBarButtonClickedEventArgs>>(); + } + + private void CheckInfoBarNotRemoved(IInfoBar infoBar) + { + infoBarManager.DidNotReceive().DetachInfoBar(infoBar); + } + + private void CheckInfoBarWithEventsAdded(IInfoBar infoBar, string text) + { + text ??= FixSuggestionResources.InfoBarDefaultMessage; + var buttonTexts = new[]{FixSuggestionResources.InfoBarButtonMoreInfo, FixSuggestionResources.InfoBarButtonShowLogs}; + infoBarManager.Received(1).AttachInfoBarToMainWindow( + text, + SonarLintImageMoniker.OfficialSonarLintMoniker, + buttonTexts); + + infoBar.Received(1).Closed += Arg.Any<EventHandler>(); + infoBar.Received(1).ButtonClick += Arg.Any<EventHandler<InfoBarButtonClickedEventArgs>>(); + } +} diff --git a/src/IssueViz/Editor/IIssueSpanCalculator.cs b/src/IssueViz/Editor/IIssueSpanCalculator.cs index cf1a6d4e67..f813d1e98e 100644 --- a/src/IssueViz/Editor/IIssueSpanCalculator.cs +++ b/src/IssueViz/Editor/IIssueSpanCalculator.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core.Analysis; @@ -34,6 +33,17 @@ public interface IIssueSpanCalculator /// Returns null if no textRange is passed /// </summary> SnapshotSpan? CalculateSpan(ITextRange range, ITextSnapshot currentSnapshot); + + /// <summary> + /// Returns the text span that is in the range of the provided lines + /// Returns null if the provided lines can not be found the snapshot + /// </summary> + SnapshotSpan? CalculateSpan(ITextSnapshot snapshot, int startLine, int endLine); + + /// <summary> + /// Returns true only if the provided <param name="text"/> has the same hash as the text contained in the <paramref name="snapshotSpan"/> + /// </summary> + bool IsSameHash(SnapshotSpan snapshotSpan, string text); } [Export(typeof(IIssueSpanCalculator))] @@ -105,6 +115,26 @@ internal IssueSpanCalculator(IChecksumCalculator checksumCalculator) return snapshotSpan; } + public SnapshotSpan? CalculateSpan(ITextSnapshot snapshot, int startLine, int endLine) + { + if (startLine < 1 || endLine < 1 || startLine > snapshot.LineCount || endLine > snapshot.LineCount || startLine > endLine) + { + return null; + } + var startPosition = snapshot.GetLineFromLineNumber(startLine - 1).Start.Position; + var endPosition = snapshot.GetLineFromLineNumber(endLine - 1).End.Position; + var span = Span.FromBounds(startPosition, endPosition); + return new SnapshotSpan(snapshot, span); + } + + public bool IsSameHash(SnapshotSpan snapshotSpan, string text) + { + var snapsShotHash = checksumCalculator.Calculate(snapshotSpan.GetText()); + var textHash = checksumCalculator.Calculate(text); + + return snapsShotHash == textHash; + } + private static bool RangeHasHash(ITextRange range) => !string.IsNullOrEmpty(range.LineHash); diff --git a/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs b/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs new file mode 100644 index 0000000000..5da49aefc5 --- /dev/null +++ b/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs @@ -0,0 +1,188 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.IO; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.IssueVisualization.Editor; +using SonarLint.VisualStudio.IssueVisualization.OpenInIde; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; + +namespace SonarLint.VisualStudio.IssueVisualization.FixSuggestion; + +[Export(typeof(IFixSuggestionHandler))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class FixSuggestionHandler : IFixSuggestionHandler +{ + private readonly IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator; + private readonly IIDEWindowService ideWindowService; + private readonly IFixSuggestionNotification fixSuggestionNotification; + private readonly IThreadHandling threadHandling; + private readonly ILogger logger; + private readonly IDocumentNavigator documentNavigator; + private readonly IIssueSpanCalculator issueSpanCalculator; + + [ImportingConstructor] + internal FixSuggestionHandler( + ILogger logger, + IDocumentNavigator documentNavigator, + IIssueSpanCalculator issueSpanCalculator, + IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator, + IIDEWindowService ideWindowService, + IFixSuggestionNotification fixSuggestionNotification) : + this( + ThreadHandling.Instance, + logger, + documentNavigator, + issueSpanCalculator, + openInIdeConfigScopeValidator, + ideWindowService, + fixSuggestionNotification) + { + } + + internal FixSuggestionHandler( + IThreadHandling threadHandling, + ILogger logger, + IDocumentNavigator documentNavigator, + IIssueSpanCalculator issueSpanCalculator, + IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator, + IIDEWindowService ideWindowService, + IFixSuggestionNotification fixSuggestionNotification) + { + this.threadHandling = threadHandling; + this.logger = logger; + this.documentNavigator = documentNavigator; + this.issueSpanCalculator = issueSpanCalculator; + this.openInIdeConfigScopeValidator = openInIdeConfigScopeValidator; + this.ideWindowService = ideWindowService; + this.fixSuggestionNotification = fixSuggestionNotification; + } + + public void ApplyFixSuggestion(ShowFixSuggestionParams parameters) + { + try + { + logger.WriteLine(FixSuggestionResources.ProcessingRequest, parameters.configurationScopeId, parameters.fixSuggestion.suggestionId); + ideWindowService.BringToFront(); + fixSuggestionNotification.ClearAsync().Forget(); + + if (!ValidateConfiguration(parameters.configurationScopeId, out var configurationScopeRoot, out var failureReason)) + { + logger.WriteLine(FixSuggestionResources.GetConfigScopeRootPathFailed, parameters.configurationScopeId, failureReason); + fixSuggestionNotification.InvalidRequestAsync(failureReason).Forget(); + return; + } + + threadHandling.RunOnUIThread(() => ApplyAndShowAppliedFixSuggestions(parameters, configurationScopeRoot)); + logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, parameters.configurationScopeId, parameters.fixSuggestion.suggestionId); + } + catch (Exception exception) when (!ErrorHandler.IsCriticalException(exception)) + { + logger.WriteLine(FixSuggestionResources.ProcessingRequestFailed, parameters.configurationScopeId, parameters.fixSuggestion.suggestionId, exception.Message); + } + } + + private bool ValidateConfiguration(string configurationScopeId, out string configurationScopeRoot, out string failureReason) + { + return openInIdeConfigScopeValidator.TryGetConfigurationScopeRoot(configurationScopeId, out configurationScopeRoot, out failureReason); + } + + private void ApplyAndShowAppliedFixSuggestions(ShowFixSuggestionParams parameters, string configurationScopeRoot) + { + var absoluteFilePath = Path.Combine(configurationScopeRoot, parameters.fixSuggestion.fileEdit.idePath); + var textView = GetFileContent(absoluteFilePath); + if (!ValidateFileExists(textView, absoluteFilePath)) + { + return; + } + ApplySuggestedChanges(textView, parameters.fixSuggestion.fileEdit.changes, absoluteFilePath); + } + + private void ApplySuggestedChanges(ITextView textView, List<ChangesDto> changes, string filePath) + { + var textEdit = textView.TextBuffer.CreateEdit(); + try + { + for (var i = changes.Count - 1; i >= 0; i--) + { + var changeDto = changes[i]; + + var spanToUpdate = issueSpanCalculator.CalculateSpan(textView.TextSnapshot, changeDto.beforeLineRange.startLine, changeDto.beforeLineRange.endLine); + if (!ValidateIssueStillExists(spanToUpdate, changeDto, filePath)) + { + return; + } + + if (i == 0) + { + textView.Caret.MoveTo(spanToUpdate.Value.Start); + textView.ViewScroller.EnsureSpanVisible(spanToUpdate.Value, EnsureSpanVisibleOptions.AlwaysCenter); + } + textEdit.Replace(spanToUpdate.Value, changeDto.after); + } + textEdit.Apply(); + } + finally + { + textEdit.Cancel(); + } + } + + private bool ValidateIssueStillExists(SnapshotSpan? spanToUpdate, ChangesDto changeDto, string filePath) + { + if (spanToUpdate.HasValue && issueSpanCalculator.IsSameHash(spanToUpdate.Value, changeDto.before)) + { + return true; + } + + fixSuggestionNotification.UnableToLocateIssueAsync(filePath).Forget(); + return false; + } + + private bool ValidateFileExists(ITextView fileContent, string absoluteFilePath) + { + if (fileContent != null) + { + return true; + } + + fixSuggestionNotification.UnableToOpenFileAsync(absoluteFilePath).Forget(); + return false; + } + + private ITextView GetFileContent(string filePath) + { + try + { + return documentNavigator.Open(filePath); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(Resources.ERR_OpenDocumentException, filePath, ex.Message); + return null; + } + } +} diff --git a/src/IssueViz/FixSuggestion/FixSuggestionResources.Designer.cs b/src/IssueViz/FixSuggestion/FixSuggestionResources.Designer.cs new file mode 100644 index 0000000000..c6bab4bf50 --- /dev/null +++ b/src/IssueViz/FixSuggestion/FixSuggestionResources.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace SonarLint.VisualStudio.IssueVisualization.FixSuggestion { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class FixSuggestionResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal FixSuggestionResources() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonarLint.VisualStudio.IssueVisualization.FixSuggestion.FixSuggestionResources", typeof(FixSuggestionResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to [Fix Suggestion in IDE] Done processing request. Configuration scope: {0}, SuggestionId: {1}. + /// </summary> + internal static string DoneProcessingRequest { + get { + return ResourceManager.GetString("DoneProcessingRequest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to [Fix Suggestion in IDE] Could not determine configuration scope root path for scope [{0}] due to: {1}. + /// </summary> + internal static string GetConfigScopeRootPathFailed { + get { + return ResourceManager.GetString("GetConfigScopeRootPathFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Learn more. + /// </summary> + internal static string InfoBarButtonMoreInfo { + get { + return ResourceManager.GetString("InfoBarButtonMoreInfo", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Show logs. + /// </summary> + internal static string InfoBarButtonShowLogs { + get { + return ResourceManager.GetString("InfoBarButtonShowLogs", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Could not handle Open in IDE request. See the Logs for more information.. + /// </summary> + internal static string InfoBarDefaultMessage { + get { + return ResourceManager.GetString("InfoBarDefaultMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unable to process Fix Suggestion in IDE request. Reason: {0}. + /// </summary> + internal static string InfoBarInvalidRequest { + get { + return ResourceManager.GetString("InfoBarInvalidRequest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Fix Suggestion in IDE. Could not locate the issue in the file. Please ensure the file ({0}) has not been modified.. + /// </summary> + internal static string InfoBarUnableToLocateFixSuggestion { + get { + return ResourceManager.GetString("InfoBarUnableToLocateFixSuggestion", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Fix Suggestion in IDE. Could not open File: {0}. Please ensure that you're on the correct branch and the file has not been deleted locally.. + /// </summary> + internal static string InfoBarUnableToOpenFile { + get { + return ResourceManager.GetString("InfoBarUnableToOpenFile", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to [Fix Suggestion in IDE] Processing request. Configuration scope: {0}, SuggestionId: {1}. + /// </summary> + internal static string ProcessingRequest { + get { + return ResourceManager.GetString("ProcessingRequest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to [Fix Suggestion in IDE] Fail to process request for configuration scope: {0}, SuggestionId: {1} due to {2}.. + /// </summary> + internal static string ProcessingRequestFailed { + get { + return ResourceManager.GetString("ProcessingRequestFailed", resourceCulture); + } + } + } +} diff --git a/src/IssueViz/FixSuggestion/FixSuggestionResources.resx b/src/IssueViz/FixSuggestion/FixSuggestionResources.resx new file mode 100644 index 0000000000..d3e7aff8d3 --- /dev/null +++ b/src/IssueViz/FixSuggestion/FixSuggestionResources.resx @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="DoneProcessingRequest" xml:space="preserve"> + <value>[Fix Suggestion in IDE] Done processing request. Configuration scope: {0}, SuggestionId: {1}</value> + </data> + <data name="ProcessingRequest" xml:space="preserve"> + <value>[Fix Suggestion in IDE] Processing request. Configuration scope: {0}, SuggestionId: {1}</value> + </data> + <data name="ProcessingRequestFailed" xml:space="preserve"> + <value>[Fix Suggestion in IDE] Fail to process request for configuration scope: {0}, SuggestionId: {1} due to {2}.</value> + </data> + <data name="GetConfigScopeRootPathFailed" xml:space="preserve"> + <value>[Fix Suggestion in IDE] Could not determine configuration scope root path for scope [{0}] due to: {1}</value> + </data> + <data name="InfoBarButtonMoreInfo" xml:space="preserve"> + <value>Learn more</value> + </data> + <data name="InfoBarButtonShowLogs" xml:space="preserve"> + <value>Show logs</value> + </data> + <data name="InfoBarDefaultMessage" xml:space="preserve"> + <value>Could not handle Open in IDE request. See the Logs for more information.</value> + </data> + <data name="InfoBarUnableToOpenFile" xml:space="preserve"> + <value>Fix Suggestion in IDE. Could not open File: {0}. Please ensure that you're on the correct branch and the file has not been deleted locally.</value> + </data> + <data name="InfoBarInvalidRequest" xml:space="preserve"> + <value>Unable to process Fix Suggestion in IDE request. Reason: {0}</value> + </data> + <data name="InfoBarUnableToLocateFixSuggestion" xml:space="preserve"> + <value>Fix Suggestion in IDE. Could not locate the issue in the file. Please ensure the file ({0}) has not been modified.</value> + </data> +</root> \ No newline at end of file diff --git a/src/IssueViz/FixSuggestion/IFixSuggestionHandler.cs b/src/IssueViz/FixSuggestion/IFixSuggestionHandler.cs new file mode 100644 index 0000000000..1c332843d8 --- /dev/null +++ b/src/IssueViz/FixSuggestion/IFixSuggestionHandler.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; + +namespace SonarLint.VisualStudio.IssueVisualization.FixSuggestion; + +public interface IFixSuggestionHandler +{ + void ApplyFixSuggestion(ShowFixSuggestionParams parameters); +} diff --git a/src/IssueViz/FixSuggestion/IFixSuggestionNotification.cs b/src/IssueViz/FixSuggestion/IFixSuggestionNotification.cs new file mode 100644 index 0000000000..c0fcb06f6d --- /dev/null +++ b/src/IssueViz/FixSuggestion/IFixSuggestionNotification.cs @@ -0,0 +1,150 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.InfoBar; + +namespace SonarLint.VisualStudio.IssueVisualization.FixSuggestion; + +public interface IFixSuggestionNotification +{ + Task UnableToOpenFileAsync(string filePath); + Task InvalidRequestAsync(string reason); + Task UnableToLocateIssueAsync(string filePath); + Task ShowAsync(string text); + Task ClearAsync(); +} + +[Export(typeof(IFixSuggestionNotification))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal sealed class FixSuggestionNotification : IFixSuggestionNotification, IDisposable +{ + private readonly IInfoBarManager infoBarManager; + private readonly IOutputWindowService outputWindowService; + private readonly IBrowserService browService; + private readonly IThreadHandling threadHandling; + private readonly object lockObject = new(); + private IInfoBar currentInfoBar; + + [ImportingConstructor] + public FixSuggestionNotification(IInfoBarManager infoBarManager, + IOutputWindowService outputWindowService, + IBrowserService browService, + IThreadHandling threadHandling) + { + this.infoBarManager = infoBarManager; + this.outputWindowService = outputWindowService; + this.browService = browService; + this.threadHandling = threadHandling; + } + + public async Task UnableToOpenFileAsync(string filePath) + { + var unableToOpenFileMsg = string.Format(FixSuggestionResources.InfoBarUnableToOpenFile, filePath); + await ShowAsync(unableToOpenFileMsg); + } + + public async Task InvalidRequestAsync(string reason) + { + var unableToOpenFileMsg = string.Format(FixSuggestionResources.InfoBarInvalidRequest, reason); + await ShowAsync(unableToOpenFileMsg); + } + + public async Task UnableToLocateIssueAsync(string filePath) + { + var unableToOpenFileMsg = string.Format(FixSuggestionResources.InfoBarUnableToLocateFixSuggestion, filePath); + await ShowAsync(unableToOpenFileMsg); + } + + public async Task ShowAsync(string text) + { + await threadHandling.RunOnUIThreadAsync(() => + { + lock (lockObject) + { + RemoveExistingInfoBar(); + AddInfoBar(text); + } + }); + } + + public async Task ClearAsync() + { + await threadHandling.RunOnUIThreadAsync(() => + { + lock (lockObject) + { + RemoveExistingInfoBar(); + } + }); + } + + private void AddInfoBar(string text) + { + string[] buttonTexts = [FixSuggestionResources.InfoBarButtonMoreInfo, FixSuggestionResources.InfoBarButtonShowLogs]; + var textToShow = text ?? FixSuggestionResources.InfoBarDefaultMessage; + currentInfoBar = infoBarManager.AttachInfoBarToMainWindow(textToShow, SonarLintImageMoniker.OfficialSonarLintMoniker, buttonTexts); + Debug.Assert(currentInfoBar != null, "currentInfoBar != null"); + + currentInfoBar.ButtonClick += HandleInfoBarAction; + currentInfoBar.Closed += CurrentInfoBar_Closed; + } + + private void HandleInfoBarAction(object sender, InfoBarButtonClickedEventArgs e) + { + if (e.ClickedButtonText == FixSuggestionResources.InfoBarButtonMoreInfo) + { + browService.Navigate(DocumentationLinks.OpenInIdeIssueLocation); + } + + if (e.ClickedButtonText == FixSuggestionResources.InfoBarButtonShowLogs) + { + outputWindowService.Show(); + } + } + + private void RemoveExistingInfoBar() + { + if (currentInfoBar != null) + { + currentInfoBar.ButtonClick -= HandleInfoBarAction; + currentInfoBar.Closed -= CurrentInfoBar_Closed; + infoBarManager.DetachInfoBar(currentInfoBar); + currentInfoBar = null; + } + } + + private void CurrentInfoBar_Closed(object sender, EventArgs e) + { + lock (lockObject) + { + RemoveExistingInfoBar(); + } + } + + public void Dispose() + { + lock (lockObject) + { + RemoveExistingInfoBar(); + } + } +} diff --git a/src/IssueViz/IssueViz.csproj b/src/IssueViz/IssueViz.csproj index 2a27e1ccae..5a20da8e84 100644 --- a/src/IssueViz/IssueViz.csproj +++ b/src/IssueViz/IssueViz.csproj @@ -53,12 +53,23 @@ <CopyToOutputDirectory>Never</CopyToOutputDirectory> </Page> + <Compile Update="FixSuggestion\FixSuggestionResources.Designer.cs"> + <DesignTime>True</DesignTime> + <AutoGen>True</AutoGen> + <DependentUpon>FixSuggestionResources.resx</DependentUpon> + </Compile> + <Compile Update="Resources.Designer.cs"> <DesignTime>True</DesignTime> <AutoGen>True</AutoGen> <DependentUpon>Resources.resx</DependentUpon> </Compile> + <EmbeddedResource Update="FixSuggestion\FixSuggestionResources.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>FixSuggestionResources.Designer.cs</LastGenOutput> + </EmbeddedResource> + <EmbeddedResource Update="Resources.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Resources.Designer.cs</LastGenOutput> diff --git a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs index e5749b93ec..8747db8bde 100644 --- a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs +++ b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs @@ -93,7 +93,7 @@ public void Start() var constantsProvider = Substitute.For<ISLCoreConstantsProvider>(); constantsProvider.ClientConstants.Returns(new ClientConstantsDto("SLVS_Integration_Tests", $"SLVS_Integration_Tests/{VersionHelper.SonarLintVersion}", Process.GetCurrentProcess().Id)); - constantsProvider.FeatureFlags.Returns(new FeatureFlagsDto(true, true, false, true, false, false, true, false)); + constantsProvider.FeatureFlags.Returns(new FeatureFlagsDto(true, true, false, true, false, false, true, false, false)); constantsProvider.TelemetryConstants.Returns(new TelemetryClientConstantAttributesDto("slvs_integration_tests", "SLVS Integration Tests", VersionHelper.SonarLintVersion, "17.0", new())); SetLanguagesConfigurationToDefaults(constantsProvider); diff --git a/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs b/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs index 191377faed..b85e671c23 100644 --- a/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; -using System.Threading.Tasks; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -66,5 +64,15 @@ public void DidChangeMatchedSonarProjectBranch_ReturnsTaskCompleted(object param result.Should().Be(Task.CompletedTask); } + + [TestMethod] + public async Task MatchProjectBranchAsync_ReturnsAlwaysTrue() + { + var testSubject = new BranchListener(); + + var response = await testSubject.MatchProjectBranchAsync(new MatchProjectBranchParams("my_config_scope_id", "the_branch_name")); + + response.Should().BeEquivalentTo(new MatchProjectBranchResponse(true)); + } } } diff --git a/src/SLCore.Listeners.UnitTests/Implementation/ShowFixSuggestionListenerTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/ShowFixSuggestionListenerTests.cs new file mode 100644 index 0000000000..23b8c5ecce --- /dev/null +++ b/src/SLCore.Listeners.UnitTests/Implementation/ShowFixSuggestionListenerTests.cs @@ -0,0 +1,70 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.IssueVisualization.FixSuggestion; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; +using FileEditDto = SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models.FileEditDto; + +namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation; + +[TestClass] +public class ShowFixSuggestionListenerTests +{ + private ShowFixSuggestionListener testSubject; + private IFixSuggestionHandler fixSuggestionHandler; + + [TestInitialize] + public void Initialize() + { + fixSuggestionHandler = Substitute.For<IFixSuggestionHandler>(); + testSubject = new ShowFixSuggestionListener(fixSuggestionHandler); + } + + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported<ShowFixSuggestionListener, ISLCoreListener>( + MefTestHelpers.CreateExport<IFixSuggestionHandler>()); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent<ShowFixSuggestionListener>(); + } + + [TestMethod] + public void ShowFixSuggestion_CallsHandler() + { + var listOfChanges = new List<ChangesDto> + { + new(new LineRangeDto(10, 10), "public void test()", "private void test()") + }; + var fileEditDto = new FileEditDto(@"C:\Users\test\TestProject\AFile.cs", listOfChanges); + var fixSuggestionDto = new FixSuggestionDto("SUGGESTION_ID", "AN EXPLANATION", fileEditDto); + var parameters = new ShowFixSuggestionParams("CONFIG_SCOPE_ID", "S1234", fixSuggestionDto); + + testSubject.ShowFixSuggestion(parameters); + + fixSuggestionHandler.Received(1).ApplyFixSuggestion(parameters); + } +} diff --git a/src/SLCore.Listeners/Implementation/BranchListener.cs b/src/SLCore.Listeners/Implementation/BranchListener.cs index 4a8ca833c3..b25421b6eb 100644 --- a/src/SLCore.Listeners/Implementation/BranchListener.cs +++ b/src/SLCore.Listeners/Implementation/BranchListener.cs @@ -18,9 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Threading.Tasks; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -49,5 +47,14 @@ public Task DidChangeMatchedSonarProjectBranchAsync(object parameters) { return Task.CompletedTask; } + + public Task<MatchProjectBranchResponse> MatchProjectBranchAsync(MatchProjectBranchParams parameters) + { + // At the moment we don't need to match the project branch as there is logic to handle the cases + // where there is a mismatch between the project branch and the server branch + // This is currently not fully supported because it depends on the showMessage method + // https://sonarsource.atlassian.net/browse/SLVS-1494 + return Task.FromResult(new MatchProjectBranchResponse(true)); + } } } diff --git a/src/SLCore.Listeners/Implementation/ShowFixSuggestionListener.cs b/src/SLCore.Listeners/Implementation/ShowFixSuggestionListener.cs new file mode 100644 index 0000000000..0a04d3ad1c --- /dev/null +++ b/src/SLCore.Listeners/Implementation/ShowFixSuggestionListener.cs @@ -0,0 +1,44 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.IssueVisualization.FixSuggestion; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; + +namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation; + +[Export(typeof(ISLCoreListener))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class ShowFixSuggestionListener : IShowFixSuggestionListener +{ + private readonly IFixSuggestionHandler fixSuggestionHandler; + + [ImportingConstructor] + public ShowFixSuggestionListener(IFixSuggestionHandler fixSuggestionHandler) + { + this.fixSuggestionHandler = fixSuggestionHandler; + } + + public void ShowFixSuggestion(ShowFixSuggestionParams parameters) + { + fixSuggestionHandler.ApplyFixSuggestion(parameters); + } +} diff --git a/src/SLCore.UnitTests/Listener/Branch/MatchProjectBranchParamsTests.cs b/src/SLCore.UnitTests/Listener/Branch/MatchProjectBranchParamsTests.cs new file mode 100644 index 0000000000..e5d0e8c561 --- /dev/null +++ b/src/SLCore.UnitTests/Listener/Branch/MatchProjectBranchParamsTests.cs @@ -0,0 +1,45 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Listener.Branch; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Branch; + +[TestClass] +public class MatchProjectBranchParamsTests +{ + [TestMethod] + public void Deserialize_AsExpected() + { + var expectedObject = new MatchProjectBranchParams("CONFIG_SCOPE_ID", "remote-branch-name"); + + const string serializedParams = """ + { + "configurationScopeId": "CONFIG_SCOPE_ID", + "serverBranchToMatch": "remote-branch-name" + } + """; + + var deserializedObject = JsonConvert.DeserializeObject<MatchProjectBranchParams>(serializedParams); + + deserializedObject.Should().Be(expectedObject); + } +} diff --git a/src/SLCore.UnitTests/Listener/Branch/MatchProjectBranchResponseTests.cs b/src/SLCore.UnitTests/Listener/Branch/MatchProjectBranchResponseTests.cs new file mode 100644 index 0000000000..fee99eccf9 --- /dev/null +++ b/src/SLCore.UnitTests/Listener/Branch/MatchProjectBranchResponseTests.cs @@ -0,0 +1,46 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Listener.Branch; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Branch; + +[TestClass] +public class MatchProjectBranchResponseTests +{ + [TestMethod] + [DataRow(true, "true")] + [DataRow(false, "false")] + public void Serialize_AsExpected(bool isBranchMatched, string expectedResponse) + { + var testSubject = new MatchProjectBranchResponse(isBranchMatched); + + var expectedString = $$""" + { + "isBranchMatched": {{expectedResponse}} + } + """; + + var serializedString = JsonConvert.SerializeObject(testSubject, Formatting.Indented); + + serializedString.Should().Be(expectedString); + } +} diff --git a/src/SLCore.UnitTests/Listener/FixSuggestion/ShowFixSuggestionParamsTests.cs b/src/SLCore.UnitTests/Listener/FixSuggestion/ShowFixSuggestionParamsTests.cs new file mode 100644 index 0000000000..e8aff94c84 --- /dev/null +++ b/src/SLCore.UnitTests/Listener/FixSuggestion/ShowFixSuggestionParamsTests.cs @@ -0,0 +1,70 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; +using FileEditDto = SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models.FileEditDto; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.FixSuggestion; + +[TestClass] +public class ShowFixSuggestionParamsTests +{ + [TestMethod] + public void Serialize_AsExpected() + { + var listOfChanges = new List<ChangesDto> + { + new(new LineRangeDto(10, 10), "public void test()", "private void test()") + }; + var fileEditDto = new FileEditDto(@"C:\Users\test\TestProject\AFile.cs", listOfChanges); + var fixSuggestionDto = new FixSuggestionDto("SUGGESTION_ID", "AN EXPLANATION", fileEditDto); + var testSubject = new ShowFixSuggestionParams("CONFIG_SCOPE_ID", "S1234", fixSuggestionDto); + + const string expectedString = """ + { + "configurationScopeId": "CONFIG_SCOPE_ID", + "issueKey": "S1234", + "fixSuggestion": { + "suggestionId": "SUGGESTION_ID", + "explanation": "AN EXPLANATION", + "fileEdit": { + "idePath": "C:\\Users\\test\\TestProject\\AFile.cs", + "changes": [ + { + "beforeLineRange": { + "startLine": 10, + "endLine": 10 + }, + "before": "public void test()", + "after": "private void test()" + } + ] + } + } + } + """; + + var serializedString = JsonConvert.SerializeObject(testSubject, Formatting.Indented); + + serializedString.Should().Be(expectedString); + } +} diff --git a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs index b410c59204..179d0faa7c 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs @@ -44,7 +44,7 @@ public class SLCoreInstanceHandleTests private const string UserHome = "userHomeSl"; private static readonly ClientConstantsDto ClientConstants = new(default, default, default); - private static readonly FeatureFlagsDto FeatureFlags = new(default, default, default, default, default, default, default, default); + private static readonly FeatureFlagsDto FeatureFlags = new(default, default, default, default, default, default, default, default, default); private static readonly TelemetryClientConstantAttributesDto TelemetryConstants = new(default, default, default, default, default); private static readonly SonarQubeConnectionConfigurationDto SonarQubeConnection1 = new("sq1", true, "http://localhost/"); diff --git a/src/SLCore.UnitTests/Service/Lifecycle/InitializeParamsTests.cs b/src/SLCore.UnitTests/Service/Lifecycle/InitializeParamsTests.cs index cb417992de..0771e99e17 100644 --- a/src/SLCore.UnitTests/Service/Lifecycle/InitializeParamsTests.cs +++ b/src/SLCore.UnitTests/Service/Lifecycle/InitializeParamsTests.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Service.Connection.Models; @@ -37,7 +36,7 @@ public void Serialize_AsExpected() var testSubject = new InitializeParams( new ClientConstantsDto("TESTname", "TESTagent", 11223344), new HttpConfigurationDto(new SslConfigurationDto()), - new FeatureFlagsDto(false, true, false, true, false, true, false, false), + new FeatureFlagsDto(false, true, false, true, false, true, false, false, false), "storageRoot", "workDir", ["myplugin1", "myplugin2"], @@ -77,7 +76,8 @@ [new SonarCloudConnectionConfigurationDto("con2", false, "organization1")], "shouldManageServerSentEvents": false, "enableDataflowBugDetection": true, "shouldManageFullSynchronization": false, - "enableTelemetry": false + "enableTelemetry": false, + "canOpenFixSuggestion": false }, "storageRoot": "storageRoot", "workDir": "workDir", diff --git a/src/SLCore/Listener/Branch/IBranchListener.cs b/src/SLCore/Listener/Branch/IBranchListener.cs index 9fda6881ef..1e335f0681 100644 --- a/src/SLCore/Listener/Branch/IBranchListener.cs +++ b/src/SLCore/Listener/Branch/IBranchListener.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Threading.Tasks; using SonarLint.VisualStudio.SLCore.Core; namespace SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -38,4 +37,12 @@ public interface IBranchListener : ISLCoreListener /// <param name="parameters">Parameter's here for compability we discard it</param> /// <remarks>This will be implemented properly in the future when needed but features we support does not need branch awareness for now</remarks> Task DidChangeMatchedSonarProjectBranchAsync(object parameters); + + /// <summary> + /// Used for checking whether a locally checked out branch matches a candidate branch name (not necessarily a Sonar branch). + /// For example, in "show fix suggestion" use-case, to match a local branch with a PR branch that originated a fix suggestion + /// </summary> + /// <param name="parameters">The remote branch details to match</param> + /// <returns>Is local branch matching the remote branch</returns> + Task<MatchProjectBranchResponse> MatchProjectBranchAsync(MatchProjectBranchParams parameters); } diff --git a/src/SLCore/Listener/Branch/MatchProjectBranchParams.cs b/src/SLCore/Listener/Branch/MatchProjectBranchParams.cs new file mode 100644 index 0000000000..eb41a08a37 --- /dev/null +++ b/src/SLCore/Listener/Branch/MatchProjectBranchParams.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.Branch; + +public record MatchProjectBranchParams(string configurationScopeId, string serverBranchToMatch); diff --git a/src/SLCore/Listener/Branch/MatchProjectBranchResponse.cs b/src/SLCore/Listener/Branch/MatchProjectBranchResponse.cs new file mode 100644 index 0000000000..e3ab7e9fa3 --- /dev/null +++ b/src/SLCore/Listener/Branch/MatchProjectBranchResponse.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.Branch; + +public record MatchProjectBranchResponse(bool isBranchMatched); diff --git a/src/SLCore/Listener/FixSuggestion/IShowFixSuggestionListener.cs b/src/SLCore/Listener/FixSuggestion/IShowFixSuggestionListener.cs new file mode 100644 index 0000000000..d9a8804ad4 --- /dev/null +++ b/src/SLCore/Listener/FixSuggestion/IShowFixSuggestionListener.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Core; + +namespace SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; + +public interface IShowFixSuggestionListener : ISLCoreListener +{ + void ShowFixSuggestion(ShowFixSuggestionParams parameters); +} diff --git a/src/SLCore/Listener/FixSuggestion/Models/ChangesDto.cs b/src/SLCore/Listener/FixSuggestion/Models/ChangesDto.cs new file mode 100644 index 0000000000..ae58b02f17 --- /dev/null +++ b/src/SLCore/Listener/FixSuggestion/Models/ChangesDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; + +public record ChangesDto(LineRangeDto beforeLineRange, string before, string after); diff --git a/src/SLCore/Listener/FixSuggestion/Models/FileEditDto.cs b/src/SLCore/Listener/FixSuggestion/Models/FileEditDto.cs new file mode 100644 index 0000000000..ba666ff98b --- /dev/null +++ b/src/SLCore/Listener/FixSuggestion/Models/FileEditDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; + +public record FileEditDto(string idePath, List<ChangesDto> changes); diff --git a/src/SLCore/Listener/FixSuggestion/Models/FixSuggestionDto.cs b/src/SLCore/Listener/FixSuggestion/Models/FixSuggestionDto.cs new file mode 100644 index 0000000000..329fa5fc9a --- /dev/null +++ b/src/SLCore/Listener/FixSuggestion/Models/FixSuggestionDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; + +public record FixSuggestionDto(string suggestionId, string explanation, FileEditDto fileEdit); diff --git a/src/SLCore/Listener/FixSuggestion/Models/LineRangeDto.cs b/src/SLCore/Listener/FixSuggestion/Models/LineRangeDto.cs new file mode 100644 index 0000000000..2c4927cd4a --- /dev/null +++ b/src/SLCore/Listener/FixSuggestion/Models/LineRangeDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; + +public record LineRangeDto(int startLine, int endLine); diff --git a/src/SLCore/Listener/FixSuggestion/ShowFixSuggestionParams.cs b/src/SLCore/Listener/FixSuggestion/ShowFixSuggestionParams.cs new file mode 100644 index 0000000000..f61e5d553f --- /dev/null +++ b/src/SLCore/Listener/FixSuggestion/ShowFixSuggestionParams.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; + +namespace SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; + +public record ShowFixSuggestionParams(string configurationScopeId, string issueKey, FixSuggestionDto fixSuggestion); diff --git a/src/SLCore/Service/Lifecycle/Models/FeatureFlagsDto.cs b/src/SLCore/Service/Lifecycle/Models/FeatureFlagsDto.cs index 3391e33b5e..b289c850cc 100644 --- a/src/SLCore/Service/Lifecycle/Models/FeatureFlagsDto.cs +++ b/src/SLCore/Service/Lifecycle/Models/FeatureFlagsDto.cs @@ -28,5 +28,6 @@ public record FeatureFlagsDto( bool shouldManageServerSentEvents, bool enableDataflowBugDetection, bool shouldManageFullSynchronization, - bool enableTelemetry); + bool enableTelemetry, + bool canOpenFixSuggestion); }