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/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; diff --git a/Directory.Packages.props b/Directory.Packages.props index d081ad5a..187b71a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,11 +11,12 @@ - - + + - + + @@ -28,13 +29,13 @@ - - - - - + + + + + - \ 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/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 9508909c..81d21e73 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 @@ -56,35 +55,27 @@ 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 - if nodeMethodDecl.Identifier.ValueText = sym.Name then - let methodArityMatches = - let symMethod = sym :?> IMethodSymbol - symMethod.Parameters.Length = nodeMethodDecl.ParameterList.Parameters.Count + let methodArityMatches = + symMethod.Parameters.Length = m.ParameterList.Parameters.Count - collectIdentifier nodeMethodDecl.Identifier methodArityMatches - else if node :? TypeDeclarationSyntax then - let typeDecl = node :?> TypeDeclarationSyntax + collectIdentifier m.Identifier methodArityMatches - if typeDecl.Identifier.ValueText = sym.Name then - collectIdentifier typeDecl.Identifier false + | _, (:? TypeDeclarationSyntax as t) when t.Identifier.ValueText = sym.Name -> + collectIdentifier t.Identifier false - else if node :? PropertyDeclarationSyntax then - let propertyDecl = node :?> PropertyDeclarationSyntax + | _, (:? PropertyDeclarationSyntax as p) when p.Identifier.ValueText = sym.Name -> + collectIdentifier p.Identifier false - if propertyDecl.Identifier.ValueText = sym.Name then - collectIdentifier propertyDecl.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 @@ -119,6 +110,27 @@ type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = | _ -> NotImplementedException(string invocation.Method) |> raise +type CleanCodeGenOptionsProxy(logMessage) = + static let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" + static let generator = ProxyGenerator() + + static let cleanCodeGenOptionsProvTypeMaybe = + workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider" + |> Option.ofObj + + + member __.Create() = + 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 member __.Intercept(invocation: IInvocation) = @@ -131,17 +143,7 @@ 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 - + invocation.ReturnValue <- CleanCodeGenOptionsProxy(logMessage).Create() | _ -> NotImplementedException(string invocation.Method) |> raise type PickMembersServiceInterceptor(_logMessage) = @@ -229,15 +231,7 @@ type ExtractClassOptionsServiceInterceptor(_logMessage) = | "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 |]) + invocation.ReturnValue <- Task.fromResult (extractClassOptionsValue.GetType(), extractClassOptionsValue) | "GetExtractClassOptions" -> let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol @@ -249,60 +243,67 @@ 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 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" -> + Task.fromResult ( + extractInterfaceOptionsResultType, + Activator.CreateInstance( + extractInterfaceOptionsResultType, + false, // isCancelled + argExtractableMembers, + argDefaultInterfaceName, + fileName, + location, + CleanCodeGenOptionsProxy(logMessage).Create() + ) + ) - let extractInterfaceOptionsResultValue = + | _ -> Activator.CreateInstance( extractInterfaceOptionsResultType, false, // isCancelled - argExtractableMembers.ToImmutableArray(), + argExtractableMembers, argDefaultInterfaceName, fileName, - location, - cleanCodeGenerationOptionsProvider + location ) - 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 - type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = interface IInterceptor with member __.Intercept(invocation: IInvocation) = @@ -346,13 +347,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 <- Task.fromResult (remoteHostClientType, null) | _ -> NotImplementedException(string invocation.Method) |> raise diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index b3b4fb4e..55eb4470 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -2,7 +2,9 @@ module CSharpLanguageServer.Util open System open System.Runtime.InteropServices +open System.Threading.Tasks open System.IO +open System.Reflection let nonNull name (value: 'T when 'T: null) : 'T = if Object.ReferenceEquals(value, null) then @@ -74,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 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..7789d49d 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs @@ -32,7 +32,12 @@ let testEditorConfigFormatting () = match textEdits with | Some tes -> let expectedClassContents = - File.ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) + File + .ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) + .ReplaceLineEndings("\n") - Assert.AreEqual(expectedClassContents, classFile.GetFileContentsWithTextEditsApplied(tes)) + let actualClassContents = + 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 2bab71d9..8173657a 100644 --- a/tests/CSharpLanguageServer.Tests/HoverTests.fs +++ b/tests/CSharpLanguageServer.Tests/HoverTests.fs @@ -29,7 +29,7 @@ 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) + Assert.AreEqual("```csharp\nvoid Class.Method(string arg)\n```", c.Value.ReplaceLineEndings("\n")) | _ -> failwith "C1 was expected" Assert.IsTrue(hover.Range.IsNone) @@ -55,12 +55,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.ReplaceLineEndings("\n") ) | _ -> 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..29dfcba0 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,9 @@ let setupServerClient (clientProfile: ClientProfile) (testDataDirName: string) = DirectoryInfo(Path.Combine(testAssemblyLocationDir, "..", "..", "..", testDataDirName)) new ClientController(clientActor, actualTestDataDirName) + + +module TextEdit = + let normalizeNewText (s: TextEdit) = + { s with + NewText = s.NewText.ReplaceLineEndings("\n") }