diff --git a/src/Core.UnitTests/FocusOnNewCodeServiceTests.cs b/src/IssueViz.UnitTests/NewCode/FocusOnNewCodeServiceTests.cs similarity index 69% rename from src/Core.UnitTests/FocusOnNewCodeServiceTests.cs rename to src/IssueViz.UnitTests/NewCode/FocusOnNewCodeServiceTests.cs index 5583d77f2b..f533bf1032 100644 --- a/src/Core.UnitTests/FocusOnNewCodeServiceTests.cs +++ b/src/IssueViz.UnitTests/NewCode/FocusOnNewCodeServiceTests.cs @@ -19,11 +19,15 @@ */ using System.ComponentModel.Composition.Primitives; +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Initialization; using SonarLint.VisualStudio.Integration; +using SonarLint.VisualStudio.IssueVisualization.NewCode; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.NewCode; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Core.UnitTests; +namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.NewCode; [TestClass] public class FocusOnNewCodeServiceTests @@ -31,6 +35,9 @@ public class FocusOnNewCodeServiceTests private ISonarLintSettings sonarLintSettings; private NoOpThreadHandler threadHandling; private IInitializationProcessorFactory initializationProcessorFactory; + private NoOpThreadHandler threadHandler; + private ISLCoreServiceProvider serviceProvider; + private INewCodeSLCoreService newCodeSlCoreService; private FocusOnNewCodeService testSubject; [TestInitialize] @@ -42,7 +49,11 @@ private void TestInitialize(bool isEnabled) sonarLintSettings.IsFocusOnNewCodeEnabled.Returns(isEnabled); threadHandling = Substitute.ForPartsOf(); initializationProcessorFactory = MockableInitializationProcessor.CreateFactory(threadHandling, Substitute.ForPartsOf()); - testSubject = new FocusOnNewCodeService(sonarLintSettings, initializationProcessorFactory); + threadHandler = Substitute.ForPartsOf(); + serviceProvider = Substitute.For(); + newCodeSlCoreService = Substitute.For(); + SetUpSlCoreService(true); + testSubject = new FocusOnNewCodeService(sonarLintSettings, initializationProcessorFactory, serviceProvider, threadHandler); testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); } @@ -52,7 +63,9 @@ public void MefCtor_CheckIsExported() Export[] exports = [ MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport() + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), ]; MefTestHelpers.CheckTypeCanBeImported(exports); @@ -79,6 +92,7 @@ public void Ctor_InitializesCorrectly(bool isEnabled) _ = sonarLintSettings.IsFocusOnNewCodeEnabled; // this doesn't actually assert anything due to how NSub works, but is left here to make the test easier to understand testSubject.InitializationProcessor.InitializeAsync(); // from CreateTestSubject }); + serviceProvider.DidNotReceiveWithAnyArgs().TryGetTransientService(out Arg.Any()); } [DataTestMethod] @@ -95,4 +109,31 @@ public void Set_UpdatesSettingAndRaisesEvent(bool isEnabled) testSubject.Current.IsEnabled.Should().Be(isEnabled); handler.Received(1).Invoke(testSubject, Arg.Is(e => e.NewStatus.IsEnabled == isEnabled)); } + + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public void Set_NotifiesSlCore(bool isSlCoreInitialized) + { + SetUpSlCoreService(isSlCoreInitialized); + + testSubject.Set(true); + + Received.InOrder(() => + { + threadHandling.RunOnBackgroundThread(Arg.Any>>()); + serviceProvider.TryGetTransientService(out Arg.Any()); + if (isSlCoreInitialized) + { + newCodeSlCoreService.DidToggleFocus(); + } + }); + } + + private void SetUpSlCoreService(bool isInitialized) => + serviceProvider.TryGetTransientService(out Arg.Any()).Returns(info => + { + info[0] = newCodeSlCoreService; + return isInitialized; + }); } diff --git a/src/Core/FocusOnNewCodeService.cs b/src/IssueViz/NewCode/FocusOnNewCodeService.cs similarity index 68% rename from src/Core/FocusOnNewCodeService.cs rename to src/IssueViz/NewCode/FocusOnNewCodeService.cs index 596311fd22..4702d6f677 100644 --- a/src/Core/FocusOnNewCodeService.cs +++ b/src/IssueViz/NewCode/FocusOnNewCodeService.cs @@ -19,10 +19,15 @@ */ using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Initialization; using SonarLint.VisualStudio.Integration; +using SonarLint.VisualStudio.SLCore; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.NewCode; -namespace SonarLint.VisualStudio.Core; +namespace SonarLint.VisualStudio.IssueVisualization.NewCode; [Export(typeof(IFocusOnNewCodeService))] [Export(typeof(IFocusOnNewCodeServiceUpdater))] @@ -30,11 +35,18 @@ namespace SonarLint.VisualStudio.Core; public class FocusOnNewCodeService : IFocusOnNewCodeServiceUpdater { private readonly ISonarLintSettings sonarLintSettings; + private readonly ISLCoreServiceProvider slCoreServiceProvider; + private readonly IThreadHandling threadHandling; [ImportingConstructor] - public FocusOnNewCodeService(ISonarLintSettings sonarLintSettings, IInitializationProcessorFactory initializationProcessorFactory) + public FocusOnNewCodeService(ISonarLintSettings sonarLintSettings, + IInitializationProcessorFactory initializationProcessorFactory, + ISLCoreServiceProvider slCoreServiceProvider, + IThreadHandling threadHandling) { this.sonarLintSettings = sonarLintSettings; + this.slCoreServiceProvider = slCoreServiceProvider; + this.threadHandling = threadHandling; InitializationProcessor = initializationProcessorFactory.CreateAndStart([], () => { // sonarLintSettings needs UI thread to initialize settings storage, so the first property access may not be free-threaded @@ -49,8 +61,18 @@ public void Set(bool isEnabled) { sonarLintSettings.IsFocusOnNewCodeEnabled = isEnabled; Current = new(sonarLintSettings.IsFocusOnNewCodeEnabled); + NotifySlCoreNewCodeToggled(); Changed?.Invoke(this, new(Current)); } + private void NotifySlCoreNewCodeToggled() => + threadHandling.RunOnBackgroundThread(() => + { + if (slCoreServiceProvider.TryGetTransientService(out INewCodeSLCoreService newCodeService)) + { + newCodeService.DidToggleFocus(); + } + }).Forget(); + public event EventHandler Changed; } diff --git a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs index 393a5311d4..9182117a43 100644 --- a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs +++ b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs @@ -124,6 +124,9 @@ public async Task Start(TestLogger testLogger) noOpActiveSolutionBoundTracker.CurrentConfiguration.Returns(BindingConfiguration.Standalone); var noOpConfigScopeUpdater = Substitute.For(); + var focusOnNewCodeService = Substitute.For(); + focusOnNewCodeService.Current.Returns(new FocusOnNewCodeStatus(false)); + slCoreInstanceHandle = new SLCoreInstanceHandle(new SLCoreRpcFactory(slCoreTestProcessFactory, slCoreLocator, new SLCoreJsonRpcFactory(new RpcMethodNameTransformer()), new RpcDebugger(new FileSystem(), Path.Combine(privateFolder, "logrpc.log")), @@ -141,6 +144,7 @@ public async Task Start(TestLogger testLogger) noOpConfigScopeUpdater, slCoreRulesSettingsProvider, Substitute.For(), + focusOnNewCodeService, new NoOpThreadHandler()); await InitializeAndWaitForSloopLog(testLogger); @@ -157,7 +161,7 @@ private async Task InitializeAndWaitForSloopLog(TestLogger slCoreLogger) EventHandler eventHandler = (_, _) => tcs.TrySetResult(true); slCoreLogger.LogMessageAdded += eventHandler; - slCoreInstanceHandle.Initialize(); + await slCoreInstanceHandle.InitializeAsync(); try { diff --git a/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs b/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs index b84cf49a5c..d97a061e3f 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs @@ -50,6 +50,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -76,6 +77,7 @@ public void CreateInstance_ReturnsNonNull() var threadHandling = Substitute.For(); var slCoreRuleSettingsProvider = Substitute.For(); var telemetryMigrationProvider = Substitute.For(); + var focusOnNewCodeService = Substitute.For(); var testSubject = new SLCoreInstanceFactory( islCoreRpcFactory, @@ -91,6 +93,7 @@ public void CreateInstance_ReturnsNonNull() slCoreRuleSettingsProvider, telemetryMigrationProvider, esLintBridgeLocator, + focusOnNewCodeService, threadHandling); testSubject.CreateInstance().Should().NotBeNull(); diff --git a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs index 77b28e69ba..d3638b204c 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs @@ -72,6 +72,7 @@ public class SLCoreInstanceHandleTests private ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider; private SLCoreInstanceHandle testSubject; private ISlCoreTelemetryMigrationProvider telemetryMigrationProvider; + private IFocusOnNewCodeService focusOnNewCodeService; [TestInitialize] public void TestInitialize() @@ -90,6 +91,8 @@ public void TestInitialize() threadHandling = Substitute.ForPartsOf(); slCoreRuleSettingsProvider = Substitute.For(); telemetryMigrationProvider = Substitute.For(); + focusOnNewCodeService = Substitute.For(); + focusOnNewCodeService.Current.Returns(new FocusOnNewCodeStatus(false)); testSubject = new SLCoreInstanceHandle( slCoreRpcFactory, @@ -105,6 +108,7 @@ public void TestInitialize() configScopeUpdater, slCoreRuleSettingsProvider, telemetryMigrationProvider, + focusOnNewCodeService, threadHandling); } @@ -116,7 +120,7 @@ public void Initialize_RpcManagerThrows_DoesNotCatch() var exception = new Exception(exceptionMessage); rpcManager.When(x => x.Initialize(Arg.Any())).Throw(exception); - var act = () => testSubject.Initialize(); + var act = () => testSubject.InitializeAsync(); act.Should().ThrowExactly().WithMessage(exceptionMessage); } @@ -124,7 +128,7 @@ public void Initialize_RpcManagerThrows_DoesNotCatch() [DataTestMethod] [DataRow("some/node/path", "vsix/esLintBridge")] [DataRow(null, null)] - public void Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, string esLintBridgePath) + public async Task Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, string esLintBridgePath) { SetUpLanguages([], []); SetUpFullConfiguration(out _); @@ -133,7 +137,7 @@ public void Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, var telemetryMigrationDto = new TelemetryMigrationDto(default, default, default); telemetryMigrationProvider.Get().Returns(telemetryMigrationDto); - testSubject.Initialize(); + await testSubject.InitializeAsync(); Received.InOrder(() => { @@ -170,7 +174,7 @@ public void Initialize_UsesProvidedLanguageConfiguration() SetUpLanguages(standalone, connected); SetUpFullConfiguration(out _); - testSubject.Initialize(); + testSubject.InitializeAsync(); var initializeParams = (InitializeParams)rpcManager.ReceivedCalls().Single().GetArguments().Single()!; initializeParams.enabledLanguagesInStandaloneMode.Should().BeSameAs(standalone); @@ -183,7 +187,7 @@ public void Initialize_ProvidesRulesSettings() SetUpFullConfiguration(out _); slCoreRuleSettingsProvider.GetSLCoreRuleSettings().Returns(new Dictionary() { { "rule1", new StandaloneRuleConfigDto(true, []) } }); - testSubject.Initialize(); + testSubject.InitializeAsync(); rpcManager.Received(1).Initialize(Arg.Is(param => param.standaloneRuleConfigByKey.SequenceEqual(slCoreRuleSettingsProvider.GetSLCoreRuleSettings()))); } @@ -194,7 +198,7 @@ public void Dispose_Initialized_ShutsDownAndDisposesRpc() SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); - testSubject.Initialize(); + testSubject.InitializeAsync(); rpcManager.ClearReceivedCalls(); testSubject.Dispose(); @@ -215,7 +219,7 @@ public void Dispose_IgnoresShutdownException() SetUpFullConfiguration(out var rpc); rpcManager.When(x => x.Shutdown()).Do(_ => throw new Exception()); - testSubject.Initialize(); + testSubject.InitializeAsync(); rpcManager.ClearReceivedCalls(); var act = () => testSubject.Dispose(); @@ -229,7 +233,7 @@ public void Dispose_ConnectionDied_DisposesRpc() SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); - testSubject.Initialize(); + testSubject.InitializeAsync(); rpcManager.ClearSubstitute(); rpcManager.ClearReceivedCalls(); diff --git a/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs b/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs index e86ac6153e..3294906687 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs @@ -60,7 +60,7 @@ public void StartInstance_UpdatesCounterAndInitializesOnABackgroundThread() { threadHandling.ThrowIfOnUIThread(); factory.CreateInstance(); - handle.Initialize(); + handle.InitializeAsync(); _ = handle.ShutdownTask; }); } @@ -109,7 +109,7 @@ public async Task StartInstance_InstanceDies_RaisesEventAndResets() { threadHandling.ThrowIfOnUIThread(); factory.CreateInstance(); - handle.Initialize(); + handle.InitializeAsync(); _ = handle.ShutdownTask; handle.Dispose(); activeConfigScopeTracker.Reset(); @@ -142,7 +142,7 @@ public void StartInstance_InstanceInitializationThrows_RaisesEventAndResets() { var slCoreHandler = CreateTestSubject(out var factory, out var threadHandling, out var logger, out var activeConfigScopeTracker, out _); SetUpHandleFactory(factory, out var handle, out _); - handle.When(x => x.Initialize()).Do(_ => throw new Exception()); + handle.When(x => x.InitializeAsync()).Do(_ => throw new Exception()); var task = slCoreHandler.StartInstanceAsync(); @@ -157,7 +157,7 @@ public void StartInstance_InstanceInitializationThrows_RaisesEventAndResets() { threadHandling.ThrowIfOnUIThread(); factory.CreateInstance(); - handle.Initialize(); + handle.InitializeAsync(); handle.Dispose(); activeConfigScopeTracker.Reset(); }); diff --git a/src/SLCore/ISLCoreInstanceFactory.cs b/src/SLCore/ISLCoreInstanceFactory.cs index 7e79441a4e..4023d39656 100644 --- a/src/SLCore/ISLCoreInstanceFactory.cs +++ b/src/SLCore/ISLCoreInstanceFactory.cs @@ -54,6 +54,7 @@ internal class SLCoreInstanceFactory : ISLCoreInstanceFactory private readonly ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider; private readonly ISlCoreTelemetryMigrationProvider telemetryMigrationProvider; private readonly IEsLintBridgeLocator esLintBridgeLocator; + private readonly IFocusOnNewCodeService focusOnNewCodeService; [ImportingConstructor] public SLCoreInstanceFactory( @@ -70,6 +71,7 @@ public SLCoreInstanceFactory( ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider, ISlCoreTelemetryMigrationProvider telemetryMigrationProvider, IEsLintBridgeLocator esLintBridgeLocator, + IFocusOnNewCodeService focusOnNewCodeService, IThreadHandling threadHandling) { this.slCoreRpcFactory = slCoreRpcFactory; @@ -85,6 +87,7 @@ public SLCoreInstanceFactory( this.slCoreRuleSettingsProvider = slCoreRuleSettingsProvider; this.telemetryMigrationProvider = telemetryMigrationProvider; this.esLintBridgeLocator = esLintBridgeLocator; + this.focusOnNewCodeService = focusOnNewCodeService; this.threadHandling = threadHandling; } @@ -103,5 +106,6 @@ public ISLCoreInstanceHandle CreateInstance() => configScopeUpdater, slCoreRuleSettingsProvider, telemetryMigrationProvider, + focusOnNewCodeService, threadHandling); } diff --git a/src/SLCore/ISLCoreInstanceHandle.cs b/src/SLCore/ISLCoreInstanceHandle.cs index b797ad32ed..0140ca1d60 100644 --- a/src/SLCore/ISLCoreInstanceHandle.cs +++ b/src/SLCore/ISLCoreInstanceHandle.cs @@ -36,7 +36,7 @@ namespace SonarLint.VisualStudio.SLCore; internal interface ISLCoreInstanceHandle : IDisposable { - void Initialize(); + Task InitializeAsync(); Task ShutdownTask { get; } } @@ -54,6 +54,7 @@ internal sealed class SLCoreInstanceHandle : ISLCoreInstanceHandle private readonly ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginJarProvider; private readonly ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider; private readonly ISlCoreTelemetryMigrationProvider telemetryMigrationProvider; + private readonly IFocusOnNewCodeService focusOnNewCodeService; private readonly IEsLintBridgeLocator esLintBridgeLocator; private readonly INodeLocationProvider nodeLocator; private readonly IThreadHandling threadHandling; @@ -74,6 +75,7 @@ internal SLCoreInstanceHandle( IConfigScopeUpdater configScopeUpdater, ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider, ISlCoreTelemetryMigrationProvider telemetryMigrationProvider, + IFocusOnNewCodeService focusOnNewCodeService, IThreadHandling threadHandling) { this.slCoreRpcFactory = slCoreRpcFactory; @@ -89,13 +91,16 @@ internal SLCoreInstanceHandle( this.threadHandling = threadHandling; this.slCoreRuleSettingsProvider = slCoreRuleSettingsProvider; this.telemetryMigrationProvider = telemetryMigrationProvider; + this.focusOnNewCodeService = focusOnNewCodeService; this.esLintBridgeLocator = esLintBridgeLocator; } - public void Initialize() + public async Task InitializeAsync() { threadHandling.ThrowIfOnUIThread(); + await focusOnNewCodeService.InitializationProcessor.InitializeAsync(); + SLCoreRpc = slCoreRpcFactory.StartNewRpcInstance(); var serverConnectionConfigurations = serverConnectionConfigurationProvider.GetServerConnections(); @@ -116,7 +121,7 @@ public void Initialize() serverConnectionConfigurations.Values.OfType().ToList(), sonarlintUserHome, standaloneRuleConfigByKey: slCoreRuleSettingsProvider.GetSLCoreRuleSettings(), - isFocusOnNewCode: false, + isFocusOnNewCode: focusOnNewCodeService.Current.IsEnabled, constantsProvider.TelemetryConstants, telemetryMigrationProvider.Get(), new LanguageSpecificRequirements(new JsTsRequirementsDto(nodeLocator.Get(), esLintBridgeLocator.Get())), @@ -124,7 +129,7 @@ public void Initialize() slCoreRpcManager.Initialize(initializationParams); - UpdateConfigurationScopeForCurrentSolutionAsync().Forget(); + await UpdateConfigurationScopeForCurrentSolutionAsync(); } private async Task UpdateConfigurationScopeForCurrentSolutionAsync() diff --git a/src/SLCore/ISLCoreInstanceHandler.cs b/src/SLCore/ISLCoreInstanceHandler.cs index c1412b921f..dcdea215e5 100644 --- a/src/SLCore/ISLCoreInstanceHandler.cs +++ b/src/SLCore/ISLCoreInstanceHandler.cs @@ -103,7 +103,7 @@ private async Task LaunchInstanceAsync() try { logger.WriteLine(SLCoreStrings.SLCoreHandler_StartingInstance); - currentInstanceHandle.Initialize(); + await currentInstanceHandle.InitializeAsync(); await currentInstanceHandle.ShutdownTask; } catch (Exception e) diff --git a/src/SLCore/Service/NewCode/INewCodeSLCoreService.cs b/src/SLCore/Service/NewCode/INewCodeSLCoreService.cs new file mode 100644 index 0000000000..f250dd181e --- /dev/null +++ b/src/SLCore/Service/NewCode/INewCodeSLCoreService.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource Sàrl + * 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; +using SonarLint.VisualStudio.SLCore.Protocol; + +namespace SonarLint.VisualStudio.SLCore.Service.NewCode; + +[JsonRpcClass("newCode")] +public interface INewCodeSLCoreService : ISLCoreService +{ + void DidToggleFocus(); +}