From aeb801fda41154e0b6f011b65c425bbbff13e321 Mon Sep 17 00:00:00 2001 From: Alex Lawrence Date: Tue, 16 Sep 2025 22:34:11 +0100 Subject: [PATCH 1/9] feat: support dotnet 9+ extract interface code action --- .gitignore | 2 + Directory.Packages.props | 78 ++++---- csharp-language-server.sln | 28 ++- nuget.config | 13 ++ .../CSharpLanguageServer.fsproj | 1 + src/CSharpLanguageServer/ReflectedTypes.fs | 15 ++ src/CSharpLanguageServer/RoslynHelpers.fs | 170 ++++++++---------- 7 files changed, 169 insertions(+), 138 deletions(-) create mode 100644 nuget.config create mode 100644 src/CSharpLanguageServer/ReflectedTypes.fs diff --git a/.gitignore b/.gitignore index d80b8c08..8b57fc20 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ obj .ionide/ .idea/ nupkg/ +.vs/ +release/ diff --git a/Directory.Packages.props b/Directory.Packages.props index d081ad5a..5d3582dd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,40 +1,40 @@ - - - - true - true - - - 17.14.8 - 4.14.0 - 1.9.1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + true + true + + + 17.14.8 + 4.14.0 + 1.9.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/csharp-language-server.sln b/csharp-language-server.sln index 2907584b..6beb0cbc 100644 --- a/csharp-language-server.sln +++ b/csharp-language-server.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36429.23 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3ADA171B-D93F-4D34-97EA-5BB06662678B}" EndProject @@ -11,10 +11,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{35DF1A49 EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CSharpLanguageServer.Tests", "tests\CSharpLanguageServer.Tests\CSharpLanguageServer.Tests.fsproj", "{754F8FCE-6DC2-43E2-832D-00A8B830D192}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + FEATURES.md = FEATURES.md + global.json = global.json + LICENSE = LICENSE + nuget.config = nuget.config + README.md = README.md + EndProjectSection +EndProject Global - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 @@ -49,8 +61,14 @@ Global {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|x86.ActiveCfg = Release|Any CPU {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection GlobalSection(NestedProjects) = preSolution {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02} = {3ADA171B-D93F-4D34-97EA-5BB06662678B} {754F8FCE-6DC2-43E2-832D-00A8B830D192} = {35DF1A49-B49A-42CB-B43B-8E8120AD9B0E} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6E645033-9E5A-4849-9814-68EC7EA428FE} + EndGlobalSection EndGlobal diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..5e7f1ccd --- /dev/null +++ b/nuget.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index cd3b6aa9..c82ae078 100644 --- a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj +++ b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/CSharpLanguageServer/ReflectedTypes.fs b/src/CSharpLanguageServer/ReflectedTypes.fs new file mode 100644 index 00000000..00649df1 --- /dev/null +++ b/src/CSharpLanguageServer/ReflectedTypes.fs @@ -0,0 +1,15 @@ +module CSharpLanguageServer.ReflectedTypes + +open System +open System.Threading.Tasks + +open CSharpLanguageServer.Util + +type TaskOfType (taskType: Type) = + static let fromResultMethod = + typeof.GetMethod("FromResult") + |> nonNull (sprintf "%s.FromResult()" (string typeof)) + let typedFromResultMethod = fromResultMethod.MakeGenericMethod([| taskType |]) + + member __.FromResult(resultValue: obj | null) = + typedFromResultMethod.Invoke(null, [| resultValue |]) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 9508909c..e215d69e 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -8,7 +8,6 @@ open System.Threading open System.Threading.Tasks open System.Collections.Immutable open System.Text.RegularExpressions - open Castle.DynamicProxy open ICSharpCode.Decompiler open ICSharpCode.Decompiler.CSharp @@ -30,6 +29,7 @@ open CSharpLanguageServer open CSharpLanguageServer.Conversions open CSharpLanguageServer.Logging open CSharpLanguageServer.Util +open ReflectedTypes type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) @@ -56,27 +56,29 @@ type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = override __.Visit(node: SyntaxNode | null) = let node = node |> nonNull "node" - if sym.Kind = SymbolKind.Method then - if node :? MethodDeclarationSyntax then - let nodeMethodDecl = node :?> MethodDeclarationSyntax + match sym.Kind, node with + | SymbolKind.Method, (:? MethodDeclarationSyntax as m) + when m.Identifier.ValueText = sym.Name -> + let symMethod = sym :?> IMethodSymbol + let methodArityMatches = + symMethod.Parameters.Length = m.ParameterList.Parameters.Count + collectIdentifier m.Identifier methodArityMatches - if nodeMethodDecl.Identifier.ValueText = sym.Name then - let methodArityMatches = - let symMethod = sym :?> IMethodSymbol - symMethod.Parameters.Length = nodeMethodDecl.ParameterList.Parameters.Count + | _, (:? TypeDeclarationSyntax as t) + when t.Identifier.ValueText = sym.Name -> + collectIdentifier t.Identifier false - collectIdentifier nodeMethodDecl.Identifier methodArityMatches - else if node :? TypeDeclarationSyntax then - let typeDecl = node :?> TypeDeclarationSyntax + | _, (:? PropertyDeclarationSyntax as p) + when p.Identifier.ValueText = sym.Name -> + collectIdentifier p.Identifier false - if typeDecl.Identifier.ValueText = sym.Name then - collectIdentifier typeDecl.Identifier false + | _, (:? EventDeclarationSyntax as e) + when e.Identifier.ValueText = sym.Name -> + collectIdentifier e.Identifier false + // TODO: collect other type of syntax nodes too - else if node :? PropertyDeclarationSyntax then - let propertyDecl = node :?> PropertyDeclarationSyntax + | _ -> () - if propertyDecl.Identifier.ValueText = sym.Name then - collectIdentifier propertyDecl.Identifier false else if node :? EventDeclarationSyntax then let eventDecl = node :?> EventDeclarationSyntax @@ -119,7 +121,15 @@ type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = | _ -> NotImplementedException(string invocation.Method) |> raise -type LegacyWorkspaceOptionServiceInterceptor(logMessage) = +type CleanCodeGenOptionsProxy(logMessage) = + static let workspacesAssembly = Assembly.Load("Microsoft.CodeAnalysis.Workspaces") + static let generator = ProxyGenerator() + let interceptor = CleanCodeGenerationOptionsProviderInterceptor(logMessage) + + member __.Create() = + workspacesAssembly.GetType("Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider") + +type LegacyWorkspaceOptionServiceInterceptor (logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = //logMessage (sprintf "LegacyWorkspaceOptionServiceInterceptor: %s" (string invocation.Method)) @@ -131,18 +141,9 @@ type LegacyWorkspaceOptionServiceInterceptor(logMessage) = | "GetGenerateConstructorFromMembersOptionsAddNullChecks" -> invocation.ReturnValue <- box true | "get_GenerateOverrides" -> invocation.ReturnValue <- box true | "get_CleanCodeGenerationOptionsProvider" -> - let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" - - let cleanCodeGenOptionsProvType = - workspacesAssembly.GetType - "Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider" - - let generator = ProxyGenerator() - let interceptor = CleanCodeGenerationOptionsProviderInterceptor logMessage - let proxy = generator.CreateClassProxy(cleanCodeGenOptionsProvType, interceptor) - invocation.ReturnValue <- proxy - - | _ -> NotImplementedException(string invocation.Method) |> raise + invocation.ReturnValue <- CleanCodeGenOptionsProxy(logMessage).Create() + | _ -> + NotImplementedException(string invocation.Method) |> raise type PickMembersServiceInterceptor(_logMessage) = interface IInterceptor with @@ -228,16 +229,8 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = match invocation.Method.Name with | "GetExtractClassOptionsAsync" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - let extractClassOptionsValue = getExtractClassOptionsImpl argOriginalType - - let fromResultMethod = - typeof.GetMethod "FromResult" - |> nonNull (sprintf "%s.FromResult()" (string typeof)) - - let typedFromResultMethod = - fromResultMethod.MakeGenericMethod [| extractClassOptionsValue.GetType() |] - - invocation.ReturnValue <- typedFromResultMethod.Invoke(null, [| extractClassOptionsValue |]) + let extractClassOptionsValue = getExtractClassOptionsImpl(argOriginalType) + invocation.ReturnValue <- TaskOfType(extractClassOptionsValue.GetType()).FromResult(extractClassOptionsValue) | "GetExtractClassOptions" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol @@ -249,59 +242,54 @@ type ExtractInterfaceOptionsServiceInterceptor(logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = - match invocation.Method.Name with - | "GetExtractInterfaceOptionsAsync" -> - let argExtractableMembers = invocation.Arguments[2] :?> List - let argDefaultInterfaceName = invocation.Arguments[3] :?> string - - let fileName = sprintf "%s.cs" argDefaultInterfaceName - - let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" - - let extractInterfaceOptionsResultType = - featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult" - |> nonNull - "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" - - let locationEnumType = - extractInterfaceOptionsResultType.GetNestedType "ExtractLocation" - |> nonNull "extractInterfaceOptionsResultType.GetNestedType('ExtractLocation')" - - let location = Enum.Parse(locationEnumType, "NewFile") // or "SameFile" - - let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" - - let cleanCodeGenOptionsProvType = - workspacesAssembly.GetType - "Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider" - - let generator = ProxyGenerator() - let interceptor = CleanCodeGenerationOptionsProviderInterceptor logMessage - - let cleanCodeGenerationOptionsProvider = - generator.CreateClassProxy(cleanCodeGenOptionsProvType, interceptor) - - let extractInterfaceOptionsResultValue = - Activator.CreateInstance( + let (argExtractableMembers, argDefaultInterfaceName) = + match invocation.Method.Name, invocation.Arguments[1], invocation.Arguments[2], invocation.Arguments[3] with + | "GetExtractInterfaceOptions", (:? ImmutableArray as extractableMembers), (:? string as interfaceName), _ -> + (extractableMembers,interfaceName) + | "GetExtractInterfaceOptions", _, (:? ImmutableArray as extractableMembers), (:? string as interfaceName) -> + (extractableMembers, interfaceName) + | "GetExtractInterfaceOptionsAsync", _, (:? List as extractableMembers), (:? string as interfaceName) -> + (extractableMembers.ToImmutableArray(), interfaceName) + | _ -> + NotImplementedException(string invocation.Method.Name) |> raise + + + let fileName = sprintf "%s.cs" argDefaultInterfaceName + + let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") + + let extractInterfaceOptionsResultType = + featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult") + |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" + + let locationEnumType = + extractInterfaceOptionsResultType.GetNestedType("ExtractLocation") + |> nonNull "extractInterfaceOptionsResultType.GetNestedType('ExtractLocation')" + + let location = + Enum.Parse(locationEnumType, "NewFile") + |> fun v -> Convert.ChangeType(v, locationEnumType) + + invocation.ReturnValue <- + match invocation.Method.Name with + | "GetExtractInterfaceOptionsAsync" -> + TaskOfType(extractInterfaceOptionsResultType).FromResult(Activator.CreateInstance( extractInterfaceOptionsResultType, false, // isCancelled - argExtractableMembers.ToImmutableArray(), + argExtractableMembers, argDefaultInterfaceName, fileName, location, - cleanCodeGenerationOptionsProvider - ) + CleanCodeGenOptionsProxy(logMessage).Create())) - let fromResultMethod = - typeof.GetMethod "FromResult" - |> nonNull (sprintf "%s.FromResult()" (string typeof)) - - let typedFromResultMethod = - fromResultMethod.MakeGenericMethod [| extractInterfaceOptionsResultType |] - - invocation.ReturnValue <- typedFromResultMethod.Invoke(null, [| extractInterfaceOptionsResultValue |]) - - | _ -> NotImplementedException(string invocation.Method.Name) |> raise + | _ -> + Activator.CreateInstance( + extractInterfaceOptionsResultType, + false, // isCancelled + argExtractableMembers, + argDefaultInterfaceName, + fileName, + location) type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = interface IInterceptor with @@ -346,13 +334,7 @@ type RemoteHostClientProviderInterceptor(_logMessage) = workspacesAssembly.GetType "Microsoft.CodeAnalysis.Remote.RemoteHostClient" |> nonNull "GetType(Microsoft.CodeAnalysis.Remote.RemoteHostClient)" - let fromResultMI = - typeof.GetMethod("FromResult", BindingFlags.Static ||| BindingFlags.Public) - |> nonNull (sprintf "%s.FromResult()" (string typeof)) - - let genericMethod = fromResultMI.MakeGenericMethod remoteHostClientType - let nullResultTask = genericMethod.Invoke(null, [| null |]) - invocation.ReturnValue <- nullResultTask + invocation.ReturnValue <- TaskOfType(remoteHostClientType).FromResult(null) | _ -> NotImplementedException(string invocation.Method) |> raise From 2ecef155bca5dbb86cb6098a7843adbcd951bad9 Mon Sep 17 00:00:00 2001 From: Alex Lawrence Date: Wed, 17 Sep 2025 10:40:01 +0100 Subject: [PATCH 2/9] chore: move TaskOfType to Utils --- Directory.Packages.props | 80 +++++++++++----------- src/CSharpLanguageServer/ReflectedTypes.fs | 15 ---- src/CSharpLanguageServer/RoslynHelpers.fs | 1 - src/CSharpLanguageServer/Util.fs | 10 +++ 4 files changed, 50 insertions(+), 56 deletions(-) delete mode 100644 src/CSharpLanguageServer/ReflectedTypes.fs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5d3582dd..ef64aaba 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,40 +1,40 @@ - - - - true - true - - - 17.14.8 - 4.14.0 - 1.9.1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + true + true + + + 17.14.8 + 4.14.0 + 1.9.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CSharpLanguageServer/ReflectedTypes.fs b/src/CSharpLanguageServer/ReflectedTypes.fs deleted file mode 100644 index 00649df1..00000000 --- a/src/CSharpLanguageServer/ReflectedTypes.fs +++ /dev/null @@ -1,15 +0,0 @@ -module CSharpLanguageServer.ReflectedTypes - -open System -open System.Threading.Tasks - -open CSharpLanguageServer.Util - -type TaskOfType (taskType: Type) = - static let fromResultMethod = - typeof.GetMethod("FromResult") - |> nonNull (sprintf "%s.FromResult()" (string typeof)) - let typedFromResultMethod = fromResultMethod.MakeGenericMethod([| taskType |]) - - member __.FromResult(resultValue: obj | null) = - typedFromResultMethod.Invoke(null, [| resultValue |]) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index e215d69e..4081dd6b 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -29,7 +29,6 @@ open CSharpLanguageServer open CSharpLanguageServer.Conversions open CSharpLanguageServer.Logging open CSharpLanguageServer.Util -open ReflectedTypes type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index b3b4fb4e..977f8232 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -3,6 +3,7 @@ module CSharpLanguageServer.Util open System open System.Runtime.InteropServices open System.IO +open Microsoft.Build.Utilities let nonNull name (value: 'T when 'T: null) : 'T = if Object.ReferenceEquals(value, null) then @@ -94,3 +95,12 @@ module Async = module Map = let union map1 map2 = Map.fold (fun acc key value -> Map.add key value acc) map1 map2 + +type TaskOfType (taskType: Type) = + static let fromResultMethod = + typeof.GetMethod("FromResult") + |> nonNull (sprintf "%s.FromResult()" (string typeof)) + let typedFromResultMethod = fromResultMethod.MakeGenericMethod([| taskType |]) + + member __.FromResult(resultValue: obj | null) = + typedFromResultMethod.Invoke(null, [| resultValue |]) From 5232e6e642a3827888d36d2ec8aea025e992b51c Mon Sep 17 00:00:00 2001 From: Alex Lawrence Date: Wed, 17 Sep 2025 17:19:19 +0100 Subject: [PATCH 3/9] tests: add tests for extract interface --- Directory.Packages.props | 5 +- .../CSharpLanguageServer.fsproj | 1 - src/CSharpLanguageServer/RoslynHelpers.fs | 176 +++++++++--------- src/CSharpLanguageServer/Util.fs | 21 ++- .../AssemblyInfo.fs | 6 + .../CSharpLanguageServer.Tests.fsproj | 7 +- .../CodeActionTests.fs | 163 ++++++++++++---- .../DocumentFormattingTests.fs | 6 +- .../CSharpLanguageServer.Tests/HoverTests.fs | 11 +- .../Project/Class.cs | 0 .../Project/Project.csproj | 0 tests/CSharpLanguageServer.Tests/Tooling.fs | 14 +- 12 files changed, 267 insertions(+), 143 deletions(-) create mode 100644 tests/CSharpLanguageServer.Tests/AssemblyInfo.fs rename tests/CSharpLanguageServer.Tests/TestData/{testCodeActionOnMethodNameWorks => testCodeActions}/Project/Class.cs (100%) rename tests/CSharpLanguageServer.Tests/TestData/{testCodeActionOnMethodNameWorks => testCodeActions}/Project/Project.csproj (100%) diff --git a/Directory.Packages.props b/Directory.Packages.props index ef64aaba..187b71a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,9 +12,10 @@ - + + @@ -31,7 +32,7 @@ - + diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index c82ae078..cd3b6aa9 100644 --- a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj +++ b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj @@ -23,7 +23,6 @@ - diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 4081dd6b..078d8849 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -56,53 +56,44 @@ type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = let node = node |> nonNull "node" match sym.Kind, node with - | SymbolKind.Method, (:? MethodDeclarationSyntax as m) - when m.Identifier.ValueText = sym.Name -> - let symMethod = sym :?> IMethodSymbol - let methodArityMatches = - symMethod.Parameters.Length = m.ParameterList.Parameters.Count - collectIdentifier m.Identifier methodArityMatches + | SymbolKind.Method, (:? MethodDeclarationSyntax as m) when m.Identifier.ValueText = sym.Name -> + let symMethod = sym :?> IMethodSymbol - | _, (:? TypeDeclarationSyntax as t) - when t.Identifier.ValueText = sym.Name -> - collectIdentifier t.Identifier false + let methodArityMatches = + symMethod.Parameters.Length = m.ParameterList.Parameters.Count - | _, (:? PropertyDeclarationSyntax as p) - when p.Identifier.ValueText = sym.Name -> - collectIdentifier p.Identifier false + collectIdentifier m.Identifier methodArityMatches - | _, (:? EventDeclarationSyntax as e) - when e.Identifier.ValueText = sym.Name -> - collectIdentifier e.Identifier false - // TODO: collect other type of syntax nodes too + | _, (:? TypeDeclarationSyntax as t) when t.Identifier.ValueText = sym.Name -> + collectIdentifier t.Identifier false - | _ -> () + | _, (:? PropertyDeclarationSyntax as p) when p.Identifier.ValueText = sym.Name -> + collectIdentifier p.Identifier false + | _, (:? EventDeclarationSyntax as e) when e.Identifier.ValueText = sym.Name -> + collectIdentifier e.Identifier false + // TODO: collect other type of syntax nodes too - else if node :? EventDeclarationSyntax then - let eventDecl = node :?> EventDeclarationSyntax - - if eventDecl.Identifier.ValueText = sym.Name then - collectIdentifier eventDecl.Identifier false + | _ -> () - // TODO: collect other type of syntax nodes too - base.Visit node + else if node :? EventDeclarationSyntax then + let eventDecl = node :?> EventDeclarationSyntax type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = match invocation.Method.Name with | "GetCleanCodeGenerationOptionsAsync" -> - let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" + let workspacesAssembly = Assembly.Load("Microsoft.CodeAnalysis.Workspaces") let cleanCodeGenOptionsType = - workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions" + workspacesAssembly.GetType("Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions") |> nonNull "workspacesAssembly.GetType('Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions')" let getDefaultMI = - cleanCodeGenOptionsType.GetMethod "GetDefault" + cleanCodeGenOptionsType.GetMethod("GetDefault") |> nonNull "cleanCodeGenOptionsType.GetMethod('GetDefault')" let argLanguageServices = invocation.Arguments[0] @@ -113,7 +104,7 @@ type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = let valueTaskType = typedefof> let valueTaskTypeForCleanCodeGenOptions = - valueTaskType.MakeGenericType [| cleanCodeGenOptionsType |] + valueTaskType.MakeGenericType([| cleanCodeGenOptionsType |]) invocation.ReturnValue <- Activator.CreateInstance(valueTaskTypeForCleanCodeGenOptions, defaultCleanCodeGenOptions) @@ -128,7 +119,7 @@ type CleanCodeGenOptionsProxy(logMessage) = member __.Create() = workspacesAssembly.GetType("Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider") -type LegacyWorkspaceOptionServiceInterceptor (logMessage) = +type LegacyWorkspaceOptionServiceInterceptor(logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = //logMessage (sprintf "LegacyWorkspaceOptionServiceInterceptor: %s" (string invocation.Method)) @@ -141,8 +132,7 @@ type LegacyWorkspaceOptionServiceInterceptor (logMessage) = | "get_GenerateOverrides" -> invocation.ReturnValue <- box true | "get_CleanCodeGenerationOptionsProvider" -> invocation.ReturnValue <- CleanCodeGenOptionsProxy(logMessage).Create() - | _ -> - NotImplementedException(string invocation.Method) |> raise + | _ -> NotImplementedException(string invocation.Method) |> raise type PickMembersServiceInterceptor(_logMessage) = interface IInterceptor with @@ -163,7 +153,7 @@ type PickMembersServiceInterceptor(_logMessage) = type ExtractClassOptionsServiceInterceptor(_logMessage) = let getExtractClassOptionsImpl (argOriginalType: INamedTypeSymbol) : Object = - let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" + let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") let typeName = "Base" + argOriginalType.Name let fileName = typeName + ".cs" @@ -172,14 +162,14 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = let immArrayType = typeof let extractClassMemberAnalysisResultType = - featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult" + featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult") |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult')" let resultListType = - typedefof>.MakeGenericType extractClassMemberAnalysisResultType + typedefof>.MakeGenericType(extractClassMemberAnalysisResultType) - let resultList = Activator.CreateInstance resultListType + let resultList = Activator.CreateInstance(resultListType) let memberFilter (m: ISymbol) = match m with @@ -194,12 +184,12 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = Activator.CreateInstance(extractClassMemberAnalysisResultType, memberToAdd, false) let resultListAddMI = - resultListType.GetMethod "Add" |> nonNull "resultListType.GetMethod('Add')" + resultListType.GetMethod("Add") |> nonNull "resultListType.GetMethod('Add')" resultListAddMI.Invoke(resultList, [| memberAnalysisResult |]) |> ignore let resultListToArrayMI = - resultListType.GetMethod "ToArray" + resultListType.GetMethod("ToArray") |> nonNull "resultListType.GetMethod('ToArray')" let resultListAsArray = resultListToArrayMI.Invoke(resultList, null) @@ -228,12 +218,12 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = match invocation.Method.Name with | "GetExtractClassOptionsAsync" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - let extractClassOptionsValue = getExtractClassOptionsImpl(argOriginalType) - invocation.ReturnValue <- TaskOfType(extractClassOptionsValue.GetType()).FromResult(extractClassOptionsValue) + let extractClassOptionsValue = getExtractClassOptionsImpl (argOriginalType) + invocation.ReturnValue <- Task.fromResult (extractClassOptionsValue.GetType(), extractClassOptionsValue) | "GetExtractClassOptions" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - invocation.ReturnValue <- getExtractClassOptionsImpl argOriginalType + invocation.ReturnValue <- getExtractClassOptionsImpl (argOriginalType) | _ -> NotImplementedException(string invocation.Method) |> raise @@ -242,16 +232,22 @@ type ExtractInterfaceOptionsServiceInterceptor(logMessage) = member __.Intercept(invocation: IInvocation) = let (argExtractableMembers, argDefaultInterfaceName) = - match invocation.Method.Name, invocation.Arguments[1], invocation.Arguments[2], invocation.Arguments[3] with - | "GetExtractInterfaceOptions", (:? ImmutableArray as extractableMembers), (:? string as interfaceName), _ -> - (extractableMembers,interfaceName) - | "GetExtractInterfaceOptions", _, (:? ImmutableArray as extractableMembers), (:? string as interfaceName) -> - (extractableMembers, interfaceName) - | "GetExtractInterfaceOptionsAsync", _, (:? List as extractableMembers), (:? string as interfaceName) -> - (extractableMembers.ToImmutableArray(), interfaceName) - | _ -> - NotImplementedException(string invocation.Method.Name) |> raise - + match + invocation.Method.Name, invocation.Arguments[1], invocation.Arguments[2], invocation.Arguments[3] + with + | "GetExtractInterfaceOptions", + (:? ImmutableArray as extractableMembers), + (:? string as interfaceName), + _ -> (extractableMembers, interfaceName) + | "GetExtractInterfaceOptions", + _, + (:? ImmutableArray as extractableMembers), + (:? string as interfaceName) -> (extractableMembers, interfaceName) + | "GetExtractInterfaceOptionsAsync", + _, + (:? List as extractableMembers), + (:? string as interfaceName) -> (extractableMembers.ToImmutableArray(), interfaceName) + | _ -> NotImplementedException(string invocation.Method.Name) |> raise let fileName = sprintf "%s.cs" argDefaultInterfaceName @@ -259,7 +255,8 @@ type ExtractInterfaceOptionsServiceInterceptor(logMessage) = let extractInterfaceOptionsResultType = featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult") - |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" + |> nonNull + "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" let locationEnumType = extractInterfaceOptionsResultType.GetNestedType("ExtractLocation") @@ -272,14 +269,18 @@ type ExtractInterfaceOptionsServiceInterceptor(logMessage) = invocation.ReturnValue <- match invocation.Method.Name with | "GetExtractInterfaceOptionsAsync" -> - TaskOfType(extractInterfaceOptionsResultType).FromResult(Activator.CreateInstance( + Task.fromResult ( extractInterfaceOptionsResultType, - false, // isCancelled - argExtractableMembers, - argDefaultInterfaceName, - fileName, - location, - CleanCodeGenOptionsProxy(logMessage).Create())) + Activator.CreateInstance( + extractInterfaceOptionsResultType, + false, // isCancelled + argExtractableMembers, + argDefaultInterfaceName, + fileName, + location, + CleanCodeGenOptionsProxy(logMessage).Create() + ) + ) | _ -> Activator.CreateInstance( @@ -288,7 +289,8 @@ type ExtractInterfaceOptionsServiceInterceptor(logMessage) = argExtractableMembers, argDefaultInterfaceName, fileName, - location) + location + ) type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = interface IInterceptor with @@ -300,7 +302,7 @@ type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = let _argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol let argSelectedMembers = invocation.Arguments[2] :?> ImmutableArray - let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" + let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") let msmOptionsType = featuresAssembly.GetType "Microsoft.CodeAnalysis.MoveStaticMembers.MoveStaticMembersOptions" @@ -327,13 +329,13 @@ type RemoteHostClientProviderInterceptor(_logMessage) = match invocation.Method.Name with | "TryGetRemoteHostClientAsync" -> - let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" + let workspacesAssembly = Assembly.Load("Microsoft.CodeAnalysis.Workspaces") let remoteHostClientType = workspacesAssembly.GetType "Microsoft.CodeAnalysis.Remote.RemoteHostClient" |> nonNull "GetType(Microsoft.CodeAnalysis.Remote.RemoteHostClient)" - invocation.ReturnValue <- TaskOfType(remoteHostClientType).FromResult(null) + invocation.ReturnValue <- Task.fromResult (remoteHostClientType, null) | _ -> NotImplementedException(string invocation.Method) |> raise @@ -414,7 +416,7 @@ let loadProjectFilenamesFromSolution (solutionPath: string) = assert Path.IsPathRooted solutionPath let projectFilenames = new List() - let solutionFile = SolutionFile.Parse solutionPath + let solutionFile = Microsoft.Build.Construction.SolutionFile.Parse(solutionPath) for project in solutionFile.ProjectsInOrder do if project.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat then @@ -480,10 +482,10 @@ let loadProjectTfms (logger: ILogger) (projs: string seq) : Map Option.ofObj - |> Option.bind (fun s -> if String.IsNullOrEmpty s then None else Some s) + |> Option.bind (fun s -> if String.IsNullOrEmpty(s) then None else Some s) let targetFramework = - match buildProject.GetPropertyValue "TargetFramework" |> noneIfEmpty with + match buildProject.GetPropertyValue("TargetFramework") |> noneIfEmpty with | Some tfm -> [ tfm.Trim() ] | None -> [] @@ -494,7 +496,7 @@ let loadProjectTfms (logger: ILogger) (projs: string seq) : Map Map.add projectFilename (targetFramework @ targetFrameworks) - projectCollection.UnloadProject buildProject + projectCollection.UnloadProject(buildProject) with :? InvalidProjectFileException as ipfe -> logger.LogDebug( "loadProjectTfms: failed to load {projectFilename}: {ex}", @@ -525,18 +527,20 @@ let resolveDefaultWorkspaceProps (logger: ILogger) projs : Map = let tryLoadSolutionOnPath (lspClient: ILspClient) (logger: ILogger) (solutionPath: string) = - assert Path.IsPathRooted solutionPath - let progress = ProgressReporter lspClient + assert Path.IsPathRooted(solutionPath) + let progress = ProgressReporter(lspClient) let logMessage m = - lspClient.WindowLogMessage + lspClient.WindowLogMessage( { Type = MessageType.Info Message = sprintf "csharp-ls: %s" m } + ) let showMessage m = - lspClient.WindowShowMessage + lspClient.WindowShowMessage( { Type = MessageType.Info Message = sprintf "csharp-ls: %s" m } + ) async { try @@ -582,7 +586,7 @@ let tryLoadSolutionFromProjectFiles (logMessage: string -> Async) (projs: string list) = - let progress = ProgressReporter lspClient + let progress = ProgressReporter(lspClient) async { do! progress.Begin($"Loading {projs.Length} project(s)...", false, $"0/{projs.Length}", 0u) @@ -625,7 +629,7 @@ let tryLoadSolutionFromProjectFiles let selectPreferredSolution (slnFiles: string list) : option = let getProjectCount (slnPath: string) = try - let sln = SolutionFile.Parse slnPath + let sln = SolutionFile.Parse(slnPath) Some(sln.ProjectsInOrder.Count, slnPath) with _ -> None @@ -641,7 +645,9 @@ let selectPreferredSolution (slnFiles: string list) : option = let findAndLoadSolutionOnDir (lspClient: ILspClient) (logger: ILogger) dir = async { let fileNotOnNodeModules (filename: string) = - filename.Split Path.DirectorySeparatorChar |> Seq.contains "node_modules" |> not + filename.Split(Path.DirectorySeparatorChar) + |> Seq.contains "node_modules" + |> not let solutionFiles = [ "*.sln"; "*.slnx" ] @@ -650,9 +656,10 @@ let findAndLoadSolutionOnDir (lspClient: ILspClient) (logger: ILogger) dir = asy |> Seq.toList let logMessage m = - lspClient.WindowLogMessage + lspClient.WindowLogMessage( { Type = MessageType.Info Message = sprintf "csharp-ls: %s" m } + ) do! logMessage (sprintf "%d solution(s) found: [%s]" solutionFiles.Length (String.Join(", ", solutionFiles))) @@ -714,20 +721,20 @@ let loadSolutionOnSolutionPathOrDir } let getContainingTypeOrThis (symbol: ISymbol) : INamedTypeSymbol = - if symbol :? INamedTypeSymbol then + if (symbol :? INamedTypeSymbol) then symbol :?> INamedTypeSymbol else symbol.ContainingType let getFullReflectionName (containingType: INamedTypeSymbol) = let stack = Stack() - stack.Push containingType.MetadataName + stack.Push(containingType.MetadataName) let mutable ns = containingType.ContainingNamespace let mutable doContinue = true while doContinue do - stack.Push ns.Name + stack.Push(ns.Name) ns <- ns.ContainingNamespace doContinue <- ns <> null && not ns.IsGlobalNamespace @@ -741,21 +748,21 @@ let tryAddDocument (solution: Solution) : Async = async { - let docDir = Path.GetDirectoryName docFilePath + let docDir = Path.GetDirectoryName(docFilePath) //logMessage (sprintf "TextDocumentDidOpen: docFilename=%s docDir=%s" docFilename docDir) let fileOnProjectDir (p: Project) = - let projectDir = Path.GetDirectoryName p.FilePath - let projectDirWithDirSepChar = projectDir + string Path.DirectorySeparatorChar + let projectDir = Path.GetDirectoryName(p.FilePath) + let projectDirWithDirSepChar = projectDir + (string Path.DirectorySeparatorChar) - docDir = projectDir || docDir.StartsWith projectDirWithDirSepChar + (docDir = projectDir) || docDir.StartsWith(projectDirWithDirSepChar) let projectOnPath = solution.Projects |> Seq.filter fileOnProjectDir |> Seq.tryHead let! newDocumentMaybe = match projectOnPath with | Some proj -> - let projectBaseDir = Path.GetDirectoryName proj.FilePath + let projectBaseDir = Path.GetDirectoryName(proj.FilePath) let docName = docFilePath.Substring(projectBaseDir.Length + 1) //logMessage (sprintf "Adding \"%s\" (\"%s\") to project %s" docName docFilePath proj.FilePath) @@ -763,7 +770,7 @@ let tryAddDocument let newDoc = proj.AddDocument( name = docName, - text = SourceText.From text, + text = SourceText.From(text), folders = null, filePath = docFilePath ) @@ -813,6 +820,9 @@ let makeDocumentFromMetadata let text = decompiler.DecompileTypeAsString fullTypeName + let mdDocumentFilename = + $"$metadata$/projects/{project.Name}/assemblies/{containingAssembly.Name}/symbols/{fullName}.cs" + let mdDocumentFilename = $"$metadata$/projects/{project.Name}/assemblies/{containingAssembly.Name}/symbols/{fullName}.cs" diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index 977f8232..31ec1196 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -2,8 +2,9 @@ module CSharpLanguageServer.Util open System open System.Runtime.InteropServices +open System.Threading.Tasks open System.IO -open Microsoft.Build.Utilities +open System.Reflection let nonNull name (value: 'T when 'T: null) : 'T = if Object.ReferenceEquals(value, null) then @@ -75,6 +76,16 @@ let formatInColumns (data: list>) : string = |> String.concat Environment.NewLine +[] +module TaskExtensions = + type Task with + static member private fromResultMI = + typeof.GetMethod("FromResult") + |> nonNull (sprintf "%s.FromResult()" (string typeof)) + + static member fromResult(taskType: Type, resultValue: obj | null) = + Task.fromResultMI.MakeGenericMethod([| taskType |]).Invoke(null, [| resultValue |]) + module Seq = let inline tryMaxBy (projection: 'T -> 'U) (source: 'T seq) : 'T option = if isNull source || Seq.isEmpty source then @@ -96,11 +107,3 @@ module Map = let union map1 map2 = Map.fold (fun acc key value -> Map.add key value acc) map1 map2 -type TaskOfType (taskType: Type) = - static let fromResultMethod = - typeof.GetMethod("FromResult") - |> nonNull (sprintf "%s.FromResult()" (string typeof)) - let typedFromResultMethod = fromResultMethod.MakeGenericMethod([| taskType |]) - - member __.FromResult(resultValue: obj | null) = - typedFromResultMethod.Invoke(null, [| resultValue |]) diff --git a/tests/CSharpLanguageServer.Tests/AssemblyInfo.fs b/tests/CSharpLanguageServer.Tests/AssemblyInfo.fs new file mode 100644 index 00000000..e9b1e998 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/AssemblyInfo.fs @@ -0,0 +1,6 @@ +module AssemblyInfo + +open NUnit.Framework + +[] +do () diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index b4b8f02e..b2109bb7 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -8,6 +8,7 @@ + @@ -24,10 +25,14 @@ + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs index 94734b91..39759fa7 100644 --- a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs +++ b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs @@ -1,42 +1,131 @@ -module CSharpLanguageServer.Tests.CodeActionTests +namespace CSharpLanguageServer.Tests open NUnit.Framework open Ionide.LanguageServerProtocol.Types - open CSharpLanguageServer.Tests.Tooling -[] -let testCodeActionOnMethodNameWorks () = - use client = - setupServerClient defaultClientProfile "TestData/testCodeActionOnMethodNameWorks" - - client.StartAndWaitForSolutionLoad() - - use classFile = client.Open("Project/Class.cs") - - let caParams0: CodeActionParams = - { TextDocument = { Uri = classFile.Uri } - Range = - { Start = { Line = 2u; Character = 16u } - End = { Line = 2u; Character = 16u } } - Context = - { Diagnostics = [||] - Only = None - TriggerKind = None } - WorkDoneToken = None - PartialResultToken = None } - - let caResult0: TextDocumentCodeActionResult option = - client.Request("textDocument/codeAction", caParams0) - - Assert.IsTrue(caResult0.IsSome) - - match caResult0 with - | Some [| U2.C2 x |] -> - Assert.AreEqual("Extract base class...", x.Title) - Assert.AreEqual(None, x.Kind) - Assert.AreEqual(None, x.Diagnostics) - Assert.AreEqual(None, x.Disabled) - Assert.IsTrue(x.Edit.IsSome) - - | _ -> failwith "Some [| U2.C1 x |] was expected" +[] +type CodeActionTests() = + + static let mutable client: ClientController = + setupServerClient defaultClientProfile "TestData/testCodeActions" + + [] + member _.Setup() = client.StartAndWaitForSolutionLoad() + + [] + member _.``code action menu appears on request``() = + use classFile = client.Open("Project/Class.cs") + + let caParams: CodeActionParams = + { TextDocument = { Uri = classFile.Uri } + Range = + { Start = { Line = 1u; Character = 0u } + End = { Line = 1u; Character = 0u } } + Context = + { Diagnostics = [||] + Only = None + TriggerKind = None } + WorkDoneToken = None + PartialResultToken = None } + + let caResult: TextDocumentCodeActionResult option = + client.Request("textDocument/codeAction", caParams) + + let assertCodeActionHasTitle (ca: CodeAction, title: string) = + Assert.AreEqual(title, ca.Title) + Assert.AreEqual(None, ca.Kind) + Assert.AreEqual(None, ca.Diagnostics) + Assert.AreEqual(None, ca.Disabled) + Assert.IsTrue(ca.Edit.IsSome) + + match caResult with + | Some [| U2.C2 generateOverrides + U2.C2 extractInterface + U2.C2 generateConstructor + U2.C2 extractBaseClass + U2.C2 addDebuggerDisplay |] -> + assertCodeActionHasTitle (generateOverrides, "Generate overrides...") + assertCodeActionHasTitle (extractInterface, "Extract interface...") + assertCodeActionHasTitle (generateConstructor, "Generate constructor 'Class()'") + assertCodeActionHasTitle (extractBaseClass, "Extract base class...") + assertCodeActionHasTitle (addDebuggerDisplay, "Add 'DebuggerDisplay' attribute") + + | _ -> failwith "Not all code actions were matched as expected" + + [] + member _.``extract base class request extracts base class``() = + use classFile = client.Open("Project/Class.cs") + + let caParams0: CodeActionParams = + { TextDocument = { Uri = classFile.Uri } + Range = + { Start = { Line = 2u; Character = 16u } + End = { Line = 2u; Character = 16u } } + Context = + { Diagnostics = [||] + Only = None + TriggerKind = None } + WorkDoneToken = None + PartialResultToken = None } + + let caResult: TextDocumentCodeActionResult option = + client.Request("textDocument/codeAction", caParams0) + + match caResult with + | Some [| U2.C2 x |] -> Assert.AreEqual("Extract base class...", x.Title) + // TODO: match extract base class edit structure + + | _ -> failwith "Some [| U2.C2 x |] was expected" + + [] + member _.``extract interface code action should extract an interface``() = + use classFile = client.Open("Project/Class.cs") + + let caArgs: CodeActionParams = + { TextDocument = { Uri = classFile.Uri } + Range = + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 0u } } + Context = + { Diagnostics = [||] + Only = Some [| "refactor.extract.interface" |] + TriggerKind = Some CodeActionTriggerKind.Invoked } + WorkDoneToken = None + PartialResultToken = None } + + let caOptions: TextDocumentCodeActionResult option = + match client.Request("textDocument/codeAction", caArgs) with + | Some opts -> opts + | None -> failwith "Expected code actions" + + let codeAction = + match caOptions |> Option.bind (Array.tryItem 1) with + | Some(U2.C2 ca) -> + Assert.AreEqual("Extract interface...", ca.Title) + ca + | _ -> failwith "Extract interface action not found" + + let expectedImplementInterfaceEdits = + { Range = + { Start = { Line = 0u; Character = 11u } + End = { Line = 0u; Character = 11u } } + NewText = " : IClass" } + + let expectedCreateInterfaceEdits = + { Range = + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 0u } } + NewText = "internal interface IClass\n{\n void Method(string arg);\n}" } + + match codeAction.Edit with + | Some { DocumentChanges = Some [| U4.C1 create; U4.C1 implement |] } -> + match create.Edits, implement.Edits with + | [| U2.C1 createEdits |], [| U2.C1 implementEdits |] -> + Assert.AreEqual(expectedCreateInterfaceEdits, createEdits |> TextEdit.normalizeNewText) + + Assert.AreEqual(expectedImplementInterfaceEdits, implementEdits |> TextEdit.normalizeNewText) + + | _ -> failwith "Expected exactly one U2.C1 edit in both create/implement" + + | _ -> failwith "Unexpected edit structure" diff --git a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs index 7143cecf..3b2b489c 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs @@ -33,6 +33,10 @@ let testEditorConfigFormatting () = | Some tes -> let expectedClassContents = File.ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) + |> Text.normalizeLineEndings + let actualClassContents = + classFile.GetFileContentsWithTextEditsApplied(tes) + |> Text.normalizeLineEndings - Assert.AreEqual(expectedClassContents, classFile.GetFileContentsWithTextEditsApplied(tes)) + Assert.AreEqual(expectedClassContents, actualClassContents) | None -> failwith "Some TextEdit's were expected" diff --git a/tests/CSharpLanguageServer.Tests/HoverTests.fs b/tests/CSharpLanguageServer.Tests/HoverTests.fs index 2bab71d9..206d8303 100644 --- a/tests/CSharpLanguageServer.Tests/HoverTests.fs +++ b/tests/CSharpLanguageServer.Tests/HoverTests.fs @@ -29,7 +29,8 @@ let testHoverWorks () = match hover.Contents with | U3.C1 c -> Assert.AreEqual(MarkupKind.Markdown, c.Kind) - Assert.AreEqual("```csharp\nvoid Class.Method(string arg)\n```", c.Value) + let actualHoverContents = c.Value |> Text.normalizeLineEndings + Assert.AreEqual("```csharp\nvoid Class.Method(string arg)\n```", actualHoverContents) | _ -> failwith "C1 was expected" Assert.IsTrue(hover.Range.IsNone) @@ -55,12 +56,8 @@ let testHoverWorks () = Assert.AreEqual(MarkupKind.Markdown, c.Kind) Assert.AreEqual( - """```csharp -string -``` - -Represents text as a sequence of UTF-16 code units.""", - c.Value.ReplaceLineEndings() + "```csharp\nstring\n```\n\nRepresents text as a sequence of UTF-16 code units.", + c.Value |> Text.normalizeLineEndings ) | _ -> failwith "C1 was expected" diff --git a/tests/CSharpLanguageServer.Tests/TestData/testCodeActionOnMethodNameWorks/Project/Class.cs b/tests/CSharpLanguageServer.Tests/TestData/testCodeActions/Project/Class.cs similarity index 100% rename from tests/CSharpLanguageServer.Tests/TestData/testCodeActionOnMethodNameWorks/Project/Class.cs rename to tests/CSharpLanguageServer.Tests/TestData/testCodeActions/Project/Class.cs diff --git a/tests/CSharpLanguageServer.Tests/TestData/testCodeActionOnMethodNameWorks/Project/Project.csproj b/tests/CSharpLanguageServer.Tests/TestData/testCodeActions/Project/Project.csproj similarity index 100% rename from tests/CSharpLanguageServer.Tests/TestData/testCodeActionOnMethodNameWorks/Project/Project.csproj rename to tests/CSharpLanguageServer.Tests/TestData/testCodeActions/Project/Project.csproj diff --git a/tests/CSharpLanguageServer.Tests/Tooling.fs b/tests/CSharpLanguageServer.Tests/Tooling.fs index 73054a83..112cc449 100644 --- a/tests/CSharpLanguageServer.Tests/Tooling.fs +++ b/tests/CSharpLanguageServer.Tests/Tooling.fs @@ -14,6 +14,7 @@ open NUnit.Framework open Newtonsoft.Json.Linq open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.Server +open System.Text.RegularExpressions let indexJToken (name: string) (jobj: option) : option = jobj |> Option.bind (fun p -> p[name] |> Option.ofObj) @@ -328,9 +329,9 @@ let processClientEvent (state: ClientState) (post: ClientEvent -> unit) msg : As if rpcMsg.ContainsKey("result") || rpcMsg.ContainsKey("error") then post (ServerRpcCallResultOrError rpcMsg) else if rpcMsg.ContainsKey("method") then - let rpcMsgId: JValue = rpcMsg["id"] :?> JValue + let rpcMsgId: JValue = rpcMsg["id"] :?> JValue | null let rpcMsgMethod: string = string rpcMsg["method"] - let rpcMsgParams: JObject = rpcMsg["params"] :?> JObject + let rpcMsgParams: JObject = rpcMsg["params"] :?> JObject | null post (ClientRpcCall(rpcMsgId, rpcMsgMethod, rpcMsgParams)) else failwith (sprintf "RpcMessageReceived: unknown json rpc msg type: %s" (string rpcMsg)) @@ -853,3 +854,12 @@ let setupServerClient (clientProfile: ClientProfile) (testDataDirName: string) = DirectoryInfo(Path.Combine(testAssemblyLocationDir, "..", "..", "..", testDataDirName)) new ClientController(clientActor, actualTestDataDirName) + +module Text = + let normalizeLineEndings (s: string) = + Regex.Replace(s, @"\r\n|\n\r|\r|\n", "\n") + +module TextEdit = + let normalizeNewText (s: TextEdit) = + { s with + NewText = s.NewText |> Text.normalizeLineEndings } From 9f0333f55618e00083510f1b04dae8ec3a801138 Mon Sep 17 00:00:00 2001 From: alsi-lawr Date: Fri, 19 Sep 2025 11:22:35 +0100 Subject: [PATCH 4/9] chore: rebase conflicts --- src/CSharpLanguageServer/RoslynHelpers.fs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 078d8849..c8f6e124 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -77,8 +77,7 @@ type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = | _ -> () - else if node :? EventDeclarationSyntax then - let eventDecl = node :?> EventDeclarationSyntax + base.Visit(node) type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = interface IInterceptor with @@ -820,9 +819,6 @@ let makeDocumentFromMetadata let text = decompiler.DecompileTypeAsString fullTypeName - let mdDocumentFilename = - $"$metadata$/projects/{project.Name}/assemblies/{containingAssembly.Name}/symbols/{fullName}.cs" - let mdDocumentFilename = $"$metadata$/projects/{project.Name}/assemblies/{containingAssembly.Name}/symbols/{fullName}.cs" @@ -830,3 +826,4 @@ let makeDocumentFromMetadata let mdDocument = SourceText.From text |> mdDocumentEmpty.WithText mdDocument, text + From c0031069de9057d7eea4796de8fd1e06c629e75e Mon Sep 17 00:00:00 2001 From: alsi-lawr Date: Fri, 19 Sep 2025 11:26:36 +0100 Subject: [PATCH 5/9] chore: reformat --- src/CSharpLanguageServer/RoslynHelpers.fs | 1 - src/CSharpLanguageServer/Util.fs | 15 +++++++-------- .../DocumentFormattingTests.fs | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index c8f6e124..859353a7 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -826,4 +826,3 @@ let makeDocumentFromMetadata let mdDocument = SourceText.From text |> mdDocumentEmpty.WithText mdDocument, text - diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index 31ec1196..55eb4470 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -4,7 +4,7 @@ open System open System.Runtime.InteropServices open System.Threading.Tasks open System.IO -open System.Reflection +open System.Reflection let nonNull name (value: 'T when 'T: null) : 'T = if Object.ReferenceEquals(value, null) then @@ -76,15 +76,15 @@ let formatInColumns (data: list>) : string = |> String.concat Environment.NewLine -[] +[] module TaskExtensions = type Task with - static member private fromResultMI = - typeof.GetMethod("FromResult") + static member private fromResultMI = + typeof.GetMethod("FromResult") |> nonNull (sprintf "%s.FromResult()" (string typeof)) - - static member fromResult(taskType: Type, resultValue: obj | null) = - Task.fromResultMI.MakeGenericMethod([| taskType |]).Invoke(null, [| resultValue |]) + + static member fromResult(taskType: Type, resultValue: obj | null) = + Task.fromResultMI.MakeGenericMethod([| taskType |]).Invoke(null, [| resultValue |]) module Seq = let inline tryMaxBy (projection: 'T -> 'U) (source: 'T seq) : 'T option = @@ -106,4 +106,3 @@ module Async = module Map = let union map1 map2 = Map.fold (fun acc key value -> Map.add key value acc) map1 map2 - diff --git a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs index 3b2b489c..c92dfb4e 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs @@ -34,9 +34,9 @@ let testEditorConfigFormatting () = let expectedClassContents = File.ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) |> Text.normalizeLineEndings + let actualClassContents = - classFile.GetFileContentsWithTextEditsApplied(tes) - |> Text.normalizeLineEndings + classFile.GetFileContentsWithTextEditsApplied(tes) |> Text.normalizeLineEndings Assert.AreEqual(expectedClassContents, actualClassContents) | None -> failwith "Some TextEdit's were expected" From c76561b84184a650d58b5eccdbf2f3da5ddb11c6 Mon Sep 17 00:00:00 2001 From: alsi-lawr Date: Fri, 19 Sep 2025 11:45:50 +0100 Subject: [PATCH 6/9] fix: clean code gen provider proxy now returns the proxy and not the type fix: undo formatting changes from rebase --- src/CSharpLanguageServer/RoslynHelpers.fs | 110 ++++++++++++---------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 859353a7..81d21e73 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -77,22 +77,22 @@ type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = | _ -> () - base.Visit(node) + base.Visit node type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = match invocation.Method.Name with | "GetCleanCodeGenerationOptionsAsync" -> - let workspacesAssembly = Assembly.Load("Microsoft.CodeAnalysis.Workspaces") + let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" let cleanCodeGenOptionsType = - workspacesAssembly.GetType("Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions") + workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions" |> nonNull "workspacesAssembly.GetType('Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions')" let getDefaultMI = - cleanCodeGenOptionsType.GetMethod("GetDefault") + cleanCodeGenOptionsType.GetMethod "GetDefault" |> nonNull "cleanCodeGenOptionsType.GetMethod('GetDefault')" let argLanguageServices = invocation.Arguments[0] @@ -103,7 +103,7 @@ type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = let valueTaskType = typedefof> let valueTaskTypeForCleanCodeGenOptions = - valueTaskType.MakeGenericType([| cleanCodeGenOptionsType |]) + valueTaskType.MakeGenericType [| cleanCodeGenOptionsType |] invocation.ReturnValue <- Activator.CreateInstance(valueTaskTypeForCleanCodeGenOptions, defaultCleanCodeGenOptions) @@ -111,12 +111,25 @@ type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = | _ -> NotImplementedException(string invocation.Method) |> raise type CleanCodeGenOptionsProxy(logMessage) = - static let workspacesAssembly = Assembly.Load("Microsoft.CodeAnalysis.Workspaces") + static let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" static let generator = ProxyGenerator() - let interceptor = CleanCodeGenerationOptionsProviderInterceptor(logMessage) + + static let cleanCodeGenOptionsProvTypeMaybe = + workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider" + |> Option.ofObj + member __.Create() = - workspacesAssembly.GetType("Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider") + let interceptor = CleanCodeGenerationOptionsProviderInterceptor logMessage + + let proxyMaybe = + cleanCodeGenOptionsProvTypeMaybe + |> Option.map (fun cleanCodeGenOptionsProvType -> + generator.CreateClassProxy(cleanCodeGenOptionsProvType, interceptor)) + + match proxyMaybe with + | Some proxy -> proxy + | None -> failwith "Could not create CleanCodeGenerationOptionsProvider proxy" type LegacyWorkspaceOptionServiceInterceptor(logMessage) = interface IInterceptor with @@ -152,7 +165,7 @@ type PickMembersServiceInterceptor(_logMessage) = type ExtractClassOptionsServiceInterceptor(_logMessage) = let getExtractClassOptionsImpl (argOriginalType: INamedTypeSymbol) : Object = - let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") + let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" let typeName = "Base" + argOriginalType.Name let fileName = typeName + ".cs" @@ -161,14 +174,14 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = let immArrayType = typeof let extractClassMemberAnalysisResultType = - featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult") + featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult" |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult')" let resultListType = - typedefof>.MakeGenericType(extractClassMemberAnalysisResultType) + typedefof>.MakeGenericType extractClassMemberAnalysisResultType - let resultList = Activator.CreateInstance(resultListType) + let resultList = Activator.CreateInstance resultListType let memberFilter (m: ISymbol) = match m with @@ -183,12 +196,12 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = Activator.CreateInstance(extractClassMemberAnalysisResultType, memberToAdd, false) let resultListAddMI = - resultListType.GetMethod("Add") |> nonNull "resultListType.GetMethod('Add')" + resultListType.GetMethod "Add" |> nonNull "resultListType.GetMethod('Add')" resultListAddMI.Invoke(resultList, [| memberAnalysisResult |]) |> ignore let resultListToArrayMI = - resultListType.GetMethod("ToArray") + resultListType.GetMethod "ToArray" |> nonNull "resultListType.GetMethod('ToArray')" let resultListAsArray = resultListToArrayMI.Invoke(resultList, null) @@ -217,12 +230,12 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = match invocation.Method.Name with | "GetExtractClassOptionsAsync" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - let extractClassOptionsValue = getExtractClassOptionsImpl (argOriginalType) + let extractClassOptionsValue = getExtractClassOptionsImpl argOriginalType invocation.ReturnValue <- Task.fromResult (extractClassOptionsValue.GetType(), extractClassOptionsValue) | "GetExtractClassOptions" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - invocation.ReturnValue <- getExtractClassOptionsImpl (argOriginalType) + invocation.ReturnValue <- getExtractClassOptionsImpl argOriginalType | _ -> NotImplementedException(string invocation.Method) |> raise @@ -230,35 +243,35 @@ type ExtractInterfaceOptionsServiceInterceptor(logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = - let (argExtractableMembers, argDefaultInterfaceName) = + let argExtractableMembers, argDefaultInterfaceName = match invocation.Method.Name, invocation.Arguments[1], invocation.Arguments[2], invocation.Arguments[3] with | "GetExtractInterfaceOptions", (:? ImmutableArray as extractableMembers), (:? string as interfaceName), - _ -> (extractableMembers, interfaceName) + _ -> extractableMembers, interfaceName | "GetExtractInterfaceOptions", _, (:? ImmutableArray as extractableMembers), - (:? string as interfaceName) -> (extractableMembers, interfaceName) + (:? string as interfaceName) -> extractableMembers, interfaceName | "GetExtractInterfaceOptionsAsync", _, (:? List as extractableMembers), - (:? string as interfaceName) -> (extractableMembers.ToImmutableArray(), interfaceName) + (:? string as interfaceName) -> extractableMembers.ToImmutableArray(), interfaceName | _ -> NotImplementedException(string invocation.Method.Name) |> raise let fileName = sprintf "%s.cs" argDefaultInterfaceName - let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") + let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" let extractInterfaceOptionsResultType = - featuresAssembly.GetType("Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult") + featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult" |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" let locationEnumType = - extractInterfaceOptionsResultType.GetNestedType("ExtractLocation") + extractInterfaceOptionsResultType.GetNestedType "ExtractLocation" |> nonNull "extractInterfaceOptionsResultType.GetNestedType('ExtractLocation')" let location = @@ -301,7 +314,7 @@ type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = let _argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol let argSelectedMembers = invocation.Arguments[2] :?> ImmutableArray - let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") + let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" let msmOptionsType = featuresAssembly.GetType "Microsoft.CodeAnalysis.MoveStaticMembers.MoveStaticMembersOptions" @@ -328,7 +341,7 @@ type RemoteHostClientProviderInterceptor(_logMessage) = match invocation.Method.Name with | "TryGetRemoteHostClientAsync" -> - let workspacesAssembly = Assembly.Load("Microsoft.CodeAnalysis.Workspaces") + let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" let remoteHostClientType = workspacesAssembly.GetType "Microsoft.CodeAnalysis.Remote.RemoteHostClient" @@ -415,7 +428,7 @@ let loadProjectFilenamesFromSolution (solutionPath: string) = assert Path.IsPathRooted solutionPath let projectFilenames = new List() - let solutionFile = Microsoft.Build.Construction.SolutionFile.Parse(solutionPath) + let solutionFile = SolutionFile.Parse solutionPath for project in solutionFile.ProjectsInOrder do if project.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat then @@ -481,10 +494,10 @@ let loadProjectTfms (logger: ILogger) (projs: string seq) : Map Option.ofObj - |> Option.bind (fun s -> if String.IsNullOrEmpty(s) then None else Some s) + |> Option.bind (fun s -> if String.IsNullOrEmpty s then None else Some s) let targetFramework = - match buildProject.GetPropertyValue("TargetFramework") |> noneIfEmpty with + match buildProject.GetPropertyValue "TargetFramework" |> noneIfEmpty with | Some tfm -> [ tfm.Trim() ] | None -> [] @@ -495,7 +508,7 @@ let loadProjectTfms (logger: ILogger) (projs: string seq) : Map Map.add projectFilename (targetFramework @ targetFrameworks) - projectCollection.UnloadProject(buildProject) + projectCollection.UnloadProject buildProject with :? InvalidProjectFileException as ipfe -> logger.LogDebug( "loadProjectTfms: failed to load {projectFilename}: {ex}", @@ -526,20 +539,18 @@ let resolveDefaultWorkspaceProps (logger: ILogger) projs : Map = let tryLoadSolutionOnPath (lspClient: ILspClient) (logger: ILogger) (solutionPath: string) = - assert Path.IsPathRooted(solutionPath) - let progress = ProgressReporter(lspClient) + assert Path.IsPathRooted solutionPath + let progress = ProgressReporter lspClient let logMessage m = - lspClient.WindowLogMessage( + lspClient.WindowLogMessage { Type = MessageType.Info Message = sprintf "csharp-ls: %s" m } - ) let showMessage m = - lspClient.WindowShowMessage( + lspClient.WindowShowMessage { Type = MessageType.Info Message = sprintf "csharp-ls: %s" m } - ) async { try @@ -585,7 +596,7 @@ let tryLoadSolutionFromProjectFiles (logMessage: string -> Async) (projs: string list) = - let progress = ProgressReporter(lspClient) + let progress = ProgressReporter lspClient async { do! progress.Begin($"Loading {projs.Length} project(s)...", false, $"0/{projs.Length}", 0u) @@ -628,7 +639,7 @@ let tryLoadSolutionFromProjectFiles let selectPreferredSolution (slnFiles: string list) : option = let getProjectCount (slnPath: string) = try - let sln = SolutionFile.Parse(slnPath) + let sln = SolutionFile.Parse slnPath Some(sln.ProjectsInOrder.Count, slnPath) with _ -> None @@ -644,9 +655,7 @@ let selectPreferredSolution (slnFiles: string list) : option = let findAndLoadSolutionOnDir (lspClient: ILspClient) (logger: ILogger) dir = async { let fileNotOnNodeModules (filename: string) = - filename.Split(Path.DirectorySeparatorChar) - |> Seq.contains "node_modules" - |> not + filename.Split Path.DirectorySeparatorChar |> Seq.contains "node_modules" |> not let solutionFiles = [ "*.sln"; "*.slnx" ] @@ -655,10 +664,9 @@ let findAndLoadSolutionOnDir (lspClient: ILspClient) (logger: ILogger) dir = asy |> Seq.toList let logMessage m = - lspClient.WindowLogMessage( + lspClient.WindowLogMessage { Type = MessageType.Info Message = sprintf "csharp-ls: %s" m } - ) do! logMessage (sprintf "%d solution(s) found: [%s]" solutionFiles.Length (String.Join(", ", solutionFiles))) @@ -720,20 +728,20 @@ let loadSolutionOnSolutionPathOrDir } let getContainingTypeOrThis (symbol: ISymbol) : INamedTypeSymbol = - if (symbol :? INamedTypeSymbol) then + if symbol :? INamedTypeSymbol then symbol :?> INamedTypeSymbol else symbol.ContainingType let getFullReflectionName (containingType: INamedTypeSymbol) = let stack = Stack() - stack.Push(containingType.MetadataName) + stack.Push containingType.MetadataName let mutable ns = containingType.ContainingNamespace let mutable doContinue = true while doContinue do - stack.Push(ns.Name) + stack.Push ns.Name ns <- ns.ContainingNamespace doContinue <- ns <> null && not ns.IsGlobalNamespace @@ -747,21 +755,21 @@ let tryAddDocument (solution: Solution) : Async = async { - let docDir = Path.GetDirectoryName(docFilePath) + let docDir = Path.GetDirectoryName docFilePath //logMessage (sprintf "TextDocumentDidOpen: docFilename=%s docDir=%s" docFilename docDir) let fileOnProjectDir (p: Project) = - let projectDir = Path.GetDirectoryName(p.FilePath) - let projectDirWithDirSepChar = projectDir + (string Path.DirectorySeparatorChar) + let projectDir = Path.GetDirectoryName p.FilePath + let projectDirWithDirSepChar = projectDir + string Path.DirectorySeparatorChar - (docDir = projectDir) || docDir.StartsWith(projectDirWithDirSepChar) + docDir = projectDir || docDir.StartsWith projectDirWithDirSepChar let projectOnPath = solution.Projects |> Seq.filter fileOnProjectDir |> Seq.tryHead let! newDocumentMaybe = match projectOnPath with | Some proj -> - let projectBaseDir = Path.GetDirectoryName(proj.FilePath) + let projectBaseDir = Path.GetDirectoryName proj.FilePath let docName = docFilePath.Substring(projectBaseDir.Length + 1) //logMessage (sprintf "Adding \"%s\" (\"%s\") to project %s" docName docFilePath proj.FilePath) @@ -769,7 +777,7 @@ let tryAddDocument let newDoc = proj.AddDocument( name = docName, - text = SourceText.From(text), + text = SourceText.From text, folders = null, filePath = docFilePath ) From 717f6aff9bb9443ccaad4e3cfc74c979efc94489 Mon Sep 17 00:00:00 2001 From: alsi-lawr Date: Fri, 19 Sep 2025 12:00:23 +0100 Subject: [PATCH 7/9] chore: drop redundant normalizeLineEndings function --- tests/CSharpLanguageServer.Tests/CodeActionTests.fs | 2 +- .../CSharpLanguageServer.Tests/DocumentFormattingTests.fs | 7 ++++--- tests/CSharpLanguageServer.Tests/HoverTests.fs | 5 ++--- tests/CSharpLanguageServer.Tests/Tooling.fs | 5 +---- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs index 39759fa7..52bbc9e9 100644 --- a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs +++ b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs @@ -122,7 +122,7 @@ type CodeActionTests() = | Some { DocumentChanges = Some [| U4.C1 create; U4.C1 implement |] } -> match create.Edits, implement.Edits with | [| U2.C1 createEdits |], [| U2.C1 implementEdits |] -> - Assert.AreEqual(expectedCreateInterfaceEdits, createEdits |> TextEdit.normalizeNewText) + Assert.AreEqual(expectedCreateInterfaceEdits, createEdits.ReplaceLineEndings("\n")) Assert.AreEqual(expectedImplementInterfaceEdits, implementEdits |> TextEdit.normalizeNewText) diff --git a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs index c92dfb4e..7789d49d 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs @@ -32,11 +32,12 @@ let testEditorConfigFormatting () = match textEdits with | Some tes -> let expectedClassContents = - File.ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) - |> Text.normalizeLineEndings + File + .ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) + .ReplaceLineEndings("\n") let actualClassContents = - classFile.GetFileContentsWithTextEditsApplied(tes) |> Text.normalizeLineEndings + classFile.GetFileContentsWithTextEditsApplied(tes).ReplaceLineEndings("\n") Assert.AreEqual(expectedClassContents, actualClassContents) | None -> failwith "Some TextEdit's were expected" diff --git a/tests/CSharpLanguageServer.Tests/HoverTests.fs b/tests/CSharpLanguageServer.Tests/HoverTests.fs index 206d8303..8173657a 100644 --- a/tests/CSharpLanguageServer.Tests/HoverTests.fs +++ b/tests/CSharpLanguageServer.Tests/HoverTests.fs @@ -29,8 +29,7 @@ let testHoverWorks () = match hover.Contents with | U3.C1 c -> Assert.AreEqual(MarkupKind.Markdown, c.Kind) - let actualHoverContents = c.Value |> Text.normalizeLineEndings - Assert.AreEqual("```csharp\nvoid Class.Method(string arg)\n```", actualHoverContents) + Assert.AreEqual("```csharp\nvoid Class.Method(string arg)\n```", c.Value.ReplaceLineEndings("\n")) | _ -> failwith "C1 was expected" Assert.IsTrue(hover.Range.IsNone) @@ -57,7 +56,7 @@ let testHoverWorks () = Assert.AreEqual( "```csharp\nstring\n```\n\nRepresents text as a sequence of UTF-16 code units.", - c.Value |> Text.normalizeLineEndings + c.Value.ReplaceLineEndings("\n") ) | _ -> failwith "C1 was expected" diff --git a/tests/CSharpLanguageServer.Tests/Tooling.fs b/tests/CSharpLanguageServer.Tests/Tooling.fs index 112cc449..29dfcba0 100644 --- a/tests/CSharpLanguageServer.Tests/Tooling.fs +++ b/tests/CSharpLanguageServer.Tests/Tooling.fs @@ -855,11 +855,8 @@ let setupServerClient (clientProfile: ClientProfile) (testDataDirName: string) = new ClientController(clientActor, actualTestDataDirName) -module Text = - let normalizeLineEndings (s: string) = - Regex.Replace(s, @"\r\n|\n\r|\r|\n", "\n") module TextEdit = let normalizeNewText (s: TextEdit) = { s with - NewText = s.NewText |> Text.normalizeLineEndings } + NewText = s.NewText.ReplaceLineEndings("\n") } From f98c5e33ea3b63782d51375b2c6f119737e93083 Mon Sep 17 00:00:00 2001 From: alsi-lawr Date: Fri, 19 Sep 2025 12:02:58 +0100 Subject: [PATCH 8/9] fix: fix incorrect use of replacelineendings on textedit type --- tests/CSharpLanguageServer.Tests/CodeActionTests.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs index 52bbc9e9..39759fa7 100644 --- a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs +++ b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs @@ -122,7 +122,7 @@ type CodeActionTests() = | Some { DocumentChanges = Some [| U4.C1 create; U4.C1 implement |] } -> match create.Edits, implement.Edits with | [| U2.C1 createEdits |], [| U2.C1 implementEdits |] -> - Assert.AreEqual(expectedCreateInterfaceEdits, createEdits.ReplaceLineEndings("\n")) + Assert.AreEqual(expectedCreateInterfaceEdits, createEdits |> TextEdit.normalizeNewText) Assert.AreEqual(expectedImplementInterfaceEdits, implementEdits |> TextEdit.normalizeNewText) From fd1bfaa73c2fb33ac837659090d24853ab75b397 Mon Sep 17 00:00:00 2001 From: alsi-lawr Date: Fri, 19 Sep 2025 12:09:07 +0100 Subject: [PATCH 9/9] docs: update CHANGELOG --- CHANGELOG.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7face532..b462884b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +* Fix `textDocument/codeAction` for extracting an interface + - By @alsi-lawr in https://github.com/razzmatazz/csharp-language-server/pull/267 * Will use static server capability registration, unless required (i.e. for workspace/didChangeWatchedFiles). - By @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/263 * Fix semantic tokens for multi-line literals @@ -137,7 +139,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Upgrade dependencies: Roslyn, ICSharpCode.Decompiler, Microsoft.Build; * Fix for a crash in serverEventLoop - By @kstatz12 in https://github.com/razzmatazz/csharp-language-server/issues/113 -* Possible fixes to https://github.com/razzmatazz/csharp-language-server/issues/62 +* Possible fixes to https://github.com/razzmatazz/csharp-language-server/issues/62 and https://github.com/razzmatazz/csharp-language-server/issues/57 - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/112 * Fix issues with code actions and other functionality that was @@ -159,12 +161,12 @@ with `csharp.` settings; ## [0.8.0] - 2023-05-06 / Varėna * Add more symbols to documentSymbols & codeLens - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/87 -* Support type and call hierarchy +* Support type and call hierarchy - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/74 * Semantic token types fix - - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/84 + - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/84 * Fix crash if there is no newline at the end of the last line - - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/83 + - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/83 ### More about Varėna, Lithuania - [Varėna on WP](https://en.wikipedia.org/wiki/Var%C4%97na) @@ -186,7 +188,7 @@ with `csharp.` settings; * Semantic token improvements - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/70 * Inlay hint support - - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/71 + - By Adam Tao @tcx4c70 in https://github.com/razzmatazz/csharp-language-server/pull/71 and https://github.com/razzmatazz/csharp-language-server/pull/73 * Remove timeout on handler for `codelens/resolve` -- that wasn't a good idea to begin with @@ -248,7 +250,7 @@ with `csharp.` settings; ## [0.5.5] - 2022-08-23 / Prienai * Fix intermittent server crashes after upgrading to latest Ionide.LanguageServerProtocol: - https://github.com/razzmatazz/csharp-language-server/issues/44 - + ### More about Prienai, Lithuania - [Google Images on Prienai](https://www.google.com/search?tbm=isch&q=prienai+lithuania) - [Prienai on WP](https://en.wikipedia.org/wiki/Prienai) @@ -258,7 +260,7 @@ with `csharp.` settings; * Properly format + "localize" symbol names in `textDocument/documentSymbol` response; - Reported by @joefbsjr in https://github.com/razzmatazz/csharp-language-server/issues/42 * Properly format ITypeSymbol (structs/enums/classes) when displaying type info on `workspace/symbol` and other LSP requests; - - Reported by @joefbsjr in https://github.com/razzmatazz/csharp-language-server/issues/41 + - Reported by @joefbsjr in https://github.com/razzmatazz/csharp-language-server/issues/41 * Load solution-in-sync when initializing. This will help "server initializing" notification work better for clients that depend on `initialize` request not to complete until the server/solution is properly loaded initialized. - Reported by @joefbsjr in https://github.com/razzmatazz/csharp-language-server/issues/40 @@ -282,7 +284,7 @@ with `csharp.` settings; - fixes https://github.com/razzmatazz/csharp-language-server/issues/35 by @Decodetalkers; * Expose actual csharp compiler diagnostics ids (e.g. CS0117) for nicer diagnostics messages. -## [0.5.1] - 2022-05-19 / Straigiškė +## [0.5.1] - 2022-05-19 / Straigiškė - Fix another long-standing bug with incremental document synchronisation; - very visible on nvim but affects all editors/clients; - reported by @Decodetalkers https://github.com/razzmatazz/csharp-language-server/issues/31; @@ -321,12 +323,12 @@ with `csharp.` settings; ## [0.3.0] - Run diagnostics resolution outside the main state actor so we don't lock up other processing; -- Add timeout for codelens requests to avoid excessive CPU usage as that is prone to run for a long time; +- Add timeout for codelens requests to avoid excessive CPU usage as that is prone to run for a long time; - Really fix write-request serialization; - Revert the emacs-29 fix, didn't do much. ## [0.2.1] -- Carefully observe incoming requests from StreamJsonRpc to actually serialize write-access to solution state; +- Carefully observe incoming requests from StreamJsonRpc to actually serialize write-access to solution state; - Should fix sync issues (another attempt..) - Attempt to fix an issue with emacs-29 by setting `bufferSize` in `Console.OpenStandardInput` call. @@ -348,7 +350,7 @@ with `csharp.` settings; - Needs client implementation of `workspace/executeCommand`: `csharp.showReferences`; - Nicer on-hover markdown that should match the context better; - Expose properties & events on textDocument/documentSymbol; -- Pull `add using import` code action to the top of action list, mark it prefered and with `Kind`, [csharp-language-server#9](https://github.com/razzmatazz/csharp-language-server/issues/9). +- Pull `add using import` code action to the top of action list, mark it prefered and with `Kind`, [csharp-language-server#9](https://github.com/razzmatazz/csharp-language-server/issues/9). ## [0.1.7] - Bump roslyn libs to a newer version;