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") }