diff --git a/CHANGELOG.md b/CHANGELOG.md index d51a0669..18495ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - By @CoolCoderSuper https://github.com/razzmatazz/csharp-language-server/pull/226 * Fix how doc strings are rendered - https://github.com/razzmatazz/csharp-language-server/pull/228 +* Implement (experimental) support for loading multi-tfm solutions + - https://github.com/razzmatazz/csharp-language-server/pull/205 ## [0.17.0] - 2025-04-30 / Krokšlys * Upgrade Roslyn to 4.13.0 diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index 9df5b842..195abaca 100644 --- a/src/CSharpLanguageServer/Lsp/Server.fs +++ b/src/CSharpLanguageServer/Lsp/Server.fs @@ -310,7 +310,8 @@ module Server = let serverCreator client = new CSharpLspServer(client, settings) :> ICSharpLspServer - let clientCreator = CSharpLspClient + let clientCreator = + fun (a, b) -> new CSharpLspClient(a, b) Ionide.LanguageServerProtocol.Server.start requestHandlings diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 03a3b536..820e38f4 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -375,6 +375,73 @@ type CSharpLspHostServices () = let interceptor = WorkspaceServicesInterceptor() generator.CreateClassProxyWithTarget(services, interceptor) + +let loadProjectFilenamesFromSolution solutionPath = + let projectFilenames = new List() + + let solutionFile = Microsoft.Build.Construction.SolutionFile.Parse(solutionPath) + for project in solutionFile.ProjectsInOrder do + if project.ProjectType = Microsoft.Build.Construction.SolutionProjectType.KnownToBeMSBuildFormat then + projectFilenames.Add(project.AbsolutePath) + + projectFilenames |> Set.ofSeq + + +let resolveTargetFrameworkWorkspaceProps (logger: ILog) (projs: string seq) props = + let tfms = new List() + + for projectFilename in projs do + let projectCollection = new Microsoft.Build.Evaluation.ProjectCollection(); + let props = new Dictionary(); + + let buildProject = projectCollection.LoadProject(projectFilename, props, toolsVersion=null) + + let noneIfEmpty s = + s |> Option.ofObj + |> Option.bind (fun s -> if String.IsNullOrEmpty(s) then None else Some s) + + let targetFramework = buildProject.GetPropertyValue("TargetFramework") |> noneIfEmpty + + match targetFramework with + | Some tfm -> + tfms.Add(tfm.Trim()) + | _ -> () + + let targetFrameworks = buildProject.GetPropertyValue("TargetFrameworks") |> noneIfEmpty + + match targetFrameworks with + | Some semicolonSeparatedTfms -> + for tfm in semicolonSeparatedTfms.Split(";") do + tfms.Add(tfm.Trim()) + | _ -> () + + projectCollection.UnloadProject(buildProject) + + let distinctTfms = tfms |> Set.ofSeq + + logger.debug ( + Log.setMessage "resolveDefaultWorkspaceProps: distinctTfms={distinctTfms}" + >> Log.addContext "distinctTfms" (string distinctTfms) + ) + + if distinctTfms.Count > 1 then + // select the highest tfm + let selectedTargetFramework = + distinctTfms + |> Seq.sortByDescending (fun s -> s) + |> Seq.head + + props + |> Map.add "TargetFramework" selectedTargetFramework + else + props + + +let resolveDefaultWorkspaceProps (logger: ILog) projs = + Map.empty + |> resolveTargetFrameworkWorkspaceProps logger projs + + let tryLoadSolutionOnPath (lspClient: ILspClient) (logger: ILog) @@ -399,7 +466,16 @@ let tryLoadSolutionOnPath do! progress.Begin(beginMessage) do! logMessage beginMessage - let msbuildWorkspace = MSBuildWorkspace.Create(CSharpLspHostServices()) + let projs = loadProjectFilenamesFromSolution solutionPath + let workspaceProps = resolveDefaultWorkspaceProps logger projs + + if workspaceProps.Count > 0 then + logger.info ( + Log.setMessage "Will use these MSBuild props: {workspaceProps}" + >> Log.addContext "workspaceProps" (string workspaceProps) + ) + + let msbuildWorkspace = MSBuildWorkspace.Create(workspaceProps, CSharpLspHostServices()) msbuildWorkspace.LoadMetadataForReferencedProjects <- true let! solution = msbuildWorkspace.OpenSolutionAsync(solutionPath) |> Async.AwaitTask @@ -436,8 +512,17 @@ let tryLoadSolutionFromProjectFiles do! progress.Begin($"Loading {projs.Length} project(s)...", false, $"0/{projs.Length}", 0u) let loadedProj = ref 0 - let msbuildWorkspace = MSBuildWorkspace.Create(CSharpLspHostServices()) + let workspaceProps = resolveDefaultWorkspaceProps logger projs + + if workspaceProps.Count > 0 then + logger.info ( + Log.setMessage "Will use these MSBuild props: {workspaceProps}" + >> Log.addContext "workspaceProps" (string workspaceProps) + ) + + let msbuildWorkspace = MSBuildWorkspace.Create(workspaceProps, CSharpLspHostServices()) msbuildWorkspace.LoadMetadataForReferencedProjects <- true + for file in projs do if projs.Length < 10 then do! logMessage (sprintf "loading project \"%s\".." file) diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index da7bd783..7e41da10 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -4,6 +4,7 @@ false false + FS0988 diff --git a/tests/CSharpLanguageServer.Tests/InitializationTests.fs b/tests/CSharpLanguageServer.Tests/InitializationTests.fs index 842d1167..19ca833e 100644 --- a/tests/CSharpLanguageServer.Tests/InitializationTests.fs +++ b/tests/CSharpLanguageServer.Tests/InitializationTests.fs @@ -5,6 +5,25 @@ open NUnit.Framework open CSharpLanguageServer.Tests.Tooling open Ionide.LanguageServerProtocol.Types +let assertHoverWorks (client: ClientController) file pos expectedMarkupContent = + use classFile = client.Open(file) + + let hover0Params: HoverParams = + { TextDocument = { Uri = classFile.Uri } + Position = pos + WorkDoneToken = None + } + + let hover0: Hover option = client.Request("textDocument/hover", hover0Params) + + match hover0 with + | Some { Contents = U3.C1 markupContent; Range = None } -> + Assert.AreEqual(MarkupKind.Markdown, markupContent.Kind) + Assert.AreEqual(expectedMarkupContent, markupContent.Value) + + | x -> failwithf "'{ Contents = U3.C1 markupContent; Range = None }' was expected but '%s' received" (string x) + + [] let testServerRegistersCapabilitiesWithTheClient () = use client = setupServerClient defaultClientProfile @@ -120,11 +139,28 @@ let testServerRegistersCapabilitiesWithTheClient () = [] let testSlnxSolutionFileWillBeFoundAndLoaded () = - use client = setupServerClient defaultClientProfile - "TestData/testSlnx" + use client = setupServerClient defaultClientProfile "TestData/testSlnx" client.StartAndWaitForSolutionLoad() Assert.IsTrue(client.ServerMessageLogContains(fun m -> m.Contains "1 solution(s) found")) Assert.IsTrue(client.ServerDidRespondTo "initialize") Assert.IsTrue(client.ServerDidRespondTo "initialized") + + assertHoverWorks + client + "Project/Class.cs" { Line = 2u; Character = 16u } + "```csharp\nvoid Class.MethodA(string arg)\n```" + + +[] +let testMultiTargetProjectLoads () = + use client = setupServerClient defaultClientProfile "TestData/testMultiTargetProjectLoads" + client.StartAndWaitForSolutionLoad() + + Assert.IsTrue(client.ServerMessageLogContains(fun m -> m.Contains "loading project")) + + assertHoverWorks + client + "Project/Class.cs" { Line = 2u; Character = 16u } + "```csharp\nvoid Class.Method(string arg)\n```" diff --git a/tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Class.cs b/tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Class.cs new file mode 100644 index 00000000..b0339d85 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Class.cs @@ -0,0 +1,6 @@ +class Class +{ + public void Method(string arg) + { + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Project.csproj b/tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Project.csproj new file mode 100644 index 00000000..5300706a --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Project.csproj @@ -0,0 +1,6 @@ + + + Exe + net6.0;net8.0 + +