From 3a6f460f7efe694fdf867167128fb6803cbbfb06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:00:15 -0400 Subject: [PATCH 01/11] chore(deps): bump actions/setup-java in /.github/workflows (#124) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v6.0.0...v6.0.1) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 623f85c..0d3532d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -91,7 +91,7 @@ jobs: dotnet-version: '9.x' - name: install Java JDK - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: java-version: 17 architecture: 'x64' From 05907ecea376dc907e2f279cca23ffa9e4a05dab Mon Sep 17 00:00:00 2001 From: Yatmund Christiansen Date: Sat, 19 Sep 2026 14:01:31 +0100 Subject: [PATCH 02/11] fix: navigate to handler from a Send/Publish call site and stop truncating the handler search (#121) --- CHANGELOG.md | 7 ++ .../Actions/GoToHandlerAction.cs | 15 ++-- .../Actions/GoToHandlerNavigationAction.cs | 11 +-- ...MediatorRequestNavigateFromHereProvider.cs | 8 +- .../Psi/Search/InheritorsConsumer.cs | 2 +- .../ReSharper/Psi/Tree/MediatorCallSite.cs | 81 +++++++++++++++++++ .../Services/Find/HandlerSelector.cs | 9 ++- .../Services/Find/IHandlerSelector.cs | 2 +- 8 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 src/dotnet/MediatorPlugin/ReSharper/Psi/Tree/MediatorCallSite.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d3fef31..50abfea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. +## 26.09.04XX +- "Go to Handler" now also works with the caret anywhere inside a `mediator.Send(...)` / + `Publish(...)` call: the first argument's type is resolved, so there is no need to navigate to the + request declaration first. +- Raise the inheritor search cap from 50 to 300 so solutions with many handlers no longer have the + matching handler truncated away before it can be selected. + ## 26.08.20XX - Fix issue #117: updated SDK to 2026.2. diff --git a/src/dotnet/MediatorPlugin/Actions/GoToHandlerAction.cs b/src/dotnet/MediatorPlugin/Actions/GoToHandlerAction.cs index 28e572b..397b53c 100644 --- a/src/dotnet/MediatorPlugin/Actions/GoToHandlerAction.cs +++ b/src/dotnet/MediatorPlugin/Actions/GoToHandlerAction.cs @@ -9,6 +9,7 @@ using JetBrains.TextControl; using JetBrains.Util; using ReSharper.MediatorPlugin.Diagnostics; +using ReSharper.MediatorPlugin.ReSharper.Psi.Tree; using ReSharper.MediatorPlugin.Services.Find; using ReSharper.MediatorPlugin.Services.Navigation; @@ -81,14 +82,12 @@ protected override Action ExecutePsiTransaction(ISolution solution private static IIdentifier? GetSelectedMediatrRequest(LanguageIndependentContextActionDataProvider dataProvider) { var selectedTreeNode = dataProvider.GetSelectedTreeNode(); - - if (selectedTreeNode is IIdentifier identifier) - { - Logger.Instance.Log(LoggingLevel.VERBOSE, $"Selected tree node is an identifier: {identifier.Name}"); - return identifier; - } - Logger.Instance.Log(LoggingLevel.VERBOSE, "Selected tree node is not an identifier"); - return null; + IIdentifier? identifier = MediatorCallSite.ResolveRequestIdentifier(selectedTreeNode); + + if (identifier is null) + Logger.Instance.Log(LoggingLevel.VERBOSE, "Selected tree node is not a Mediator request or dispatch call"); + + return identifier; } } \ No newline at end of file diff --git a/src/dotnet/MediatorPlugin/Actions/GoToHandlerNavigationAction.cs b/src/dotnet/MediatorPlugin/Actions/GoToHandlerNavigationAction.cs index f39e2dc..d670311 100644 --- a/src/dotnet/MediatorPlugin/Actions/GoToHandlerNavigationAction.cs +++ b/src/dotnet/MediatorPlugin/Actions/GoToHandlerNavigationAction.cs @@ -7,6 +7,7 @@ using JetBrains.ReSharper.Feature.Services.Navigation.ContextNavigation; using JetBrains.ReSharper.Psi.Tree; using ReSharper.MediatorPlugin.Diagnostics; +using ReSharper.MediatorPlugin.ReSharper.Psi.Tree; using ReSharper.MediatorPlugin.Services.Find; using ReSharper.MediatorPlugin.Services.Navigation; @@ -23,7 +24,9 @@ public bool Update(IDataContext context, ActionPresentation presentation, Delega { var selectedTreeNode = context.GetSelectedTreeNode(); - if (selectedTreeNode is not IIdentifier identifier) + IIdentifier? identifier = MediatorCallSite.ResolveRequestIdentifier(selectedTreeNode); + + if (identifier is null) return nextUpdate.Invoke(); return _handlerSelector.IsMediatorRequestSupported(identifier); @@ -36,12 +39,6 @@ public void Execute(IDataContext context, DelegateExecute nextExecute) var solution = context.GetComponent(); var selectedTreeNode = context.GetSelectedTreeNode(); - if (selectedTreeNode is not IIdentifier) - { - Logger.Instance.Log(LoggingLevel.VERBOSE, "Selected element is not an identifier"); - return; - } - _handlerSelector.NavigateToHandler ( solution, diff --git a/src/dotnet/MediatorPlugin/Navigations/Providers/MediatorRequestNavigateFromHereProvider.cs b/src/dotnet/MediatorPlugin/Navigations/Providers/MediatorRequestNavigateFromHereProvider.cs index cf83cd1..173560d 100644 --- a/src/dotnet/MediatorPlugin/Navigations/Providers/MediatorRequestNavigateFromHereProvider.cs +++ b/src/dotnet/MediatorPlugin/Navigations/Providers/MediatorRequestNavigateFromHereProvider.cs @@ -33,18 +33,12 @@ IDataContext dataContext var solution = dataContext.GetComponent(); var selectedTreeNode = dataContext.GetSelectedTreeNode(); - if (selectedTreeNode is not IIdentifier) - { - Logger.Instance.Log(LoggingLevel.VERBOSE, $"Selected element is not an instance {nameof(IIdentifier)}"); - return; - } - _handlerSelector.NavigateToHandler ( solution, selectedTreeNode, new DataContextNavigationOptionsFactory(dataContext) - ); + ); } ) }; diff --git a/src/dotnet/MediatorPlugin/ReSharper/Psi/Search/InheritorsConsumer.cs b/src/dotnet/MediatorPlugin/ReSharper/Psi/Search/InheritorsConsumer.cs index 56763ac..9a91456 100644 --- a/src/dotnet/MediatorPlugin/ReSharper/Psi/Search/InheritorsConsumer.cs +++ b/src/dotnet/MediatorPlugin/ReSharper/Psi/Search/InheritorsConsumer.cs @@ -6,7 +6,7 @@ namespace ReSharper.MediatorPlugin.ReSharper.Psi.Search; internal sealed class InheritorsConsumer : IFindResultConsumer { - private const int MaxInheritors = 50; + private const int MaxInheritors = 300; private readonly HashSet _elements = new(); diff --git a/src/dotnet/MediatorPlugin/ReSharper/Psi/Tree/MediatorCallSite.cs b/src/dotnet/MediatorPlugin/ReSharper/Psi/Tree/MediatorCallSite.cs new file mode 100644 index 0000000..a641619 --- /dev/null +++ b/src/dotnet/MediatorPlugin/ReSharper/Psi/Tree/MediatorCallSite.cs @@ -0,0 +1,81 @@ +using System.Linq; +using JetBrains.Diagnostics; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Util; +using ReSharper.MediatorPlugin.Diagnostics; + +namespace ReSharper.MediatorPlugin.ReSharper.Psi.Tree; + +internal static class MediatorCallSite +{ + public static IIdentifier? ResolveRequestIdentifier(ITreeNode? node) + { + if (node is null) + return null; + + return ResolveFromEnclosingCall(node) ?? node as IIdentifier; + } + + private static IIdentifier? ResolveFromEnclosingCall(ITreeNode node) + { + IInvocationExpression? invocation = node.GetContainingNode(returnThis: true); + + if (invocation is null) + { + Logger.Instance.Log(LoggingLevel.VERBOSE, "> Caret is not inside an invocation expression"); + return null; + } + + foreach (ICSharpArgument argument in invocation.Arguments) + { + IIdentifier? identifier = ArgumentTypeDeclarationIdentifier(argument.Value); + + if (identifier is not null) + return identifier; + } + + Logger.Instance.Log(LoggingLevel.VERBOSE, $"> No invocation argument resolved to a navigable type declaration (method '{InvokedMethodName(invocation)}')"); + return null; + } + + private static IIdentifier? ArgumentTypeDeclarationIdentifier(ICSharpExpression? argumentValue) + { + ITypeElement? typeElement = ResolveTypeElement(argumentValue); + + if (typeElement is null) + return null; + + if (typeElement.GetDeclarations().FirstOrDefault() is not IClassLikeDeclaration declaration) + { + Logger.Instance.Log(LoggingLevel.VERBOSE, $"> Argument type '{typeElement.GetClrName().FullName}' has no source declaration to navigate from"); + return null; + } + + Logger.Instance.Log(LoggingLevel.VERBOSE, $"> Resolved invocation argument to type '{declaration.DeclaredName}'"); + return declaration.NameIdentifier; + } + + private static ITypeElement? ResolveTypeElement(ICSharpExpression? expression) + { + switch (expression) + { + case null: + return null; + + case IObjectCreationExpression objectCreation + when objectCreation.TypeReference?.Resolve().DeclaredElement is ITypeElement created: + return created; + + default: + return (expression.GetExpressionType().ToIType() as IDeclaredType)?.GetTypeElement(); + } + } + + private static string InvokedMethodName(IInvocationExpression invocation) + { + return (invocation.InvokedExpression as IReferenceExpression)?.Reference.GetName() ?? ""; + } +} diff --git a/src/dotnet/MediatorPlugin/Services/Find/HandlerSelector.cs b/src/dotnet/MediatorPlugin/Services/Find/HandlerSelector.cs index 7d7898c..d5b4913 100644 --- a/src/dotnet/MediatorPlugin/Services/Find/HandlerSelector.cs +++ b/src/dotnet/MediatorPlugin/Services/Find/HandlerSelector.cs @@ -7,6 +7,7 @@ using JetBrains.ReSharper.Psi.Tree; using JetBrains.TextControl; using ReSharper.MediatorPlugin.Diagnostics; +using ReSharper.MediatorPlugin.ReSharper.Psi.Tree; using ReSharper.MediatorPlugin.Services.Libraries; using ReSharper.MediatorPlugin.Services.Navigation; using System.Collections.Generic; @@ -34,13 +35,15 @@ IIdentifier identifier public void NavigateToHandler ( ISolution solution, - ITreeNode selectedTreeNode, + ITreeNode? selectedTreeNode, INavigationOptionsFactory navigationOptionsFactory ) { - if (selectedTreeNode is not IIdentifier selectedIdentifier) + IIdentifier? selectedIdentifier = MediatorCallSite.ResolveRequestIdentifier(selectedTreeNode); + + if (selectedIdentifier is null) { - Logger.Instance.Log(LoggingLevel.VERBOSE, $"Selected element is not an instance {nameof(IIdentifier)}"); + Logger.Instance.Log(LoggingLevel.VERBOSE, "Selected element is not a Mediator request or dispatch call"); return; } diff --git a/src/dotnet/MediatorPlugin/Services/Find/IHandlerSelector.cs b/src/dotnet/MediatorPlugin/Services/Find/IHandlerSelector.cs index 6464f1d..9fe7942 100644 --- a/src/dotnet/MediatorPlugin/Services/Find/IHandlerSelector.cs +++ b/src/dotnet/MediatorPlugin/Services/Find/IHandlerSelector.cs @@ -14,7 +14,7 @@ IIdentifier identifier void NavigateToHandler ( ISolution solution, - ITreeNode selectedTreeNode, + ITreeNode? selectedTreeNode, INavigationOptionsFactory navigationOptionsFactory ); } \ No newline at end of file From 42446efe6b02e550e5730c96afc3092d8333a8e8 Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:27:49 -0400 Subject: [PATCH 03/11] test: add file-resolution and caret-based lookup tests for MediatR Go to Handler Adds a way to verify which handler file the plugin resolves for a given request, both by name lookup (AssertHandlerFilesFor, run across the shared MediatR v12/v13/v14 fixture) and by placing a real caret via SDK text-control support and resolving the PSI node under it, mirroring how GoToHandlerAction gets its selected identifier in production. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0184Nxbg3pykkbbRL1E1o15M --- .../GoToHandler.CaretTests.V12.cs | 8 +++ .../GoToHandler.CaretTests.V13.cs | 8 +++ .../GoToHandler.CaretTests.V14.cs | 8 +++ .../GoToHandler.CaretTests.cs | 59 +++++++++++++++++++ .../Infrastructure/TestSolutionFiles.cs | 7 +++ .../MediatorPlugin.Tests/LookupTestBase.cs | 53 ++++++++++++++++- .../MediatR.Requests.Tests.cs | 12 ++++ .../MediatorPlugin.Tests.csproj | 9 +++ .../data/GoToHandlerCaretTests/Handler.cs | 14 +++++ .../data/GoToHandlerCaretTests/Request.cs | 7 +++ 10 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs new file mode 100644 index 0000000..ed9a3af --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs @@ -0,0 +1,8 @@ +using Octelys.MediatorPlugin.Tests.Infrastructure; + +namespace Octelys.MediatorPlugin.Tests; + +[MediatRV12TestPackages] +public sealed class GoToHandlerCaretTestsV12 : GoToHandlerCaretTestsBase +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs new file mode 100644 index 0000000..8fdfa8e --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs @@ -0,0 +1,8 @@ +using Octelys.MediatorPlugin.Tests.Infrastructure; + +namespace Octelys.MediatorPlugin.Tests; + +[MediatRV13TestPackages] +public sealed class GoToHandlerCaretTestsV13 : GoToHandlerCaretTestsBase +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs new file mode 100644 index 0000000..4e8a1ba --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs @@ -0,0 +1,8 @@ +using Octelys.MediatorPlugin.Tests.Infrastructure; + +namespace Octelys.MediatorPlugin.Tests; + +[MediatRV14TestPackages] +public sealed class GoToHandlerCaretTestsV14 : GoToHandlerCaretTestsBase +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs new file mode 100644 index 0000000..7a087ad --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs @@ -0,0 +1,59 @@ +using System.IO; +using System.Linq; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Feature.Services.Util; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using JetBrains.TextControl; +using NUnit.Framework; +using ReSharper.MediatorPlugin.Services.Libraries; + +namespace Octelys.MediatorPlugin.Tests; + +/// +/// Positions a real caret (via the "{caret}" marker in Request.cs) on a Mediator request type +/// and asserts which handler file the plugin resolves for it. Unlike , +/// which locates the request's declaration identifier directly through the PSI, this test goes +/// through the same caret-to-PSI resolution () that the plugin's +/// own actions use (e.g. GoToHandlerAction.GetSelectedMediatrRequest), so it exercises the literal +/// "caret sits on this token" scenario rather than simulating it by name. +/// +[TestNetCoreLatest] +[TestReferences("System.Runtime")] +public abstract class GoToHandlerCaretTestsBase : BaseTestWithTextControl +{ + protected override string RelativeTestDataPath => "GoToHandlerCaretTests"; + + [Test] + public void Request() => DoNamedTest("Handler.cs"); + + protected override void DoTest + ( + Lifetime lifetime, + ISolution solution + ) + { + solution.GetPsiServices().Files.CommitAllDocuments(); + + using (ReadLockCookie.Create()) + { + ITextControl textControl = OpenTextControl(lifetime, GetCaretPosition()); + + IIdentifier requestIdentifier = TextControlToPsi.GetElement(solution, textControl); + + Assert.That(requestIdentifier, Is.Not.Null, "No identifier was found at the caret."); + + ITypeElement handler = new LibraryAdaptor().FindHandlers(requestIdentifier).Single(); + + string handlerFileName = handler + .GetSourceFiles() + .Select(sourceFile => Path.GetFileName(sourceFile.GetLocation().FullPath)) + .Single(); + + Assert.That(handlerFileName, Is.EqualTo("Handler.cs")); + } + } +} diff --git a/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs b/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs index 7f328d6..43a4301 100644 --- a/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs +++ b/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs @@ -14,6 +14,13 @@ public static class TestSolutionFiles { private static readonly string TestSolutionPath = ResolveTestSolutionPath(); + /// + /// Absolute path to the TestSolution folder, used to turn an on-disk file path (e.g. a + /// resolved handler's source file) back into the relative path form used elsewhere in these + /// tests (e.g. "MediatR/Handlers/GetEntityHandler.cs"). + /// + public static string RootPath => TestSolutionPath; + public static string[] MediatR { get; } = LibraryFiles("MediatR"); public static string[] Mediator { get; } = LibraryFiles("Mediator"); diff --git a/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs b/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs index 3bcc55e..8ce851d 100644 --- a/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs +++ b/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using JetBrains.Lifetimes; using JetBrains.ProjectModel; @@ -9,6 +10,7 @@ using JetBrains.ReSharper.Resources.Shell; using JetBrains.ReSharper.TestFramework; using NUnit.Framework; +using Octelys.MediatorPlugin.Tests.Infrastructure; using ReSharper.MediatorPlugin.Services.Libraries; namespace Octelys.MediatorPlugin.Tests; @@ -23,6 +25,7 @@ public abstract class LookupTestBase : BaseTestWithSingleProject { private string _requestTypeName; private string[] _expectedHandlerNames; + private string[] _expectedHandlerFilePaths; protected override string RelativeTestDataPath => "TestSolution"; @@ -42,7 +45,18 @@ ISolution solution { IIdentifier requestIdentifier = FindDeclarationIdentifier(solution, _requestTypeName); - IEnumerable handlers = new LibraryAdaptor().FindHandlers(requestIdentifier); + ITypeElement[] handlers = new LibraryAdaptor().FindHandlers(requestIdentifier).ToArray(); + + if (_expectedHandlerFilePaths is not null) + { + string[] handlerFilePaths = handlers + .SelectMany(GetRelativeFilePaths) + .OrderBy(path => path) + .ToArray(); + + Assert.That(handlerFilePaths, Is.EqualTo(_expectedHandlerFilePaths.OrderBy(path => path).ToArray())); + return; + } string[] handlerNames = handlers .Select(handler => handler.ShortName) @@ -70,6 +84,43 @@ params string[] fileNames DoTestSolution(fileNames); } + /// + /// Loads the given files as a test solution and asserts that the request or notification + /// resolves to handlers declared in exactly the expected files (relative to + /// test/data/TestSolution, e.g. "MediatR/Handlers/GetEntityHandler.cs") - i.e. the file(s) + /// "Go to Handler" would navigate to when the caret sits on the request. The lookup itself + /// runs in . + /// + protected void AssertHandlerFilesFor + ( + string requestTypeName, + string[] expectedHandlerFilePaths, + params string[] fileNames + ) + { + _requestTypeName = requestTypeName; + _expectedHandlerFilePaths = expectedHandlerFilePaths; + + DoTestSolution(fileNames); + } + + /// + /// Resolves the on-disk file(s) declaring the given handler back to their path relative to + /// test/data/TestSolution, matching the relative path form used to load test files (e.g. via + /// ). + /// + private static IEnumerable GetRelativeFilePaths + ( + ITypeElement handler + ) + { + return handler + .GetSourceFiles() + .Select(sourceFile => sourceFile.GetLocation().FullPath) + .Select(fullPath => fullPath.Substring(TestSolutionFiles.RootPath.Length + 1)) + .Select(relativePath => relativePath.Replace(Path.DirectorySeparatorChar, '/')); + } + /// /// Returns the name token of the type declaration, which is the node the plugin receives /// when the caret sits on a request in the editor. may be a diff --git a/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs index 5232371..f902267 100644 --- a/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs @@ -115,4 +115,16 @@ public void FindHandlers_HandlerImplementsSharedGenericHandlerInterface_HandlerR TestSolutionFiles.MediatR ); } + + [Test] + public void FindHandlers_CaretOnRequest_HandlerFileReturned() + { + // Act & Assert. + AssertHandlerFilesFor + ( + "Domain.GetEntityRequest", + ["MediatR/Handlers/GetEntityHandler.cs"], + TestSolutionFiles.MediatR + ); + } } \ No newline at end of file diff --git a/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj b/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj index f502f60..7ba8337 100644 --- a/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj +++ b/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj @@ -52,6 +52,15 @@ Mediator.Requests.Tests.cs + + GoToHandler.CaretTests.cs + + + GoToHandler.CaretTests.cs + + + GoToHandler.CaretTests.cs + diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs new file mode 100644 index 0000000..341c369 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Domain; +using MediatR; + +namespace Application; + +internal sealed class GetEntityRequestHandler : IRequestHandler +{ + public Task Handle(GetEntityRequest request, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs new file mode 100644 index 0000000..f087915 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Domain; + +public class GetEntity{caret}Request : IRequest +{ +} From 90259a32175a0c426b11ad4f16588469de75ec8c Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:39:58 -0400 Subject: [PATCH 04/11] fix: reset the other lookup expectation field to stop cross-test leakage The JetBrains test framework reuses one LookupTestBase instance across every [Test] method in a fixture (solution loading is too expensive to redo per test), so _expectedHandlerNames and _expectedHandlerFilePaths survived from whichever assertion ran last. Once a caret-file-lookup test set _expectedHandlerFilePaths, every later name-based test in the same fixture instance silently ran the file-path branch of DoTest against its stale expectation instead of its own assertion, failing CI with mismatched expected/actual handler paths. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0184Nxbg3pykkbbRL1E1o15M --- src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs b/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs index 8ce851d..609686b 100644 --- a/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs +++ b/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs @@ -80,6 +80,7 @@ params string[] fileNames { _requestTypeName = requestTypeName; _expectedHandlerNames = expectedHandlerNames; + _expectedHandlerFilePaths = null; DoTestSolution(fileNames); } @@ -99,6 +100,7 @@ params string[] fileNames ) { _requestTypeName = requestTypeName; + _expectedHandlerNames = null; _expectedHandlerFilePaths = expectedHandlerFilePaths; DoTestSolution(fileNames); From dc7ad8d244db79aef61ec9a42bece892717433ab Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:44:14 -0400 Subject: [PATCH 05/11] fix: correct namespace and class name in GoToHandlerCaretTests request file --- .../test/data/GoToHandlerCaretTests/Request.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs index f087915..114d762 100644 --- a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs @@ -1,7 +1,7 @@ using MediatR; -namespace Domain; +namespace Domain{caret}; -public class GetEntity{caret}Request : IRequest +public class GetEntityRequest : IRequest { } From ab9e53746cb2c7605f4bddc409c4b671852d0e6c Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:58:43 -0400 Subject: [PATCH 06/11] test: expand GoToHandlerCaretTests with additional caret-based resolution scenarios Adds tests for caret placement scenarios on identifiers not resolving to handlers, including namespace identifiers and MediatR `Send` calls. Refactors test logic to handle cases with no expected handler matches. --- .../GoToHandler.CaretTests.cs | 35 ++++++++++++++++--- .../GoToHandlerCaretTests/CaretInNamespace.cs | 7 ++++ .../GoToHandlerCaretTests/CaretOnSendCall.cs | 24 +++++++++++++ .../data/GoToHandlerCaretTests/Request.cs | 4 +-- .../RequestCaretAtBeginning.cs | 7 ++++ 5 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs create mode 100644 src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs index 7a087ad..3bd32b6 100644 --- a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs +++ b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs @@ -14,22 +14,39 @@ namespace Octelys.MediatorPlugin.Tests; /// -/// Positions a real caret (via the "{caret}" marker in Request.cs) on a Mediator request type -/// and asserts which handler file the plugin resolves for it. Unlike , +/// Positions a real caret (via the "{caret}" marker in each test's data file) and asserts which +/// handler file the plugin resolves, if any, for the identifier under it. Unlike , /// which locates the request's declaration identifier directly through the PSI, this test goes /// through the same caret-to-PSI resolution () that the plugin's /// own actions use (e.g. GoToHandlerAction.GetSelectedMediatrRequest), so it exercises the literal -/// "caret sits on this token" scenario rather than simulating it by name. +/// "caret sits on this token" scenario rather than simulating it by name. +/// and place the caret on identifiers that are not a Mediator request +/// declaration and assert that no handler is resolved. /// [TestNetCoreLatest] [TestReferences("System.Runtime")] public abstract class GoToHandlerCaretTestsBase : BaseTestWithTextControl { + private static readonly string[] TestsExpectingNoHandler = + { + nameof(CaretInNamespace), + nameof(CaretOnSendCall) + }; + protected override string RelativeTestDataPath => "GoToHandlerCaretTests"; [Test] public void Request() => DoNamedTest("Handler.cs"); + [Test] + public void RequestCaretAtBeginning() => DoNamedTest("Handler.cs"); + + [Test] + public void CaretInNamespace() => DoNamedTest("Handler.cs"); + + [Test] + public void CaretOnSendCall() => DoNamedTest("Handler.cs"); + protected override void DoTest ( Lifetime lifetime, @@ -46,9 +63,17 @@ ISolution solution Assert.That(requestIdentifier, Is.Not.Null, "No identifier was found at the caret."); - ITypeElement handler = new LibraryAdaptor().FindHandlers(requestIdentifier).Single(); + ITypeElement[] handlers = new LibraryAdaptor().FindHandlers(requestIdentifier).ToArray(); + + if (TestsExpectingNoHandler.Contains(TestName)) + { + Assert.That(handlers, Is.Empty, "Expected no handler to be resolved from this caret position."); + + return; + } - string handlerFileName = handler + string handlerFileName = handlers + .Single() .GetSourceFiles() .Select(sourceFile => Path.GetFileName(sourceFile.GetLocation().FullPath)) .Single(); diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs new file mode 100644 index 0000000..17f285b --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace {caret}Domain; + +public class GetEntityRequest : IRequest +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs new file mode 100644 index 0000000..1f97e88 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; + +namespace Domain; + +public class GetEntityRequest : IRequest +{ +} + +public class GetEntityRequestSender +{ + private readonly IMediator _mediator; + + public GetEntityRequestSender(IMediator mediator) + { + _mediator = mediator; + } + + public Task SendAsync(CancellationToken cancellationToken) + { + return _mediator.{caret}Send(new GetEntityRequest(), cancellationToken); + } +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs index 114d762..f087915 100644 --- a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs @@ -1,7 +1,7 @@ using MediatR; -namespace Domain{caret}; +namespace Domain; -public class GetEntityRequest : IRequest +public class GetEntity{caret}Request : IRequest { } diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs new file mode 100644 index 0000000..afde079 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Domain; + +public class {caret}GetEntityRequest : IRequest +{ +} From 558bc03b8e00625b727e63bd6837aabcfa79364c Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:15:24 -0400 Subject: [PATCH 07/11] fix: rename variable in GoToHandlerCaretTests to improve test clarity Renames `TestName` to `TestMethodName` for better alignment with its purpose and to prevent ambiguity in test logic. --- src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs index 3bd32b6..d5049dd 100644 --- a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs +++ b/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs @@ -65,7 +65,7 @@ ISolution solution ITypeElement[] handlers = new LibraryAdaptor().FindHandlers(requestIdentifier).ToArray(); - if (TestsExpectingNoHandler.Contains(TestName)) + if (TestsExpectingNoHandler.Contains(TestMethodName)) { Assert.That(handlers, Is.Empty, "Expected no handler to be resolved from this caret position."); From d66fd15be3f3eee73b8655e89ac85b1187bcd369 Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:22:26 -0400 Subject: [PATCH 08/11] feat: add MediatR callsite classes for DotNetCore6 integration Introduces `MediatRCallsite` and `MediatorCallsite` classes to handle MediatR request sending with `CustomClassRequest` and `NewMediatorRequest`. --- .../MediatR/MediatRCallsite.cs | 19 +++++++++++++++++++ .../Mediator/MediatorCallsite.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs create mode 100644 test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs diff --git a/test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs b/test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs new file mode 100644 index 0000000..b47912d --- /dev/null +++ b/test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs @@ -0,0 +1,19 @@ +using DotNetCore6_Domain.MediatR.Requests; +using MediatR; + +namespace DotNetCore6_Domain.MediatR; + +public class MediatRCallsite +{ + private readonly IMediator _mediator; + + public MediatRCallsite(IMediator mediator) + { + _mediator = mediator; + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + await _mediator.Send(new CustomClassRequest(), cancellationToken); + } +} \ No newline at end of file diff --git a/test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs b/test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs new file mode 100644 index 0000000..73d81d8 --- /dev/null +++ b/test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs @@ -0,0 +1,19 @@ +using DotNetCore6_Domain.Mediator.Requests; +using MediatR; + +namespace DotNetCore6_Domain.Mediator; + +public class MediatorCallsite +{ + private readonly IMediator _mediator; + + public MediatorCallsite(IMediator mediator) + { + _mediator = mediator; + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + await _mediator.Send(new NewMediatorRequest(), cancellationToken); + } +} \ No newline at end of file From 58573aa264088236100ae50f74e67927d5d22d7c Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:37:32 -0400 Subject: [PATCH 09/11] refactor: rename GoToHandlerCaretTests classes for consistency with MediatR naming conventions Renames classes and file references in GoToHandlerCaretTests to align with the MediatR naming standard, ensuring uniformity across test files. --- ....V12.cs => MediatR.GoToHandler.CaretTests.V12.cs} | 2 +- ....V13.cs => MediatR.GoToHandler.CaretTests.V13.cs} | 2 +- ....V14.cs => MediatR.GoToHandler.CaretTests.V14.cs} | 2 +- ...retTests.cs => MediatR.GoToHandler.CaretTests.cs} | 2 +- .../MediatorPlugin.Tests/MediatorPlugin.Tests.csproj | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) rename src/dotnet/MediatorPlugin.Tests/{GoToHandler.CaretTests.V12.cs => MediatR.GoToHandler.CaretTests.V12.cs} (58%) rename src/dotnet/MediatorPlugin.Tests/{GoToHandler.CaretTests.V13.cs => MediatR.GoToHandler.CaretTests.V13.cs} (58%) rename src/dotnet/MediatorPlugin.Tests/{GoToHandler.CaretTests.V14.cs => MediatR.GoToHandler.CaretTests.V14.cs} (58%) rename src/dotnet/MediatorPlugin.Tests/{GoToHandler.CaretTests.cs => MediatR.GoToHandler.CaretTests.cs} (97%) diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V12.cs similarity index 58% rename from src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs rename to src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V12.cs index ed9a3af..fb84666 100644 --- a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V12.cs +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V12.cs @@ -3,6 +3,6 @@ namespace Octelys.MediatorPlugin.Tests; [MediatRV12TestPackages] -public sealed class GoToHandlerCaretTestsV12 : GoToHandlerCaretTestsBase +public sealed class MediatrGoToHandlerCaretTestsV12 : MediatrGoToHandlerCaretTestsBase { } diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V13.cs similarity index 58% rename from src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs rename to src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V13.cs index 8fdfa8e..d609537 100644 --- a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V13.cs +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V13.cs @@ -3,6 +3,6 @@ namespace Octelys.MediatorPlugin.Tests; [MediatRV13TestPackages] -public sealed class GoToHandlerCaretTestsV13 : GoToHandlerCaretTestsBase +public sealed class MediatrGoToHandlerCaretTestsV13 : MediatrGoToHandlerCaretTestsBase { } diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V14.cs similarity index 58% rename from src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs rename to src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V14.cs index 4e8a1ba..7049d62 100644 --- a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.V14.cs +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V14.cs @@ -3,6 +3,6 @@ namespace Octelys.MediatorPlugin.Tests; [MediatRV14TestPackages] -public sealed class GoToHandlerCaretTestsV14 : GoToHandlerCaretTestsBase +public sealed class MediatrGoToHandlerCaretTestsV14 : MediatrGoToHandlerCaretTestsBase { } diff --git a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.cs similarity index 97% rename from src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs rename to src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.cs index d5049dd..9d400a3 100644 --- a/src/dotnet/MediatorPlugin.Tests/GoToHandler.CaretTests.cs +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.cs @@ -25,7 +25,7 @@ namespace Octelys.MediatorPlugin.Tests; /// [TestNetCoreLatest] [TestReferences("System.Runtime")] -public abstract class GoToHandlerCaretTestsBase : BaseTestWithTextControl +public abstract class MediatrGoToHandlerCaretTestsBase : BaseTestWithTextControl { private static readonly string[] TestsExpectingNoHandler = { diff --git a/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj b/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj index 7ba8337..0baf2db 100644 --- a/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj +++ b/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj @@ -52,14 +52,14 @@ Mediator.Requests.Tests.cs - - GoToHandler.CaretTests.cs + + MediatR.GoToHandler.CaretTests.cs - - GoToHandler.CaretTests.cs + + MediatR.GoToHandler.CaretTests.cs - - GoToHandler.CaretTests.cs + + MediatR.GoToHandler.CaretTests.cs From ea19b2a6f72b149cadfaf05a02b5327cc3c94023 Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:39:44 -0400 Subject: [PATCH 10/11] chore: update changelog version header to 26.09.19XX --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50abfea..060855a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. -## 26.09.04XX +## 26.09.19XX - "Go to Handler" now also works with the caret anywhere inside a `mediator.Send(...)` / `Publish(...)` call: the first argument's type is resolved, so there is no need to navigate to the request declaration first. From cc2456f2d2cba0e2ada787cf5ed6c7615cdc298d Mon Sep 17 00:00:00 2001 From: kzryzstof <38137839+kzryzstof@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:41:51 -0400 Subject: [PATCH 11/11] chore: ignore `TestResult.xml` test output in .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2e785c9..61eefd6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ _dotTrace* # NuGet tools .nuget/ + +**/TestResult.xml