From 16e4d68f3d3b2e68a6a29914cdcc85258eeed0b0 Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:13:15 +0100 Subject: [PATCH 1/7] [VS] Resolve DLL symbols to source in a Roslyn compilation Co-Authored-By: Claude Opus 5.5 --- .../ForeignSymbolResolverTests.cs | 138 ++++++++++++++++++ .../LanguageService.Tests.csproj | 3 + .../ForeignSource/ForeignLocation.cs | 32 ++++ .../ForeignSource/ForeignSymbolKey.cs | 27 ++++ .../ForeignSource/ForeignSymbolResolver.cs | 88 +++++++++++ .../LanguageService/LanguageService.csproj | 4 + .../LanguageService2022.csproj | 4 + 7 files changed, 296 insertions(+) create mode 100644 src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs create mode 100644 src/VisualStudio/LanguageService/ForeignSource/ForeignLocation.cs create mode 100644 src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs create mode 100644 src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs diff --git a/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs b/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs new file mode 100644 index 0000000000..0fa2441d35 --- /dev/null +++ b/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using XSharp.LanguageService.ForeignSource; +using Xunit; + +namespace XSharp.LanguageService.Tests +{ + public class ForeignSymbolResolverTests + { + private const string CratePath = @"C:\lib\Crate.cs"; + + // one declaration per line, so that the expected (0-based) line numbers can be read off + private static readonly string CrateSource = string.Join("\n", + "namespace Lib {", // 0 + " public class Crate {", // 1 + " public Crate(int size) { }", // 2 + " public void Open() { }", // 3 + " public void Open(string key) { }", // 4 + " public int Size { get; set; }", // 5 + " public T Get(int index) { return default(T); }",// 6 + " public class Lid { }", // 7 + " }", // 8 + " public interface IBox { void Close(); }", // 9 + " public enum Color { Red, Green }", // 10 + " public class Plain { }", // 11 + "}"); + + private static Compilation Compile(params (string path, string source)[] files) + { + var trees = files.Select(f => CSharpSyntaxTree.ParseText(f.source, path: f.path)); + return CSharpCompilation.Create("Lib", trees, + new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + private static IReadOnlyList Resolve(ForeignSymbolKey key) + { + return ForeignSymbolResolver.Resolve(new[] { Compile((CratePath, CrateSource)) }, key); + } + + private static ForeignSymbolKey Type(string typeDocId) => new ForeignSymbolKey(typeDocId, null, null, 0); + + private static ForeignSymbolKey Member(string typeDocId, string memberDocId, string name, int parameterCount) + => new ForeignSymbolKey(typeDocId, memberDocId, name, parameterCount); + + private static void AssertSingle(IReadOnlyList found, int line) + { + Assert.Single(found); + Assert.Equal(CratePath, found[0].FilePath); + Assert.Equal(line, found[0].StartLine); + } + + [Fact] + public void TypeResolvesToItsDeclaration() + { + var found = Resolve(Type("T:Lib.Crate")); + AssertSingle(found, 1); + Assert.Equal("Crate", found[0].Name); + } + + [Fact] + public void OverloadPickedByDocId() + { + AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.Open(System.String)", "Open", 1)), 4); + } + + [Fact] + public void PropertyResolves() + { + AssertSingle(Resolve(Member("T:Lib.Crate", "P:Lib.Crate.Size", "Size", 0)), 5); + } + + [Fact] + public void NestedTypeResolves() + { + AssertSingle(Resolve(Type("T:Lib.Crate.Lid")), 7); + } + + [Fact] + public void InterfaceAndEnumResolve() + { + AssertSingle(Resolve(Type("T:Lib.IBox")), 9); + AssertSingle(Resolve(Type("T:Lib.Color")), 10); + } + + [Fact] + public void ConstructorResolves() + { + AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.#ctor(System.Int32)", ".ctor", 1)), 2); + } + + [Fact] + public void UnresolvedDocIdFallsBackToNameAndParameterCount() + { + // the X# doc ID misses the ``1 of the generic method + AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.Get(System.Int32)", "Get", 1)), 6); + } + + [Fact] + public void UnknownMemberFallsBackToType() + { + AssertSingle(Resolve(Member("T:Lib.Crate", "M:Lib.Crate.Gone", "Gone", 0)), 1); + } + + [Fact] + public void ImplicitConstructorGoesToType() + { + AssertSingle(Resolve(Member("T:Lib.Plain", "M:Lib.Plain.#ctor", ".ctor", 0)), 11); + } + + [Fact] + public void PartialClassGivesBothParts() + { + var compilation = Compile( + (@"C:\lib\A.cs", "namespace Lib { public partial class Duo { } }"), + (@"C:\lib\B.cs", "namespace Lib { public partial class Duo { public void M() { } } }")); + var found = ForeignSymbolResolver.Resolve(new[] { compilation }, Type("T:Lib.Duo")); + Assert.Equal(new[] { @"C:\lib\A.cs", @"C:\lib\B.cs" }, found.Select(f => f.FilePath).OrderBy(p => p).ToArray()); + } + + [Fact] + public void UnknownTypeGivesNothing() + { + Assert.Empty(Resolve(Type("T:Lib.Missing"))); + } + + [Fact] + public void DuplicatesAcrossCompilationsCollapse() + { + // a multi-targeted project: one Roslyn project (compilation) per target framework, same files + var found = ForeignSymbolResolver.Resolve( + new[] { Compile((CratePath, CrateSource)), Compile((CratePath, CrateSource)) }, Type("T:Lib.Crate")); + AssertSingle(found, 1); + } + } +} diff --git a/src/VisualStudio/LanguageService.Tests/LanguageService.Tests.csproj b/src/VisualStudio/LanguageService.Tests/LanguageService.Tests.csproj index 6c90dd1817..cd999d662b 100644 --- a/src/VisualStudio/LanguageService.Tests/LanguageService.Tests.csproj +++ b/src/VisualStudio/LanguageService.Tests/LanguageService.Tests.csproj @@ -14,6 +14,9 @@ + + + diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignLocation.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignLocation.cs new file mode 100644 index 0000000000..d4fd9394e3 --- /dev/null +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignLocation.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// +namespace XSharp.LanguageService.ForeignSource +{ + /// A declaration in the source of a C# or VB project of the solution. Lines and columns are 0-based. + internal sealed class ForeignLocation + { + internal ForeignLocation(string filePath, int startLine, int startColumn, int endLine, int endColumn, string name, string containerName) + { + FilePath = filePath; + StartLine = startLine; + StartColumn = startColumn; + EndLine = endLine; + EndColumn = endColumn; + Name = name; + ContainerName = containerName; + } + + internal string FilePath { get; } + internal int StartLine { get; } + internal int StartColumn { get; } + internal int EndLine { get; } + internal int EndColumn { get; } + /// The name of the declared symbol. + internal string Name { get; } + /// The containing type, or the namespace for a type. + internal string ContainerName { get; } + } +} diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs new file mode 100644 index 0000000000..81af2ec7ca --- /dev/null +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// +namespace XSharp.LanguageService.ForeignSource +{ + /// What to look for in a C# or VB compilation: a type, or a member of that type. + internal sealed class ForeignSymbolKey + { + internal ForeignSymbolKey(string typeDocId, string memberDocId, string memberName, int parameterCount) + { + TypeDocId = typeDocId; + MemberDocId = memberDocId; + MemberName = memberName; + ParameterCount = parameterCount; + } + + /// The XML doc ID of the type (T:Ns.Type). + internal string TypeDocId { get; } + /// The XML doc ID of the member, or null when the type itself is wanted. + internal string MemberDocId { get; } + /// The member name as X# spells it (.ctor for a constructor); used when the doc ID does not resolve. + internal string MemberName { get; } + internal int ParameterCount { get; } + } +} diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs new file mode 100644 index 0000000000..d4c68fcb17 --- /dev/null +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace XSharp.LanguageService.ForeignSource +{ + /// + /// Maps a symbol that X# read from the output DLL of a C# or VB project back to its declarations in that project's + /// source. The doc ID decides; when it does not resolve (generics), the member name and parameter count do, and + /// when the member has no source (implicit constructor) the type does. + /// + internal static class ForeignSymbolResolver + { + internal static IReadOnlyList Resolve(IEnumerable compilations, ForeignSymbolKey key) + { + var result = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var compilation in compilations) + { + foreach (var symbol in Find(compilation, key)) + { + foreach (var location in symbol.Locations.Where(l => l.IsInSource)) + { + var span = location.GetLineSpan(); + if (!seen.Add(span.Path + "|" + span.StartLinePosition.Line)) + continue; + result.Add(new ForeignLocation(span.Path, span.StartLinePosition.Line, span.StartLinePosition.Character, + span.EndLinePosition.Line, span.EndLinePosition.Character, symbol.Name, Container(symbol))); + } + } + } + return result; + } + + private static IEnumerable Find(Compilation compilation, ForeignSymbolKey key) + { + if (key.MemberDocId != null) + { + var members = InSource(DocumentationCommentId.GetSymbolsForDeclarationId(key.MemberDocId, compilation)); + if (members.Count > 0) + return members; + } + var type = DocumentationCommentId.GetFirstSymbolForDeclarationId(key.TypeDocId, compilation) as INamedTypeSymbol; + if (type == null) + return Enumerable.Empty(); + if (key.MemberDocId != null) + { + var named = key.MemberName == ".ctor" ? type.InstanceConstructors.Cast().ToList() : type.GetMembers(key.MemberName).ToList(); + var counted = named.Where(m => ParameterCount(m) == key.ParameterCount).ToList(); + var members = InSource(counted.Count > 0 ? counted : named); + if (members.Count > 0) + return members; + } + return new[] { type }; + } + + private static List InSource(IEnumerable symbols) + { + return symbols.Where(s => s.Locations.Any(l => l.IsInSource)).ToList(); + } + + private static int ParameterCount(ISymbol symbol) + { + switch (symbol) + { + case IMethodSymbol method: + return method.Parameters.Length; + case IPropertySymbol property: + return property.Parameters.Length; + default: + return 0; + } + } + + private static string Container(ISymbol symbol) + { + if (symbol.ContainingType != null) + return symbol.ContainingType.ToDisplayString(); + return symbol.ContainingNamespace?.ToDisplayString() ?? ""; + } + } +} diff --git a/src/VisualStudio/LanguageService/LanguageService.csproj b/src/VisualStudio/LanguageService/LanguageService.csproj index 44b60cdcb4..acefeb4db4 100644 --- a/src/VisualStudio/LanguageService/LanguageService.csproj +++ b/src/VisualStudio/LanguageService/LanguageService.csproj @@ -160,6 +160,9 @@ + + + @@ -343,6 +346,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + 0.15.0 diff --git a/src/VisualStudio/LanguageService/LanguageService2022.csproj b/src/VisualStudio/LanguageService/LanguageService2022.csproj index d95bc8e6cf..9574303875 100644 --- a/src/VisualStudio/LanguageService/LanguageService2022.csproj +++ b/src/VisualStudio/LanguageService/LanguageService2022.csproj @@ -160,6 +160,9 @@ + + + @@ -330,6 +333,7 @@ 17.0.551 + 0.15.0 From 7b8add994e763d37a4d744d2bc1b40d090dc7a95 Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:15:10 +0100 Subject: [PATCH 2/7] [VS] Find the C#/VB project that produced a referenced DLL Co-Authored-By: Claude Opus 5.5 --- .../ForeignProjectsTests.cs | 54 +++++++++++++++++ .../ForeignSource/ForeignProjects.cs | 59 +++++++++++++++++++ .../LanguageService/LanguageService.csproj | 1 + .../LanguageService2022.csproj | 1 + .../ProjectSystem/XProject.prg | 7 +++ 5 files changed, 122 insertions(+) create mode 100644 src/VisualStudio/LanguageService.Tests/ForeignProjectsTests.cs create mode 100644 src/VisualStudio/LanguageService/ForeignSource/ForeignProjects.cs diff --git a/src/VisualStudio/LanguageService.Tests/ForeignProjectsTests.cs b/src/VisualStudio/LanguageService.Tests/ForeignProjectsTests.cs new file mode 100644 index 0000000000..7cd9ecbec5 --- /dev/null +++ b/src/VisualStudio/LanguageService.Tests/ForeignProjectsTests.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using XSharp.LanguageService.ForeignSource; +using Xunit; + +namespace XSharp.LanguageService.Tests +{ + public class ForeignProjectsTests + { + private static KeyValuePair[] Outputs(params (string project, string dll)[] pairs) + { + var result = new KeyValuePair[pairs.Length]; + for (int i = 0; i < pairs.Length; i++) + result[i] = new KeyValuePair(pairs[i].project, pairs[i].dll); + return result; + } + + [Fact] + public void FindsTheProjectForItsOutput() + { + var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll")); + Assert.Equal(@"C:\s\Lib\Lib.csproj", ForeignProjects.FindProjectFile(outputs, @"C:\s\Lib\bin\Debug\Lib.dll")); + } + + [Fact] + public void MatchesDespiteCaseAndRelativeSegments() + { + var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll")); + Assert.Equal(@"C:\s\Lib\Lib.csproj", ForeignProjects.FindProjectFile(outputs, @"c:\S\lib\bin\Release\..\Debug\LIB.dll")); + } + + [Fact] + public void XSharpProjectsAreIgnored() + { + var outputs = Outputs((@"C:\s\App\App.xsproj", @"C:\s\App\bin\App.dll"), (@"C:\s\Old\Old.xsprj", @"C:\s\Old\bin\Old.dll")); + Assert.Null(ForeignProjects.FindProjectFile(outputs, @"C:\s\App\bin\App.dll")); + Assert.Null(ForeignProjects.FindProjectFile(outputs, @"C:\s\Old\bin\Old.dll")); + } + + [Fact] + public void UnknownDllGivesNull() + { + var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll")); + Assert.Null(ForeignProjects.FindProjectFile(outputs, @"C:\s\Other\bin\Debug\Other.dll")); + } + + [Fact] + public void NullOrEmptyAssemblyGivesNull() + { + var outputs = Outputs((@"C:\s\Lib\Lib.csproj", @"C:\s\Lib\bin\Debug\Lib.dll")); + Assert.Null(ForeignProjects.FindProjectFile(outputs, null)); + Assert.Null(ForeignProjects.FindProjectFile(outputs, "")); + } + } +} diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignProjects.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignProjects.cs new file mode 100644 index 0000000000..660bb0ea2a --- /dev/null +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignProjects.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using System.IO; +using XSharpModel; + +namespace XSharp.LanguageService.ForeignSource +{ + /// Finds the C# or VB project of the solution whose output DLL an assembly was read from. + internal static class ForeignProjects + { + /// The project file, or null when no referenced C#/VB project of an X# project produces this DLL. + internal static string FindProjectFile(string assemblyFile) + { + if (string.IsNullOrEmpty(assemblyFile)) + return null; + foreach (var project in XSolution.Projects) + { + var found = FindProjectFile(project.ProjectOutputs, assemblyFile); + if (found != null) + return found; + } + return null; + } + + internal static string FindProjectFile(IEnumerable> outputs, string assemblyFile) + { + var wanted = Normalize(assemblyFile); + if (wanted == null) + return null; + foreach (var output in outputs) + { + if (XProject.IsXSharpProject(output.Key)) + continue; + if (string.Equals(Normalize(output.Value), wanted, StringComparison.OrdinalIgnoreCase)) + return output.Key; + } + return null; + } + + private static string Normalize(string path) + { + if (string.IsNullOrEmpty(path)) + return null; + try + { + return Path.GetFullPath(path); + } + catch (Exception) + { + return null; + } + } + } +} diff --git a/src/VisualStudio/LanguageService/LanguageService.csproj b/src/VisualStudio/LanguageService/LanguageService.csproj index acefeb4db4..cec705da06 100644 --- a/src/VisualStudio/LanguageService/LanguageService.csproj +++ b/src/VisualStudio/LanguageService/LanguageService.csproj @@ -161,6 +161,7 @@ + diff --git a/src/VisualStudio/LanguageService/LanguageService2022.csproj b/src/VisualStudio/LanguageService/LanguageService2022.csproj index 9574303875..2d4eb612d2 100644 --- a/src/VisualStudio/LanguageService/LanguageService2022.csproj +++ b/src/VisualStudio/LanguageService/LanguageService2022.csproj @@ -161,6 +161,7 @@ + diff --git a/src/VisualStudio/XSharpCodeModelXs/ProjectSystem/XProject.prg b/src/VisualStudio/XSharpCodeModelXs/ProjectSystem/XProject.prg index 50cf51b805..ccb7585fcc 100644 --- a/src/VisualStudio/XSharpCodeModelXs/ProjectSystem/XProject.prg +++ b/src/VisualStudio/XSharpCodeModelXs/ProjectSystem/XProject.prg @@ -1714,6 +1714,13 @@ CLASS XProject END GET END PROPERTY + /// The referenced projects and their output DLLs (key = project file, value = output DLL). + PROPERTY ProjectOutputs AS KeyValuePair[] + GET + RETURN SELF:_projectOutputDLLs:ToArray() + END GET + END PROPERTY + #endregion PRIVATE METHOD WriteOutputMessage(message AS STRING) AS VOID From dcd3a33cb1c76c03534123adf417eff88b861198 Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:19:09 +0100 Subject: [PATCH 3/7] [VS] F12 opens C#/VB source of the solution instead of the generated header Co-Authored-By: Claude Opus 5.5 --- .../Commands/GotoDefinition.cs | 24 +++- .../ForeignSource/ForeignNavigator.cs | 136 ++++++++++++++++++ .../ForeignSource/ForeignSymbolKey.cs | 15 ++ .../LanguageService/LanguageService.csproj | 1 + .../LanguageService2022.csproj | 1 + 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs diff --git a/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs b/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs index 6c60b83498..bd4aa4b4f7 100644 --- a/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs +++ b/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs @@ -10,6 +10,7 @@ using XSharp.Support; using System.Collections; using System.Collections.Generic; +using XSharp.LanguageService.ForeignSource; namespace XSharp.LanguageService { class XSharpGotoDefinition @@ -33,13 +34,14 @@ internal static void Goto(IXSymbol element, ITextView TextView, CompletionState } else if (element is XPETypeSymbol petype) { - GotoSystemType(TextView, petype, petype); - + if (!GotoForeignSource(petype, petype)) + GotoSystemType(TextView, petype, petype); } else if (element is XPEMemberSymbol pemember) { var petype2 = pemember.Parent as XPETypeSymbol; - GotoSystemType(TextView, petype2, pemember); + if (!GotoForeignSource(petype2, pemember)) + GotoSystemType(TextView, petype2, pemember); } else { @@ -48,6 +50,22 @@ internal static void Goto(IXSymbol element, ITextView TextView, CompletionState buttons: OLEMSGBUTTON.OLEMSGBUTTON_OK); } } + /// + /// A DLL symbol from a C# or VB project of the solution: open its source. False when it is not one, or its + /// source cannot be found; the caller then shows the generated header. + /// + private static bool GotoForeignSource(XPETypeSymbol petype, XPESymbol element) + { + var found = ForeignNavigator.Find(petype, element); + if (found.Count == 0) + return false; + if (found.Count == 1) + ForeignNavigator.Open(found[0]); + else + ForeignNavigator.Present(found, element, $"'{element.Name}' definitions"); + return true; + } + internal static void GotoDefn(ITextView TextView) { try diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs new file mode 100644 index 0000000000..1e387616dc --- /dev/null +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Community.VisualStudio.Toolkit; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.FindAllReferences; +using XSharp.LanguageService.Commands; +using XSharp.LanguageService.FindReferences; +using XSharp.LanguageService.GoToImplementation; +using XSharp.Settings; +using XSharp.Support; +using XSharpModel; +using Task = System.Threading.Tasks.Task; + +namespace XSharp.LanguageService.ForeignSource +{ + /// + /// The source of a symbol that X# read from the output DLL of a C# or VB project of the solution, found through + /// VS's Roslyn workspace. Any failure gives no locations: the callers then show the generated header as before. + /// + internal static class ForeignNavigator + { + private const int TimeoutMilliseconds = 3000; + + /// The declarations of (the type itself or one of its members). UI thread. + internal static IReadOnlyList Find(XPETypeSymbol type, XPESymbol element) + { + var none = Array.Empty(); + try + { + if (type == null || element == null) + return none; + var key = ForeignSymbolKey.From(type, element); + var assembly = (element as XPEMemberSymbol)?.IsExtension == true + ? ((XPEMemberSymbol)element).DeclaringTypeSym?.Assembly ?? type.Assembly + : type.Assembly; + var projectFile = ForeignProjects.FindProjectFile(assembly?.FileName); + if (projectFile == null) + return none; + var workspace = Workspace(); + if (workspace == null) + return none; + var projects = workspace.CurrentSolution.Projects + .Where(p => string.Equals(p.FilePath, projectFile, StringComparison.OrdinalIgnoreCase)).ToList(); + if (projects.Count == 0) + return none; + var compilations = ThreadHelper.JoinableTaskFactory.Run(() => CompilationsAsync(projects)); + var found = ForeignSymbolResolver.Resolve(compilations, key); + XSharpGotoDefinition.WriteOutputMessage($"ForeignNavigator: {projectFile} {key.MemberDocId ?? key.TypeDocId} -> {found.Count} location(s)"); + return found; + } + catch (Exception ex) + { + Logger.Exception(ex, "ForeignNavigator.Find failed"); + return none; + } + } + + /// Opens the declaration in its editor (the C# or VB editor). + internal static void Open(ForeignLocation location) + { + XDocuments.Open(location.FilePath, location.StartLine, location.StartColumn, false); + } + + /// Several declarations (partial classes): the Find All References list, as Go To Implementation shows them. + internal static void Present(IReadOnlyList locations, IXSymbol target, string title) + { + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var hits = locations.Select(l => DeclarationHit.Create(l.FilePath, ProjectName(l.FilePath), l.StartLine, l.StartColumn, + LineText(l.FilePath, l.StartLine), l.Name, l.ContainerName)).ToList(); + var service = await VS.GetRequiredServiceAsync(); + var window = service.StartSearch(title); + var source = new ReferencesTableDataSource(target, await FindReferencesCommand.CreateTextFormatAsync()); + window.Manager.AddSource(source, ReferencesTableDataSource.Columns); + source.Add(hits); + source.Complete(); + } + catch (Exception ex) + { + Logger.Exception(ex, "ForeignNavigator.Present failed"); + } + }).FileAndForget("XSharp/ForeignNavigator/Present"); + } + + private static VisualStudioWorkspace Workspace() + { + var componentModel = ServiceProvider.GlobalProvider.GetService(typeof(SComponentModel)) as IComponentModel; + return componentModel?.GetService(); + } + + private static async Task> CompilationsAsync(IList projects) + { + using (var cts = new CancellationTokenSource(TimeoutMilliseconds)) + { + var tasks = projects.Select(p => p.GetCompilationAsync(cts.Token)).ToList(); + var all = Task.WhenAll(tasks); + await Task.WhenAny(all, Task.Delay(TimeoutMilliseconds)).ConfigureAwait(false); + return tasks.Where(t => t.Status == TaskStatus.RanToCompletion && t.Result != null).Select(t => t.Result).ToList(); + } + } + + private static string LineText(string filePath, int line) + { + try + { + return File.ReadLines(filePath).Skip(line).FirstOrDefault() ?? ""; + } + catch (Exception) + { + return ""; + } + } + + private static string ProjectName(string filePath) + { + var workspace = Workspace(); + var documentId = workspace?.CurrentSolution.GetDocumentIdsWithFilePath(filePath).FirstOrDefault(); + return documentId == null ? "" : workspace.CurrentSolution.GetProject(documentId.ProjectId)?.Name ?? ""; + } + } +} diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs index 81af2ec7ca..badd66e4ca 100644 --- a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolKey.cs @@ -3,6 +3,8 @@ // Licensed under the Apache License, Version 2.0. // See License.txt in the project root for license information. // +using XSharpModel; + namespace XSharp.LanguageService.ForeignSource { /// What to look for in a C# or VB compilation: a type, or a member of that type. @@ -23,5 +25,18 @@ internal ForeignSymbolKey(string typeDocId, string memberDocId, string memberNam /// The member name as X# spells it (.ctor for a constructor); used when the doc ID does not resolve. internal string MemberName { get; } internal int ParameterCount { get; } + + /// The key for a type, or a member of that type, read from a DLL. + internal static ForeignSymbolKey From(XPETypeSymbol type, XPESymbol element) + { + if (element is XPEMemberSymbol member) + { + // an extension method is declared in its static class, not in the type it extends + if (member.IsExtension && member.DeclaringTypeSym != null) + type = member.DeclaringTypeSym; + return new ForeignSymbolKey(type.XMLSignature, member.XMLSignature, member.Name, member.Parameters.Count); + } + return new ForeignSymbolKey(type.XMLSignature, null, null, 0); + } } } diff --git a/src/VisualStudio/LanguageService/LanguageService.csproj b/src/VisualStudio/LanguageService/LanguageService.csproj index cec705da06..ed40b8a232 100644 --- a/src/VisualStudio/LanguageService/LanguageService.csproj +++ b/src/VisualStudio/LanguageService/LanguageService.csproj @@ -161,6 +161,7 @@ + diff --git a/src/VisualStudio/LanguageService/LanguageService2022.csproj b/src/VisualStudio/LanguageService/LanguageService2022.csproj index 2d4eb612d2..9c9a0eaccd 100644 --- a/src/VisualStudio/LanguageService/LanguageService2022.csproj +++ b/src/VisualStudio/LanguageService/LanguageService2022.csproj @@ -161,6 +161,7 @@ + From 95b69d6ad805b8409f6511b1e4a0b7236331af95 Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:22:19 +0100 Subject: [PATCH 4/7] [VS] Peek Definition and Go To Implementation into C#/VB source Co-Authored-By: Claude Opus 5.5 --- .../Commands/GoToImplementationCommand.cs | 20 +++++++++++ .../XSharpDefinitionPeekItem.cs | 35 +++++++++++++++++-- .../PeekDefinition/XSharpPeekItemSource.cs | 9 +++++ .../PeekDefinition/XSharpResultSource.cs | 33 +++++++++-------- 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/VisualStudio/LanguageService/Commands/GoToImplementationCommand.cs b/src/VisualStudio/LanguageService/Commands/GoToImplementationCommand.cs index a6b1f5c5bb..fbfc7b4a7e 100644 --- a/src/VisualStudio/LanguageService/Commands/GoToImplementationCommand.cs +++ b/src/VisualStudio/LanguageService/Commands/GoToImplementationCommand.cs @@ -14,6 +14,7 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Threading; using XSharp.LanguageService.FindReferences; +using XSharp.LanguageService.ForeignSource; using XSharp.LanguageService.GoToImplementation; using XSharp.Settings; using XSharp.Support; @@ -172,6 +173,22 @@ private static void Navigate(object result, SourceHierarchy hierarchy, ITextView XSharpGotoDefinition.Goto(symbol, textView, state); } + /// One row for a DLL symbol from a C# or VB project of the solution: its declaration in that source. + private static ReferenceHit ForeignHit(IXSymbol symbol) + { + IReadOnlyList found = null; + if (symbol is XPETypeSymbol peType) + found = ForeignNavigator.Find(peType, peType); + else if (symbol is XPEMemberSymbol peMember && peMember.Parent is XPETypeSymbol parent) + found = ForeignNavigator.Find(parent, peMember); + if (found == null || found.Count == 0) + return null; + var first = found[0]; + var lineText = System.IO.File.Exists(first.FilePath) + ? System.IO.File.ReadLines(first.FilePath).Skip(first.StartLine).FirstOrDefault() : null; + return DeclarationHit.Create(first.FilePath, null, first.StartLine, first.StartColumn, lineText, first.Name, first.ContainerName); + } + /// One row: the declaration, for a DLL symbol in the generated source F12 opens. private static ReferenceHit Hit(object result, SourceHierarchy hierarchy) { @@ -182,6 +199,9 @@ private static ReferenceHit Hit(object result, SourceHierarchy hierarchy) row.StartLine, row.StartColumn, row.SourceCode, node.Name, node.FullName); } var symbol = result as IXSymbol; + var foreign = ForeignHit(symbol); + if (foreign != null) + return foreign; var entity = symbol as XSourceEntity; if (entity == null && symbol is XPETypeSymbol peType) entity = XSharpGotoDefinition.FindSystemElement(peType, peType); diff --git a/src/VisualStudio/LanguageService/PeekDefinition/XSharpDefinitionPeekItem.cs b/src/VisualStudio/LanguageService/PeekDefinition/XSharpDefinitionPeekItem.cs index 86bcf3d8d6..387018f7dd 100644 --- a/src/VisualStudio/LanguageService/PeekDefinition/XSharpDefinitionPeekItem.cs +++ b/src/VisualStudio/LanguageService/PeekDefinition/XSharpDefinitionPeekItem.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.VisualStudio.Language.Intellisense; +using XSharp.LanguageService.ForeignSource; using XSharpModel; namespace XSharp.LanguageService @@ -7,11 +9,38 @@ namespace XSharp.LanguageService class XSharpDefinitionPeekItem : IPeekableItem { internal readonly IPeekResultFactory _peekResultFactory; - internal XSourceSymbol _gotoElement; + // what the peek window shows: a file, a 0-based range in it, and the editor that opens it + internal readonly string FilePath; + internal readonly int StartLine; + internal readonly int StartColumn; + internal readonly int EndLine; + internal readonly int EndColumn; + internal readonly string Label; + internal readonly Guid EditorGuid; public XSharpDefinitionPeekItem(XSourceSymbol gotoElement, IPeekResultFactory peekResultFactory) { - _gotoElement = gotoElement; + var range = gotoElement.Range; + FilePath = gotoElement.File.FullPath; + StartLine = range.StartLine; + StartColumn = range.StartColumn; + EndLine = range.EndLine; + EndColumn = range.EndColumn; + Label = gotoElement.Name; + EditorGuid = new Guid(XSharpConstants.EditorFactoryGuidString); + _peekResultFactory = peekResultFactory; + } + + /// A declaration in a C# or VB project of the solution, shown in its own editor. + public XSharpDefinitionPeekItem(ForeignLocation target, IPeekResultFactory peekResultFactory) + { + FilePath = target.FilePath; + StartLine = target.StartLine; + StartColumn = target.StartColumn; + EndLine = target.EndLine; + EndColumn = target.EndColumn; + Label = target.Name; + EditorGuid = Guid.Empty; _peekResultFactory = peekResultFactory; } diff --git a/src/VisualStudio/LanguageService/PeekDefinition/XSharpPeekItemSource.cs b/src/VisualStudio/LanguageService/PeekDefinition/XSharpPeekItemSource.cs index 0d024f980f..f78a406ca2 100644 --- a/src/VisualStudio/LanguageService/PeekDefinition/XSharpPeekItemSource.cs +++ b/src/VisualStudio/LanguageService/PeekDefinition/XSharpPeekItemSource.cs @@ -3,6 +3,7 @@ using Microsoft.VisualStudio.Text; using System; using XSharpModel; +using XSharp.LanguageService.ForeignSource; using XSharp.Settings; using XSharp.Support; namespace XSharp.LanguageService @@ -68,6 +69,14 @@ public void AugmentPeekSession(IPeekSession session, IList peekab { return; } + // a symbol from a C# or VB project of the solution: its source + var foreign = ForeignNavigator.Find(petype, pesymbol); + if (foreign.Count > 0) + { + foreach (var target in foreign) + peekableItems.Add(new XSharpDefinitionPeekItem(target, _peekResultFactory)); + return; + } var entity = XSharpGotoDefinition.FindSystemElement(petype, pesymbol); if (entity != null) peekableItems.Add(new XSharpDefinitionPeekItem(entity, _peekResultFactory)); diff --git a/src/VisualStudio/LanguageService/PeekDefinition/XSharpResultSource.cs b/src/VisualStudio/LanguageService/PeekDefinition/XSharpResultSource.cs index dbcea68486..1861397fe9 100644 --- a/src/VisualStudio/LanguageService/PeekDefinition/XSharpResultSource.cs +++ b/src/VisualStudio/LanguageService/PeekDefinition/XSharpResultSource.cs @@ -22,31 +22,30 @@ public void FindResults(string relationshipName, IPeekResultCollection resultCol { return; } - var fileName = Path.GetFileName(this.peekableItem._gotoElement.File.FullPath); + var item = this.peekableItem; + var fileName = Path.GetFileName(item.FilePath); if (string.IsNullOrEmpty(fileName)) return; - var label = this.peekableItem._gotoElement.Name; - var title = string.Format("{0} - ({1}, {2})", fileName, this.peekableItem._gotoElement.Range.StartLine, this.peekableItem._gotoElement.Range.StartColumn+1); + var label = item.Label ?? ""; + var title = string.Format("{0} - ({1}, {2})", fileName, item.StartLine, item.StartColumn+1); - using (var displayInfo = new PeekResultDisplayInfo2(label: label, labelTooltip: this.peekableItem._gotoElement.File.FullPath, title: title, titleTooltip: this.peekableItem._gotoElement.File.FullPath, startIndexOfTokenInLabel: 0, lengthOfTokenInLabel: label.Length)) + using (var displayInfo = new PeekResultDisplayInfo2(label: label, labelTooltip: item.FilePath, title: title, titleTooltip: item.FilePath, startIndexOfTokenInLabel: 0, lengthOfTokenInLabel: label.Length)) { - var range = this.peekableItem._gotoElement.Range; - - var result = peekableItem._peekResultFactory.Create + var result = item._peekResultFactory.Create ( displayInfo, default, - this.peekableItem._gotoElement.File.FullPath, - range.StartLine , - range.StartColumn , - range.EndLine , - range.EndColumn , - range.StartLine , - range.StartColumn , - range.EndLine , - range.EndColumn , + item.FilePath, + item.StartLine , + item.StartColumn , + item.EndLine , + item.EndColumn , + item.StartLine , + item.StartColumn , + item.EndLine , + item.EndColumn , false, - new Guid(XSharpConstants.EditorFactoryGuidString) + item.EditorGuid ); resultCollection.Add(result); From e9506c64fc4df7667e00d888025a9d24272d8c39 Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:28:06 +0100 Subject: [PATCH 5/7] [VS] Foreign source: nested types in metadata spelling (Outer/Inner) Co-Authored-By: Claude Opus 5.5 --- .../ForeignSymbolResolverTests.cs | 8 ++++++++ .../ForeignSource/ForeignSymbolResolver.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs b/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs index 0fa2441d35..0c70c81c7f 100644 --- a/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs +++ b/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs @@ -78,6 +78,14 @@ public void NestedTypeResolves() AssertSingle(Resolve(Type("T:Lib.Crate.Lid")), 7); } + [Fact] + public void NestedTypeInMetadataSpellingResolves() + { + // X# takes the type name from Mono.Cecil, which separates nested types with '/' (reflection: '+') + AssertSingle(Resolve(Type("T:Lib.Crate/Lid")), 7); + AssertSingle(Resolve(Type("T:Lib.Crate+Lid")), 7); + } + [Fact] public void InterfaceAndEnumResolve() { diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs index d4c68fcb17..59d174a092 100644 --- a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs @@ -38,15 +38,21 @@ internal static IReadOnlyList Resolve(IEnumerable return result; } + /// Doc IDs separate nested types with '.'; the metadata names X# reads use '/' (Cecil) or '+' (reflection). + private static string DocId(string id) + { + return id?.Replace('/', '.').Replace('+', '.'); + } + private static IEnumerable Find(Compilation compilation, ForeignSymbolKey key) { if (key.MemberDocId != null) { - var members = InSource(DocumentationCommentId.GetSymbolsForDeclarationId(key.MemberDocId, compilation)); + var members = InSource(DocumentationCommentId.GetSymbolsForDeclarationId(DocId(key.MemberDocId), compilation)); if (members.Count > 0) return members; } - var type = DocumentationCommentId.GetFirstSymbolForDeclarationId(key.TypeDocId, compilation) as INamedTypeSymbol; + var type = DocumentationCommentId.GetFirstSymbolForDeclarationId(DocId(key.TypeDocId), compilation) as INamedTypeSymbol; if (type == null) return Enumerable.Empty(); if (key.MemberDocId != null) From ffa04a58da06c15b9f30f9fceb0f668b567781bf Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:38:43 +0100 Subject: [PATCH 6/7] [VS] Foreign source: skip generated files; keep Roslyn out of Find's JIT Co-Authored-By: Claude Opus 5.5 --- .../ForeignSymbolResolverTests.cs | 35 +++++++++++++++++++ .../ForeignSource/ForeignNavigator.cs | 31 +++++++++++----- .../ForeignSource/ForeignSymbolResolver.cs | 23 +++++++----- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs b/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs index 0c70c81c7f..1d42055c7e 100644 --- a/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs +++ b/src/VisualStudio/LanguageService.Tests/ForeignSymbolResolverTests.cs @@ -128,6 +128,41 @@ public void PartialClassGivesBothParts() Assert.Equal(new[] { @"C:\lib\A.cs", @"C:\lib\B.cs" }, found.Select(f => f.FilePath).OrderBy(p => p).ToArray()); } + // a source generator's tree has a virtual path; nothing on disk can be opened there + private const string GeneratedPath = @"Generator\Gen\Duo.g.cs"; + + private static Compilation CompileWithGeneratedPart() + { + return Compile( + (@"C:\lib\A.cs", "namespace Lib { public partial class Duo { } }"), + (GeneratedPath, "namespace Lib { public partial class Duo { public int Name { get; set; } } }")); + } + + private static bool OnDisk(string path) => path != GeneratedPath; + + [Fact] + public void GeneratedMemberFallsBackToType() + { + var found = ForeignSymbolResolver.Resolve(new[] { CompileWithGeneratedPart() }, + Member("T:Lib.Duo", "P:Lib.Duo.Name", "Name", 0), OnDisk); + Assert.Single(found); + Assert.Equal(@"C:\lib\A.cs", found[0].FilePath); + } + + [Fact] + public void PartialTypeDropsGeneratedPart() + { + var found = ForeignSymbolResolver.Resolve(new[] { CompileWithGeneratedPart() }, Type("T:Lib.Duo"), OnDisk); + Assert.Single(found); + Assert.Equal(@"C:\lib\A.cs", found[0].FilePath); + } + + [Fact] + public void NothingOnDiskGivesNothing() + { + Assert.Empty(ForeignSymbolResolver.Resolve(new[] { Compile((CratePath, CrateSource)) }, Type("T:Lib.Crate"), p => false)); + } + [Fact] public void UnknownTypeGivesNothing() { diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs index 1e387616dc..6a4bbbac0f 100644 --- a/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignNavigator.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Community.VisualStudio.Toolkit; @@ -48,25 +49,37 @@ internal static IReadOnlyList Find(XPETypeSymbol type, XPESymbo var projectFile = ForeignProjects.FindProjectFile(assembly?.FileName); if (projectFile == null) return none; - var workspace = Workspace(); - if (workspace == null) - return none; - var projects = workspace.CurrentSolution.Projects - .Where(p => string.Equals(p.FilePath, projectFile, StringComparison.OrdinalIgnoreCase)).ToList(); - if (projects.Count == 0) - return none; - var compilations = ThreadHelper.JoinableTaskFactory.Run(() => CompilationsAsync(projects)); - var found = ForeignSymbolResolver.Resolve(compilations, key); + var found = FindInWorkspace(projectFile, key); XSharpGotoDefinition.WriteOutputMessage($"ForeignNavigator: {projectFile} {key.MemberDocId ?? key.TypeDocId} -> {found.Count} location(s)"); return found; } catch (Exception ex) { + // also a Roslyn assembly that cannot be loaded (an older VS): it fails when FindInWorkspace is compiled Logger.Exception(ex, "ForeignNavigator.Find failed"); return none; } } + /// + /// All Roslyn work lives here, not in : when a Roslyn assembly cannot be loaded, the JIT fails + /// while compiling this method, which happens inside Find's try, and F12 falls back to the generated header. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static IReadOnlyList FindInWorkspace(string projectFile, ForeignSymbolKey key) + { + var workspace = Workspace(); + if (workspace == null) + return Array.Empty(); + var projects = workspace.CurrentSolution.Projects + .Where(p => string.Equals(p.FilePath, projectFile, StringComparison.OrdinalIgnoreCase)).ToList(); + if (projects.Count == 0) + return Array.Empty(); + var compilations = ThreadHelper.JoinableTaskFactory.Run(() => CompilationsAsync(projects)); + // generated sources (source generators) have virtual paths that cannot be opened + return ForeignSymbolResolver.Resolve(compilations, key, File.Exists); + } + /// Opens the declaration in its editor (the C# or VB editor). internal static void Open(ForeignLocation location) { diff --git a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs index 59d174a092..8980c2dcb3 100644 --- a/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs +++ b/src/VisualStudio/LanguageService/ForeignSource/ForeignSymbolResolver.cs @@ -17,15 +17,19 @@ namespace XSharp.LanguageService.ForeignSource /// internal static class ForeignSymbolResolver { - internal static IReadOnlyList Resolve(IEnumerable compilations, ForeignSymbolKey key) + /// + /// Whether a source file can be opened. Source generators produce trees with virtual paths; their locations are + /// skipped, and the search goes on as if the symbol had no source there (member → type → nothing). Null: all files. + /// + internal static IReadOnlyList Resolve(IEnumerable compilations, ForeignSymbolKey key, Func isOnDisk = null) { var result = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var compilation in compilations) { - foreach (var symbol in Find(compilation, key)) + foreach (var symbol in Find(compilation, key, isOnDisk)) { - foreach (var location in symbol.Locations.Where(l => l.IsInSource)) + foreach (var location in symbol.Locations.Where(l => Usable(l, isOnDisk))) { var span = location.GetLineSpan(); if (!seen.Add(span.Path + "|" + span.StartLinePosition.Line)) @@ -44,8 +48,14 @@ private static string DocId(string id) return id?.Replace('/', '.').Replace('+', '.'); } - private static IEnumerable Find(Compilation compilation, ForeignSymbolKey key) + private static bool Usable(Location location, Func isOnDisk) { + return location.IsInSource && (isOnDisk == null || isOnDisk(location.SourceTree.FilePath)); + } + + private static IEnumerable Find(Compilation compilation, ForeignSymbolKey key, Func isOnDisk) + { + List InSource(IEnumerable symbols) => symbols.Where(s => s.Locations.Any(l => Usable(l, isOnDisk))).ToList(); if (key.MemberDocId != null) { var members = InSource(DocumentationCommentId.GetSymbolsForDeclarationId(DocId(key.MemberDocId), compilation)); @@ -66,11 +76,6 @@ private static IEnumerable Find(Compilation compilation, ForeignSymbolK return new[] { type }; } - private static List InSource(IEnumerable symbols) - { - return symbols.Where(s => s.Locations.Any(l => l.IsInSource)).ToList(); - } - private static int ParameterCount(ISymbol symbol) { switch (symbol) From 900e1b989568949ac955232803b0d5f854b57769 Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 9 Nov 2026 17:50:09 +0100 Subject: [PATCH 7/7] [VS] F12 jumps straight to the overload that takes the arguments Co-Authored-By: Claude Opus 5.5 --- .../GotoDefinitionTests.cs | 55 +++++++++++++++++++ .../Commands/GotoDefinition.cs | 18 +++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/VisualStudio/LanguageService.Tests/GotoDefinitionTests.cs b/src/VisualStudio/LanguageService.Tests/GotoDefinitionTests.cs index ad80fbfdac..0140f9ee4e 100644 --- a/src/VisualStudio/LanguageService.Tests/GotoDefinitionTests.cs +++ b/src/VisualStudio/LanguageService.Tests/GotoDefinitionTests.cs @@ -19,5 +19,60 @@ public void TheTypeStaysNextToAKeyword() var type = new XSourceTypeSymbol("Crate", Kind.Class, Modifiers.Public, new TextRange(1, 1, 10, 1), new TextInterval(0, 100), null, null); Assert.Equal(new IXSymbol[] { type }, XSharpGotoDefinition.Targets(new List { new XKeywordSymbol("CLASS"), type })); } + + private static XSourceMemberSymbol Method(string name, params (string Name, string Type)[] parameters) + { + var member = new XSourceMemberSymbol(name, Kind.Method, Modifiers.Public, new TextRange(2, 1, 3, 1), new TextInterval(0, 10), "VOID", null); + member.CallingConvention = CallingConvention.None; + foreach (var p in parameters) + { + var par = new XSourceParameterSymbol(member, p.Name, new TextRange(2, 1, 2, 1), new TextInterval(0, 1), p.Type); + par.ParamType = ParamType.As; + member.AddParameter(par); + } + return member; + } + + // F12 with the caret in of ; identifiers have no known type + private static IList PickAt(string text, string name, params IXSymbol[] overloads) + { + var tokens = TestLexer.Lex(text); + return XSharpGotoDefinition.Overloads(overloads, tokens, text.IndexOf(name) + 1, t => null); + } + + private static readonly XSourceMemberSymbol OpenNone = Method("Open"); + private static readonly XSourceMemberSymbol OpenKey = Method("Open", ("key", "STRING")); + private static readonly XSourceMemberSymbol OpenNumber = Method("Open", ("number", "INT")); + + [Fact] + public void TheArgumentCountPicksTheOverload() + { + Assert.Equal(new IXSymbol[] { OpenKey }, PickAt("c:Open(x)\n", "Open", OpenNone, OpenKey)); + Assert.Equal(new IXSymbol[] { OpenNone }, PickAt("c:Open()\n", "Open", OpenNone, OpenKey)); + } + + [Fact] + public void TheArgumentTypePicksTheOverload() + { + Assert.Equal(new IXSymbol[] { OpenKey }, PickAt("c:Open(\"x\")\n", "Open", OpenNone, OpenKey, OpenNumber)); + } + + [Fact] + public void UnknownArgumentTypesKeepTheCandidates() + { + Assert.Equal(new IXSymbol[] { OpenKey, OpenNumber }, PickAt("c:Open(x)\n", "Open", OpenNone, OpenKey, OpenNumber)); + } + + [Fact] + public void WithoutACallAllOverloadsStay() + { + Assert.Equal(new IXSymbol[] { OpenNone, OpenKey }, PickAt("x := c:Open\n", "Open", OpenNone, OpenKey)); + } + + [Fact] + public void ASingleTargetIsKept() + { + Assert.Equal(new IXSymbol[] { OpenKey }, PickAt("c:Open()\n", "Open", OpenKey)); + } } } diff --git a/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs b/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs index bd4aa4b4f7..52add52201 100644 --- a/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs +++ b/src/VisualStudio/LanguageService/Commands/GotoDefinition.cs @@ -10,7 +10,9 @@ using XSharp.Support; using System.Collections; using System.Collections.Generic; +using XSharp.LanguageService.FindReferences; using XSharp.LanguageService.ForeignSource; +using LanguageService.SyntaxTree; namespace XSharp.LanguageService { class XSharpGotoDefinition @@ -71,10 +73,12 @@ internal static void GotoDefn(ITextView TextView) try { ModelWalker.Suspend(); - var result = TextView.GetSymbolUnderCursor(out var state,out _, out _); + var result = TextView.GetSymbolUnderCursor(out var state, out var location, out _); // ThreadHelper.ThrowIfNotOnUIThread(); result = Targets(result); + if (location != null) + result = Overloads(result, location.Document?.Tokens, location.Position, id => SymbolResolver.TypeOf(location, id)); if (result.Count == 1) { Goto(result[0], TextView, state); @@ -97,6 +101,18 @@ internal static void GotoDefn(ITextView TextView) } } + /// + /// At a call site, the overloads that take these arguments (count, then the types that are known), as Find + /// References and Go To Implementation pick them: one left means F12 jumps there. Without a call, or when the + /// arguments rule nothing out, all targets stay and F12 shows the list as before. + /// + internal static IList Overloads(IList targets, IList tokens, int position, Func identifierType) + { + if (targets.Count < 2 || tokens == null) + return targets; + return OverloadFilter.Filter(targets, OverloadFilter.ArgumentsAt(tokens, position), identifierType); + } + /// /// The symbols F12 can go to. A keyword (the caret on CLASS) has no definition: like nothing found, not /// "Cannot navigate". Files that are included more than once because of multi targetting are deduplicated.